state-fu 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +40 -0
- data/README.textile +293 -0
- data/Rakefile +114 -0
- data/lib/binding.rb +292 -0
- data/lib/event.rb +192 -0
- data/lib/executioner.rb +120 -0
- data/lib/hooks.rb +39 -0
- data/lib/interface.rb +132 -0
- data/lib/lathe.rb +538 -0
- data/lib/machine.rb +184 -0
- data/lib/method_factory.rb +243 -0
- data/lib/persistence.rb +116 -0
- data/lib/persistence/active_record.rb +34 -0
- data/lib/persistence/attribute.rb +47 -0
- data/lib/persistence/base.rb +100 -0
- data/lib/persistence/relaxdb.rb +23 -0
- data/lib/persistence/session.rb +7 -0
- data/lib/sprocket.rb +58 -0
- data/lib/state-fu.rb +56 -0
- data/lib/state.rb +48 -0
- data/lib/support/active_support_lite/array.rb +9 -0
- data/lib/support/active_support_lite/array/access.rb +60 -0
- data/lib/support/active_support_lite/array/conversions.rb +202 -0
- data/lib/support/active_support_lite/array/extract_options.rb +21 -0
- data/lib/support/active_support_lite/array/grouping.rb +109 -0
- data/lib/support/active_support_lite/array/random_access.rb +13 -0
- data/lib/support/active_support_lite/array/wrapper.rb +25 -0
- data/lib/support/active_support_lite/blank.rb +67 -0
- data/lib/support/active_support_lite/cattr_reader.rb +57 -0
- data/lib/support/active_support_lite/keys.rb +57 -0
- data/lib/support/active_support_lite/misc.rb +59 -0
- data/lib/support/active_support_lite/module.rb +1 -0
- data/lib/support/active_support_lite/module/delegation.rb +130 -0
- data/lib/support/active_support_lite/object.rb +9 -0
- data/lib/support/active_support_lite/string.rb +38 -0
- data/lib/support/active_support_lite/symbol.rb +16 -0
- data/lib/support/applicable.rb +41 -0
- data/lib/support/arrays.rb +197 -0
- data/lib/support/core_ext.rb +90 -0
- data/lib/support/exceptions.rb +106 -0
- data/lib/support/has_options.rb +16 -0
- data/lib/support/logger.rb +165 -0
- data/lib/support/methodical.rb +17 -0
- data/lib/support/no_stdout.rb +55 -0
- data/lib/support/plotter.rb +62 -0
- data/lib/support/vizier.rb +300 -0
- data/lib/tasks/spec_last.rake +55 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/transition.rb +338 -0
- data/lib/transition_query.rb +224 -0
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +118 -0
- data/spec/features/plotter_spec.rb +97 -0
- data/spec/features/shared_log_spec.rb +7 -0
- data/spec/features/singleton_machine_spec.rb +39 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
- data/spec/features/transition_boolean_comparison_spec.rb +101 -0
- data/spec/helper.rb +13 -0
- data/spec/integration/active_record_persistence_spec.rb +202 -0
- data/spec/integration/binding_extension_spec.rb +41 -0
- data/spec/integration/class_accessor_spec.rb +117 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/example_01_document_spec.rb +133 -0
- data/spec/integration/example_02_string_spec.rb +88 -0
- data/spec/integration/instance_accessor_spec.rb +97 -0
- data/spec/integration/lathe_extension_spec.rb +67 -0
- data/spec/integration/machine_duplication_spec.rb +101 -0
- data/spec/integration/relaxdb_persistence_spec.rb +97 -0
- data/spec/integration/requirement_reflection_spec.rb +270 -0
- data/spec/integration/state_definition_spec.rb +163 -0
- data/spec/integration/transition_spec.rb +1033 -0
- data/spec/spec.opts +9 -0
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +948 -0
- data/spec/units/binding_spec.rb +192 -0
- data/spec/units/event_spec.rb +214 -0
- data/spec/units/exceptions_spec.rb +82 -0
- data/spec/units/lathe_spec.rb +570 -0
- data/spec/units/machine_spec.rb +229 -0
- data/spec/units/method_factory_spec.rb +366 -0
- data/spec/units/sprocket_spec.rb +69 -0
- data/spec/units/state_spec.rb +59 -0
- metadata +171 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module StateFu
|
2
|
+
module Persistence
|
3
|
+
class ActiveRecord < StateFu::Persistence::Base
|
4
|
+
|
5
|
+
def self.prepare_field( klass, field_name )
|
6
|
+
_field_name = field_name
|
7
|
+
Logger.debug("Preparing ActiveRecord field #{klass}.#{field_name}")
|
8
|
+
|
9
|
+
# this adds a before_save hook to ensure that the field is initialized
|
10
|
+
# (and the initial state set) before create.
|
11
|
+
klass.send :before_create, :state_fu!
|
12
|
+
|
13
|
+
# it's usually a good idea to do this:
|
14
|
+
# validates_presence_of _field_name
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# We already checked that they exist, or we'd be using the
|
20
|
+
# Attribute version, so just do the simplest thing we can.
|
21
|
+
|
22
|
+
def read_attribute
|
23
|
+
Logger.debug "Read attribute #{field_name}, got #{object.send(:read_attribute,field_name)} for #{object.inspect}"
|
24
|
+
object.send( :read_attribute, field_name )
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_attribute( string_value )
|
28
|
+
Logger.debug "Write attribute #{field_name} to #{string_value} for #{object.inspect}"
|
29
|
+
object.send( :write_attribute, field_name, string_value )
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module StateFu
|
2
|
+
module Persistence
|
3
|
+
class Attribute < StateFu::Persistence::Base
|
4
|
+
|
5
|
+
def self.prepare_field( klass, field_name )
|
6
|
+
# ensure getter exists
|
7
|
+
unless klass.instance_methods.map(&:to_sym).include?( field_name.to_sym )
|
8
|
+
Logger.debug "Adding attr_reader :#{field_name} for #{klass}"
|
9
|
+
_field_name = field_name
|
10
|
+
klass.class_eval do
|
11
|
+
private
|
12
|
+
attr_reader _field_name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# ensure setter exists
|
17
|
+
unless klass.instance_methods.map(&:to_sym).include?( :"#{field_name}=" )
|
18
|
+
Logger.debug "Adding attr_writer :#{field_name}= for #{klass}"
|
19
|
+
_field_name = field_name
|
20
|
+
klass.class_eval do
|
21
|
+
private
|
22
|
+
attr_writer _field_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def b; binding; end
|
28
|
+
private
|
29
|
+
|
30
|
+
# Read / write our strings to a plain old instance variable
|
31
|
+
# Define it if it doesn't exist the first time we go to read it
|
32
|
+
|
33
|
+
def read_attribute
|
34
|
+
string = object.send( field_name )
|
35
|
+
Logger.debug "Read attribute #{field_name}, got #{string.inspect} for #{object.inspect}"
|
36
|
+
string
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_attribute( string_value )
|
40
|
+
writer_method = "#{field_name}="
|
41
|
+
Logger.debug "Writing attribute #{field_name} -> #{string_value.inspect} for #{object.inspect}"
|
42
|
+
object.send( writer_method, string_value )
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module StateFu
|
2
|
+
|
3
|
+
class InvalidStateName < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
module Persistence
|
7
|
+
class Base
|
8
|
+
|
9
|
+
attr_reader :binding, :field_name, :current_state
|
10
|
+
|
11
|
+
def self.prepare_class( klass )
|
12
|
+
unless klass.instance_methods.include?( :method_missing_before_state_fu )
|
13
|
+
alias_method :method_missing_before_state_fu, :method_missing
|
14
|
+
klass.class_eval do
|
15
|
+
def method_missing( method_name, *args, &block )
|
16
|
+
state_fu!
|
17
|
+
begin
|
18
|
+
send( method_name, *args, &block )
|
19
|
+
rescue NoMethodError => e
|
20
|
+
method_missing_before_state_fu( method_name, *args, &block )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# define this method in subclasses to do any preparation
|
28
|
+
def self.prepare_field( klass, field_name )
|
29
|
+
Logger.warn("Abstract method in #{self}.prepare_field called. Override me!")
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize( binding, field_name )
|
33
|
+
|
34
|
+
@binding = binding
|
35
|
+
@field_name = field_name
|
36
|
+
@current_state = find_current_state()
|
37
|
+
|
38
|
+
if current_state.nil?
|
39
|
+
Logger.warn("undefined state for binding #{binding} on #{object} with field_name #{field_name.inspect}")
|
40
|
+
Logger.warn("Machine for #{object} has no states: #{machine}") if machine.states.empty?
|
41
|
+
else
|
42
|
+
persist!
|
43
|
+
Logger.debug("#{object} resumes #{binding.method_name} at #{current_state.name}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_current_state
|
48
|
+
string = read_attribute()
|
49
|
+
if string.blank?
|
50
|
+
machine.initial_state
|
51
|
+
else
|
52
|
+
state_name = string.to_sym
|
53
|
+
state = machine.states[ state_name ] || raise( StateFu::InvalidStateName, string )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def reload
|
58
|
+
@current_state = find_current_state()
|
59
|
+
end
|
60
|
+
|
61
|
+
def machine
|
62
|
+
binding.machine
|
63
|
+
end
|
64
|
+
|
65
|
+
def object
|
66
|
+
binding.object
|
67
|
+
end
|
68
|
+
|
69
|
+
def klass
|
70
|
+
binding.target
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_state=( state )
|
74
|
+
raise(ArgumentError, state.inspect) unless state.is_a?(StateFu::State)
|
75
|
+
@current_state = state
|
76
|
+
persist!
|
77
|
+
end
|
78
|
+
|
79
|
+
def value()
|
80
|
+
@current_state && @current_state.name.to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def persist!
|
84
|
+
write_attribute( value() )
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def read_attribute
|
90
|
+
raise "Abstract method! override me"
|
91
|
+
end
|
92
|
+
|
93
|
+
def write_attribute( string_value )
|
94
|
+
raise "Abstract method! override me"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module StateFu
|
2
|
+
module Persistence
|
3
|
+
class RelaxDB < StateFu::Persistence::Base
|
4
|
+
|
5
|
+
def self.prepare_field( klass, field_name )
|
6
|
+
_field_name = field_name
|
7
|
+
#puts "relaxdb.before_save?"
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def read_attribute
|
13
|
+
object.send( field_name )
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_attribute( string_value )
|
17
|
+
object.send( "#{field_name}=", string_value )
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
data/lib/sprocket.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module StateFu
|
2
|
+
# the abstract superclass of State & Event
|
3
|
+
# defines behaviours shared by both classes
|
4
|
+
class Sprocket
|
5
|
+
include Applicable # define apply!
|
6
|
+
include HasOptions
|
7
|
+
|
8
|
+
attr_reader :machine, :name, :hooks
|
9
|
+
|
10
|
+
def initialize(machine, name, options={})
|
11
|
+
@machine = machine
|
12
|
+
@name = name.to_sym
|
13
|
+
@options = options.symbolize_keys!
|
14
|
+
@hooks = StateFu::Hooks.for( self )
|
15
|
+
end
|
16
|
+
|
17
|
+
# sneaky way to make some comparisons / duck typing a bit cleaner
|
18
|
+
alias_method :to_sym, :name
|
19
|
+
|
20
|
+
def add_hook slot, name, value
|
21
|
+
@hooks[slot.to_sym] << [name.to_sym, value]
|
22
|
+
end
|
23
|
+
|
24
|
+
# yields a lathe for self; useful for updating machine definitions on the fly
|
25
|
+
def lathe(options={}, &block)
|
26
|
+
StateFu::Lathe.new( machine, self, options, &block )
|
27
|
+
end
|
28
|
+
|
29
|
+
def deep_copy
|
30
|
+
raise NotImeplementedError # abstract
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"#<#{self.class}::#{self.object_id} @name=#{name.inspect}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
# allows state == <name> || event == <name> to return true
|
38
|
+
def == other
|
39
|
+
if other.is_a?(Symbol)
|
40
|
+
self.name == other
|
41
|
+
else
|
42
|
+
super other
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# allows case equality tests against the state/event's name
|
47
|
+
# eg
|
48
|
+
# case state
|
49
|
+
# when :new
|
50
|
+
# ...
|
51
|
+
# end
|
52
|
+
def === other
|
53
|
+
self.to_sym === other.to_sym || super(other)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
data/lib/state-fu.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# State-Fu
|
4
|
+
#
|
5
|
+
# State-Fu is a framework for state-oriented programming in ruby.
|
6
|
+
#
|
7
|
+
# You can use it to define state machines, workflows, rules engines,
|
8
|
+
# and the behaviours which relate to states and transitions between
|
9
|
+
# them.
|
10
|
+
#
|
11
|
+
# It is powerful and flexible enough to drive entire applications, or
|
12
|
+
# substantial parts of them. It is designed as a library for authors,
|
13
|
+
# as well as users, of libraries: State-Fu goes to great lengths to
|
14
|
+
# impose very few limits on your ability to introspect, manipulate and
|
15
|
+
# extend the core features.
|
16
|
+
#
|
17
|
+
# It is also delightfully elegant and easy to use for simple things.
|
18
|
+
|
19
|
+
[ 'support/core_ext',
|
20
|
+
'support/logger',
|
21
|
+
'support/applicable',
|
22
|
+
'support/arrays',
|
23
|
+
'support/methodical',
|
24
|
+
'support/has_options',
|
25
|
+
'support/vizier',
|
26
|
+
'support/plotter',
|
27
|
+
'support/exceptions',
|
28
|
+
'executioner',
|
29
|
+
'machine',
|
30
|
+
'lathe',
|
31
|
+
'method_factory',
|
32
|
+
'binding',
|
33
|
+
'persistence',
|
34
|
+
'persistence/base',
|
35
|
+
'persistence/active_record',
|
36
|
+
'persistence/attribute',
|
37
|
+
'persistence/relaxdb',
|
38
|
+
'sprocket',
|
39
|
+
'state',
|
40
|
+
'event',
|
41
|
+
'hooks',
|
42
|
+
'interface',
|
43
|
+
'transition',
|
44
|
+
'transition_query' ].each { |lib| require File.expand_path(File.join(File.dirname(__FILE__),lib))}
|
45
|
+
|
46
|
+
module StateFu
|
47
|
+
DEFAULT = :default
|
48
|
+
DEFAULT_FIELD = :state_fu_field
|
49
|
+
|
50
|
+
def self.included( klass )
|
51
|
+
klass.extend( Interface::ClassMethods )
|
52
|
+
klass.send( :include, Interface::InstanceMethods )
|
53
|
+
klass.extend( Interface::Aliases )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/lib/state.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module StateFu
|
2
|
+
class State < StateFu::Sprocket
|
3
|
+
|
4
|
+
attr_reader :entry_requirements, :exit_requirements, :own_events
|
5
|
+
alias_method :requirements, :entry_requirements
|
6
|
+
|
7
|
+
def initialize(machine, name, options={})
|
8
|
+
@entry_requirements = [].extend ArrayWithSymbolAccessor
|
9
|
+
@exit_requirements = [].extend ArrayWithSymbolAccessor
|
10
|
+
@own_events = [].extend EventArray
|
11
|
+
super( machine, name, options )
|
12
|
+
end
|
13
|
+
|
14
|
+
def events
|
15
|
+
machine.events.from(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def before?(other)
|
19
|
+
machine.states.index(self) < machine.states.index(machine.states[other])
|
20
|
+
end
|
21
|
+
|
22
|
+
def after?(other)
|
23
|
+
machine.states.index(self) > machine.states.index(machine.states[other])
|
24
|
+
end
|
25
|
+
|
26
|
+
# display nice and short
|
27
|
+
def inspect
|
28
|
+
s = self.to_s
|
29
|
+
s = s[0,s.length-1]
|
30
|
+
display_hooks = hooks.dup
|
31
|
+
display_hooks.each do |k,v|
|
32
|
+
display_hooks.delete(k) if v.empty?
|
33
|
+
end
|
34
|
+
unless display_hooks.empty?
|
35
|
+
s << " hooks=#{display_hooks.inspect}"
|
36
|
+
end
|
37
|
+
unless entry_requirements.empty?
|
38
|
+
s << " entry_requirements=#{entry_requirements.inspect}"
|
39
|
+
end
|
40
|
+
unless exit_requirements.empty?
|
41
|
+
s << " exit_requirements=#{exit_requirements.inspect}"
|
42
|
+
end
|
43
|
+
s << ">"
|
44
|
+
s
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.join( File.dirname( __FILE__),'array/extract_options')
|
2
|
+
require File.join( File.dirname( __FILE__),'array/random_access')
|
3
|
+
require File.join( File.dirname( __FILE__),'array/grouping')
|
4
|
+
|
5
|
+
class Array #:nodoc:all:
|
6
|
+
include ActiveSupport::CoreExtensions::Array::ExtractOptions
|
7
|
+
include ActiveSupport::CoreExtensions::Array::RandomAccess
|
8
|
+
include ActiveSupport::CoreExtensions::Array::Grouping
|
9
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActiveSupport #:nodoc:all
|
2
|
+
module CoreExtensions #:nodoc
|
3
|
+
module Array #:nodoc
|
4
|
+
# Makes it easier to access parts of an array.
|
5
|
+
module Access #:nodoc:all
|
6
|
+
# Returns the tail of the array from +position+.
|
7
|
+
#
|
8
|
+
# %w( a b c d ).from(0) # => %w( a b c d )
|
9
|
+
# %w( a b c d ).from(2) # => %w( c d )
|
10
|
+
# %w( a b c d ).from(10) # => nil
|
11
|
+
# %w().from(0) # => nil
|
12
|
+
#:nodoc
|
13
|
+
def from(position)
|
14
|
+
self[position..-1]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the beginning of the array up to +position+.
|
18
|
+
#
|
19
|
+
# %w( a b c d ).to(0) # => %w( a )
|
20
|
+
# %w( a b c d ).to(2) # => %w( a b c )
|
21
|
+
# %w( a b c d ).to(10) # => %w( a b c d )
|
22
|
+
# %w().to(0) # => %w()
|
23
|
+
#:nodoc
|
24
|
+
def to(position)
|
25
|
+
self[0..position]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Equal to <tt>self[1]</tt>.
|
29
|
+
#:nodoc
|
30
|
+
def second
|
31
|
+
self[1]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Equal to <tt>self[2]</tt>.
|
35
|
+
#:nodoc
|
36
|
+
def third
|
37
|
+
self[2]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Equal to <tt>self[3]</tt>.
|
41
|
+
#:nodoc
|
42
|
+
def fourth
|
43
|
+
self[3]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Equal to <tt>self[4]</tt>.
|
47
|
+
#:nodoc
|
48
|
+
def fifth
|
49
|
+
self[4]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
53
|
+
#:nodoc
|
54
|
+
def forty_two
|
55
|
+
self[41]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|