state_machine 0.9.4 → 0.10.0

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 (68) hide show
  1. data/CHANGELOG.rdoc +20 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +74 -4
  4. data/Rakefile +3 -3
  5. data/lib/state_machine.rb +51 -24
  6. data/lib/state_machine/{guard.rb → branch.rb} +34 -40
  7. data/lib/state_machine/callback.rb +13 -18
  8. data/lib/state_machine/error.rb +13 -0
  9. data/lib/state_machine/eval_helpers.rb +3 -0
  10. data/lib/state_machine/event.rb +67 -30
  11. data/lib/state_machine/event_collection.rb +20 -3
  12. data/lib/state_machine/extensions.rb +3 -3
  13. data/lib/state_machine/integrations.rb +7 -0
  14. data/lib/state_machine/integrations/active_model.rb +149 -59
  15. data/lib/state_machine/integrations/active_model/versions.rb +30 -0
  16. data/lib/state_machine/integrations/active_record.rb +74 -148
  17. data/lib/state_machine/integrations/active_record/locale.rb +0 -7
  18. data/lib/state_machine/integrations/active_record/versions.rb +149 -0
  19. data/lib/state_machine/integrations/base.rb +64 -0
  20. data/lib/state_machine/integrations/data_mapper.rb +50 -39
  21. data/lib/state_machine/integrations/data_mapper/observer.rb +47 -12
  22. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  23. data/lib/state_machine/integrations/mongo_mapper.rb +37 -64
  24. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  25. data/lib/state_machine/integrations/mongo_mapper/versions.rb +102 -0
  26. data/lib/state_machine/integrations/mongoid.rb +297 -0
  27. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  28. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  29. data/lib/state_machine/integrations/sequel.rb +99 -55
  30. data/lib/state_machine/integrations/sequel/versions.rb +40 -0
  31. data/lib/state_machine/machine.rb +273 -136
  32. data/lib/state_machine/machine_collection.rb +21 -13
  33. data/lib/state_machine/node_collection.rb +6 -1
  34. data/lib/state_machine/path.rb +120 -0
  35. data/lib/state_machine/path_collection.rb +90 -0
  36. data/lib/state_machine/state.rb +28 -9
  37. data/lib/state_machine/state_collection.rb +1 -1
  38. data/lib/state_machine/transition.rb +65 -6
  39. data/lib/state_machine/transition_collection.rb +1 -1
  40. data/test/files/en.yml +8 -0
  41. data/test/functional/state_machine_test.rb +15 -2
  42. data/test/unit/branch_test.rb +890 -0
  43. data/test/unit/callback_test.rb +9 -36
  44. data/test/unit/error_test.rb +43 -0
  45. data/test/unit/event_collection_test.rb +67 -33
  46. data/test/unit/event_test.rb +165 -38
  47. data/test/unit/integrations/active_model_test.rb +103 -3
  48. data/test/unit/integrations/active_record_test.rb +90 -43
  49. data/test/unit/integrations/base_test.rb +87 -0
  50. data/test/unit/integrations/data_mapper_test.rb +105 -44
  51. data/test/unit/integrations/mongo_mapper_test.rb +261 -64
  52. data/test/unit/integrations/mongoid_test.rb +1529 -0
  53. data/test/unit/integrations/sequel_test.rb +33 -49
  54. data/test/unit/integrations_test.rb +4 -0
  55. data/test/unit/invalid_event_test.rb +15 -2
  56. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  57. data/test/unit/invalid_transition_test.rb +72 -2
  58. data/test/unit/machine_collection_test.rb +55 -61
  59. data/test/unit/machine_test.rb +388 -26
  60. data/test/unit/node_collection_test.rb +14 -4
  61. data/test/unit/path_collection_test.rb +266 -0
  62. data/test/unit/path_test.rb +485 -0
  63. data/test/unit/state_collection_test.rb +30 -0
  64. data/test/unit/state_test.rb +82 -35
  65. data/test/unit/transition_collection_test.rb +48 -44
  66. data/test/unit/transition_test.rb +198 -41
  67. metadata +111 -74
  68. data/test/unit/guard_test.rb +0 -909
