ruby-state-machine 0.0.3 → 1.2.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 665d443a898a9f324aa6c3940c99287be592a92a
4
+ data.tar.gz: dad2a4f3bda51408aae0a0dcb9d456052bc7ee6f
5
+ SHA512:
6
+ metadata.gz: e42f12cfd35f6b1e787627262d4ec3d1776bc0e6e412ac1513c883e21389cc677b7c023a4a9613869cc2ca0a8623b08ab4b3b3acf9a3ca87685031638a4f9ff2
7
+ data.tar.gz: 5c709191cdab80cc7b21ee2eaecb5d66d33194fea49eecb2d5e584a83baf1aded13896b528fe771aefa04e862ae1eadb46eba0922cade197ecc01fa0833ebf3d
@@ -0,0 +1,86 @@
1
+ ## Ruby State Machine
2
+ Ruby State Machine (ruby-state-machine) is a full-featured state machine gem for use within ruby. It can also be used in Rails. This was written because we required a state machine that allowed different actions to be performed based on the previous and current events, as well as injecting logic (a "decider") to determine the next event.
3
+
4
+ ## Installation:
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ gem 'ruby-state-machine'
9
+
10
+ And then execute:
11
+
12
+ $ bundle
13
+
14
+ Or install it yourself as:
15
+
16
+ $ gem install ruby-state-machine
17
+
18
+ ## Location:
19
+ Here: http://github.com/tangledpath/ruby-state-machine
20
+
21
+ RDocs: http://ruby-state-mach.rubyforge.org/
22
+
23
+
24
+ ## USAGE:
25
+
26
+ ```ruby
27
+ require 'ruby-state-machine/state_machine'
28
+
29
+ # Note, a state machine is not created directly; instead, the behavior of a state
30
+ # machine is added through a mixin, e.g.:
31
+ class SampleMachine
32
+ include StateMachine
33
+ state_machine :states => [:a_state, :b_state, :c_state, :d_state], :events => [:w_event, :x_event, :y_event, :z_event]
34
+ state_transition :state=>:a_state, :event=>:x_event, :next=>:c_state # Define next state for :a_state when :x_event is sent
35
+ state_transition :state=>:a_state, :event=>:y_event, :next=>:a_state # Define next state for :a_state when :y_event is sent
36
+ state_transition :state=>:a_state, :event=>:z_event, :next=>:b_state # ...
37
+
38
+ state_transition :state=>:b_state, :event=>:w_event, :next=>:b_state
39
+ state_transition :state=>:b_state, :event=>:y_event, :next=>:c_state
40
+ state_transition :state=>:b_state, :event=>:z_event, :next=>:a_state
41
+
42
+ state_transition :state=>:c_state, :event=>:x_event, :next=>:b_state
43
+ end
44
+
45
+ sm = SampleMachine.new
46
+ puts sm.current_state # :a_state
47
+ sm.send_event(:x_event)
48
+ puts sm.current_state # :c_state
49
+ ```
50
+
51
+ For examples of other functionality, including branching, deciders, lambdas, etc, see http://ruby-state-mach.rubyforge.org/StateMachine/ClassMethods.html#state_transition-instance_method.
52
+
53
+
54
+ ## CONTRIBUTE
55
+
56
+ 1. Fork it
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
61
+
62
+ ## LICENSE:
63
+
64
+ (The MIT License)
65
+
66
+ Copyright (c) 2007-2013 Steven Miers
67
+
68
+ Permission is hereby granted, free of charge, to any person obtaining
69
+ a copy of this software and associated documentation files (the
70
+ 'Software'), to deal in the Software without restriction, including
71
+ without limitation the rights to use, copy, modify, merge, publish,
72
+ distribute, sublicense, and/or sell copies of the Software, and to
73
+ permit persons to whom the Software is furnished to do so, subject to
74
+ the following conditions:
75
+
76
+ The above copyright notice and this permission notice shall be
77
+ included in all copies or substantial portions of the Software.
78
+
79
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
80
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
81
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
82
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
83
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
84
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
85
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
86
+
@@ -1,6 +1,6 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
1
+ require "ruby-state-machine/version"
2
+ #require "ruby-state-machine/state_machine"
3
3
 
4
4
  module RubyStateMachine
5
- VERSION = '0.0.3'
5
+
6
6
  end
