state_machines 0.0.1 → 0.0.2

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