From 163859caef30e58200eba042b93e0fe74815616b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sun, 18 May 2014 14:21:35 +0200 Subject: [PATCH] may configure to not allow direct assignment for persisted AASM models #53 --- CHANGELOG.md | 1 + README.md | 28 +++++++++++++++++++ lib/aasm/base.rb | 16 +++++++++-- lib/aasm/errors.rb | 1 + spec/database.rb | 2 +- spec/models/persistence.rb | 17 ++++++++++- .../active_record_persistence_spec.rb | 22 +++++++++++++-- 7 files changed, 80 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f495f3b..b11de4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 4.0.0 (not yet released) + * may configure to not allow direct assignment for persisted AASM models (see [issue #53](https://github.com/aasm/aasm/issues/53)) * **DSL change**: callbacks don't require `to_state` parameter anymore, but still support it (closing issues [#11](https://github.com/aasm/aasm/issues/11), diff --git a/README.md b/README.md index 800503c..bc507c8 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,34 @@ class Job < ActiveRecord::Base end ``` +If you want to make sure that the _AASM_ column for storing the state is not directly assigned, +configure _AASM_ to not allow direct assignment, like this: + +```ruby +class Job < ActiveRecord::Base + include AASM + + aasm :no_direct_assignment => true do + state :sleeping, :initial => true + state :running + + event :run do + transitions :from => :sleeping, :to => :running + end + end + +end +``` + +resulting in this: + +```ruby +job = Job.create +job.aasm_state # => 'sleeping' +job.aasm_state = :running # => raises AASM::NoDirectAssignmentError +job.aasm_state # => 'sleeping' +``` + ### Sequel AASM also supports [Sequel](http://sequel.jeremyevans.net/) besides _ActiveRecord_ and _Mongoid_. diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index dba9ae0..867393c 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -4,7 +4,7 @@ module AASM def initialize(clazz, options={}, &block) @clazz = clazz @state_machine = AASM::StateMachine[@clazz] - @state_machine.config.column = options[:column].to_sym if options[:column] + @state_machine.config.column ||= (options[:column] || :aasm_state).to_sym @options = options # let's cry if the transition is invalid @@ -13,11 +13,21 @@ module AASM # create named scopes for each state configure :create_scopes, true - # don't store any new state if the model is invalid + # don't store any new state if the model is invalid (in ActiveRecord) configure :skip_validation_on_save, false - # use requires_new for nested transactions + # use requires_new for nested transactions (in ActiveRecord) configure :requires_new_transaction, true + + # set to true to forbid direct assignment of aasm_state column (in ActiveRecord) + configure :no_direct_assignment, false + + + if @state_machine.config.no_direct_assignment + @clazz.send(:define_method, "#{@state_machine.config.column}=") do |state_name| + raise AASM::NoDirectAssignmentError.new('direct assignment of AASM column has been disabled (see AASM configuration for this class)') + end + end end def initial_state(new_initial_state=nil) diff --git a/lib/aasm/errors.rb b/lib/aasm/errors.rb index 2b629d6..9b975d0 100644 --- a/lib/aasm/errors.rb +++ b/lib/aasm/errors.rb @@ -1,4 +1,5 @@ module AASM class InvalidTransition < RuntimeError; end class UndefinedState < RuntimeError; end + class NoDirectAssignmentError < RuntimeError; end end diff --git a/spec/database.rb b/spec/database.rb index 2c38243..ae9037d 100644 --- a/spec/database.rb +++ b/spec/database.rb @@ -1,5 +1,5 @@ ActiveRecord::Migration.suppress_messages do - %w{gates readers writers transients simples simple_new_dsls no_scopes thieves localizer_test_models persisted_states provided_and_persisted_states}.each do |table_name| + %w{gates readers writers transients simples simple_new_dsls no_scopes no_direct_assignments thieves localizer_test_models persisted_states provided_and_persisted_states}.each do |table_name| ActiveRecord::Migration.create_table table_name, :force => true do |t| t.string "aasm_state" end diff --git a/spec/models/persistence.rb b/spec/models/persistence.rb index e585e21..2b06334 100644 --- a/spec/models/persistence.rb +++ b/spec/models/persistence.rb @@ -48,7 +48,22 @@ end class NoScope < ActiveRecord::Base include AASM aasm :create_scopes => false do - state :ignored_scope + state :pending, :initial => true + state :running + event :run do + transitions :from => :pending, :to => :running + end + end +end + +class NoDirectAssignment < ActiveRecord::Base + include AASM + aasm :no_direct_assignment => true do + state :pending, :initial => true + state :running + event :run do + transitions :from => :pending, :to => :running + end end end diff --git a/spec/unit/persistence/active_record_persistence_spec.rb b/spec/unit/persistence/active_record_persistence_spec.rb index 83de287..a28fe38 100644 --- a/spec/unit/persistence/active_record_persistence_spec.rb +++ b/spec/unit/persistence/active_record_persistence_spec.rb @@ -93,10 +93,28 @@ describe "named scopes with the new DSL" do end it "does not create scopes if requested" do - expect(NoScope).not_to respond_to(:ignored_scope) + expect(NoScope).not_to respond_to(:pending) end -end +end # scopes + +describe "direct assignment" do + it "is allowed by default" do + obj = NoScope.create + expect(obj.aasm_state.to_sym).to eql :pending + + obj.aasm_state = :running + expect(obj.aasm_state.to_sym).to eql :running + end + + it "is forbidden if configured" do + obj = NoDirectAssignment.create + expect(obj.aasm_state.to_sym).to eql :pending + + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + end +end # direct assignment describe 'initial states' do