1
0
Fork 0
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:
Thorsten Böttger 2016-02-09 23:01:57 +13:00
commit d9a1c141e9
6 changed files with 187 additions and 17 deletions

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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

View 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

View 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