@@ -0,0 +1,8 @@
1
+ class BoundedArray < Array
2
+ attr_accessor :bounded_size
3
+ def push(args)
4
+ super
5
+ @bounded_size ||= 10
6
+ shift until size <= bounded_size
7
+ end
8
+ end
@@ -0,0 +1,252 @@
1
+ require 'ruby-state-machine/bounded_array'
2
+
3
+ # StateMachine
4
+ # Simple and flexible state machine. Define a state machine with an array of states,
5
+ # an array of events, and one or more transition actions. A transition can be
6
+ # as simple as the symbol for the next state, a lambda (code) to execute. Primitive
7
+ # branching can also be achieved if necessary by using a "decider" instance method.
8
+ #
9
+ # The {StateMachineTest} (view source) also contains examples and unit tests for most (if not all)
10
+ # of the available functionality.
11
+ # Also see {http://ruby-state-mach.rubyforge.org/ README} for examples
12
+ # @see ClassMethods#state_transition StateTransition for full details on the variations available.
13
+ # @see StateMachineTest
14
+ module StateMachine
15
+ def self.included(base)
16
+ base.extend StateMachine::ClassMethods
17
+ end
18
+
19
+
20
+ module ClassMethods
21
+ attr_accessor :machine
22
+
23
+ # Create state machine with the following options:
24
+ # states Array[symbols], each symbol is name of state
25
+ # events Array[symbols], each symbol is name of event
26
+ # default_state symbol, symbol is one of states
27
+ def state_machine(opts)
28
+ @machine = StateMachine::Machine.new(opts)
29
+ self.send(:include, StateMachine::InstanceMethods)
30
+ end
31
+
32
+ # Add a transition to state machine with the following keys:
33
+ # :state=>state (symbol) for which transition is being declared
34
+ # :event=>event (symbol) which causes transition
35
+ # :decider=>name of instance method on state machine specialization that "decides"
36
+ # next state in case of multiple actions for next state (see below).
37
+ # Method in question should return name of next state, or name of action
38
+ # if actions are named (not required).
39
+ # :next=>action to take for this transition, one of:
40
+ # 1) state name(symbol), or special symbol (:stay), which stays at current state
41
+ # 2) hash{:state=>state name(symbol), :action=>code/lambda(string), :name=>id of action, for decider}
42
+ # 3) array of 1 and/or 2. Decider required in this case.
43
+ #
44
+ # Example 1:
45
+ # state_transition :state=>:c_state, :event=>:w_event, :decider => :c_state_decider,
46
+ # :next=>[
47
+ # {:state=>:a_state, :action=>lambda{|o, args| o.increment_value(7)} },
48
+ # {:state=>:b_state, :action=>lambda{|o, args| o.increment_value(-10)} }
49
+ # ]
50
+ #
51
+ # Example 2 (stays at current state):
52
+ # state_transition :state=>:c_state, :event=>:y_event, :next=>{:state=>:stay, :action=>lambda{|o, args| o.increment_value(-3)}}
53
+ #
54
+ # Example 3:
55
+ # state_transition :state=>:b_state, :event=>:z_event, :next=>:a_state
56
+ def state_transition(opts)
57
+ @machine.add_transition(opts)
58
+ end
59
+
60
+ def default_state; @machine.default_state; end
61
+ def states; @machine.states; end
62
+ def events; @machine.events; end
63
+ def transitions; @machine.transitions; end
64
+ def next_state(*args); @machine.next_state(*args); end
65
+ def next_state_instruction(*args); @machine.next_state_instruction(*args); end
66
+ end
67
+
68
+ module InstanceMethods
69
+ attr_accessor :event_history
70
+
71
+ def initialize(args=nil)
72
+ @current_state = default_state
73
+ @event_history = BoundedArray.new
74
+ # Attempt to call super:
75
+ begin
76
+ super(args)
77
+ rescue ArgumentError
78
+ super()
79
+ end
80
+ #puts "Current state is #{@current_state} #{@current_state.class}"
81
+ end
82
+
83
+ def send_event(event)
84
+ # puts "Sending event: #{event}"
85
+ check_current_state
86
+ next_state_instruction = self.class.next_state_instruction(@current_state, event)
87
+ if next_state_instruction.nil?
88
+ cs = @current_state
89
+ # This was causing problems in unit tests:
90
+ # @current_state = default_state
91
+ # puts "Returned to default state: [#{@current_state}]."
92
+ raise InvalidStateError, "No valid next state for #{cs.inspect} using event #{event.inspect} (#{cs.class}/#{event.class})."
93
+ end
94
+
95
+ if(next_state_instruction)
96
+ if(!String(next_state_instruction[:decider]).empty?)
97
+ # Decider present, call method:
98
+ decide_id = execute(next_state_instruction[:decider], event)
99
+
100
+ # Error checking:
101
+ if String(decide_id).empty?
102
+ raise InvalidStateError,
103
+ "Decider returned blank/nil for #{@current_state} using event #{event}. Next state from decider: [#{decide_id.inspect}]. Possible next states are [#{next_state_instruction[:next].inspect}]"
104
+ end
105
+
106
+ # Find next state:
107
+ instruction = Array(next_state_instruction[:next]).detect { |i|
108
+ i[:state].to_sym==decide_id.to_sym or i[:name].to_sym==decide_id.to_sym
109
+ }
110
+
111
+ # Error checking:
112
+ if instruction.nil?
113
+ raise InvalidStateError,
114
+ "No valid next instruction for #{@current_state} using event #{event}. Next state from decider: [#{decide_id.inspect}(#{decide_id.class})]. Possible next states are [#{next_state_instruction[:next].inspect}]"
115
+ end
116
+
117
+ # Do it:
118
+ process_instruction(instruction, event)
119
+ else
120
+ # Do it:
121
+ process_instruction(next_state_instruction[:next], event)
122
+ end
123
+ @current_state
124
+ end
125
+ @event_history.push(event)
126
+ end
127
+
128
+ def current_state
129
+ check_current_state
130
+ @current_state
131
+ end
132
+
133
+ def current_state=state
134
+ @current_state = state.to_sym
135
+ end
136
+
137
+ def default_state
138
+ (self.class.default_state) ? self.class.default_state : self.class.states.first
139
+ end
140
+
141
+ private
142
+
143
+ def process_instruction(instruction, event)
144
+ if instruction.nil?
145
+ @current_state = default_state
146
+ puts "Returned to default state: [#{@current_state}]."
147
+ raise InvalidStateError, "No valid next instruction for #{@current_state} using event #{event}"
148
+ end
149
+
150
+ if(instruction.is_a?Symbol)
151
+ change_state(instruction)
152
+ else
153
+ #puts "Processing action: #{instruction[:action]}"
154
+ execute(instruction[:action], event)
155
+ change_state(instruction[:state])
156
+ end
157
+ end
158
+
159
+ def execute(action, event)
160
+ unless action.nil?
161
+ if Symbol === action
162
+ self.method(action).call(:event=>event)
163
+ else
164
+ action.call(self, :event=>event)
165
+ end
166
+ end
167
+ end
168
+
169
+ def change_state(state_sym)
170
+ case(state_sym)
171
+ when :back
172
+ raise InvalidStateError, "Back is reserved but not yet implemented."
173
+ when :stay
174
+ @current_state = @current_state # nop, but should queue
175
+ else
176
+ @current_state = state_sym
177
+ end
178
+ end
179
+
180
+ def check_current_state
181
+ raise InvalidStateError, "No valid current state. Please call super() from state machine impl." if @current_state.nil?
182
+ end
183
+
184
+ end
185
+
186
+ class Machine
187
+ attr_accessor :states
188
+ attr_accessor :events
189
+ attr_accessor :transitions
190
+ attr_accessor :default_state
191
+
192
+ def initialize(opts)
193
+ @states=opts[:states].collect{|state| state.to_sym}
194
+ @events=opts[:events].collect{|event| event.to_sym}
195
+ @default_state=opts[:default_state].to_sym if opts[:default_state]
196
+ @transitions=[]
197
+ end
198
+
199
+ def add_transition(opts)
200
+ new_opts=strings_to_sym(opts)
201
+
202
+ # Some validation for arrays of actions:
203
+ if (new_opts[:next].is_a?Array)
204
+ if new_opts[:next].length==1
205
+ new_opts[:next] = new_opts[:next].first
206
+ elsif new_opts[:next].length>1
207
+ raise ArgumentError, "A decider must be present for multiple actions." if String(new_opts[:decider]).empty?
208
+ end
209
+ end
210
+
211
+ @transitions << new_opts
212
+ end
213
+
214
+ def strings_to_sym(hash)
215
+ new_hash=hash.dup
216
+ hash.each do |k, v|
217
+ unless (v.nil? or (v.respond_to?:empty? and v.empty? ))
218
+ if (v.is_a?(Hash))
219
+ new_hash[k] = strings_to_sym(v)
220
+ elsif (v.is_a?(Array))
221
+ new_array = v.dup
222
+ v.each_with_index do |array_hash, index|
223
+ new_array[index] = strings_to_sym(array_hash) if array_hash.is_a?(Hash)
224
+ end
225
+ new_hash[k]=new_array
226
+ elsif (v.is_a?(String))
227
+ new_hash[k] = v.to_sym
228
+ end
229
+ end
230
+ end
231
+ new_hash
232
+ end
233
+
234
+ # Next state (symbol only) for given state and event.
235
+ # (equivalent to next_state_instruction(state, event)[:next_state])
236
+ def next_state(state, event)
237
+ transition = next_state_instruction(state, event)
238
+ ns = transition ? transition[:next] : nil
239
+ (ns.is_a?Symbol) ? ns : ns && ns[:state]
240
+ end
241
+
242
+ # Next state instruction (as hash) for given state and event:
243
+ def next_state_instruction(state, event)
244
+ @transitions.detect{|t| t[:state]==state and t[:event]==event}
245
+ end
246
+ end
247
+
248
+ class InvalidStateError < Exception #:nodoc:
249
+
250
+ end
251
+
252
+ end
@@ -0,0 +1,8 @@
1
+ module RubyStateMachine
2
+ module VERSION
3
+ MAJOR = 1
4
+ MINOR = 2
5
+ TINY = 0
6
+ STRING = [MAJOR, MINOR, TINY].join('.')
7
+ end
8
+ end
metadata CHANGED
@@ -1,85 +1,50 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: ruby-state-machine
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.3
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
5
  platform: ruby
