state_machine 1.1.0 → 1.1.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 (77) hide show
  1. data/.yardopts +0 -1
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +1 -1
  4. data/README.md +97 -5
  5. data/gemfiles/active_model-3.0.0.gemfile.lock +1 -1
  6. data/gemfiles/active_model-3.0.5.gemfile.lock +1 -1
  7. data/gemfiles/active_model-3.1.1.gemfile.lock +1 -1
  8. data/gemfiles/active_record-2.0.0.gemfile.lock +1 -1
  9. data/gemfiles/active_record-2.0.5.gemfile.lock +1 -1
  10. data/gemfiles/active_record-2.1.0.gemfile.lock +1 -1
  11. data/gemfiles/active_record-2.1.2.gemfile.lock +1 -1
  12. data/gemfiles/active_record-2.2.3.gemfile.lock +1 -1
  13. data/gemfiles/active_record-2.3.12.gemfile.lock +1 -1
  14. data/gemfiles/active_record-3.0.0.gemfile.lock +1 -1
  15. data/gemfiles/active_record-3.0.5.gemfile.lock +1 -1
  16. data/gemfiles/active_record-3.1.1.gemfile.lock +1 -1
  17. data/gemfiles/data_mapper-0.10.2.gemfile.lock +1 -1
  18. data/gemfiles/data_mapper-0.9.11.gemfile.lock +1 -1
  19. data/gemfiles/data_mapper-0.9.4.gemfile.lock +1 -1
  20. data/gemfiles/data_mapper-0.9.7.gemfile.lock +1 -1
  21. data/gemfiles/data_mapper-1.0.0.gemfile.lock +1 -1
  22. data/gemfiles/data_mapper-1.0.1.gemfile.lock +1 -1
  23. data/gemfiles/data_mapper-1.0.2.gemfile.lock +1 -1
  24. data/gemfiles/data_mapper-1.1.0.gemfile.lock +1 -1
  25. data/gemfiles/data_mapper-1.2.0.gemfile.lock +1 -1
  26. data/gemfiles/default.gemfile.lock +1 -1
  27. data/gemfiles/graphviz-0.9.0.gemfile.lock +1 -1
  28. data/gemfiles/graphviz-0.9.21.gemfile.lock +1 -1
  29. data/gemfiles/graphviz-1.0.0.gemfile.lock +1 -1
  30. data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +1 -1
  31. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +1 -1
  32. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +1 -1
  33. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +1 -1
  34. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +1 -1
  35. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +1 -1
  36. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +1 -1
  37. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +1 -1
  38. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +1 -1
  39. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +1 -1
  40. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +1 -1
  41. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +1 -1
  42. data/gemfiles/mongoid-2.0.0.gemfile.lock +1 -1
  43. data/gemfiles/mongoid-2.1.4.gemfile.lock +1 -1
  44. data/gemfiles/mongoid-2.2.4.gemfile.lock +1 -1
  45. data/gemfiles/mongoid-2.3.3.gemfile.lock +1 -1
  46. data/gemfiles/sequel-2.11.0.gemfile.lock +1 -1
  47. data/gemfiles/sequel-2.12.0.gemfile.lock +1 -1
  48. data/gemfiles/sequel-2.8.0.gemfile.lock +1 -1
  49. data/gemfiles/sequel-3.0.0.gemfile.lock +1 -1
  50. data/gemfiles/sequel-3.13.0.gemfile.lock +1 -1
  51. data/gemfiles/sequel-3.14.0.gemfile.lock +1 -1
  52. data/gemfiles/sequel-3.23.0.gemfile.lock +1 -1
  53. data/gemfiles/sequel-3.24.0.gemfile.lock +1 -1
  54. data/gemfiles/sequel-3.29.0.gemfile.lock +1 -1
  55. data/lib/state_machine.rb +2 -499
  56. data/lib/state_machine/core.rb +2 -0
  57. data/lib/state_machine/core_ext.rb +1 -0
  58. data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
  59. data/lib/state_machine/event.rb +0 -5
  60. data/lib/state_machine/event_collection.rb +1 -1
  61. data/lib/state_machine/integrations/mongo_mapper.rb +1 -1
  62. data/lib/state_machine/integrations/mongoid.rb +1 -1
  63. data/lib/state_machine/machine.rb +12 -0
  64. data/lib/state_machine/macro_methods.rb +493 -0
  65. data/lib/state_machine/node_collection.rb +50 -18
  66. data/lib/state_machine/state.rb +0 -5
  67. data/lib/state_machine/state_collection.rb +1 -1
  68. data/lib/state_machine/version.rb +1 -1
  69. data/test/unit/event_collection_test.rb +34 -0
  70. data/test/unit/integrations/data_mapper_test.rb +38 -0
  71. data/test/unit/integrations/mongo_mapper_test.rb +28 -0
  72. data/test/unit/integrations/mongoid_test.rb +28 -0
  73. data/test/unit/machine_test.rb +15 -0
  74. data/test/unit/node_collection_test.rb +37 -9
  75. data/test/unit/state_collection_test.rb +38 -0
  76. data/test/unit/state_test.rb +1 -1
  77. metadata +11 -6
@@ -2,6 +2,8 @@ require 'state_machine/assertions'
2
2
 
3
3
  module StateMachine
4
4
  # Represents a collection of nodes in a state machine, be it events or states.
5
+ # Nodes will not differentiate between the String and Symbol versions of the
6
+ # values being indexed.
5
7
  class NodeCollection
6
8
  include Enumerable
7
9
  include Assertions
@@ -22,7 +24,13 @@ module StateMachine
22
24
 
23
25
  @machine = machine
24
26
  @nodes = []
25
- @indices = Array(options[:index]).inject({}) {|indices, attribute| indices[attribute] = {}; indices}
27
+ @index_names = Array(options[:index])
28
+ @indices = @index_names.inject({}) do |indices, name|
29
+ indices[name] = {}
30
+ indices[:"#{name}_to_s"] = {}
31
+ indices[:"#{name}_to_sym"] = {}
32
+ indices
33
+ end
26
34
  @default_index = Array(options[:index]).first
27
35
  @contexts = []
28
36
  end
@@ -79,7 +87,7 @@ module StateMachine
79
87
  # that match the new node.
80
88
  def <<(node)
81
89
  @nodes << node
82
- @indices.each {|attribute, index| index[value(node, attribute)] = node}
90
+ @index_names.each {|name| add_to_index(name, value(node, name), node)}
83
91
  @contexts.each {|context| eval_context(context, node)}
84
92
  self
85
93
  end
@@ -93,16 +101,7 @@ module StateMachine
93
101
  # has changed since it was added to the collection, the old indexed keys
94
102
  # will be replaced with the updated ones.
95
103
  def update(node)
96
- @indices.each do |attribute, index|
97
- old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
98
- new_key = value(node, attribute)
99
-
100
- # Only replace the key if it's changed
101
- if old_key != new_key
102
- index.delete(old_key)
103
- index[new_key] = node
104
- end
105
- end
104
+ @index_names.each {|name| update_index(name, node)}
106
105
  end
107
106
 
108
107
  # Calls the block once for each element in self, passing that element as a
@@ -144,12 +143,10 @@ module StateMachine
144
143
  #
145
144
  # If the key cannot be found, then nil will be returned.
146
145
  def [](key, index_name = @default_index)
147
- index = self.index(index_name)
148
- if index.include?(key)
149
- index[key]
150
- elsif @indices.include?(:"#{index_name}_to_s")
151
- self[key.to_s, :"#{index_name}_to_s"]
152
- end
146
+ self.index(index_name)[key] ||
147
+ self.index(:"#{index_name}_to_s")[key.to_s] ||
148
+ to_sym?(key) && self.index(:"#{index_name}_to_sym")[:"#{key}"] ||
149
+ nil
153
150
  end
154
151
 
155
152
  # Gets the node indexed by the given key. By default, this will look up the
@@ -181,6 +178,41 @@ module StateMachine
181
178
  node.send(attribute)
182
179
  end
183
180
 
181
+ # Adds the given key / node combination to an index, including the string
182
+ # and symbol versions of the index
183
+ def add_to_index(name, key, node)
184
+ index(name)[key] = node
185
+ index(:"#{name}_to_s")[key.to_s] = node
186
+ index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key)
187
+ end
188
+
189
+ # Removes the given key from an index, including the string and symbol
190
+ # versions of the index
191
+ def remove_from_index(name, key)
192
+ index(name).delete(key)
193
+ index(:"#{name}_to_s").delete(key.to_s)
194
+ index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key)
195
+ end
196
+
197
+ # Updates the node for the given index, including the string and symbol
198
+ # versions of the index
199
+ def update_index(name, node)
200
+ index = self.index(name)
201
+ old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
202
+ new_key = value(node, name)
203
+
204
+ # Only replace the key if it's changed
205
+ if old_key != new_key
206
+ remove_from_index(name, old_key)
207
+ add_to_index(name, new_key, node)
208
+ end
209
+ end
210
+
211
+ # Determines whether the given value can be converted to a symbol
212
+ def to_sym?(value)
213
+ "#{value}" != ''
214
+ end
215
+
184
216
  # Evaluates the given context for a particular node. This will only
