state_machine 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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