state_machines 0.0.1 → 0.0.2

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -2
  3. data/README.md +25 -0
  4. data/Rakefile +10 -1
  5. data/lib/state_machines/branch.rb +0 -4
  6. data/lib/state_machines/core.rb +23 -5
  7. data/lib/state_machines/error.rb +81 -2
  8. data/lib/state_machines/event.rb +2 -20
  9. data/lib/state_machines/event_collection.rb +25 -27
  10. data/lib/state_machines/extensions.rb +34 -34
  11. data/lib/state_machines/integrations.rb +98 -90
  12. data/lib/state_machines/integrations/base.rb +11 -60
  13. data/lib/state_machines/matcher.rb +0 -2
  14. data/lib/state_machines/node_collection.rb +0 -2
  15. data/lib/state_machines/path_collection.rb +0 -2
  16. data/lib/state_machines/state.rb +0 -3
  17. data/lib/state_machines/state_collection.rb +17 -19
  18. data/lib/state_machines/state_context.rb +1 -6
  19. data/lib/state_machines/transition.rb +0 -56
  20. data/lib/state_machines/version.rb +1 -1
  21. data/spec/spec_helper.rb +1 -0
  22. data/spec/state_machines/assertions_spec.rb +31 -0
  23. data/spec/state_machines/branch_spec.rb +827 -0
  24. data/spec/state_machines/callbacks_spec.rb +706 -0
  25. data/spec/state_machines/errors_spec.rb +1 -0
  26. data/spec/state_machines/event_collection_spec.rb +401 -0
  27. data/spec/state_machines/event_spec.rb +1140 -0
  28. data/spec/{helpers → state_machines}/helper_spec.rb +0 -0
  29. data/spec/state_machines/integration_base_spec.rb +12 -0
  30. data/spec/state_machines/integration_spec.rb +132 -0
  31. data/spec/state_machines/invalid_event_spec.rb +19 -0
  32. data/spec/state_machines/invalid_parallel_transition_spec.rb +18 -0
  33. data/spec/state_machines/invalid_transition_spec.rb +114 -0
  34. data/spec/state_machines/machine_collection_spec.rb +606 -0
  35. data/spec/{machine_spec.rb → state_machines/machine_spec.rb} +11 -2
  36. data/spec/{matcher_helpers_spec.rb → state_machines/matcher_helpers_spec.rb} +0 -0
  37. data/spec/{matcher_spec.rb → state_machines/matcher_spec.rb} +0 -0
  38. data/spec/{node_collection_spec.rb → state_machines/node_collection_spec.rb} +0 -0
  39. data/spec/{path_collection_spec.rb → state_machines/path_collection_spec.rb} +0 -0
  40. data/spec/{path_spec.rb → state_machines/path_spec.rb} +0 -0
  41. data/spec/{state_collection_spec.rb → state_machines/state_collection_spec.rb} +0 -0
  42. data/spec/{state_context_spec.rb → state_machines/state_context_spec.rb} +0 -0
  43. data/spec/{state_machine_spec.rb → state_machines/state_machine_spec.rb} +0 -0
  44. data/spec/{state_spec.rb → state_machines/state_spec.rb} +0 -0
  45. data/spec/{transition_collection_spec.rb → state_machines/transition_collection_spec.rb} +0 -0
  46. data/spec/{transition_spec.rb → state_machines/transition_spec.rb} +0 -0
  47. data/spec/support/migration_helpers.rb +9 -0
  48. data/state_machines.gemspec +3 -1
  49. metadata +68 -45
  50. data/lib/state_machines/yard.rb +0 -8
  51. data/spec/errors/default_spec.rb +0 -14
  52. data/spec/errors/with_message_spec.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6093c9c0c586ab9b799627afd14d9cf686a13881
4
- data.tar.gz: f7457d8d6f8ed9df8fa7da0fed6e74599aa59712
3
+ metadata.gz: 3eee931d0f563b659a9e786dc0eea93ed9f9f537
4
+ data.tar.gz: f53da4d08eecd750e4b416f5926e31b49be341f1
5
5
  SHA512:
6
- metadata.gz: 9222dfe808bb45c75521177c470dff45522cae32155b2e65e18a221d53cae07d2b59b5431a0717b8ab2f6a472efc944bf13c7f1527b2e8913295ec135335a6e3
7
- data.tar.gz: b7f3fbfd31a5062b745ca131a7916739baca538c0254ed2ec495e2e57f7e13561bd4ef0da772a4aa035d560649f6f0d74f5707c87c200bf40f3de3348c05db58
6
+ metadata.gz: c1fde9ad6a6b0fb3a3f066a87f7f80c90d78c9f7d633f92f67ff0f3104bc01d16bddf4485f1b24ab3dc35bd14e6e348b8bc3ee8f45cce9edc89ef771869b9229
7
+ data.tar.gz: 9f07e5b0c0d579557d0a4648b909922a85f45653366ed37e738a3176d52e4ef5ed586471395ea1ca1fcf22109a2d3dda0139e7676f9199666348c108904cabdc
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in state_machines.gemspec
4
2
  gemspec
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/seuros/state_machines.svg?branch=master)](https://travis-ci.org/seuros/state_machines)
2
+ [![Code Climate](https://codeclimate.com/github/seuros/state_machines.png)](https://codeclimate.com/github/seuros/state_machines)
1
3
  # State Machines
2
4
 
3
5
  State Machines adds support for creating state machines for attributes on any Ruby class.
@@ -20,6 +22,29 @@ Or install it yourself as:
20
22
 
21
23
  TODO: Write usage instructions here
22
24
 
25
+ ## Dependencies
26
+
27
+ Ruby versions officially supported and tested:
28
+
29
+ * Ruby (MRI) 1.9.3+
30
+ * JRuby (1.9)
31
+ * Rubinius
32
+
33
+
34
+
35
+ For graphing state machine:
36
+
37
+ * [state_machines-graphviz](http://github.com/seuros/state_machines-graphviz)
38
+
39
+ For documenting state machines:
40
+
41
+ * [state_machines-yard](http://github.com/seuros/state_machines-yard)
42
+
43
+
44
+ ## TODO
45
+
46
+ Add matchers/assertions for rspec and minitest
47
+
23
48
  ## Contributing
24
49
 
25
50
  1. Fork it ( https://github.com/seuros/state_machines/fork )
data/Rakefile CHANGED
@@ -1 +1,10 @@
1
- require 'bundler/gem_tasks'
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+
9
+ desc 'Default: run all tests.'
10
+ task :default => :spec
@@ -1,7 +1,3 @@
1
- require 'state_machines/matcher'
2
- require 'state_machines/eval_helpers'
3
- require 'state_machines/assertions'
4
-
5
1
  module StateMachines
6
2
  # Represents a set of requirements that must be met in order for a transition
7
3
  # or callback to occur. Branches verify that the event, from state, and to
@@ -3,23 +3,41 @@
3
3
  # * StateMachines::MacroMethods which adds the state_machine DSL to your class
4
4
  # * A set of initializers for setting state_machine defaults based on the current
5
5
  # running environment (such as within Rails)
6
- require 'state_machines/error'
7
6
  require 'state_machines/assertions'
7
+ require 'state_machines/error'
8
8
 
9
- require 'state_machines/machine_collection'
10
9
  require 'state_machines/extensions'
11
10
 
12
- require 'state_machines/integrations/base'
13
11
  require 'state_machines/integrations'
12
+ require 'state_machines/integrations/base'
13
+
14
+ require 'state_machines/eval_helpers'
15
+
16
+ require 'singleton'
17
+ require 'state_machines/matcher'
18
+ require 'state_machines/matcher_helpers'
19
+
20
+ require 'state_machines/transition'
21
+ require 'state_machines/transition_collection'
22
+
23
+ require 'state_machines/branch'
14
24
 
15
25
  require 'state_machines/helper_module'
16
26
  require 'state_machines/state'
17
- require 'state_machines/event'
18
27
  require 'state_machines/callback'
19
28
  require 'state_machines/node_collection'
29
+
30
+ require 'state_machines/state_context'
31
+ require 'state_machines/state'
20
32
  require 'state_machines/state_collection'
33
+
34
+ require 'state_machines/event'
21
35
  require 'state_machines/event_collection'
36
+
37
+ require 'state_machines/path'
22
38
  require 'state_machines/path_collection'
23
- require 'state_machines/matcher_helpers'
39
+
24
40
  require 'state_machines/machine'
41
+ require 'state_machines/machine_collection'
42
+
25
43
  require 'state_machines/macro_methods'
@@ -3,11 +3,90 @@ module StateMachines
3
3
  class Error < StandardError
4
4
  # The object that failed
5
5
  attr_reader :object
6
-
6
+
7
7
  def initialize(object, message = nil) #:nodoc:
8
8
  @object = object
9
-
9
+
10
10
  super(message)
11
11
  end
12
12
  end
13
+
14
+ # An invalid integration was specified
15
+ class IntegrationNotFound < Error
16
+ def initialize(name)
17
+ super(nil, "#{name.inspect} is an invalid integration")
18
+ end
19
+ end
20
+
21
+ # An invalid integration was registered
22
+ class IntegrationError < StandardError
23
+ end
24
+
25
+ # An invalid event was specified
26
+ class InvalidEvent < Error
27
+ # The event that was attempted to be run
28
+ attr_reader :event
29
+
30
+ def initialize(object, event_name) #:nodoc:
31
+ @event = event_name
32
+
33
+ super(object, "#{event.inspect} is an unknown state machine event")
34
+ end
35
+ end
36
+ # An invalid transition was attempted
37
+ class InvalidTransition < Error
38
+ # The machine attempting to be transitioned
39
+ attr_reader :machine
40
+
41
+ # The current state value for the machine
42
+ attr_reader :from
43
+
44
+ def initialize(object, machine, event) #:nodoc:
45
+ @machine = machine
46
+ @from_state = machine.states.match!(object)
47
+ @from = machine.read(object, :state)
48
+ @event = machine.events.fetch(event)
49
+ errors = machine.errors_for(object)
50
+
51
+ message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
52
+ message << " (Reason(s): #{errors})" unless errors.empty?
53
+ super(object, message)
54
+ end
55
+
56
+ # The event that triggered the failed transition
57
+ def event
58
+ @event.name
59
+ end
60
+
61
+ # The fully-qualified name of the event that triggered the failed transition
62
+ def qualified_event
63
+ @event.qualified_name
64
+ end
65
+
66
+ # The name for the current state
67
+ def from_name
68
+ @from_state.name
69
+ end
70
+
71
+ # The fully-qualified name for the current state
72
+ def qualified_from_name
73
+ @from_state.qualified_name
74
+ end
75
+ end
76
+
77
+ # A set of transition failed to run in parallel
78
+ class InvalidParallelTransition < Error
79
+ # The set of events that failed the transition(s)
80
+ attr_reader :events
81
+
82
+ def initialize(object, events) #:nodoc:
83
+ @events = events
84
+
85
+ super(object, "Cannot run events in parallel: #{events * ', '}")
86
+ end
87
+ end
88
+
89
+ # A method was called in an invalid state context
90
+ class InvalidContext < Error
91
+ end
13
92
  end
@@ -1,22 +1,4 @@
1
- require 'state_machines/transition'
2
- require 'state_machines/branch'
3
- require 'state_machines/assertions'
4
- require 'state_machines/matcher_helpers'
5
- require 'state_machines/error'
6
-
7
1
  module StateMachines
8
- # An invalid event was specified
9
- class InvalidEvent < Error
10
- # The event that was attempted to be run
11
- attr_reader :event
12
-
13
- def initialize(object, event_name) #:nodoc:
14
- @event = event_name
15
-
16
- super(object, "#{event.inspect} is an unknown state machine event")
17
- end
18
- end
19
-
20
2
  # An event defines an action that transitions an attribute from one state to
21
3
  # another. The state that an attribute is transitioned to depends on the
22
4
  # branches configured for the event.
@@ -58,8 +40,8 @@ module StateMachines
58
40
  reset
59
41
 
60
42
  # Output a warning if another event has a conflicting qualified name
61
- if conflict = machine.owner_class.state_machines.detect { |other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] }
62
- name, other_machine = conflict
43
+ if conflict = machine.owner_class.state_machines.detect { |_other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] }
44
+ _name, other_machine = conflict
63
45
  warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
64
46
  else
65
47
  add_actions
@@ -1,12 +1,10 @@
1
- require 'state_machines/node_collection'
2
-
3
1
  module StateMachines
4
2
  # Represents a collection of events in a state machine
5
3
  class EventCollection < NodeCollection
6
4
  def initialize(machine) #:nodoc:
7
5
  super(machine, :index => [:name, :qualified_name])
8
6
  end
9
-
7
+
10
8
  # Gets the list of events that can be fired on the given object.
11
9
  #
12
10
  # Valid requirement options:
@@ -41,9 +39,9 @@ module StateMachines
41
39
  # vehicle.state = 'idling'
42
40
  # events.valid_for(vehicle) # => [#<StateMachines::Event name=:park transitions=[:idling => :parked]>]
43
41
  def valid_for(object, requirements = {})
44
- match(requirements).select {|event| event.can_fire?(object, requirements)}
42
+ match(requirements).select { |event| event.can_fire?(object, requirements) }
45
43
  end
46
-
44
+
47
45
  # Gets the list of transitions that can be run on the given object.
48
46
  #
49
47
  # Valid requirement options:
@@ -81,9 +79,9 @@ module StateMachines
81
79
  # # Search for explicit transitions regardless of the current state
82
80
  # events.transitions_for(vehicle, :from => :parked) # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
83
81
  def transitions_for(object, requirements = {})
84
- match(requirements).map {|event| event.transition_for(object, requirements)}.compact
82
+ match(requirements).map { |event| event.transition_for(object, requirements) }.compact
85
83
  end
86
-
84
+
87
85
  # Gets the transition that should be performed for the event stored in the
88
86
  # given object's event attribute. This also takes an additional parameter
89
87
  # for automatically invalidating the object if the event or transition are
@@ -115,27 +113,27 @@ module StateMachines
115
113
  # events.attribute_transition_for(vehicle) # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
116
114
  def attribute_transition_for(object, invalidate = false)
117
115
  return unless machine.action
118
-
119
- result = machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
120
- if event = self[event_name.to_sym, :name]
121
- event.transition_for(object) || begin
122
- # No valid transition: invalidate
123
- machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
124
- false
125
- end
126
- else
127
- # Event is unknown: invalidate
128
- machine.invalidate(object, :event, :invalid) if invalidate
129
- false
130
- end
131
- end
132
-
133
- result
116
+
117
+ # TODO, simplify
118
+ machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
119
+ if event = self[event_name.to_sym, :name]
120
+ event.transition_for(object) || begin
121
+ # No valid transition: invalidate
122
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
123
+ false
124
+ end
125
+ else
126
+ # Event is unknown: invalidate
127
+ machine.invalidate(object, :event, :invalid) if invalidate
128
+ false
129
+ end
130
+ end
131
+
134
132
  end
135
-
133
+
136
134
  private
137
- def match(requirements) #:nodoc:
138
- requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
139
- end
135
+ def match(requirements) #:nodoc:
136
+ requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
137
+ end
140
138
  end
141
139
  end
@@ -1,4 +1,3 @@
1
-
2
1
  module StateMachines
3
2
  module ClassMethods
4
3
  def self.extended(base) #:nodoc:
@@ -6,20 +5,20 @@ module StateMachines
6
5
  @state_machines = MachineCollection.new
7
6
  end
8
7
  end
9
-
8
+
10
9
  # Gets the current list of state machines defined for this class. This
11
10
  # class-level attribute acts like an inheritable attribute. The attribute
12
11
  # is available to each subclass, each having a copy of its superclass's
13
12
  # attribute.
14
- #
13
+ #
15
14
  # The hash of state machines maps <tt>:attribute</tt> => +machine+, e.g.
16
- #
15
+ #
17
16
  # Vehicle.state_machines # => {:state => #<StateMachines::Machine:0xb6f6e4a4 ...>}
18
17
  def state_machines
19
18
  @state_machines ||= superclass.state_machines.dup
20
19
  end
21
20
  end
22
-
21
+
23
22
  module InstanceMethods
24
23
  # Runs one or more events in parallel. All events will run through the
25
24
  # following steps:
@@ -27,7 +26,7 @@ module StateMachines
27
26
  # * Persist state
28
27
  # * Invoke action
29
28
  # * After callbacks
30
- #
29
+ #
31
30
  # For example, if two events (for state machines A and B) are run in
32
31
  # parallel, the order in which steps are run is:
33
32
  # * A - Before transition callbacks
@@ -38,62 +37,62 @@ module StateMachines
38
37
  # * B - Invoke action (only if different than A's action)
39
38
  # * A - After transition callbacks
40
39
  # * B - After transition callbacks
41
- #
40
+ #
42
41
  # *Note* that multiple events on the same state machine / attribute cannot
43
42
  # be run in parallel. If this is attempted, an ArgumentError will be
44
43
  # raised.
45
- #
44
+ #
46
45
  # == Halting callbacks
47
- #
46
+ #
48
47
  # When running multiple events in parallel, special consideration should
49
48
  # be taken with regard to how halting within callbacks affects the flow.
50
- #
49
+ #
51
50
  # For *before* callbacks, any <tt>:halt</tt> error that's thrown will
52
51
  # immediately cancel the perform for all transitions. As a result, it's
53
52
  # possible for one event's transition to affect the continuation of
54
53
  # another.
55
- #
54
+ #
56
55
  # On the other hand, any <tt>:halt</tt> error that's thrown within an
57
56
  # *after* callback with only affect that event's transition. Other
58
57
  # transitions will continue to run their own callbacks.
59
- #
58
+ #
60
59
  # == Example
61
- #
60
+ #
62
61
  # class Vehicle
63
62
  # state_machine :initial => :parked do
64
63
  # event :ignite do
65
64
  # transition :parked => :idling
66
65
  # end
67
- #
66
+ #
68
67
  # event :park do
69
68
  # transition :idling => :parked
70
69
  # end
71
70
  # end
72
- #
71
+ #
73
72
  # state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
74
73
  # event :enable do
75
74
  # transition all => :active
76
75
  # end
77
- #
76
+ #
78
77
  # event :disable do
79
78
  # transition all => :off
80
79
  # end
81
80
  # end
82
81
  # end
83
- #
82
+ #
84
83
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
85
84
  # vehicle.state # => "parked"
86
85
  # vehicle.alarm_state # => "active"
87
- #
86
+ #
88
87
  # vehicle.fire_events(:ignite, :disable_alarm) # => true
89
88
  # vehicle.state # => "idling"
90
89
  # vehicle.alarm_state # => "off"
91
- #
90
+ #
92
91
  # # If any event fails, the entire event chain fails
93
92
  # vehicle.fire_events(:ignite, :enable_alarm) # => false
94
93
  # vehicle.state # => "idling"
95
94
  # vehicle.alarm_state # => "off"
96
- #
95
+ #
97
96
  # # Exception raised on invalid event
98
97
  # vehicle.fire_events(:park, :invalid) # => StateMachines::InvalidEvent: :invalid is an unknown event
99
98
  # vehicle.state # => "idling"
@@ -101,48 +100,49 @@ module StateMachines
101
100
  def fire_events(*events)
102
101
  self.class.state_machines.fire_events(self, *events)
103
102
  end
104
-
103
+
105
104
  # Run one or more events in parallel. If any event fails to run, then
106
105
  # a StateMachines::InvalidTransition exception will be raised.
107
- #
106
+ #
108
107
  # See StateMachines::InstanceMethods#fire_events for more information.
109
- #
108
+ #
110
109
  # == Example
111
- #
110
+ #
112
111
  # class Vehicle
113
112
  # state_machine :initial => :parked do
114
113
  # event :ignite do
115
114
  # transition :parked => :idling
116
115
  # end
117
- #
116
+ #
118
117
  # event :park do
119
118
  # transition :idling => :parked
120
119
  # end
121
120
  # end
122
- #
121
+ #
123
122
  # state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
124
123
  # event :enable do
125
124
  # transition all => :active
126
125
  # end
127
- #
126
+ #
128
127
  # event :disable do
129
128
  # transition all => :off
130
129
  # end
131
130
  # end
132
131
  # end
133
- #
132
+ #
134
133
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
135
134
  # vehicle.fire_events(:ignite, :disable_alarm) # => true
136
- #
135
+ #
137
136
  # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
138
137
  def fire_events!(*events)
139
138
  run_action = [true, false].include?(events.last) ? events.pop : true
140
- fire_events(*(events + [run_action])) || raise(StateMachines::InvalidParallelTransition.new(self, events))
139
+ fire_events(*(events + [run_action])) || fail(StateMachines::InvalidParallelTransition.new(self, events))
141
140
  end
142
-
141
+
143
142
  protected
144
- def initialize_state_machines(options = {}, &block) #:nodoc:
145
- self.class.state_machines.initialize_states(self, options, &block)
146
- end
143
+
144
+ def initialize_state_machines(options = {}, &block) #:nodoc:
145
+ self.class.state_machines.initialize_states(self, options, &block)
146
+ end
147
147
  end
148
148
  end