mirror of
https://github.com/aasm/aasm
synced 2023-03-27 23:22:41 -04:00
Merge pull request #296 from HoyaBoya/feature/dry_across_models
Make It Easier To Extend AASM With Custom Logic Across Models
This commit is contained in:
commit
d9a1c141e9
6 changed files with 187 additions and 17 deletions
65
README.md
65
README.md
|
@ -417,6 +417,71 @@ SimpleMultipleExample.aasm(:work).states
|
|||
*Final note*: Support for multiple state machines per class is a pretty new feature
|
||||
(since version `4.3`), so please bear with us in case it doesn't work as expected.
|
||||
|
||||
### Extending AASM
|
||||
|
||||
AASM allows you to easily extend `AASM::Base` for your own application purposes.
|
||||
|
||||
Let's suppose we have common logic across many AASM models. We can embody this logic in a sub-class of `AASM::Base`.
|
||||
|
||||
```
|
||||
class CustomAASMBase < AASM::Base
|
||||
# A custom transiton that we want available across many AASM models.
|
||||
def count_transitions!
|
||||
klass.class_eval do
|
||||
aasm :with_klass => CustomAASMBase do
|
||||
after_all_transitions :increment_transition_count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A custom annotation that we want available across many AASM models.
|
||||
def requires_guards!
|
||||
klass.class_eval do
|
||||
attr_reader :authorizable_called,
|
||||
:transition_count,
|
||||
:fillable_called
|
||||
|
||||
def authorizable?
|
||||
@authorizable_called = true
|
||||
end
|
||||
|
||||
def fillable?
|
||||
@fillable_called = true
|
||||
end
|
||||
|
||||
def increment_transition_count
|
||||
@transition_count ||= 0
|
||||
@transition_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
When we declare our model that has an AASM state machine, we simply declare the AASM block with a `:with` key to our own class.
|
||||
|
||||
```
|
||||
class SimpleCustomExample
|
||||
include AASM
|
||||
|
||||
# Let's build an AASM state machine with our custom class.
|
||||
aasm :with_klass => CustomAASMBase do
|
||||
requires_guards!
|
||||
count_transitions!
|
||||
|
||||
state :initialised, :initial => true
|
||||
state :filled_out
|
||||
state :authorised
|
||||
|
||||
event :fill_out do
|
||||
transitions :from => :initialised, :to => :filled_out, :guard => :fillable?
|
||||
end
|
||||
event :authorise do
|
||||
transitions :from => :filled_out, :to => :authorised, :guard => :authorizable?
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
### ActiveRecord
|
||||
|
|
|
@ -38,6 +38,12 @@ module AASM
|
|||
|
||||
AASM::StateMachine[self][state_machine_name] ||= AASM::StateMachine.new(state_machine_name)
|
||||
|
||||
# use a default despite the DSL configuration default.
|
||||
# this is because configuration hasn't been setup for the AASM class but we are accessing a DSL option already for the class.
|
||||
aasm_klass = options[:with_klass] || AASM::Base
|
||||
|
||||
raise ArgumentError, "The class #{aasm_klass} must inherit from AASM::Base!" unless aasm_klass.ancestors.include?(AASM::Base)
|
||||
|
||||
@aasm ||= {}
|
||||
if @aasm[state_machine_name]
|
||||
# make sure to use provided options
|
||||
|
@ -46,7 +52,7 @@ module AASM
|
|||
end
|
||||
else
|
||||
# create a new base
|
||||
@aasm[state_machine_name] = AASM::Base.new(
|
||||
@aasm[state_machine_name] = aasm_klass.new(
|
||||
self,
|
||||
state_machine_name,
|
||||
AASM::StateMachine[self][state_machine_name],
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
module AASM
|
||||
class Base
|
||||
|
||||
attr_reader :state_machine
|
||||
attr_reader :klass,
|
||||
:state_machine
|
||||
|
||||
def initialize(klass, name, state_machine, options={}, &block)
|
||||
@klass = klass
|
||||
@name = name
|
||||
# @state_machine = @klass.aasm(@name).state_machine
|
||||
# @state_machine = klass.aasm(@name).state_machine
|
||||
@state_machine = state_machine
|
||||
@state_machine.config.column ||= (options[:column] || default_column).to_sym
|
||||
# @state_machine.config.column = options[:column].to_sym if options[:column] # master
|
||||
|
@ -27,12 +28,15 @@ module AASM
|
|||
# set to true to forbid direct assignment of aasm_state column (in ActiveRecord)
|
||||
configure :no_direct_assignment, false
|
||||
|
||||
# allow a AASM::Base sub-class to be used for state machine
|
||||
configure :with_klass, AASM::Base
|
||||
|
||||
configure :enum, nil
|
||||
|
||||
# make sure to raise an error if no_direct_assignment is enabled
|
||||
# and attribute is directly assigned though
|
||||
aasm_name = @name
|
||||
@klass.send :define_method, "#{@state_machine.config.column}=", ->(state_name) do
|
||||
klass.send :define_method, "#{@state_machine.config.column}=", ->(state_name) do
|
||||
if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
|
||||
raise AASM::NoDirectAssignmentError.new(
|
||||
'direct assignment of AASM column has been disabled (see AASM configuration for this class)'
|
||||
|
@ -63,19 +67,19 @@ module AASM
|
|||
|
||||
# define a state
|
||||
def state(name, options={})
|
||||
@state_machine.add_state(name, @klass, options)
|
||||
@state_machine.add_state(name, klass, options)
|
||||
|
||||
if @klass.instance_methods.include?("#{name}?")
|
||||
warn "#{@klass.name}: The aasm state name #{name} is already used!"
|
||||
if klass.instance_methods.include?("#{name}?")
|
||||
warn "#{klass.name}: The aasm state name #{name} is already used!"
|
||||
end
|
||||
|
||||
aasm_name = @name
|
||||
@klass.send :define_method, "#{name}?", ->() do
|
||||
klass.send :define_method, "#{name}?", ->() do
|
||||
aasm(:"#{aasm_name}").current_state == :"#{name}"
|
||||
end
|
||||
|
||||
unless @klass.const_defined?("STATE_#{name.upcase}")
|
||||
@klass.const_set("STATE_#{name.upcase}", name)
|
||||
unless klass.const_defined?("STATE_#{name.upcase}")
|
||||
klass.const_set("STATE_#{name.upcase}", name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -83,8 +87,8 @@ module AASM
|
|||
def event(name, options={}, &block)
|
||||
@state_machine.add_event(name, options, &block)
|
||||
|
||||
if @klass.instance_methods.include?("may_#{name}?".to_sym)
|
||||
warn "#{@klass.name}: The aasm event name #{name} is already used!"
|
||||
if klass.instance_methods.include?("may_#{name}?".to_sym)
|
||||
warn "#{klass.name}: The aasm event name #{name} is already used!"
|
||||
end
|
||||
|
||||
# an addition over standard aasm so that, before firing an event, you can ask
|
||||
|
@ -92,16 +96,16 @@ module AASM
|
|||
# on the transition will let this happen.
|
||||
aasm_name = @name
|
||||
|
||||
@klass.send :define_method, "may_#{name}?", ->(*args) do
|
||||
klass.send :define_method, "may_#{name}?", ->(*args) do
|
||||
aasm(:"#{aasm_name}").may_fire_event?(:"#{name}", *args)
|
||||
end
|
||||
|
||||
@klass.send :define_method, "#{name}!", ->(*args, &block) do
|
||||
klass.send :define_method, "#{name}!", ->(*args, &block) do
|
||||
aasm(:"#{aasm_name}").current_event = :"#{name}!"
|
||||
aasm_fire_event(:"#{aasm_name}", :"#{name}", {:persist => true}, *args, &block)
|
||||
end
|
||||
|
||||
@klass.send :define_method, "#{name}", ->(*args, &block) do
|
||||
klass.send :define_method, "#{name}", ->(*args, &block) do
|
||||
aasm(:"#{aasm_name}").current_event = :"#{name}"
|
||||
aasm_fire_event(:"#{aasm_name}", :"#{name}", {:persist => false}, *args, &block)
|
||||
end
|
||||
|
@ -145,7 +149,7 @@ module AASM
|
|||
|
||||
# aasm.event(:event_name).human?
|
||||
def human_event_name(event) # event_name?
|
||||
AASM::Localizer.new.human_event_name(@klass, event)
|
||||
AASM::Localizer.new.human_event_name(klass, event)
|
||||
end
|
||||
|
||||
def states_for_select
|
||||
|
|
|
@ -18,6 +18,9 @@ module AASM
|
|||
# forbid direct assignment in aasm_state column (in ActiveRecord)
|
||||
attr_accessor :no_direct_assignment
|
||||
|
||||
# allow a AASM::Base sub-class to be used for state machine
|
||||
attr_accessor :with_klass
|
||||
|
||||
attr_accessor :enum
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
53
spec/models/simple_custom_example.rb
Normal file
53
spec/models/simple_custom_example.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
class CustomAASMBase < AASM::Base
|
||||
# A custom transiton that we want available across many AASM models.
|
||||
def count_transitions!
|
||||
klass.class_eval do
|
||||
aasm :with_klass => CustomAASMBase do
|
||||
after_all_transitions :increment_transition_count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A custom annotation that we want available across many AASM models.
|
||||
def requires_guards!
|
||||
klass.class_eval do
|
||||
attr_reader :authorizable_called,
|
||||
:transition_count,
|
||||
:fillable_called
|
||||
|
||||
def authorizable?
|
||||
@authorizable_called = true
|
||||
end
|
||||
|
||||
def fillable?
|
||||
@fillable_called = true
|
||||
end
|
||||
|
||||
def increment_transition_count
|
||||
@transition_count ||= 0
|
||||
@transition_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SimpleCustomExample
|
||||
include AASM
|
||||
|
||||
# Let's build an AASM state machine with our custom class.
|
||||
aasm :with_klass => CustomAASMBase do
|
||||
requires_guards!
|
||||
count_transitions!
|
||||
|
||||
state :initialised, :initial => true
|
||||
state :filled_out
|
||||
state :authorised
|
||||
|
||||
event :fill_out do
|
||||
transitions :from => :initialised, :to => :filled_out, :guard => :fillable?
|
||||
end
|
||||
event :authorise do
|
||||
transitions :from => :filled_out, :to => :authorised, :guard => :authorizable?
|
||||
end
|
||||
end
|
||||
end
|
39
spec/unit/simple_custom_example_spec.rb
Normal file
39
spec/unit/simple_custom_example_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Custom AASM::Base' do
|
||||
context 'when aasm_with invoked with SimpleCustomExample' do
|
||||
let(:simple_custom) { SimpleCustomExample.new }
|
||||
|
||||
subject do
|
||||
simple_custom.fill_out!
|
||||
simple_custom.authorise
|
||||
end
|
||||
|
||||
it 'has invoked authorizable?' do
|
||||
expect { subject }.to change { simple_custom.authorizable_called }.from(nil).to(true)
|
||||
end
|
||||
|
||||
it 'has invoked fillable?' do
|
||||
expect { subject }.to change { simple_custom.fillable_called }.from(nil).to(true)
|
||||
end
|
||||
|
||||
it 'has two transition counts' do
|
||||
expect { subject }.to change { simple_custom.transition_count }.from(nil).to(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when aasm_with invoked with non AASM::Base' do
|
||||
subject do
|
||||
Class.new do
|
||||
include AASM
|
||||
|
||||
aasm :with_klass => String do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'should raise an ArgumentError' do
|
||||
expect { subject }.to raise_error(ArgumentError, 'The class String must inherit from AASM::Base!')
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue