finite_machine 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f31937c5d49cee8c974c90b8339c1e3aa57f4866
4
- data.tar.gz: 5386d3a848f6dd01d05d174d165447366c80ed19
3
+ metadata.gz: 1b14a2db5bcab11179dbd4cd9abd10826cda11e1
4
+ data.tar.gz: 1b5046cc5cb28b7cd082bce38088ed34c66d6aba
5
5
  SHA512:
6
- metadata.gz: d8ca32da8076f07a43c53650ebdb99a22ba4e0fb4f721db972caeed7df91fb70feecde074b022e79798b5318c07ab59fddfadc91ddf131d4a4ea4d32b477e946
7
- data.tar.gz: 674cc1ae00eb4fafe21adc37721a2bc92f7a482c2755599dbf1c884bb8e1e7f7591bdcaaf09922e54c2204fd3cd64042f43db5b3c9a939c7a2e0c93321ab940a
6
+ metadata.gz: 340d0ff2ea70b6f54127e06c2d2d6f7fe44309e44f5c8c10125aee29a363d8587890369cca037a7b09d614396f8758919f7b2cea67cdca8f15ed5e2679c1315a
7
+ data.tar.gz: 0372935e0396e9863700879fb906330d191d2f013d844eae8af58b4853a94b23a6b547c797779da77aeaf71db5fb38d38031ef4f7134864b2708aa900633f06a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ 0.8.1 (July 5, 2014)
2
+
3
+ * Add EventsChain to handle internal events logic
4
+ * Add EventBuilder to handle events construction
5
+
1
6
  0.8.0 (June 22, 2014)
2
7
 
3
8
  * Add silent option for state machine events to allow turning on/off