185
217
  # evaluate the context if the node matches.
186
218
  def eval_context(context, node)
@@ -96,11 +96,6 @@ module StateMachine
96
96
  @methods = methods.dup
97
97
  end
98
98
 
99
- # Converts the name of this state to a string
100
- def name_to_s
101
- name.to_s
102
- end
103
-
104
99
  # Determines whether there are any states that can be transitioned to from
105
100
  # this state. If there are none, then this state is considered *final*.
106
101
  # Any objects in a final state will remain so forever given the current
@@ -4,7 +4,7 @@ module StateMachine
4
4
  # Represents a collection of states in a state machine
5
5
  class StateCollection < NodeCollection
6
6
  def initialize(machine) #:nodoc:
7
- super(machine, :index => [:name, :name_to_s, :qualified_name, :value])
7
+ super(machine, :index => [:name, :qualified_name, :value])
8
8
  end
9
9
 
10
10
  # Determines whether the given object is in a specific state. If the
@@ -1,3 +1,3 @@
1
1
  module StateMachine
2
- VERSION = '1.1.0'
2
+ VERSION = '1.1.1'
3
3
  end
@@ -47,6 +47,40 @@ class EventCollectionTest < Test::Unit::TestCase
47
47
  def test_should_index_by_qualified_name
48
48
  assert_equal @open, @events[:enable_alarm, :qualified_name]
49
49
  end
50
+
51
+ def test_should_index_by_string_qualified_name
52
+ assert_equal @open, @events['enable_alarm', :qualified_name]
53
+ end
54
+ end
55
+
56
+ class EventStringCollectionTest < Test::Unit::TestCase
57
+ def setup
58
+ machine = StateMachine::Machine.new(Class.new, :namespace => 'alarm')
59
+ @events = StateMachine::EventCollection.new(machine)
60
+
61
+ @events << @open = StateMachine::Event.new(machine, 'enable')
62
+ machine.events.concat(@events)
63
+ end
64
+
65
+ def test_should_index_by_name
66
+ assert_equal @open, @events['enable', :name]
67
+ end
68
+
69
+ def test_should_index_by_name_by_default
70
+ assert_equal @open, @events['enable']
71
+ end
72
+
73
+ def test_should_index_by_symbol_name
74
+ assert_equal @open, @events[:enable]
75
+ end
76
+
77
+ def test_should_index_by_qualified_name
78
+ assert_equal @open, @events['enable_alarm', :qualified_name]
79
+ end
80
+
81
+ def test_should_index_by_symbol_qualified_name
82
+ assert_equal @open, @events[:enable_alarm, :qualified_name]
83
+ end
50
84
  end
51
85
 
52
86
  class EventCollectionWithEventsWithTransitionsTest < Test::Unit::TestCase
@@ -107,6 +107,44 @@ module DataMapperTest
107
107
  end
108
108
  end
109
109
 
110
+ class MachineWithoutPropertyTest < BaseTestCase
111
+ def setup
112
+ @resource = new_resource
113
+ StateMachine::Machine.new(@resource, :status)
114
+ end
115
+
116
+ def test_should_define_field_with_string_type
117
+ property = @resource.properties.detect {|property| property.name == :status}
118
+ assert_not_nil property
119
+
120
+ if Gem::Version.new(::DataMapper::VERSION) >= Gem::Version.new('1.0.0')
121
+ assert_instance_of DataMapper::Property::String, property
122
+ else
123
+ assert_equal String, property.type
124
+ end
125
+ end
126
+ end
127
+
128
+ class MachineWithPropertyTest < BaseTestCase
129
+ def setup
130
+ @resource = new_resource do
131
+ property :status, Integer
132
+ end
133
+ StateMachine::Machine.new(@resource, :status)
134
+ end
135
+
136
+ def test_should_not_redefine_field
137
+ property = @resource.properties.detect {|property| property.name == :status}
138
+ assert_not_nil property
139
+
140
+ if Gem::Version.new(::DataMapper::VERSION) >= Gem::Version.new('1.0.0')
141
+ assert_instance_of DataMapper::Property::Integer, property
142
+ else
143
+ assert_equal Integer, property.type
144
+ end
145
+ end
146
+ end
147
+
110
148
  class MachineByDefaultTest < BaseTestCase
111
149
  def setup
112
150
  @resource = new_resource
@@ -75,6 +75,34 @@ module MongoMapperTest
75
75
  end
76
76
  end
77
77
 
78
+ class MachineWithoutKeyTest < BaseTestCase
79
+ def setup
80
+ @model = new_model
81
+ StateMachine::Machine.new(@model, :status)
82
+ end
83
+
84
+ def test_should_define_field_with_string_type
85
+ key = @model.keys['status']
86
+ assert_not_nil key
87
+ assert_equal String, key.type
88
+ end
89
+ end
90
+
91
+ class MachineWithKeyTest < BaseTestCase
92
+ def setup
93
+ @model = new_model do
94
+ key :status, Integer
95
+ end
96
+ StateMachine::Machine.new(@model, :status)
97
+ end
98
+
99
+ def test_should_not_redefine_field
100
+ key = @model.keys['status']
101
+ assert_not_nil key
102
+ assert_equal Integer, key.type
103
+ end
104
+ end
105
+
78
106
  class MachineByDefaultTest < BaseTestCase
79
107
  def setup
80
108
  @model = new_model
@@ -62,6 +62,34 @@ module MongoidTest
62
62
  end
63
63
  end
64
64
 
65
+ class MachineWithoutFieldTest < BaseTestCase
66
+ def setup
67
+ @model = new_model
68
+ StateMachine::Machine.new(@model, :status)
69
+ end
70
+
71
+ def test_should_define_field_with_string_type
72
+ field = @model.fields['status']
73
+ assert_not_nil field
74
+ assert_equal String, field.type
75
+ end
76
+ end
77
+
78
+ class MachineWithFieldTest < BaseTestCase
79
+ def setup
80
+ @model = new_model do
81
+ field :status, :type => Integer
82
+ end
83
+ StateMachine::Machine.new(@model, :status)
84
+ end
85
+
86
+ def test_should_not_redefine_field
87
+ field = @model.fields['status']
88
+ assert_not_nil field
89
+ assert_equal Integer, field.type
90
+ end
91
+ end
92
+
65
93
  class MachineByDefaultTest < BaseTestCase
66
94
  def setup
67
95
  @model = new_model
@@ -2082,6 +2082,15 @@ class MachineWithStatesTest < Test::Unit::TestCase
2082
2082
  exception = assert_raise(ArgumentError) {@machine.state(:first_gear, :invalid => true)}
2083
2083
  assert_equal 'Invalid key(s): invalid', exception.message
2084
2084
  end
2085
+
2086
+ def test_should_raise_exception_if_conflicting_type_used_for_name
2087
+ exception = assert_raise(ArgumentError) { @machine.state 'first_gear' }
2088
+ assert_equal '"first_gear" state defined as String, :parked defined as Symbol; all states must be consistent', exception.message
2089
+ end
2090
+
2091
+ def test_should_not_raise_exception_if_conflicting_type_is_nil_for_name
2092
+ assert_nothing_raised { @machine.state nil }
2093
+ end
2085
2094
  end
2086
2095
 
2087
2096
  class MachineWithStatesWithCustomValuesTest < Test::Unit::TestCase
@@ -2320,6 +2329,12 @@ class MachineWithEventsTest < Test::Unit::TestCase
2320
2329
  exception = assert_raise(IndexError) {@klass.human_state_event_name(:invalid)}
2321
2330
  assert_equal ':invalid is an invalid name', exception.message
2322
2331
  end
2332
+
2333
+ def test_should_raise_exception_if_conflicting_type_used_for_name
2334
+ @machine.event :park
2335
+ exception = assert_raise(ArgumentError) { @machine.event 'ignite' }
2336
+ assert_equal '"ignite" event defined as String, :park defined as Symbol; all events must be consistent', exception.message
2337
+ end
2323
2338
  end
2324
2339
 
2325
2340
  class MachineWithExistingEventTest < Test::Unit::TestCase
@@ -1,10 +1,6 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
2
 
3
3
  class Node < Struct.new(:name, :value, :machine)
4
- def name_to_s
5
- name.to_s
6
- end
7
-
8
4
  def context
9
5
  yield
10
6
  end
@@ -246,7 +242,7 @@ end
246
242
  class NodeCollectionWithStringIndexTest < Test::Unit::TestCase
247
243
  def setup
248
244
  machine = StateMachine::Machine.new(Class.new)
249
- @collection = StateMachine::NodeCollection.new(machine, :index => [:name, :name_to_s, :value])
245
+ @collection = StateMachine::NodeCollection.new(machine, :index => [:name, :value])
250
246
 
251
247
  @parked = Node.new(:parked, 1)
252
248
  @collection << @parked
@@ -257,15 +253,47 @@ class NodeCollectionWithStringIndexTest < Test::Unit::TestCase
257
253
  end
258
254
 
259
255
  def test_should_index_by_string_name
260
- assert_equal @parked, @collection['parked', :name_to_s]
256
+ assert_equal @parked, @collection['parked']
257
+ end
258
+ end
259
+
260
+ class NodeCollectionWithSymbolIndexTest < Test::Unit::TestCase
261
+ def setup
262
+ machine = StateMachine::Machine.new(Class.new)
263
+ @collection = StateMachine::NodeCollection.new(machine, :index => [:name, :value])
264
+
265
+ @parked = Node.new('parked', 1)
266
+ @collection << @parked
261
267
  end
262
268
 
263
- def test_should_fallback_to_string_index
269
+ def test_should_index_by_name
264
270
  assert_equal @parked, @collection['parked']
265
271
  end
266
272
 
267
- def test_should_not_fallback_to_string_index_if_not_available
268
- assert_nil @collection['1', :value]
273
+ def test_should_index_by_symbol_name
274
+ assert_equal @parked, @collection[:parked]
275
+ end
276
+ end
277
+
278
+ class NodeCollectionWithNumericIndexTest < Test::Unit::TestCase
279
+ def setup
280
+ machine = StateMachine::Machine.new(Class.new)
281
+ @collection = StateMachine::NodeCollection.new(machine, :index => [:name, :value])
282
+
283
+ @parked = Node.new(10, 1)
284
+ @collection << @parked
285
+ end
286
+
287
+ def test_should_index_by_name
288
+ assert_equal @parked, @collection[10]
289
+ end
290
+
291
+ def test_should_index_by_string_name
292
+ assert_equal @parked, @collection['10']
293
+ end
294
+
295
+ def test_should_index_by_symbol_name
296
+ assert_equal @parked, @collection[:'10']
269
297
  end
270
298
  end
271
299
 
@@ -49,6 +49,10 @@ class StateCollectionTest < Test::Unit::TestCase
49
49
  assert_equal @parked, @states[:parked, :qualified_name]
50
50
  end
51
51
 
52
+ def test_should_index_by_string_qualified_name
53
+ assert_equal @parked, @states['parked', :qualified_name]
54
+ end
55
+
52
56
  def test_should_index_by_value
53
57
  assert_equal @parked, @states['parked', :value]
54
58
  end
@@ -88,6 +92,40 @@ class StateCollectionTest < Test::Unit::TestCase
88
92
  end
89
93
  end
90
94
 
95
+ class StateCollectionStringTest < Test::Unit::TestCase
96
+ def setup
97
+ @klass = Class.new
98
+ @machine = StateMachine::Machine.new(@klass)
99
+ @states = StateMachine::StateCollection.new(@machine)
100
+
101
+ @states << @nil = StateMachine::State.new(@machine, nil)
102
+ @states << @parked = StateMachine::State.new(@machine, 'parked')
103
+ @machine.states.concat(@states)
104
+
105
+ @object = @klass.new
106
+ end
107
+
108
+ def test_should_index_by_name
109
+ assert_equal @parked, @states['parked', :name]
110
+ end
111
+
112
+ def test_should_index_by_name_by_default
113
+ assert_equal @parked, @states['parked']
114
+ end
115
+
116
+ def test_should_index_by_symbol_name
117
+ assert_equal @parked, @states[:parked]
118
+ end
119
+
120
+ def test_should_index_by_qualified_name
121
+ assert_equal @parked, @states['parked', :qualified_name]
122
+ end
123
+
124
+ def test_should_index_by_symbol_qualified_name
125
+ assert_equal @parked, @states[:parked, :qualified_name]
126
+ end
127
+ end
128
+
91
129
  class StateCollectionWithNamespaceTest < Test::Unit::TestCase
92
130
  def setup
93
131
  @klass = Class.new
@@ -15,7 +15,7 @@ class StateByDefaultTest < Test::Unit::TestCase
15
15
  end
16
16
 
17
17
  def test_should_have_a_qualified_name
18
- assert_equal :parked, @state.name
18
+ assert_equal :parked, @state.qualified_name
19
19
  end
20
20
 
21
21
  def test_should_have_a_human_name