@@ -0,0 +1,30 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveModel
4
+ version '2.x' do
5
+ def self.active?
6
+ !defined?(::ActiveModel::VERSION) || ::ActiveModel::VERSION::MAJOR == 2
7
+ end
8
+
9
+ def define_validation_hook
10
+ action = self.action
11
+ define_helper(:instance, :valid?) do |machine, object, _super, *|
12
+ object.class.state_machines.transitions(object, action, :after => false).perform { _super.call }
13
+ end
14
+ end
15
+ end
16
+
17
+ version '3.0.x' do
18
+ def self.active?
19
+ defined?(::ActiveModel::VERSION) && ::ActiveModel::VERSION::MAJOR == 3 && ::ActiveModel::VERSION::MINOR == 0
20
+ end
21
+
22
+ def define_validation_hook
23
+ # +around+ callbacks don't have direct access to results until AS 3.1
24
+ owner_class.set_callback(:validation, :after, 'value', :prepend => true)
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,5 @@
1
+ require 'state_machine/integrations/active_model'
2
+
1
3
  module StateMachine
2
4
  module Integrations #:nodoc:
3
5
  # Adds support for integrating state machines with ActiveRecord models.
@@ -219,15 +221,15 @@ module StateMachine
219
221
  # *are* supported. For example, if a transition for a record's +state+
220
222
  # attribute changes the state from +parked+ to +idling+ via the +ignite+
221
223
  # event, the following observer methods are supported:
222
- # * before/after_ignite_from_parked_to_idling
223
- # * before/after_ignite_from_parked
224
- # * before/after_ignite_to_idling
225
- # * before/after_ignite
226
- # * before/after_transition_state_from_parked_to_idling
227
- # * before/after_transition_state_from_parked
228
- # * before/after_transition_state_to_idling
229
- # * before/after_transition_state
230
- # * before/after_transition
224
+ # * before/after/after_failure_to-_ignite_from_parked_to_idling
225
+ # * before/after/after_failure_to-_ignite_from_parked
226
+ # * before/after/after_failure_to-_ignite_to_idling
227
+ # * before/after/after_failure_to-_ignite
228
+ # * before/after/after_failure_to-_transition_state_from_parked_to_idling
229
+ # * before/after/after_failure_to-_transition_state_from_parked
230
+ # * before/after/after_failure_to-_transition_state_to_idling
231
+ # * before/after/after_failure_to-_transition_state
232
+ # * before/after/after_failure_to-_transition
231
233
  #
232
234
  # The following class shows an example of some of these hooks:
233
235
  #
@@ -313,8 +315,11 @@ module StateMachine
313
315
  # events:
314
316
  # park: 'estacionarse'
315
317
  module ActiveRecord
318
+ include Base
316
319
  include ActiveModel
317
320
 
321
+ require 'state_machine/integrations/active_record/versions'
322
+
318
323
  # The default options to use for state machines using this integration
319
324
  @defaults = {:action => :save}
320
325
 
@@ -327,83 +332,50 @@ module StateMachine
327
332
 
328
333
  def self.extended(base) #:nodoc:
329
334
  require 'active_record/version'
330
- require 'state_machine/integrations/active_model/observer'
331
-
332
- ::ActiveRecord::Observer.class_eval do
333
- include StateMachine::Integrations::ActiveModel::Observer
334
- end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
335
-
336
- if defined?(I18n)
337
- locale = "#{File.dirname(__FILE__)}/active_record/locale.rb"
338
- I18n.load_path.unshift(locale) unless I18n.load_path.include?(locale)
339
- end
340
- end
341
-
342
- # Adds a validation error to the given object
343
- def invalidate(object, attribute, message, values = [])
344
- if defined?(I18n)
345
- super
346
- else
347
- object.errors.add(self.attribute(attribute), generate_message(message, values))
348
- end
335
+ super
349
336
  end
350
337
 
351
338
  protected
352
- # Always adds observer support
353
- def supports_observers?
354
- true
355
- end
356
-
357
- # Always adds validation support
358
- def supports_validations?
359
- true
339
+ # The name of this integration
340
+ def integration
341
+ :active_record
360
342
  end
361
343
 
