From 7acc8239b9338c16e6c684f5e0289e6446cde232 Mon Sep 17 00:00:00 2001 From: Marc Rohloff Date: Mon, 27 Feb 2023 13:04:04 -0700 Subject: [PATCH 1/5] Add support for using custom AASM::Core classes for States, Events and Transtions --- lib/aasm/base.rb | 13 +++++++++++++ lib/aasm/core/event.rb | 35 +++++++++++++++++++++-------------- lib/aasm/core/transition.rb | 5 ++++- lib/aasm/state_machine.rb | 10 +++++++--- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index 03b6fc43..82cb8a4c 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -10,6 +10,7 @@ def initialize(klass, name, state_machine, options={}, &block) @name = name # @state_machine = klass.aasm(@name).state_machine @state_machine = state_machine + @state_machine.implementation = self @state_machine.config.column ||= (options[:column] || default_column).to_sym # @state_machine.config.column = options[:column].to_sym if options[:column] # master @options = options @@ -196,6 +197,18 @@ def from_states_for_state(state, options={}) end end + def aasm_state_class + AASM::Core::State + end + + def aasm_event_class + AASM::Core::Event + end + + def aasm_transition_class + AASM::Core::Transition + end + private def default_column diff --git a/lib/aasm/core/event.rb b/lib/aasm/core/event.rb index 316b1277..e2cd7125 100644 --- a/lib/aasm/core/event.rb +++ b/lib/aasm/core/event.rb @@ -17,17 +17,7 @@ def initialize(name, state_machine, options = {}, &block) # from aasm4 @options = options # QUESTION: .dup ? - add_options_from_dsl(@options, [ - :after, - :after_commit, - :after_transaction, - :before, - :before_transaction, - :ensure, - :error, - :before_success, - :success, - ], &block) if block + add_options_from_dsl(@options, dsl_option_keys, &block) if block end # called internally by Ruby 1.9 after clone() @@ -95,13 +85,16 @@ def ==(event) ## DSL interface def transitions(definitions=nil, &block) if definitions # define new transitions + transition_class = state_machine.implementation.aasm_transition_class + raise ArgumentError, "The class #{transition_class} must inherit from AASM::Core::Transition!" unless transition_class.ancestors.include?(AASM::Core::Transition) + # Create a separate transition for each from-state to the given state Array(definitions[:from]).each do |s| - @transitions << AASM::Core::Transition.new(self, attach_event_guards(definitions.merge(:from => s.to_sym)), &block) + @transitions << transition_class.new(self, attach_event_guards(definitions.merge(:from => s.to_sym)), &block) end # Create a transition if :to is specified without :from (transitions from ANY state) if !definitions[:from] && definitions[:to] - @transitions << AASM::Core::Transition.new(self, attach_event_guards(definitions), &block) + @transitions << transition_class.new(self, attach_event_guards(definitions), &block) end end @transitions @@ -142,7 +135,7 @@ def _fire(obj, options={}, to_state=::AASM::NO_VALUE, *args) args.unshift(to_state) to_state = nil end - + # nop, to_state is a valid to-state transitions.each do |transition| @@ -174,5 +167,19 @@ def invoke_callbacks(code, record, args) .with_default_return_value(false) .invoke end + + def dsl_option_keys + [ + :after, + :after_commit, + :after_transaction, + :before, + :before_transaction, + :ensure, + :error, + :before_success, + :success, + ] + end end end # AASM diff --git a/lib/aasm/core/transition.rb b/lib/aasm/core/transition.rb index d54a554e..102434fb 100644 --- a/lib/aasm/core/transition.rb +++ b/lib/aasm/core/transition.rb @@ -8,7 +8,7 @@ class Transition alias_method :options, :opts def initialize(event, opts, &block) - add_options_from_dsl(opts, [:on_transition, :guard, :after, :success], &block) if block + add_options_from_dsl(opts, dsl_option_keys, &block) if block @event = event @from = opts[:from] @@ -79,5 +79,8 @@ def _fire_callbacks(code, record, args) Invoker.new(code, record, args).invoke end + def dsl_option_keys + [:on_transition, :guard, :after, :success] + end end end # AASM diff --git a/lib/aasm/state_machine.rb b/lib/aasm/state_machine.rb index 45b77cb0..f7dcc06f 100644 --- a/lib/aasm/state_machine.rb +++ b/lib/aasm/state_machine.rb @@ -2,7 +2,7 @@ module AASM class StateMachine # the following four methods provide the storage of all state machines - attr_accessor :states, :events, :initial_state, :config, :name, :global_callbacks + attr_accessor :states, :events, :initial_state, :config, :name, :implementation, :global_callbacks def initialize(name) @initial_state = nil @@ -28,11 +28,15 @@ def add_state(state_name, klass, options) # allow reloading, extending or redefining a state @states.delete(state_name) if @states.include?(state_name) - @states << AASM::Core::State.new(state_name, klass, self, options) + state_class = implementation.aasm_state_class + raise ArgumentError, "The class #{state_class} must inherit from AASM::Core::State!" unless state_class.ancestors.include?(AASM::Core::State) + @states << state_class.new(state_name, klass, self, options) end def add_event(name, options, &block) - @events[name] = AASM::Core::Event.new(name, self, options, &block) + event_class = implementation.aasm_event_class + raise ArgumentError, "The class #{event_class} must inherit from AASM::Core::Event!" unless event_class.ancestors.include?(AASM::Core::Event) + @events[name] = event_class.new(name, self, options, &block) end def add_global_callbacks(name, *callbacks, &block) From 234741ca4fcc1af7dde10e0041659780e73713a7 Mon Sep 17 00:00:00 2001 From: Marc Rohloff Date: Mon, 27 Feb 2023 13:04:21 -0700 Subject: [PATCH 2/5] Fix existing specs --- spec/unit/event_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/unit/event_spec.rb b/spec/unit/event_spec.rb index 55e78dc5..0ddd06d1 100644 --- a/spec/unit/event_spec.rb +++ b/spec/unit/event_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'adding an event' do - let(:state_machine) { AASM::StateMachine.new(:name) } + let(:state_machine) { AASM::StateMachine.new(:name).tap { |sm| AASM::Base.new(nil, :name, sm) } } let(:event) do AASM::Core::Event.new(:close_order, state_machine, {:success => :success_callback}) do before :before_callback @@ -36,7 +36,7 @@ end describe 'transition inspection' do - let(:state_machine) { AASM::StateMachine.new(:name) } + let(:state_machine) { AASM::StateMachine.new(:name).tap { |sm| AASM::Base.new(nil, :name, sm) } } let(:event) do AASM::Core::Event.new(:run, state_machine) do transitions :to => :running, :from => :sleeping @@ -61,7 +61,7 @@ end describe 'transition inspection without from' do - let(:state_machine) { AASM::StateMachine.new(:name) } + let(:state_machine) { AASM::StateMachine.new(:name).tap { |sm| AASM::Base.new(nil, :name, sm) } } let(:event) do AASM::Core::Event.new(:run, state_machine) do transitions :to => :running @@ -79,7 +79,7 @@ end describe 'firing an event' do - let(:state_machine) { AASM::StateMachine.new(:name) } + let(:state_machine) { AASM::StateMachine.new(:name).tap { |sm| AASM::Base.new(nil, :name, sm) } } it 'should return nil if the transitions are empty' do obj = double('object', :aasm => double('aasm', :current_state => 'open')) From d87e79562d493b96a02b4ca4a8d535afca7dc54b Mon Sep 17 00:00:00 2001 From: Marc Rohloff Date: Mon, 27 Feb 2023 13:04:33 -0700 Subject: [PATCH 3/5] Add specs for custom core classes --- spec/models/custom_aasm_base.rb | 14 ++++++ spec/models/custom_event.rb | 21 +++++++++ spec/models/custom_state.rb | 6 +++ spec/models/custom_transition.rb | 21 +++++++++ .../full_example_with_custom_aasm_base.rb | 18 ++++++++ ...eal_world_example_with_custom_aasm_base.rb | 26 +++++++++++ .../simple_example_with_custom_aasm_base.rb | 12 +++++ spec/unit/dsl_with_custom_aasm_base_spec.rb | 26 +++++++++++ spec/unit/event_subclasses_spec.rb | 44 +++++++++++++++++++ ...orld_example_with_custom_aasm_base_spec.rb | 14 ++++++ spec/unit/state_subclasses_spec.rb | 12 +++++ spec/unit/transition_subclass_spec.rb | 31 +++++++++++++ 12 files changed, 245 insertions(+) create mode 100644 spec/models/custom_aasm_base.rb create mode 100644 spec/models/custom_event.rb create mode 100644 spec/models/custom_state.rb create mode 100644 spec/models/custom_transition.rb create mode 100644 spec/models/full_example_with_custom_aasm_base.rb create mode 100644 spec/models/real_world_example_with_custom_aasm_base.rb create mode 100644 spec/models/simple_example_with_custom_aasm_base.rb create mode 100644 spec/unit/dsl_with_custom_aasm_base_spec.rb create mode 100644 spec/unit/event_subclasses_spec.rb create mode 100644 spec/unit/real_world_example_with_custom_aasm_base_spec.rb create mode 100644 spec/unit/state_subclasses_spec.rb create mode 100644 spec/unit/transition_subclass_spec.rb diff --git a/spec/models/custom_aasm_base.rb b/spec/models/custom_aasm_base.rb new file mode 100644 index 00000000..5161a4cf --- /dev/null +++ b/spec/models/custom_aasm_base.rb @@ -0,0 +1,14 @@ +class CustomAasmBase < AASM::Base + + def aasm_state_class + CustomState + end + + def aasm_event_class + CustomEvent + end + + def aasm_transition_class + CustomTransition + end +end diff --git a/spec/models/custom_event.rb b/spec/models/custom_event.rb new file mode 100644 index 00000000..006b11f5 --- /dev/null +++ b/spec/models/custom_event.rb @@ -0,0 +1,21 @@ +class CustomEvent < AASM::Core::Event + attr_reader :custom_method_args + + def custom_event_method!(value) + @custom_method_args = value + end + + def some_option + options[:some_option] + end + + def another_option + options[:another_option] + end + + private + + def dsl_option_keys + super + [:some_option, :another_option] + end +end diff --git a/spec/models/custom_state.rb b/spec/models/custom_state.rb new file mode 100644 index 00000000..f31a17e1 --- /dev/null +++ b/spec/models/custom_state.rb @@ -0,0 +1,6 @@ +class CustomState < AASM::Core::State + + def custom_state_method(value) + value * value + end +end diff --git a/spec/models/custom_transition.rb b/spec/models/custom_transition.rb new file mode 100644 index 00000000..8b249620 --- /dev/null +++ b/spec/models/custom_transition.rb @@ -0,0 +1,21 @@ +class CustomTransition < AASM::Core::Transition + attr_reader :custom_method_args + + def custom_transition_method!(value) + @custom_method_args = value + end + + def some_option + opts[:some_option] + end + + def another_option + options[:another_option] + end + + private + + def dsl_option_keys + super + [:some_option, :another_option] + end +end diff --git a/spec/models/full_example_with_custom_aasm_base.rb b/spec/models/full_example_with_custom_aasm_base.rb new file mode 100644 index 00000000..976faf39 --- /dev/null +++ b/spec/models/full_example_with_custom_aasm_base.rb @@ -0,0 +1,18 @@ +class FullExampleWithCustomAasmBase + include AASM + + aasm with_klass: CustomAasmBase do + state :initialised, :initial => true + state :filled_out + + event :fill_out, :some_option => '-- some event value --' do + another_option '-- another event value --' + custom_event_method!(41) + + transitions :from => :initialised, :to => :filled_out, :some_option => '-- some transition value --' do + another_option '-- another transition value --' + custom_transition_method! 42 + end + end + end +end diff --git a/spec/models/real_world_example_with_custom_aasm_base.rb b/spec/models/real_world_example_with_custom_aasm_base.rb new file mode 100644 index 00000000..3ed2b7d2 --- /dev/null +++ b/spec/models/real_world_example_with_custom_aasm_base.rb @@ -0,0 +1,26 @@ +class RealWorldExampleWithCustomAasmBase + include AASM + + class RequiredParamsEvent < AASM::Core::Event + def required_params!(*keys) + options[:before] ||= [] + options[:before] << ->(**args) do + missing = keys - args.keys + raise ArgumentError, "Missing required arguments #{missing.inspect}" unless missing == [] + end + end + end + class RequiredParams < AASM::Base + def aasm_event_class; RequiredParamsEvent; end + end + + aasm with_klass: RequiredParams do + state :initialised, :initial => true + state :filled_out + + event :fill_out do + required_params! :user, :quantity, :date + transitions :from => :initialised, :to => :filled_out + end + end +end diff --git a/spec/models/simple_example_with_custom_aasm_base.rb b/spec/models/simple_example_with_custom_aasm_base.rb new file mode 100644 index 00000000..8e679e6a --- /dev/null +++ b/spec/models/simple_example_with_custom_aasm_base.rb @@ -0,0 +1,12 @@ +class SimpleExampleWithCustomAasmBase + include AASM + + aasm with_klass: CustomAasmBase do + state :initialised, :initial => true + state :filled_out + + event :fill_out do + transitions :from => :initialised, :to => :filled_out + end + end +end diff --git a/spec/unit/dsl_with_custom_aasm_base_spec.rb b/spec/unit/dsl_with_custom_aasm_base_spec.rb new file mode 100644 index 00000000..e398aa02 --- /dev/null +++ b/spec/unit/dsl_with_custom_aasm_base_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe "dsl with custom ASM::Base and custom core classes" do + + let(:example) {FullExampleWithCustomAasmBase.new} + + it 'should create the expected state machine' do + aasm = example.aasm(:default) + + state = aasm.states.first + expect(state).to be_a(CustomState) + + event = aasm.events.first + expect(event).to be_a(CustomEvent) + expect(event.some_option).to eq('-- some event value --') + expect(event.another_option).to eq(['-- another event value --']) + expect(event.custom_method_args).to eq(41) + + transition = event.transitions.first + expect(transition).to be_a(CustomTransition) + expect(transition.some_option).to eq('-- some transition value --') + expect(transition.another_option).to eq(['-- another transition value --']) + expect(transition.custom_method_args).to eq(42) + end + +end diff --git a/spec/unit/event_subclasses_spec.rb b/spec/unit/event_subclasses_spec.rb new file mode 100644 index 00000000..9f00b412 --- /dev/null +++ b/spec/unit/event_subclasses_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'customized event classes' do + let(:state_machine) { AASM::StateMachine.new(:name).tap { |sm| CustomAasmBase.new(nil, :name, sm) } } + + it 'should allow custom transition options' do + opts = {:some_option => '-- some value --'} + event = CustomEvent.new(:event_name, state_machine, opts) + + expect(event.some_option).to eq('-- some value --') + end + + it 'should set custom transition options from the dsl' do + opts = { } + event = CustomEvent.new(:event_name, state_machine, opts) do + some_option '-- another value --' + end + + expect(event.some_option).to eq(['-- another value --']) + end + + it 'should allow custom transition methods' do + opts = { } + event = CustomEvent.new(:event_name, state_machine, opts) do + custom_event_method!(42) + end + + expect(event.custom_method_args).to eq(42) + end +end + +describe 'customized transition classes' do + let(:state_machine) { AASM::StateMachine.new(:name).tap { |sm| CustomAasmBase.new(nil, :name, sm) } } + let(:event) do + AASM::Core::Event.new(:event_name, state_machine) do + transitions :to => :closed, :from => [:open, :received], success: [:transition_success_callback] + end + end + + it 'should use a subclass of transition' do + transitions = event.transitions + expect(transitions.first).to be_a(CustomTransition) + end +end diff --git a/spec/unit/real_world_example_with_custom_aasm_base_spec.rb b/spec/unit/real_world_example_with_custom_aasm_base_spec.rb new file mode 100644 index 00000000..95d9f4e5 --- /dev/null +++ b/spec/unit/real_world_example_with_custom_aasm_base_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe "real world example suing ASM::Base and custom core classes" do + + let(:example) {RealWorldExampleWithCustomAasmBase.new} + + it 'should succeed with the correct parameters' do + expect { example.fill_out(:user => 1, :quantity => 3, :date => Date.today) }.not_to raise_exception + end + + it 'should raise an exception if the correct parameters are not given' do + expect { example.fill_out(:user => 1) }.to raise_exception(ArgumentError, 'Missing required arguments [:quantity, :date]') + end +end diff --git a/spec/unit/state_subclasses_spec.rb b/spec/unit/state_subclasses_spec.rb new file mode 100644 index 00000000..484664ea --- /dev/null +++ b/spec/unit/state_subclasses_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'customized event classes' do + let(:example) { SimpleExampleWithCustomAasmBase.new } + + it 'should create custom state classes' do + state = example.aasm(:default).states.first + + expect(state).to be_a(CustomState) + expect(state.custom_state_method(7)).to eq(49) + end +end diff --git a/spec/unit/transition_subclass_spec.rb b/spec/unit/transition_subclass_spec.rb new file mode 100644 index 00000000..4bd95e5f --- /dev/null +++ b/spec/unit/transition_subclass_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'custom transition sublasses do' do + let(:state_machine) { AASM::StateMachine.new(:name) } + let(:event) { AASM::Core::Event.new(:event, state_machine) } + + it 'should allow custom transition options' do + opts = {:from => 'foo', :to => 'bar', :some_option => '-- some value --'} + transition = CustomTransition.new(event, opts) + + expect(transition.some_option).to eq('-- some value --') + end + + it 'should set custom transition options from the dsl' do + opts = {:from => 'foo', :to => 'bar'} + transition = CustomTransition.new(event, opts) do + some_option '-- another value --' + end + + expect(transition.some_option).to eq(['-- another value --']) + end + + it 'should allow custom transition methods' do + opts = {:from => 'foo', :to => 'bar'} + transition = CustomTransition.new(event, opts) do + custom_transition_method!(42) + end + + expect(transition.custom_method_args).to eq(42) + end +end From c31949a2ff97919e9068c30e74821d8fed7b41f9 Mon Sep 17 00:00:00 2001 From: Marc Rohloff Date: Mon, 27 Feb 2023 13:21:57 -0700 Subject: [PATCH 4/5] Extract `build_transition` method to satisfy Code Climate complexity --- lib/aasm/core/event.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/aasm/core/event.rb b/lib/aasm/core/event.rb index e2cd7125..30c72347 100644 --- a/lib/aasm/core/event.rb +++ b/lib/aasm/core/event.rb @@ -85,16 +85,13 @@ def ==(event) ## DSL interface def transitions(definitions=nil, &block) if definitions # define new transitions - transition_class = state_machine.implementation.aasm_transition_class - raise ArgumentError, "The class #{transition_class} must inherit from AASM::Core::Transition!" unless transition_class.ancestors.include?(AASM::Core::Transition) - # Create a separate transition for each from-state to the given state Array(definitions[:from]).each do |s| - @transitions << transition_class.new(self, attach_event_guards(definitions.merge(:from => s.to_sym)), &block) + build_transition(attach_event_guards(definitions.merge(:from => s.to_sym)), &block) end # Create a transition if :to is specified without :from (transitions from ANY state) if !definitions[:from] && definitions[:to] - @transitions << transition_class.new(self, attach_event_guards(definitions), &block) + build_transition(attach_event_guards(definitions), &block) end end @transitions @@ -110,6 +107,13 @@ def to_s private + def build_transition(definitions=nil, &block) + transition_class = state_machine.implementation.aasm_transition_class + raise ArgumentError, "The class #{transition_class} must inherit from AASM::Core::Transition!" unless transition_class.ancestors.include?(AASM::Core::Transition) + + @transitions << transition_class.new(self, definitions, &block) + end + def attach_event_guards(definitions) unless @guards.empty? given_guards = Array(definitions.delete(:guard) || definitions.delete(:guards) || definitions.delete(:if)) From b08c94c2b085d7ff058ae13e4790a94d7ba7c93a Mon Sep 17 00:00:00 2001 From: Marc Rohloff Date: Thu, 16 Mar 2023 13:08:57 -0600 Subject: [PATCH 5/5] Extract transition building to a module to satisyf CodeClimate --- lib/aasm.rb | 1 + lib/aasm/core/event.rb | 26 ++++---------------------- lib/aasm/transition_builder.rb | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 lib/aasm/transition_builder.rb diff --git a/lib/aasm.rb b/lib/aasm.rb index abaa94c8..ab723cf1 100644 --- a/lib/aasm.rb +++ b/lib/aasm.rb @@ -3,6 +3,7 @@ require 'aasm/configuration' require 'aasm/base' require 'aasm/dsl_helper' +require 'aasm/transition_builder' require 'aasm/instance_base' require 'aasm/core/transition' require 'aasm/core/event' diff --git a/lib/aasm/core/event.rb b/lib/aasm/core/event.rb index 30c72347..86a4a408 100644 --- a/lib/aasm/core/event.rb +++ b/lib/aasm/core/event.rb @@ -3,6 +3,7 @@ module AASM::Core class Event include AASM::DslHelper + include AASM::TransitionBuilder attr_reader :name, :state_machine, :options, :default_display_name @@ -86,12 +87,12 @@ def ==(event) def transitions(definitions=nil, &block) if definitions # define new transitions # Create a separate transition for each from-state to the given state - Array(definitions[:from]).each do |s| - build_transition(attach_event_guards(definitions.merge(:from => s.to_sym)), &block) + Array(definitions[:from]).each do |from| + build_transition(definitions, from, &block) end # Create a transition if :to is specified without :from (transitions from ANY state) if !definitions[:from] && definitions[:to] - build_transition(attach_event_guards(definitions), &block) + build_transition(definitions, &block) end end @transitions @@ -107,25 +108,6 @@ def to_s private - def build_transition(definitions=nil, &block) - transition_class = state_machine.implementation.aasm_transition_class - raise ArgumentError, "The class #{transition_class} must inherit from AASM::Core::Transition!" unless transition_class.ancestors.include?(AASM::Core::Transition) - - @transitions << transition_class.new(self, definitions, &block) - end - - def attach_event_guards(definitions) - unless @guards.empty? - given_guards = Array(definitions.delete(:guard) || definitions.delete(:guards) || definitions.delete(:if)) - definitions[:guards] = @guards + given_guards # from aasm4 - end - unless @unless.empty? - given_unless = Array(definitions.delete(:unless)) - definitions[:unless] = given_unless + @unless - end - definitions - end - def _fire(obj, options={}, to_state=::AASM::NO_VALUE, *args) result = options[:test_only] ? false : nil clear_failed_callbacks diff --git a/lib/aasm/transition_builder.rb b/lib/aasm/transition_builder.rb new file mode 100644 index 00000000..9ec25779 --- /dev/null +++ b/lib/aasm/transition_builder.rb @@ -0,0 +1,28 @@ +module AASM + module TransitionBuilder + + private + + def build_transition(definitions, from = nil, &block) + transition_class = state_machine.implementation.aasm_transition_class + raise ArgumentError, "The class #{transition_class} must inherit from AASM::Core::Transition!" unless transition_class.ancestors.include?(AASM::Core::Transition) + + definitions = definitions.merge(:from => from.to_sym) if from + + @transitions << transition_class.new(self, attach_event_guards(definitions), &block) + end + + def attach_event_guards(definitions) + unless @guards.empty? + given_guards = Array(definitions.delete(:guard) || definitions.delete(:guards) || definitions.delete(:if)) + definitions[:guards] = @guards + given_guards # from aasm4 + end + unless @unless.empty? + given_unless = Array(definitions.delete(:unless)) + definitions[:unless] = given_unless + @unless + end + definitions + end + + end +end