enum_state_machine 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. metadata +83 -130
  6. data/.rvmrc +0 -1
  7. data/enum_state_machine.gemspec +0 -25
  8. data/lib/enum_state_machine.rb +0 -9
  9. data/lib/enum_state_machine/assertions.rb +0 -36
  10. data/lib/enum_state_machine/branch.rb +0 -225
  11. data/lib/enum_state_machine/callback.rb +0 -232
  12. data/lib/enum_state_machine/core.rb +0 -12
  13. data/lib/enum_state_machine/core_ext.rb +0 -2
  14. data/lib/enum_state_machine/core_ext/class/state_machine.rb +0 -5
  15. data/lib/enum_state_machine/error.rb +0 -13
  16. data/lib/enum_state_machine/eval_helpers.rb +0 -87
  17. data/lib/enum_state_machine/event.rb +0 -257
  18. data/lib/enum_state_machine/event_collection.rb +0 -141
  19. data/lib/enum_state_machine/extensions.rb +0 -149
  20. data/lib/enum_state_machine/graph.rb +0 -92
  21. data/lib/enum_state_machine/helper_module.rb +0 -17
  22. data/lib/enum_state_machine/initializers.rb +0 -4
  23. data/lib/enum_state_machine/initializers/rails.rb +0 -22
  24. data/lib/enum_state_machine/integrations.rb +0 -97
  25. data/lib/enum_state_machine/integrations/active_model.rb +0 -585
  26. data/lib/enum_state_machine/integrations/active_model/locale.rb +0 -11
  27. data/lib/enum_state_machine/integrations/active_model/observer.rb +0 -33
  28. data/lib/enum_state_machine/integrations/active_model/observer_update.rb +0 -42
  29. data/lib/enum_state_machine/integrations/active_model/versions.rb +0 -31
  30. data/lib/enum_state_machine/integrations/active_record.rb +0 -548
  31. data/lib/enum_state_machine/integrations/active_record/locale.rb +0 -20
  32. data/lib/enum_state_machine/integrations/active_record/versions.rb +0 -123
  33. data/lib/enum_state_machine/integrations/base.rb +0 -100
  34. data/lib/enum_state_machine/machine.rb +0 -2292
  35. data/lib/enum_state_machine/machine_collection.rb +0 -86
  36. data/lib/enum_state_machine/macro_methods.rb +0 -518
  37. data/lib/enum_state_machine/matcher.rb +0 -123
  38. data/lib/enum_state_machine/matcher_helpers.rb +0 -54
  39. data/lib/enum_state_machine/node_collection.rb +0 -222
  40. data/lib/enum_state_machine/path.rb +0 -120
  41. data/lib/enum_state_machine/path_collection.rb +0 -90
  42. data/lib/enum_state_machine/state.rb +0 -297
  43. data/lib/enum_state_machine/state_collection.rb +0 -112
  44. data/lib/enum_state_machine/state_context.rb +0 -138
  45. data/lib/enum_state_machine/state_enum.rb +0 -23
  46. data/lib/enum_state_machine/transition.rb +0 -470
  47. data/lib/enum_state_machine/transition_collection.rb +0 -245
  48. data/lib/enum_state_machine/version.rb +0 -3
  49. data/lib/enum_state_machine/yard.rb +0 -8
  50. data/lib/enum_state_machine/yard/handlers.rb +0 -12
  51. data/lib/enum_state_machine/yard/handlers/base.rb +0 -32
  52. data/lib/enum_state_machine/yard/handlers/event.rb +0 -25
  53. data/lib/enum_state_machine/yard/handlers/machine.rb +0 -344
  54. data/lib/enum_state_machine/yard/handlers/state.rb +0 -25
  55. data/lib/enum_state_machine/yard/handlers/transition.rb +0 -47
  56. data/lib/enum_state_machine/yard/templates.rb +0 -3
  57. data/lib/enum_state_machine/yard/templates/default/class/html/setup.rb +0 -30
  58. data/lib/enum_state_machine/yard/templates/default/class/html/state_machines.erb +0 -12
  59. data/lib/tasks/enum_state_machine.rake +0 -1
  60. data/lib/tasks/enum_state_machine.rb +0 -24
  61. data/lib/yard-enum_state_machine.rb +0 -2
  62. data/test/functional/state_machine_test.rb +0 -1066
  63. data/test/unit/integrations/active_model_test.rb +0 -1245
  64. data/test/unit/integrations/active_record_test.rb +0 -2551
  65. data/test/unit/integrations/base_test.rb +0 -104
  66. data/test/unit/integrations_test.rb +0 -71
  67. data/test/unit/invalid_event_test.rb +0 -20
  68. data/test/unit/invalid_parallel_transition_test.rb +0 -18
  69. data/test/unit/invalid_transition_test.rb +0 -115
  70. data/test/unit/machine_collection_test.rb +0 -603
  71. data/test/unit/machine_test.rb +0 -3395
  72. data/test/unit/state_machine_test.rb +0 -31
