state_machine 0.9.4 → 0.10.0

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