6
- authors:
7
- - stevenmiers
8
- autorequire:
6
+ authors:
7
+ - tangledpath
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
-
12
- date: 2009-08-18 00:00:00 -05:00
13
- default_executable:
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: hoe
17
- type: :development
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: 2.3.3
24
- version:
25
- description: FIX (describe your package)
26
- email:
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A ruby state machine
14
+ email:
27
15
  - steven.miers@gmail.com
28
16
  executables: []
29
-
30
17
  extensions: []
31
-
32
- extra_rdoc_files:
33
- - History.txt
34
- - Manifest.txt
35
- - PostInstall.txt
36
- - website/index.txt
37
- files:
38
- - History.txt
39
- - Manifest.txt
40
- - PostInstall.txt
41
- - README.rdoc
42
- - Rakefile
43
- - config/website.yml
18
+ extra_rdoc_files:
19
+ - README.md
20
+ files:
21
+ - README.md
44
22
  - lib/ruby-state-machine.rb
45
- - script/console
46
- - script/destroy
47
- - script/generate
48
- - script/txt2html
49
- - test/test_helper.rb
50
- - test/test_state_machine.rb
51
- - website/index.html
52
- - website/index.txt
53
- - website/javascripts/rounded_corners_lite.inc.js
54
- - website/stylesheets/screen.css
55
- - website/template.html.erb
56
- has_rdoc: true
57
- homepage: http://github.com/#{github_username}/#{project_name}
58
- post_install_message: PostInstall.txt
59
- rdoc_options:
60
- - --main
61
- - README.rdoc
62
- require_paths:
23
+ - lib/ruby-state-machine/bounded_array.rb
24
+ - lib/ruby-state-machine/state_machine.rb
25
+ - lib/ruby-state-machine/version.rb
26
+ homepage: http://github.com/tangledpath/ruby-state-machine
27
+ licenses: []
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
63
32
  - lib
64
- required_ruby_version: !ruby/object:Gem::Requirement
65
- requirements:
33
+ - ext
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
66
36
  - - ">="
67
- - !ruby/object:Gem::Version
68
- version: "0"
69
- version:
70
- required_rubygems_version: !ruby/object:Gem::Requirement
71
- requirements:
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
72
41
  - - ">="
73
- - !ruby/object:Gem::Version
74
- version: "0"
75
- version:
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
76
44
  requirements: []
77
-
78
- rubyforge_project: ruby-state-mach
79
- rubygems_version: 1.3.1
80
- signing_key:
81
- specification_version: 2
82
- summary: FIX (describe your package)
83
- test_files:
84
- - test/test_helper.rb
85
- - test/test_state_machine.rb
45
+ rubyforge_project: ruby-state-machine
46
+ rubygems_version: 2.6.10
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: A full-featured state machine gem for use within ruby.
50
+ test_files: []