data/README.md CHANGED
@@ -81,7 +81,8 @@ Or install it yourself as:
81
81
  * [6. Errors](#6-errors)
82
82
  * [6.1 Using target](#61-using-target)
83
83
  * [7. Integration](#7-integration)
84
- * [7.1 ActiveRecord](#71-activerecord)
84
+ * [7.1 Plain Ruby Objects](#71-plain-ruby-objects)
85
+ * [7.2 ActiveRecord](#72-activerecord)
85
86
  * [8. Tips](#7-tips)
86
87
 
87
88
  ## 1 Usage
@@ -988,12 +989,14 @@ end
988
989
 
989
990
  ## 7 Integration
990
991
 
991
- Since **FiniteMachine** is an object in its own right it leaves integration with other systems up to you. In contrast to other Ruby libraries, it does not extend from models (i.e. ActiveRecord) to transform them into a state machine or require mixing into exisiting classes.
992
+ Since **FiniteMachine** is an object in its own right, it leaves integration with other systems up to you. In contrast to other Ruby libraries, it does not extend from models (i.e. ActiveRecord) to transform them into a state machine or require mixing into exisiting classes.
993
+
994
+ ### 7.1 Plain Ruby Objects
995
+
996
+ In order to use **FiniteMachine** with an object, you need to define a method that will construct the state machine. You can implement the state machine using the `define` DSL or create a seperate object that can be instantiated. To complete integration you will need to specify `target` context to allow state machine to communicate with the other methods inside the class like so:
992
997
 
993
998
  ```ruby
994
999
  class Car
995
- attr_accessor :reverse_lights
996
-
997
1000
  def turn_reverse_lights_off
998
1001
  @reverse_lights = false
999
1002
  end
@@ -1002,6 +1005,10 @@ class Car
1002
1005
  @reverse_lights = true
1003
1006
  end
1004
1007
 
1008
+ def reverse_lights_on?
1009
+ @reverse_lights || false
1010
+ end
1011
+
1005
1012
  def gears
1006
1013
  context = self
1007
1014
  @gears ||= FiniteMachine.define do
@@ -1034,7 +1041,21 @@ class Car
1034
1041
  end
1035
1042
  ```
1036
1043
 
1037
- ### 7.1 ActiveRecord
1044
+ Having written the class, you can use it as follows:
1045
+
1046
+ ```ruby
1047
+ car = Car.new
1048
+ car.gears.current # => :neutral
1049
+ car.reverse_lights_on? # => false
1050
+
1051
+ car.gears.start # => "shifted from neutral to one"
1052
+
1053
+ car.gears.back # => "shifted from one to reverse"
1054
+ car.gears.current # => :reverse
1055
+ car.reverse_lights_on? # => true
1056
+ ```
1057
+
1058
+ ### 7.2 ActiveRecord
1038
1059
 
1039
1060
  In order to integrate **FiniteMachine** with ActiveRecord use the `target` helper to reference the current class and call ActiveRecord methods inside the callbacks to persist the state.
1040
1061
 
@@ -1042,8 +1063,9 @@ In order to integrate **FiniteMachine** with ActiveRecord use the `target` helpe
1042
1063
  class Account < ActiveRecord::Base
1043
1064
  validates :state, presence: true
1044
1065
 
1045
- def initialize
1046
- self.state = :unapproved
1066
+ def initialize(attrs = {})
1067
+ super
1068
+ @manage.restore!(state) if state
1047
1069
  end
1048
1070
 
1049
1071
  def manage
@@ -1051,7 +1073,7 @@ class Account < ActiveRecord::Base
1051
1073
  @manage ||= FiniteMachine.define do
1052
1074
  target context
1053
1075
 
1054
- initial context.state
1076
+ initial :unapproved
1055
1077
 
1056
1078
  events {
1057
1079
  event :enqueue, :unapproved => :pending
@@ -1060,7 +1082,7 @@ class Account < ActiveRecord::Base
1060
1082
 
1061
1083
  callbacks {
1062
1084
  on_enter_state do |event|
1063
- state = event.to
1085
+ self.state = event.to
1064
1086
  save
1065
1087
  end
1066
1088
  }
@@ -3,6 +3,7 @@
3
3
  require "logger"
4
4
  require "thread"
5
5
  require "sync"
6
+ require "forwardable"
6
7
 
7
8
  require "finite_machine/version"
8
9
  require "finite_machine/threadable"
@@ -15,7 +16,9 @@ require "finite_machine/async_proxy"
15
16
  require "finite_machine/async_call"
16
17
  require "finite_machine/hook_event"
17
18
  require "finite_machine/event"
19
+ require "finite_machine/event_builder"
18
20
  require "finite_machine/event_queue"
21
+ require "finite_machine/events_chain"
19
22
  require "finite_machine/hooks"
20
23
  require "finite_machine/logger"
21
24
  require "finite_machine/transition"
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # A class responsible for building event methods
5
+ class EventBuilder
6
+ include Threadable
7
+ include Safety
8
+
9
+ # The current state machine
10
+ attr_threadsafe :machine
11
+
12
+ # Initialize an EventBuilder
13
+ #
14
+ # @param [FiniteMachine::StateMachine] machine
15
+ #
16
+ # @api private
17
+ def initialize(machine)
18
+ @machine = machine
19
+ end
20
+
21
+ # Build state machine events
22
+ #
23
+ # @param [FiniteMachine::Transition] transition
24
+ # the transition for which event is build
25
+ #
26
+ # @return [FiniteMachine::Transition]
27
+ #
28
+ # @api private
29
+ def call(transition)
30
+ name = transition.name
31
+ detect_event_conflict!(name)
32
+ if machine.singleton_class.send(:method_defined?, name)
33
+ machine.events_chain.insert(name, transition)
34
+ else
35
+ define_event_transition(name, transition)
36
+ define_event_bang(name)
37
+ end
38
+ transition
39
+ end
40
+
41
+ private
42
+
43
+ # Define transition event
44
+ #
45
+ # @param [Symbol] name
46
+ # the event name
47
+ #
48
+ # @param [FiniteMachine::Transition] transition
49
+ # the transition this event is associated with
50
+ #
51
+ # @return [nil]
52
+ #
53
+ # @api private
54
+ def define_event_transition(name, transition)
55
+ silent = transition.silent
56
+ _event = FiniteMachine::Event.new(machine, name: name, silent: silent)
57
+ _event << transition
58
+ machine.events_chain.add(name, _event)
59
+
60
+ machine.send(:define_singleton_method, name) do |*args, &block|
61
+ _event.call(*args, &block)
62
+ end
63
+ end
64
+
65
+ # Define event that skips validations
66
+ #
67
+ # @param [Symbol] name
68
+ # the event name
69
+ #
70
+ # @return [nil]
71
+ #
72
+ # @api private
73
+ def define_event_bang(name)
74
+ machine.send(:define_singleton_method, "#{name}!") do
75
+ transitions = machine.transitions[name]
76
+ machine.state = transitions.values[0]
77
+ end
78
+ end
79
+ end # EventBuilder
80
+ end # FiniteMachine
@@ -0,0 +1,118 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # A class responsible for storing chain of events
5
+ class EventsChain
6
+ include Threadable
7
+ extend Forwardable
8
+
9
+ # The current state machine
10
+ attr_threadsafe :machine
11
+
12
+ # The chain of events
13
+ attr_threadsafe :chain
14
+
15
+ def_delegators :@chain, :[], :empty?
16
+
17
+ # Initialize a EventsChain
18
+ #
19
+ # @param [FiniteMachine::StateMachine] machine
20
+ # the state machine
21
+ #
22
+ # @api public
23
+ def initialize(machine)
24
+ @machine = machine
25
+ @chain = {}
26
+ end
27
+
28
+ # Insert transition under given event name
29
+ #
30
+ # @param [Symbol] name
31
+ # the event name
32
+ #
33
+ # @param [FiniteMachine::Transition]
34
+ #
35
+ # @return [nil]
36
+ #
37
+ # @api public
38
+ def insert(name, transition)
39
+ return false unless chain[name]
40
+ chain[name] << transition
41
+ end
42
+
43
+ # Add event under name
44
+ #
45
+ # @return [nil]
46
+ #
47
+ # @api public
48
+ def add(name, event)
49
+ chain[name] = event
50
+ end
51
+
52
+ # Check if event is valid and transition can be performed
53
+ #
54
+ # @return [Boolean]
55
+ #
56
+ # @api public
57
+ def valid_event?(event, *args, &block)
58
+ chain[event].next_transition.valid?(*args, &block)
59
+ end
60
+
61
+ # Select transition that passes constraints condition
62
+ #
63
+ # @param [Symbol] name
64
+ # the event name
65
+ #
66
+ # @return [FiniteMachine::Transition]
67
+ #
68
+ # @api public
69
+ def select_transition(name, *args)
70
+ chain[name].find_transition(*args)
71
+ end
72
+
73
+ # Check if any of the transition constraints passes
74
+ #
75
+ # @param [Symbol] name
76
+ # the event name
77
+ #
78
+ # @return [Boolean]
79
+ #
80
+ # @api public
81
+ def check_choice_conditions(name, *args, &block)
82
+ chain[name].state_transitions.any? do |trans|
83
+ trans.check_conditions(*args, &block)
84
+ end
85
+ end
86
+
87
+ # Reset chain
88
+ #
89
+ # @return [self]
90
+ #
91
+ # @api public
92
+ def clear
93
+ @chain.clear
94
+ self
95
+ end
96
+
97
+ # Return string representation of this chain
98
+ #
99
+ # @return [String]
100
+ #
101
+ # @api public
102
+ def to_s
103
+ chain.to_s
104
+ end
105
+
106
+ # Inspect chain content
107
+ #
108
+ # @example
109
+ # events_chain.inspect
110
+ #
111
+ # @return [String]
112
+ #
113
+ # @api public
114
+ def inspect
115
+ "<##{self.class} @chain=#{chain.inspect}>"
116
+ end
117
+ end # EventsChain
118
+ end # FiniteMachine
@@ -1,7 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'forwardable'
4
-
5
3
  module FiniteMachine
6
4
  # Base class for state machine
7
5
  class StateMachine
@@ -47,6 +45,8 @@ module FiniteMachine
47
45
 
48
46
  def_delegator :@events_dsl, :event
49
47
 
48
+ def_delegators :@events_chain, :check_choice_conditions, :select_transition
49
+
50
50
  # Initialize state machine
51
51
  #
52
52
  # @api private
@@ -58,7 +58,7 @@ module FiniteMachine
58
58
  @errors = ErrorsDSL.new(self)
59
59
  @observer = Observer.new(self)
60
60
  @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
61
- @events_chain = {}
61
+ @events_chain = EventsChain.new(self)
62
62
  @env = Environment.new(target: self)
63
63
  @dsl = DSL.new(self, attributes)
64
64
 
@@ -177,7 +177,7 @@ module FiniteMachine
177
177
  event = args.shift
178
178
  valid_state = transitions[event].key?(current)
179
179
  valid_state ||= transitions[event].key?(ANY_STATE)
180
- valid_state &&= events_chain[event].next_transition.valid?(*args, &block)
180
+ valid_state &&= events_chain.valid_event?(event, *args, &block)
181
181
  end
182
182
 
183
183
  # Checks if event cannot be triggered
@@ -4,7 +4,6 @@ module FiniteMachine
4
4
  # Class describing a transition associated with a given event
5
5
  class Transition
6
6
  include Threadable
7
- include Safety
8
7
 
9
8
  attr_threadsafe :name
10
9
 
@@ -67,8 +66,9 @@ module FiniteMachine
67
66
  _transition = new(machine, attrs)
68
67
  _transition.update_transitions
69
68
  _transition.define_state_methods
70
- _transition.define_event
71
- _transition
69
+
70
+ builder = EventBuilder.new(machine)
71
+ builder.call(_transition)
72
72
  end
73
73
 
74
74
  # Decide :to state from available transitions for this event
@@ -77,8 +77,8 @@ module FiniteMachine
77
77
  #
78
78
  # @api public
79
79
  def to_state(*args)
80
- if machine.transitions[name][from_state].is_a? Array
81
- found_trans = machine.events_chain[name].find_transition(*args)
80
+ if transition_choice?
81
+ found_trans = machine.select_transition(name, *args)
82
82
  found_trans.map[from_state]
83
83
  else
84
84
  machine.transitions[name][from_state]
@@ -116,6 +116,15 @@ module FiniteMachine
116
116
  map[state] == state || (map[ANY_STATE] == state && from_state == state)
117
117
  end
118
118
 
119
+ # Check if this transition has branching choice or not
120
+ #
121
+ # @return [Boolean]
122
+ #
123
+ # @api public
124
+ def transition_choice?
125
+ machine.transitions[name][from_state].is_a?(Array)
126
+ end
127
+
119
128
  # Check if transition can be performed according to constraints
120
129
  #
121
130
  # @param [Array] args
@@ -126,10 +135,8 @@ module FiniteMachine
126
135
  #
127
136
  # @api public
128
137
  def valid?(*args, &block)
129
- if machine.transitions[name][from_state].is_a? Array
130
- machine.events_chain[name].state_transitions.any? do |trans|
131
- trans.check_conditions(*args, &block)
132
- end
138
+ if transition_choice?
139
+ machine.check_choice_conditions(name, *args, &block)
133
140
  else
134
141
  check_conditions(*args, &block)
135
142
  end
@@ -169,39 +176,16 @@ module FiniteMachine
169
176
  end
170
177
  end
171
178
 
172
- # Define event on the machine
179
+ # Set state on the machine
173
180
  #
174
181
  # @api private
175
- def define_event
176
- detect_event_conflict!(name)
177
- if machine.singleton_class.send(:method_defined?, name)
178
- machine.events_chain[name] << self
182
+ def update_state(*args)
183
+ if transition_choice?
184
+ found_trans = machine.select_transition(name, *args)
185
+ machine.state = found_trans.to_states.first
179
186
  else
180
- define_event_transition(name)
181
- define_event_bang(name)
182
- end
183
- end
184
-
185
- # Define transition event
186
- #
187
- # @api private
188
- def define_event_transition(name)
189
- _event = FiniteMachine::Event.new(machine, name: name, silent: silent)
190
- _event << self
191
- machine.events_chain[name] = _event
192
-
193
- machine.send(:define_singleton_method, name) do |*args, &block|
194
- _event.call(*args, &block)
195
- end
196
- end
197
-
198
- # Define event that skips validations
199
- #
200
- # @api private
201
- def define_event_bang(name)
202
- machine.send(:define_singleton_method, "#{name}!") do
203
187
  transitions = machine.transitions[name]
204
- machine.state = transitions.values[0]
188
+ machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
205
189
  end
206
190
  end
207
191
 
@@ -213,14 +197,8 @@ module FiniteMachine
213
197
  def call(*args)
214
198
  sync_exclusive do
215
199
  return if cancelled
216
- transitions = machine.transitions[name]
217
200
  self.from_state = machine.state
218
- if machine.transitions[name][from_state].is_a? Array
219
- found_trans = machine.events_chain[name].find_transition(*args)
220
- machine.state = found_trans.to_states.first
221
- else
222
- machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
223
- end
201
+ update_state(*args)
224
202
  machine.initial_state = machine.state if from_state == DEFAULT_STATE
225
203
  end
226
204
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  end
@@ -14,4 +14,8 @@ describe FiniteMachine::Event, '#inspect' do
14
14
  event << transition
15
15
  expect(event.inspect).to eq("<#FiniteMachine::Event @name=test, @transitions=[#{transition.inspect}]>")
16
16
  end
17
+
18
+ it "prints event name" do
19
+ expect(event.to_s).to eq('test')
20
+ end
17
21
  end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine::EventsChain, '#clear' do
6
+ let(:object) { described_class }
7
+
8
+ let(:machine) { double(:machine) }
9
+
10
+ subject(:chain) { object.new(machine) }
11
+
12
+ it "clears chain events" do
13
+ event = double(:event)
14
+ chain.add(:validated, event)
15
+ expect(chain.empty?).to be_false
16
+
17
+ chain.clear
18
+ expect(chain.empty?).to be_true
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine::EventsChain, '#insert' do
6
+ let(:object) { described_class }
7
+
8
+ let(:machine) { double(:machine) }
9
+
10
+ let(:transition) { double(:transition) }
11
+
12
+ subject(:chain) { object.new(machine) }
13
+
14
+ it "inserts transition" do
15
+ event = double(:event)
16
+ chain.add(:validated, event)
17
+ expect(chain[:validated]).to eq(event)
18
+
19
+ expect(event).to receive(:<<).with(transition)
20
+ chain.insert(:validated, transition)
21
+ end
22
+
23
+ it "fails to insert transition" do
24
+ expect(chain.insert(:validated, transition)).to be_false
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine::EventsChain, '#insert' do
6
+ let(:object) { described_class }
7
+
8
+ let(:machine) { double(:machine) }
9
+
10
+ let(:transition) { double(:transition) }
11
+
12
+ subject(:chain) { object.new(machine) }
13
+
14
+ it "inserts transition" do
15
+ event = double(:event)
16
+ chain.add(:validated, event)
17
+ expect(chain[:validated]).to eq(event)
18
+
19
+ expect(event).to receive(:<<).with(transition)
20
+ chain.insert(:validated, transition)
21
+ end
22
+
23
+ it "fails to insert transition" do
24
+ expect(chain.insert(:validated, transition)).to be_false
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine::EventsChain, '#insert' do
6
+ let(:object) { described_class }
7
+
8
+ let(:machine) { double(:machine) }
9
+
10
+ subject(:chain) { object.new(machine) }
11
+
12
+ it "inspects empty chain" do
13
+ expect(chain.inspect).to eq("<#FiniteMachine::EventsChain @chain={}>")
14
+ end
15
+
16
+ it "inspect chain" do
17
+ event = double(:event)
18
+ chain.add(:validated, event)
19
+ expect(chain.inspect).to eq("<#FiniteMachine::EventsChain @chain=#{{validated: event}}>")
20
+ end
21
+
22
+ it "prints chain" do
23
+ event = double(:event)
24
+ chain.add(:validated, event)
25
+ expect(chain.to_s).to eq("#{{validated: event}}")
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine::EventsChain, '#select_transition' do
6
+ let(:object) { described_class }
7
+
8
+ let(:machine) { double(:machine) }
9
+
10
+ let(:transition) { double(:transition) }
11
+
12
+ subject(:chain) { object.new(machine) }
13
+
14
+ it "selects transition" do
15
+ event = double(:event)
16
+ args = double(:args)
17
+ chain.add(:validated, event)
18
+ expect(chain[:validated]).to eq(event)
19
+
20
+ expect(event).to receive(:find_transition).with(args)
21
+ chain.select_transition(:validated, args)
22
+ end
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: finite_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-22 00:00:00.000000000 Z
11
+ date: 2014-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -54,7 +54,9 @@ files:
54
54
  - lib/finite_machine/choice_merger.rb
55
55
  - lib/finite_machine/dsl.rb
56
56
  - lib/finite_machine/event.rb
57
+ - lib/finite_machine/event_builder.rb
57
58
  - lib/finite_machine/event_queue.rb
59
+ - lib/finite_machine/events_chain.rb
58
60
  - lib/finite_machine/hook_event.rb
59
61
  - lib/finite_machine/hooks.rb
60
62
  - lib/finite_machine/listener.rb
@@ -80,6 +82,11 @@ files:
80
82
  - spec/unit/event/inspect_spec.rb
81
83
  - spec/unit/event/next_transition_spec.rb
82
84
  - spec/unit/event_queue_spec.rb
85
+ - spec/unit/events_chain/check_choice_conditions_spec.rb
86
+ - spec/unit/events_chain/clear_spec.rb
87
+ - spec/unit/events_chain/insert_spec.rb
88
+ - spec/unit/events_chain/inspect_spec.rb
89
+ - spec/unit/events_chain/select_transition_spec.rb
83
90
  - spec/unit/events_spec.rb
84
91
  - spec/unit/finished_spec.rb
85
92
  - spec/unit/handlers_spec.rb
@@ -138,6 +145,11 @@ test_files:
138
145
  - spec/unit/event/inspect_spec.rb
139
146
  - spec/unit/event/next_transition_spec.rb
140
147
  - spec/unit/event_queue_spec.rb
148
+ - spec/unit/events_chain/check_choice_conditions_spec.rb
149
+ - spec/unit/events_chain/clear_spec.rb
150
+ - spec/unit/events_chain/insert_spec.rb
151
+ - spec/unit/events_chain/inspect_spec.rb
152
+ - spec/unit/events_chain/select_transition_spec.rb
141
153
  - spec/unit/events_spec.rb
142
154
  - spec/unit/finished_spec.rb
143
155
  - spec/unit/handlers_spec.rb