diff --git a/lib/aasm/persistence.rb b/lib/aasm/persistence.rb index d376322..6b01079 100644 --- a/lib/aasm/persistence.rb +++ b/lib/aasm/persistence.rb @@ -12,6 +12,9 @@ module AASM elsif hierarchy.include?("Mongoid::Document") require_files_for(:mongoid) base.send(:include, AASM::Persistence::MongoidPersistence) + elsif hierarchy.include?("Sequel::Model") + require_files_for(:sequel) + base.send(:include, AASM::Persistence::SequelPersistence) end end diff --git a/lib/aasm/persistence/sequel_persistence.rb b/lib/aasm/persistence/sequel_persistence.rb new file mode 100644 index 0000000..0801a1e --- /dev/null +++ b/lib/aasm/persistence/sequel_persistence.rb @@ -0,0 +1,108 @@ +module AASM + module Persistence + module SequelPersistence + def self.included(base) + base.send(:include, AASM::Persistence::Base) + base.send(:include, AASM::Persistence::SequelPersistence::InstanceMethods) + end + + module InstanceMethods + def before_validation + aasm_ensure_initial_state + super + end + + def before_create + aasm_ensure_initial_state + super + end + + # Returns the value of the aasm_column - called from aasm.current_state + # + # If it's a new record, and the aasm state column is blank it returns the initial state + # + # class Foo < Sequel::Model + # include AASM + # aasm :column => :status do + # state :opened + # state :closed + # end + # end + # + # foo = Foo.new + # foo.current_state # => :opened + # foo.close + # foo.current_state # => :closed + # + # foo = Foo[1] + # foo.current_state # => :opened + # foo.aasm_state = nil + # foo.current_state # => nil + # + # NOTE: intended to be called from an event + # + # This allows for nil aasm states - be sure to add validation to your model + def aasm_read_state + state = send(self.class.aasm_column) + if new? && state.to_s.strip.empty? + aasm.determine_state_name(self.class.aasm.initial_state) + elsif state.nil? + nil + else + state.to_sym + end + end + + # Ensures that if the aasm_state column is nil and the record is new + # that the initial state gets populated before validation on create + # + # foo = Foo.new + # foo.aasm_state # => nil + # foo.valid? + # foo.aasm_state # => "open" (where :open is the initial state) + # + # + # foo = Foo.find(:first) + # foo.aasm_state # => 1 + # foo.aasm_state = nil + # foo.valid? + # foo.aasm_state # => nil + # + def aasm_ensure_initial_state + aasm.enter_initial_state if + send(self.class.aasm_column).to_s.strip.empty? + end + + # Writes state to the state column and persists it to the database + # + # foo = Foo[1] + # foo.aasm.current_state # => :opened + # foo.close! + # foo.aasm.current_state # => :closed + # Foo[1].aasm.current_state # => :closed + # + # NOTE: intended to be called from an event + def aasm_write_state state + aasm_column = self.class.aasm_column + update_ony({aasm_column => state.to_s}, aasm_column) + end + + # Writes state to the state column, but does not persist it to the database + # + # foo = Foo[1] + # foo.aasm.current_state # => :opened + # foo.close + # foo.aasm.current_state # => :closed + # Foo[1].aasm.current_state # => :opened + # foo.save + # foo.aasm.current_state # => :closed + # Foo[1].aasm.current_state # => :closed + # + # NOTE: intended to be called from an event + def aasm_write_state_without_persistence state + send("#{self.class.aasm_column}=", state.to_s) + end + end + end + end +end diff --git a/spec/unit/persistence/sequel_persistence_spec.rb b/spec/unit/persistence/sequel_persistence_spec.rb new file mode 100644 index 0000000..0eb970b --- /dev/null +++ b/spec/unit/persistence/sequel_persistence_spec.rb @@ -0,0 +1,103 @@ + +describe 'sequel' do + begin + require 'sequel' + require 'logger' + require 'spec_helper' + + before(:all) do + db = Sequel.sqlite + # if you want to see the statements while running the spec enable the following line + # db.loggers << Logger.new($stderr) + db.create_table(:models) do + primary_key :id + String :status + end + + @model = Class.new(Sequel::Model(db)) do + set_dataset(:models) + attr_accessor :default + include AASM + aasm :column => :status + aasm do + state :alpha, :initial => true + state :beta + state :gamma + event :release do + transitions :from => [:alpha, :beta, :gamma], :to => :beta + end + end + end + end + + describe "instance methods" do + let(:model) {@model.new} + + it "should respond to aasm persistence methods" do + expect(model).to respond_to(:aasm_read_state) + expect(model).to respond_to(:aasm_write_state) + expect(model).to respond_to(:aasm_write_state_without_persistence) + end + + it "should return the initial state when new and the aasm field is nil" do + expect(model.aasm.current_state).to eq(:alpha) + end + + it "should return the aasm column when new and the aasm field is not nil" do + model.status = "beta" + expect(model.aasm.current_state).to eq(:beta) + end + + it "should return the aasm column when not new and the aasm_column is not nil" do + allow(model).to receive(:new?).and_return(false) + model.status = "gamma" + expect(model.aasm.current_state).to eq(:gamma) + end + + it "should allow a nil state" do + allow(model).to receive(:new?).and_return(false) + model.status = nil + expect(model.aasm.current_state).to be_nil + end + + it "should call aasm_ensure_initial_state on validation before create" do + expect(model).to receive(:aasm_ensure_initial_state).and_return(true) + model.valid? + end + + it "should call aasm_ensure_initial_state before create, even if skipping validations" do + expect(model).to receive(:aasm_ensure_initial_state).and_return(true) + model.save(:validate => false) + end + end + + describe 'subclasses' do + it "should have the same states as its parent class" do + expect(Class.new(@model).aasm.states).to eq(@model.aasm.states) + end + + it "should have the same events as its parent class" do + expect(Class.new(@model).aasm.events).to eq(@model.aasm.events) + end + + it "should have the same column as its parent even for the new dsl" do + expect(@model.aasm_column).to eq(:status) + expect(Class.new(@model).aasm_column).to eq(:status) + end + end + + describe 'initial states' do + it 'should support conditions' do + @model.aasm do + initial_state lambda{ |m| m.default } + end + + expect(@model.new(:default => :beta).aasm.current_state).to eq(:beta) + expect(@model.new(:default => :gamma).aasm.current_state).to eq(:gamma) + end + end + + rescue LoadError + puts "Not running Sequel specs because sequel gem if not installed!!!" + end +end