362
- # Only runs validations on the action if using <tt>:save</tt>
363
- def runs_validations_on_action?
364
- action == :save
365
- end
366
-
367
- # Only adds dirty tracking support if ActiveRecord supports it
368
- def supports_dirty_tracking?(object)
369
- defined?(::ActiveRecord::Dirty) && object.respond_to?("#{attribute}_changed?") || super
344
+ # Loads locale files needed for translations
345
+ def load_locale
346
+ load_i18n_version
347
+ super
370
348
  end
371
349
 
372
- # Always uses the <tt>:activerecord</tt> translation scope
373
- def i18n_scope
374
- :activerecord
350
+ # Loads the version of the i18n library so that that proper
351
+ # interpolation syntax can be used
352
+ def load_i18n_version
353
+ begin
354
+ require 'i18n/version'
355
+ rescue Exception => ex
356
+ end
375
357
  end
376
358
 
377
- # The default options to use when generating messages for validation
378
- # errors
379
- def default_error_message_options(object, attribute, message)
380
- if ::ActiveRecord::VERSION::MAJOR >= 3
381
- super
382
- else
383
- {:default => @messages[message]}
384
- end
359
+ # Loads extensions to ActiveRecord's Observers
360
+ def load_observer_extensions
361
+ super
362
+ ::ActiveRecord::Observer.class_eval do
363
+ include StateMachine::Integrations::ActiveModel::Observer
364
+ end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
385
365
  end
386
366
 
387
- # Only allows translation of I18n is available
388
- def translate(klass, key, value)
389
- if defined?(I18n)
390
- super
391
- else
392
- value ? value.to_s.humanize.downcase : 'nil'
393
- end
367
+ # Only runs validations on the action if using <tt>:save</tt>
368
+ def runs_validations_on_action?
369
+ action == :save
394
370
  end
395
371
 
396
- # Attempts to look up a class's ancestors via:
397
- # * #lookup_ancestors (3.0.0+)
398
- # * #self_and_descendants_from_active_record (2.3.2 - 2.3.x)
399
- # * #self_and_descendents_from_active_record (2.0.0 - 2.3.1)
400
- def ancestors_for(klass)
401
- if ::ActiveRecord::VERSION::MAJOR >= 3
372
+ # Only allows state initialization on new records that aren't being
373
+ # created with a set of attributes that includes this machine's
374
+ # attribute.
375
+ def initialize_state?(object, options)
376
+ if object.new_record? && !object.instance_variable_defined?('@initialized_state_machines')
377
+ object.instance_variable_set('@initialized_state_machines', true)
402
378
  super
403
- elsif ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
404
- klass.self_and_descendants_from_active_record
405
- else
406
- klass.self_and_descendents_from_active_record
407
379
  end
408
380
  end
409
381
 
@@ -411,66 +383,37 @@ module StateMachine
411
383
  # initial state of the machine *before* any attributes are set on the
412
384
  # object
413
385
  def define_state_initializer
414
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
415
- # Ensure that the attributes setter gets used to force initialization
416
- # of the state machines
417
- def initialize(attributes = nil, *args)
418
- attributes ||= {}
419
- super
420
- end
421
-
422
- # Hooks in to attribute initialization to set the states *prior*
423
- # to the attributes being set
424
- def attributes=(new_attributes, *args)
425
- if new_record? && !@initialized_state_machines
426
- @initialized_state_machines = true
427
-
428
- ignore = if new_attributes
429
- attributes = new_attributes.dup
430
- attributes.stringify_keys!
431
- if ::ActiveRecord::VERSION::MAJOR >= 3
432
- sanitize_for_mass_assignment(attributes).keys
433
- else
434
- remove_attributes_protected_from_mass_assignment(attributes).keys
435
- end
436
- else
437
- []
438
- end
439
-
440
- initialize_state_machines(:dynamic => false, :ignore => ignore)
441
- super
442
- initialize_state_machines(:dynamic => true, :ignore => ignore)
443
- else
444
- super
445
- end
446
- end
447
- end_eval
386
+ # Ensure that the attributes setter gets used to force initialization
387
+ # of the state machines
388
+ define_helper(:instance, :initialize) do |machine, object, _super, *args|
389
+ _super.call(args.shift || {}, *args)
390
+ end
391
+
392
+ # Hooks in to attribute initialization to set the states *prior*
393
+ # to the attributes being set
394
+ define_helper(:instance, :attributes=) do |machine, object, _super, new_attributes, *|
395
+ object.class.state_machines.initialize_states(object, :attributes => new_attributes) { _super.call }
396
+ end
448
397
  end
