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.
Files changed (85) hide show
  1. data/LICENSE +40 -0
  2. data/README.textile +293 -0
  3. data/Rakefile +114 -0
  4. data/lib/binding.rb +292 -0
  5. data/lib/event.rb +192 -0
  6. data/lib/executioner.rb +120 -0
  7. data/lib/hooks.rb +39 -0
  8. data/lib/interface.rb +132 -0
  9. data/lib/lathe.rb +538 -0
  10. data/lib/machine.rb +184 -0
  11. data/lib/method_factory.rb +243 -0
  12. data/lib/persistence.rb +116 -0
  13. data/lib/persistence/active_record.rb +34 -0
  14. data/lib/persistence/attribute.rb +47 -0
  15. data/lib/persistence/base.rb +100 -0
  16. data/lib/persistence/relaxdb.rb +23 -0
  17. data/lib/persistence/session.rb +7 -0
  18. data/lib/sprocket.rb +58 -0
  19. data/lib/state-fu.rb +56 -0
  20. data/lib/state.rb +48 -0
  21. data/lib/support/active_support_lite/array.rb +9 -0
  22. data/lib/support/active_support_lite/array/access.rb +60 -0
  23. data/lib/support/active_support_lite/array/conversions.rb +202 -0
  24. data/lib/support/active_support_lite/array/extract_options.rb +21 -0
  25. data/lib/support/active_support_lite/array/grouping.rb +109 -0
  26. data/lib/support/active_support_lite/array/random_access.rb +13 -0
  27. data/lib/support/active_support_lite/array/wrapper.rb +25 -0
  28. data/lib/support/active_support_lite/blank.rb +67 -0
  29. data/lib/support/active_support_lite/cattr_reader.rb +57 -0
  30. data/lib/support/active_support_lite/keys.rb +57 -0
  31. data/lib/support/active_support_lite/misc.rb +59 -0
  32. data/lib/support/active_support_lite/module.rb +1 -0
  33. data/lib/support/active_support_lite/module/delegation.rb +130 -0
  34. data/lib/support/active_support_lite/object.rb +9 -0
  35. data/lib/support/active_support_lite/string.rb +38 -0
  36. data/lib/support/active_support_lite/symbol.rb +16 -0
  37. data/lib/support/applicable.rb +41 -0
  38. data/lib/support/arrays.rb +197 -0
  39. data/lib/support/core_ext.rb +90 -0
  40. data/lib/support/exceptions.rb +106 -0
  41. data/lib/support/has_options.rb +16 -0
  42. data/lib/support/logger.rb +165 -0
  43. data/lib/support/methodical.rb +17 -0
  44. data/lib/support/no_stdout.rb +55 -0
  45. data/lib/support/plotter.rb +62 -0
  46. data/lib/support/vizier.rb +300 -0
  47. data/lib/tasks/spec_last.rake +55 -0
  48. data/lib/tasks/state_fu.rake +57 -0
  49. data/lib/transition.rb +338 -0
  50. data/lib/transition_query.rb +224 -0
  51. data/spec/custom_formatter.rb +49 -0
  52. data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
  53. data/spec/features/method_missing_only_once_spec.rb +28 -0
  54. data/spec/features/not_requirements_spec.rb +118 -0
  55. data/spec/features/plotter_spec.rb +97 -0
  56. data/spec/features/shared_log_spec.rb +7 -0
  57. data/spec/features/singleton_machine_spec.rb +39 -0
  58. data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
  59. data/spec/features/transition_boolean_comparison_spec.rb +101 -0
  60. data/spec/helper.rb +13 -0
  61. data/spec/integration/active_record_persistence_spec.rb +202 -0
  62. data/spec/integration/binding_extension_spec.rb +41 -0
  63. data/spec/integration/class_accessor_spec.rb +117 -0
  64. data/spec/integration/event_definition_spec.rb +74 -0
  65. data/spec/integration/example_01_document_spec.rb +133 -0
  66. data/spec/integration/example_02_string_spec.rb +88 -0
  67. data/spec/integration/instance_accessor_spec.rb +97 -0
  68. data/spec/integration/lathe_extension_spec.rb +67 -0
  69. data/spec/integration/machine_duplication_spec.rb +101 -0
  70. data/spec/integration/relaxdb_persistence_spec.rb +97 -0
  71. data/spec/integration/requirement_reflection_spec.rb +270 -0
  72. data/spec/integration/state_definition_spec.rb +163 -0
  73. data/spec/integration/transition_spec.rb +1033 -0
  74. data/spec/spec.opts +9 -0
  75. data/spec/spec_helper.rb +132 -0
  76. data/spec/state_fu_spec.rb +948 -0
  77. data/spec/units/binding_spec.rb +192 -0
  78. data/spec/units/event_spec.rb +214 -0
  79. data/spec/units/exceptions_spec.rb +82 -0
  80. data/spec/units/lathe_spec.rb +570 -0
  81. data/spec/units/machine_spec.rb +229 -0
  82. data/spec/units/method_factory_spec.rb +366 -0
  83. data/spec/units/sprocket_spec.rb +69 -0
  84. data/spec/units/state_spec.rb +59 -0
  85. 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
+
@@ -0,0 +1,7 @@
1
+ module StateFu
2
+ module Persistence
3
+ class Session
4
+ # Rails / etc session key
5
+ end
6
+ end
7
+ end
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