From ab6d184a218ade2f7ddb42fe88c9f364e4813e69 Mon Sep 17 00:00:00 2001 From: Konstantin Burnaev Date: Sat, 10 May 2014 19:02:11 +1000 Subject: [PATCH] Added support for Rails 4.1+ enums. --- README.md | 25 ++++ lib/aasm/base.rb | 2 + .../persistence/active_record_persistence.rb | 28 ++++- .../active_record_persistence_spec.rb | 112 ++++++++++++++++++ 4 files changed, 163 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 414d0a84..8b55ea9c 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,31 @@ class Job < ActiveRecord::Base end ``` +#### ActiveRecord enums + +You can use +[enumerations](http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html) +in Rails 4.1+ for your state column: + +```ruby +class Job < ActiveRecord::Base + include AASM + + enum state { + sleeping: 5, + running: 99 + } + + aasm :column => :state, :enum => :states do + state :sleeping, :initial => true + state :running + end +end +``` + +You need to pass the name of the method which provides access to the +enumeration mapping as a value of ```enum```. + ### Mongoid AASM also supports persistence to Mongodb if you're using Mongoid. Make sure diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index dba9ae05..d854b151 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -18,6 +18,8 @@ def initialize(clazz, options={}, &block) # use requires_new for nested transactions configure :requires_new_transaction, true + + configure :enum, nil end def initial_state(new_initial_state=nil) diff --git a/lib/aasm/persistence/active_record_persistence.rb b/lib/aasm/persistence/active_record_persistence.rb index 70cbf748..501687e1 100644 --- a/lib/aasm/persistence/active_record_persistence.rb +++ b/lib/aasm/persistence/active_record_persistence.rb @@ -85,10 +85,11 @@ module InstanceMethods # NOTE: intended to be called from an event def aasm_write_state(state) old_value = read_attribute(self.class.aasm_column) - write_attribute(self.class.aasm_column, state.to_s) + aasm_write_attribute state - success = if AASM::StateMachine[self.class].config.skip_validation_on_save - self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm_column => state.to_s) == 1 + success = if aasm_skipping_validations + value = aasm_raw_attribute_value state + self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm_column => value) == 1 else self.save end @@ -113,10 +114,29 @@ def aasm_write_state(state) # # NOTE: intended to be called from an event def aasm_write_state_without_persistence(state) - write_attribute(self.class.aasm_column, state.to_s) + aasm_write_attribute state end private + def aasm_enum + AASM::StateMachine[self.class].config.enum + end + + def aasm_skipping_validations + AASM::StateMachine[self.class].config.skip_validation_on_save + end + + def aasm_write_attribute(state) + write_attribute self.class.aasm_column, aasm_raw_attribute_value(state) + end + + def aasm_raw_attribute_value(state) + if aasm_enum + value = self.class.send(aasm_enum)[state] + else + value = state.to_s + 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 diff --git a/spec/unit/persistence/active_record_persistence_spec.rb b/spec/unit/persistence/active_record_persistence_spec.rb index 83de2876..68693747 100644 --- a/spec/unit/persistence/active_record_persistence_spec.rb +++ b/spec/unit/persistence/active_record_persistence_spec.rb @@ -23,6 +23,118 @@ expect(gate).to respond_to(:aasm_write_state_without_persistence) end + context "when AASM is configured to use enum" do + let(:state_sym) { :running } + let(:state_code) { 2 } + let(:enum_name) { :states } + let(:enum) { Hash[state_sym, state_code] } + + before :each do + gate + .stub(:aasm_enum) + .and_return(enum_name) + gate.stub(:aasm_write_attribute) + gate.stub(:write_attribute) + + gate + .class + .stub(enum_name) + .and_return(enum) + end + + describe "aasm_write_state" do + context "when AASM is configured to skip validations on save" do + before :each do + gate + .stub(:aasm_skipping_validations) + .and_return(true) + end + + it "passes state code instead of state symbol to update_all" do + # stub_chain does not allow us to give expectations on call + # parameters in the middle of the chain, so we need to use + # intermediate object instead. + obj = double(Object, update_all: 1) + gate + .class + .stub(:where) + .and_return(obj) + + gate.aasm_write_state state_sym + + expect(obj).to have_received(:update_all) + .with(Hash[gate.class.aasm_column, state_code]) + end + end + + context "when AASM is not skipping validations" do + it "delegates state update to the helper method" do + # Let's pretend that validation is passed + gate.stub(:save).and_return(true) + + gate.aasm_write_state state_sym + + expect(gate).to have_received(:aasm_write_attribute).with(state_sym) + expect(gate).to_not have_received :write_attribute + end + end + end + + describe "aasm_write_state_without_persistence" do + it "delegates state update to the helper method" do + gate.aasm_write_state_without_persistence state_sym + + expect(gate).to have_received(:aasm_write_attribute).with(state_sym) + expect(gate).to_not have_received :write_attribute + end + end + + describe "aasm_raw_attribute_value" do + it "converts state symbol to state code" do + expect(gate.send(:aasm_raw_attribute_value, state_sym)) + .to eq state_code + end + end + end + + context "when AASM is configured to use string field" do + let(:state_sym) { :running } + + before :each do + gate + .stub(:aasm_enum) + .and_return(nil) + end + + describe "aasm_raw_attribute_value" do + it "converts state symbol to string" do + expect(gate.send(:aasm_raw_attribute_value, state_sym)) + .to eq state_sym.to_s + end + end + end + + describe "aasm_write_attribute helper method" do + let(:sym) { :sym } + let(:value) { 42 } + + before :each do + gate.stub(:write_attribute) + gate.stub(:aasm_raw_attribute_value) + .and_return(value) + + gate.send(:aasm_write_attribute, sym) + end + + it "generates attribute value using a helper method" do + expect(gate).to have_received(:aasm_raw_attribute_value).with(sym) + end + + it "writes attribute to the model" do + expect(gate).to have_received(:write_attribute).with(:aasm_state, value) + end + end + it "should return the initial state when new and the aasm field is nil" do expect(gate.aasm.current_state).to eq(:opened) end