449
398
 
450
- # Adds support for defining the attribute predicate, while providing
451
- # compatibility with the default predicate which determines whether
452
- # *anything* is set for the attribute's value
453
- def define_state_predicate
454
- name = self.name
455
-
456
- # Still use class_eval here instance of define_instance_method since
457
- # we need to be able to call +super+
458
- @instance_helper_module.class_eval do
459
- define_method("#{name}?") do |*args|
460
- args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
461
- end
399
+ # Uses around callbacks to run state events if using the :save hook
400
+ def define_action_hook
401
+ if action_hook == :save
402
+ owner_class.set_callback(:save, :around, self, :prepend => true)
403
+ else
404
+ super
462
405
  end
463
406
  end
464
407
 
465
- # Adds hooks into validation for automatically firing events
466
- def define_action_helpers
467
- super(action == :save ? :create_or_update : action)
408
+ # Runs state events around the machine's :save action
409
+ def around_save(object)
410
+ object.class.state_machines.transitions(object, action).perform { yield }
468
411
  end
469
412
 
470
413
  # Creates a scope for finding records *with* a particular state or
471
414
  # states for the attribute
472
415
  def create_with_scope(name)
473
- define_scope(name, lambda {|values| {:conditions => {attribute => values}}})
416
+ define_scope(name, lambda {|values| {attribute => values}})
474
417
  end
475
418
 
476
419
  # Creates a scope for finding records *without* a particular state or
@@ -478,7 +421,7 @@ module StateMachine
478
421
  def create_without_scope(name)
479
422
  define_scope(name, lambda {|values|
480
423
  connection = owner_class.connection
481
- {:conditions => ["#{connection.quote_table_name(owner_class.table_name)}.#{connection.quote_column_name(attribute)} NOT IN (?)", values]}
424
+ ["#{connection.quote_table_name(owner_class.table_name)}.#{connection.quote_column_name(attribute)} NOT IN (?)", values]
482
425
  })
483
426
  end
484
427
 
@@ -489,33 +432,16 @@ module StateMachine
489
432
  object.class.transaction {raise ::ActiveRecord::Rollback unless yield}
490
433
  end
491
434
 
492
- private
493
435
  # Defines a new named scope with the given name
494
436
  def define_scope(name, scope)
495
- if ::ActiveRecord::VERSION::MAJOR >= 3
496
- lambda {|model, values| model.where(scope.call(values)[:conditions])}
497
- else
498
- if owner_class.respond_to?(:named_scope)
499
- name = name.to_sym
500
- machine_name = self.name
501
-
502
- # Since ActiveRecord does not allow direct access to the model
503
- # being used within the evaluation of a dynamic named scope, the
504
- # scope must be generated manually. It's necessary to have access
505
- # to the model so that the state names can be translated to their
506
- # associated values and so that inheritance is respected properly.
507
- owner_class.named_scope(name)
508
- owner_class.scopes[name] = lambda do |model, *states|
509
- machine_states = model.state_machine(machine_name).states
510
- values = states.flatten.map {|state| machine_states.fetch(state).value}
511
-
512
- ::ActiveRecord::NamedScope::Scope.new(model, scope.call(values))
513
- end
514
- end
515
-
516
- # Prevent the Machine class from wrapping the scope
517
- false
518
- end
437
+ lambda {|model, values| model.where(scope.call(values))}
438
+ end
439
+
440
+ # ActiveModel's use of method_missing / respond_to for attribute methods
441
+ # breaks both ancestor lookups and defined?(super). Need to special-case
442
+ # the existence of query attribute methods.
443
+ def owner_class_ancestor_has_method?(method)
444
+ method == "#{name}?" || super
519
445
  end
520
446
  end
521
447
  end
@@ -2,13 +2,6 @@ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
2
  translations = eval(IO.read(filename), binding, filename)
3
3
  translations[:en][:activerecord] = translations[:en].delete(:activemodel)
4
4
 