@@ -1,90 +0,0 @@
1
- require 'enum_state_machine/path'
2
-
3
- module EnumStateMachine
4
- # Represents a collection of paths that are generated based on a set of
5
- # requirements regarding what states to start and end on
6
- class PathCollection < Array
7
- include Assertions
8
-
9
- # The object whose state machine is being walked
10
- attr_reader :object
11
-
12
- # The state machine these path are walking
13
- attr_reader :machine
14
-
15
- # The initial state to start each path from
16
- attr_reader :from_name
17
-
18
- # The target state for each path
19
- attr_reader :to_name
20
-
21
- # Creates a new collection of paths with the given requirements.
22
- #
23
- # Configuration options:
24
- # * <tt>:from</tt> - The initial state to start from
25
- # * <tt>:to</tt> - The target end state
26
- # * <tt>:deep</tt> - Whether to enable deep searches for the target state.
27
- # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
28
- # conditionals defined for each one
29
- def initialize(object, machine, options = {})
30
- options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
31
- assert_valid_keys(options, :from, :to, :deep, :guard)
32
-
33
- @object = object
34
- @machine = machine
35
- @from_name = machine.states.fetch(options[:from]).name
36
- @to_name = options[:to] && machine.states.fetch(options[:to]).name
37
- @guard = options[:guard]
38
- @deep = options[:deep]
39
-
40
- initial_paths.each {|path| walk(path)}
41
- end
42
-
43
- # Lists all of the states that can be transitioned from through the paths in
44
- # this collection.
45
- #
46
- # For example,
47
- #
48
- # paths.from_states # => [:parked, :idling, :first_gear, ...]
49
- def from_states
50
- map {|path| path.from_states}.flatten.uniq
51
- end
52
-
53
- # Lists all of the states that can be transitioned to through the paths in
54
- # this collection.
55
- #
56
- # For example,
57
- #
58
- # paths.to_states # => [:idling, :first_gear, :second_gear, ...]
59
- def to_states
60
- map {|path| path.to_states}.flatten.uniq
61
- end
62
-
63
- # Lists all of the events that can be fired through the paths in this
64
- # collection.
65
- #
66
- # For example,
67
- #
68
- # paths.events # => [:park, :ignite, :shift_up, ...]
69
- def events
70
- map {|path| path.events}.flatten.uniq
71
- end
72
-
73
- private
74
- # Gets the initial set of paths to walk
75
- def initial_paths
76
- machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
77
- path = Path.new(object, machine, :target => to_name, :guard => @guard)
78
- path << transition
79
- path
80
- end
81
- end
82
-
83
- # Walks down the given path. Each new path that matches the configured
84
- # requirements will be added to this collection.
85
- def walk(path)
86
- self << path if path.complete?
87
- path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
88
- end
89
- end
90
- end
@@ -1,297 +0,0 @@
1
- require 'enum_state_machine/assertions'
2
- require 'enum_state_machine/state_context'
3
-
4
- module EnumStateMachine
5
- # A state defines a value that an attribute can be in after being transitioned
6
- # 0 or more times. States can represent a value of any type in Ruby, though
7
- # the most common (and default) type is String.
8
- #
9
- # In addition to defining the machine's value, a state can also define a
10
- # behavioral context for an object when that object is in the state. See
11
- # EnumStateMachine::Machine#state for more information about how state-driven
12
- # behavior can be utilized.
13
- class State
14
- include Assertions
15
-
16
- # The state machine for which this state is defined
17
- attr_accessor :machine
18
-
19
- # The unique identifier for the state used in event and callback definitions
20
- attr_reader :name
21
-
22
- # The fully-qualified identifier for the state, scoped by the machine's
23
- # namespace
24
- attr_reader :qualified_name
25
-
26
- # The human-readable name for the state
27
- attr_writer :human_name
28
-
29
- # The value that is written to a machine's attribute when an object
30
- # transitions into this state
31
- attr_writer :value
32
-
33
- # Whether this state's value should be cached after being evaluated
34
- attr_accessor :cache
35
-
36
- # Whether or not this state is the initial state to use for new objects
37
- attr_accessor :initial
38
- alias_method :initial?, :initial
39
-
40
- # A custom lambda block for determining whether a given value matches this
41
- # state
42
- attr_accessor :matcher
43
-
44
- # Creates a new state within the context of the given machine.
45
- #
46
- # Configuration options:
47
- # * <tt>:initial</tt> - Whether this state is the beginning state for the
48
- # machine. Default is false.
49
- # * <tt>:value</tt> - The value to store when an object transitions to this
50
- # state. Default is the name (stringified).
51
- # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
52
- # then setting this to true will cache the evaluated result
53
- # * <tt>:if</tt> - Determines whether a value matches this state
54
- # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
55
- # By default, the configured value is matched.
56
- # * <tt>:human_name</tt> - The human-readable version of this state's name
57
- def initialize(machine, name, options = {}) #:nodoc:
58
- assert_valid_keys(options, :initial, :value, :cache, :if, :human_name)
59
-
60
- @machine = machine
61
- @name = name
62
- @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
63
- @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
64
- @value = options.include?(:value) ? options[:value] : name && name.to_s
65
- @cache = options[:cache]
66
- @matcher = options[:if]
67
- @initial = options[:initial] == true
68
- @context = StateContext.new(self)
69
-
70
- if name
71
- conflicting_machines = machine.owner_class.state_machines.select {|other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
72
-
73
- # Output a warning if another machine has a conflicting qualified name
74
- # for a different attribute
75
- if conflict = conflicting_machines.detect {|other_name, other_machine| other_machine.attribute != machine.attribute}
76
- name, other_machine = conflict
77
- warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
78
- elsif conflicting_machines.empty?
79
- # Only bother adding predicates when another machine for the same
80
- # attribute hasn't already done so
81
- add_predicate
82
- end
83
- end
84
- end
85
-
86
- # Creates a copy of this state, excluding the context to prevent conflicts
87
- # across different machines.
88
- def initialize_copy(orig) #:nodoc:
89
- super
90
- @context = StateContext.new(self)
91
- end
92
-
93
- # Determines whether there are any states that can be transitioned to from
94
- # this state. If there are none, then this state is considered *final*.
95
- # Any objects in a final state will remain so forever given the current
96
- # machine's definition.
97
- def final?
98
- !machine.events.any? do |event|
99
- event.branches.any? do |branch|
100
- branch.state_requirements.any? do |requirement|
101
- requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
102
- end
103
- end
104
- end
105
- end
106
-
107
- # Transforms the state name into a more human-readable format, such as
108
- # "first gear" instead of "first_gear"
109
- def human_name(klass = @machine.owner_class)
110
- @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
111
- end
112
-
113
- # Generates a human-readable description of this state's name / value:
114
- #
115
- # For example,
116
- #
117
- # State.new(machine, :parked).description # => "parked"
118
- # State.new(machine, :parked, :value => :parked).description # => "parked"
119
- # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
120
- # State.new(machine, :parked, :value => 1).description # => "parked (1)"
121
- # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
122
- #
123
- # Configuration options:
124
- # * <tt>:human_name</tt> - Whether to use this state's human name in the
125
- # description or just the internal name
126
- def description(options = {})
127
- label = options[:human_name] ? human_name : name
128
- description = label ? label.to_s : label.inspect
129
- description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
130
- description
131
- end
132
-
133
- # The value that represents this state. This will optionally evaluate the
134
- # original block if it's a lambda block. Otherwise, the static value is
135
- # returned.
136
- #
137
- # For example,
138
- #
139
- # State.new(machine, :parked, :value => 1).value # => 1
140
- # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
141
- # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
142
- def value(eval = true)
143
- if @value.is_a?(Proc) && eval
144
- if cache_value?
145
- @value = @value.call
146
- machine.states.update(self)
147
- @value
148
- else
149
- @value.call
150
- end
151
- else
152
- @value
153
- end
154
- end
155
-
156
- # Determines whether this state matches the given value. If no matcher is
157
- # configured, then this will check whether the values are equivalent.
158
- # Otherwise, the matcher will determine the result.
159
- #
160
- # For example,
161
- #
162
- # # Without a matcher
163
- # state = State.new(machine, :parked, :value => 1)
164
- # state.matches?(1) # => true
165
- # state.matches?(2) # => false
166
- #
167
- # # With a matcher
168
- # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
169
- # state.matches?(nil) # => false
170
- # state.matches?(Time.now) # => true
171
- def matches?(other_value)
172
- matcher ? matcher.call(other_value) : other_value == value
173
- end
174
-
175
- # Defines a context for the state which will be enabled on instances of
176
- # the owner class when the machine is in this state.
177
- #
178
- # This can be called multiple times. Each time a new context is created,
179
- # a new module will be included in the owner class.
180
- def context(&block)
181
- # Include the context
182
- context = @context
183
- machine.owner_class.class_eval { include context }
184
-
185
- # Evaluate the method definitions and track which ones were added
186
- old_methods = context_methods
187
- context.class_eval(&block)
188
- new_methods = context_methods.to_a.select {|(name, method)| old_methods[name] != method}
189
-
190
- # Alias new methods so that the only execute when the object is in this state
191
- new_methods.each do |(method_name, method)|
192
- context_name = context_name_for(method_name)
193
- context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
194
- alias_method :"#{context_name}", :#{method_name}
195
- def #{method_name}(*args, &block)
196
- state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
197
- options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
198
- state.call(self, :"#{context_name}", *(args + [options]), &block)
199
- end
200
- end_eval
201
- end
202
-
203
- true
204
- end
205
-
206
- # The list of methods that have been defined in this state's context
207
- def context_methods
208
- @context.instance_methods.inject({}) do |methods, name|
209
- methods.merge(name.to_sym => @context.instance_method(name))
210
- end
211
- end
212
-
213
- # Calls a method defined in this state's context on the given object. All
214
- # arguments and any block will be passed into the method defined.
215
- #
216
- # If the method has never been defined for this state, then a NoMethodError
217
- # will be raised.
218
- def call(object, method, *args, &block)
219
- options = args.last.is_a?(Hash) ? args.pop : {}
220
- options = {:method_name => method}.merge(options)
221
- state = machine.states.match!(object)
222
-
223
- if state == self && object.respond_to?(method)
224
- object.send(method, *args, &block)
225
- elsif method_missing = options[:method_missing]
226
- # Dispatch to the superclass since the object either isn't in this state
227
- # or this state doesn't handle the method
228
- begin
229
- method_missing.call
230
- rescue NoMethodError => ex
231
- if ex.name.to_s == options[:method_name].to_s && ex.args == args
232
- # No valid context for this method
233
- raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
234
- else
235
- raise
236
- end
237
- end
238
- end
239
- end
240
-
241
- # Draws a representation of this state on the given machine. This will
242
- # create a new node on the graph with the following properties:
243
- # * +label+ - The human-friendly description of the state.
244
- # * +width+ - The width of the node. Always 1.
245
- # * +height+ - The height of the node. Always 1.
246
- # * +shape+ - The actual shape of the node. If the state is a final
247
- # state, then "doublecircle", otherwise "ellipse".
248
- #
249
- # Configuration options:
250
- # * <tt>:human_name</tt> - Whether to use the state's human name for the
251
- # node's label that gets drawn on the graph
252
- def draw(graph, options = {})
253
- node = graph.add_nodes(name ? name.to_s : 'nil',
254
- :label => description(options),
255
- :width => '1',
256
- :height => '1',
257
- :shape => final? ? 'doublecircle' : 'ellipse'
258
- )
259
-
260
- # Add open arrow for initial state
261
- graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
262
-
263
- true
264
- end
265
-
266
- # Generates a nicely formatted description of this state's contents.
267
- #
268
- # For example,
269
- #
270
- # state = EnumStateMachine::State.new(machine, :parked, :value => 1, :initial => true)
271
- # state # => #<EnumStateMachine::State name=:parked value=1 initial=true context=[]>
272
- def inspect
273
- attributes = [[:name, name], [:value, @value], [:initial, initial?]]
274
- "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
275
- end
276
-
277
- private
278
- # Should the value be cached after it's evaluated for the first time?
279
- def cache_value?
280
- @cache
281
- end
282
-
283
- # Adds a predicate method to the owner class so long as a name has
284
- # actually been configured for the state
285
- def add_predicate
286
- # Checks whether the current value matches this state
287
- machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
288
- machine.states.matches?(object, name)
289
- end
290
- end
291
-
292
- # Generates the name of the method containing the actual implementation
293
- def context_name_for(method)
294
- :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
295
- end
296
- end
297
- end
@@ -1,112 +0,0 @@
1
- require 'enum_state_machine/node_collection'
2
-
3
- module EnumStateMachine
4
- # Represents a collection of states in a state machine
5
- class StateCollection < NodeCollection
6
- def initialize(machine) #:nodoc:
7
- super(machine, :index => [:name, :qualified_name, :value])
8
- end
9
-
10
- # Determines whether the given object is in a specific state. If the
11
- # object's current value doesn't match the state, then this will return
12
- # false, otherwise true. If the given state is unknown, then an IndexError
13
- # will be raised.
14
- #
15
- # == Examples
16
- #
17
- # class Vehicle
18
- # state_machine :initial => :parked do
19
- # other_states :idling
20
- # end
21
- # end
22
- #
23
- # states = Vehicle.state_machine.states
24
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
25
- #
26
- # states.matches?(vehicle, :parked) # => true
27
- # states.matches?(vehicle, :idling) # => false
28
- # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
29
- def matches?(object, name)
30
- fetch(name).matches?(machine.read(object, :state))
31
- end
32
-
33
- # Determines the current state of the given object as configured by this
34
- # state machine. This will attempt to find a known state that matches
35
- # the value of the attribute on the object.
36
- #
37
- # == Examples
38
- #
39
- # class Vehicle
40
- # state_machine :initial => :parked do
41
- # other_states :idling
42
- # end
43
- # end
44
- #
45
- # states = Vehicle.state_machine.states
46
- #
47
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
48
- # states.match(vehicle) # => #<EnumStateMachine::State name=:parked value="parked" initial=true>
49
- #
50
- # vehicle.state = 'idling'
51
- # states.match(vehicle) # => #<EnumStateMachine::State name=:idling value="idling" initial=true>
52
- #
53
- # vehicle.state = 'invalid'
54
- # states.match(vehicle) # => nil
55
- def match(object)
56
- value = machine.read(object, :state)
57
- self[value, :value] || detect {|state| state.matches?(value)}
58
- end
59
-
60
- # Determines the current state of the given object as configured by this
61
- # state machine. If no state is found, then an ArgumentError will be
62
- # raised.
63
- #
64
- # == Examples
65
- #
66
- # class Vehicle
67
- # state_machine :initial => :parked do
68
- # other_states :idling
69
- # end
70
- # end
71
- #
72
- # states = Vehicle.state_machine.states
73
- #
74
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
75
- # states.match!(vehicle) # => #<EnumStateMachine::State name=:parked value="parked" initial=true>
76
- #
77
- # vehicle.state = 'invalid'
78
- # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
79
- def match!(object)
80
- match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
81
- end
82
-
83
- # Gets the order in which states should be displayed based on where they
84
- # were first referenced. This will order states in the following priority:
85
- #
86
- # 1. Initial state
87
- # 2. Event transitions (:from, :except_from, :to, :except_to options)
88
- # 3. States with behaviors
89
- # 4. States referenced via +state+ or +other_states+
90
- # 5. States referenced in callbacks
91
- #
92
- # This order will determine how the GraphViz visualizations are rendered.
93
- def by_priority
94
- order = select {|state| state.initial}.map {|state| state.name}
95
-
96
- machine.events.each {|event| order += event.known_states}
97
- order += select {|state| state.context_methods.any?}.map {|state| state.name}
98
- order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
99
- order += keys(:name)
100
-
101
- order.uniq!
102
- order.map! {|name| self[name]}
103
- order
104
- end
105
-
106
- private
107
- # Gets the value for the given attribute on the node
108
- def value(node, attribute)
109
- attribute == :value ? node.value(false) : super
110
- end
111
- end
112
- end