5
- # Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
6
- # therefore possibly have I18n::VERSION available)
7
- begin
8
- require 'i18n/version'
9
- rescue Exception => ex
10
- end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
11
-
12
5
  # Only i18n 0.4.0+ has the new %{key} syntax
13
6
  if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
14
7
  translations[:en][:activerecord][:errors][:messages].each do |key, message|
@@ -0,0 +1,149 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveRecord
4
+ version '2.x' do
5
+ def self.active?
6
+ ::ActiveRecord::VERSION::MAJOR == 2
7
+ end
8
+
9
+ def load_locale
10
+ super if defined?(I18n)
11
+ end
12
+
13
+ def define_scope(name, scope)
14
+ if owner_class.respond_to?(:named_scope)
15
+ name = name.to_sym
16
+ machine_name = self.name
17
+
18
+ # Since ActiveRecord does not allow direct access to the model
19
+ # being used within the evaluation of a dynamic named scope, the
20
+ # scope must be generated manually. It's necessary to have access
21
+ # to the model so that the state names can be translated to their
22
+ # associated values and so that inheritance is respected properly.
23
+ owner_class.named_scope(name)
24
+ owner_class.scopes[name] = lambda do |model, *states|
25
+ machine_states = model.state_machine(machine_name).states
26
+ values = states.flatten.map {|state| machine_states.fetch(state).value}
27
+
28
+ ::ActiveRecord::NamedScope::Scope.new(model, :conditions => scope.call(values))
29
+ end
30
+ end
31
+
32
+ # Prevent the Machine class from wrapping the scope
33
+ false
34
+ end
35
+
36
+ def invalidate(object, attribute, message, values = [])
37
+ if defined?(I18n)
38
+ super
39
+ else
40
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
41
+ end
42
+ end
43
+
44
+ def translate(klass, key, value)
45
+ if defined?(I18n)
46
+ super
47
+ else
48
+ value ? value.to_s.humanize.downcase : 'nil'
49
+ end
50
+ end
51
+
52
+ def supports_observers?
53
+ true
54
+ end
55
+
56
+ def supports_validations?
57
+ true
58
+ end
59
+
60
+ def supports_mass_assignment_security?
61
+ true
62
+ end
63
+
64
+ def i18n_scope(klass)
65
+ :activerecord
66
+ end
67
+
68
+ def action_hook
69
+ action == :save ? :create_or_update : super
70
+ end
71
+
72
+ def filter_attributes(object, attributes)
73
+ object.send(:remove_attributes_protected_from_mass_assignment, attributes)
74
+ end
75
+ end
76
+
77
+ version '2.0 - 2.2.x' do
78
+ def self.active?
79
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR < 3
80
+ end
81
+
82
+ def default_error_message_options(object, attribute, message)
83
+ {:default => @messages[message]}
84
+ end
85
+ end
86
+
87
+ version '2.0 - 2.3.1' do
88
+ def self.active?
89
+ ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 2)
90
+ end
91
+
92
+ def ancestors_for(klass)
93
+ klass.self_and_descendents_from_active_record
94
+ end
95
+ end
96
+
97
+ version '2.0 - 2.3.4' do
98
+ def self.active?
99
+ ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
100
+ end
101
+
102
+ def load_i18n_version
103
+ end
104
+ end
105
+
106
+ version '2.0.x' do
107
+ def self.active?
108
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 0
109
+ end
110
+
111
+ def supports_dirty_tracking?(object)
112
+ false
113
+ end
114
+ end
115
+
116
+ version '2.1.x - 2.3.x' do
117
+ def self.active?
118
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR > 0
119
+ end
120
+
121
+ def supports_dirty_tracking?(object)
122
+ object.respond_to?("#{attribute}_changed?")
123
+ end
124
+ end
125
+
126
+ version '2.3.2 - 2.3.x' do
127
+ def self.active?
128
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
129
+ end
130
+
131
+ def ancestors_for(klass)
132
+ klass.self_and_descendants_from_active_record
133
+ end
134
+ end
135
+
136
+ version '3.0.x' do
137
+ def self.active?
138
+ ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR == 0
139
+ end
140
+
141
+ def define_action_hook
142
+ # +around+ callbacks don't have direct access to results until AS 3.1
143
+ owner_class.set_callback(:save, :after, 'value', :prepend => true) if action_hook == :save
144
+ super
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end