finite_machine 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Gemfile +1 -1
  4. data/README.md +150 -35
  5. data/lib/finite_machine.rb +1 -0
  6. data/lib/finite_machine/catchable.rb +1 -1
  7. data/lib/finite_machine/definition.rb +61 -0
  8. data/lib/finite_machine/dsl.rb +45 -10
  9. data/lib/finite_machine/event.rb +17 -1
  10. data/lib/finite_machine/hook_event.rb +66 -7
  11. data/lib/finite_machine/observer.rb +3 -3
  12. data/lib/finite_machine/state_machine.rb +36 -28
  13. data/lib/finite_machine/version.rb +1 -1
  14. data/spec/spec_helper.rb +2 -2
  15. data/spec/unit/async_events_spec.rb +1 -1
  16. data/spec/unit/callbacks_spec.rb +23 -23
  17. data/spec/unit/can_spec.rb +22 -22
  18. data/spec/unit/event/eql_spec.rb +37 -0
  19. data/spec/unit/event/initialize_spec.rb +38 -0
  20. data/spec/unit/event/inspect_spec.rb +1 -1
  21. data/spec/unit/event_queue_spec.rb +2 -2
  22. data/spec/unit/events_chain/check_choice_conditions_spec.rb +2 -2
  23. data/spec/unit/events_chain/clear_spec.rb +1 -1
  24. data/spec/unit/events_chain/insert_spec.rb +1 -1
  25. data/spec/unit/events_spec.rb +17 -20
  26. data/spec/unit/hook_event/eql_spec.rb +37 -0
  27. data/spec/unit/hook_event/initialize_spec.rb +22 -0
  28. data/spec/unit/if_unless_spec.rb +6 -6
  29. data/spec/unit/initialize_spec.rb +6 -6
  30. data/spec/unit/is_spec.rb +12 -12
  31. data/spec/unit/logger_spec.rb +1 -1
  32. data/spec/unit/respond_to_spec.rb +2 -2
  33. data/spec/unit/standalone_spec.rb +72 -0
  34. data/spec/unit/subscribers_spec.rb +2 -2
  35. data/spec/unit/target_spec.rb +59 -10
  36. data/spec/unit/{finished_spec.rb → terminated_spec.rb} +38 -8
  37. metadata +15 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1b14a2db5bcab11179dbd4cd9abd10826cda11e1
4
- data.tar.gz: 1b5046cc5cb28b7cd082bce38088ed34c66d6aba
3
+ metadata.gz: 605485a3bc03f37c758232982a53c6a1454f3f75
4
+ data.tar.gz: ea94531b99ed2901d14a801388ab26e220f0b6df
5
5
  SHA512:
6
- metadata.gz: 340d0ff2ea70b6f54127e06c2d2d6f7fe44309e44f5c8c10125aee29a363d8587890369cca037a7b09d614396f8758919f7b2cea67cdca8f15ed5e2679c1315a
7
- data.tar.gz: 0372935e0396e9863700879fb906330d191d2f013d844eae8af58b4853a94b23a6b547c797779da77aeaf71db5fb38d38031ef4f7134864b2708aa900633f06a
6
+ metadata.gz: ac08b6ac004a47a21c6ff3c104b470ebb50b083c3f692f6ce5d865ed2b6527cf0e58dce4983bfd08ed2d000ea5b80277895befada19e26e326506b6326d1bf1f
7
+ data.tar.gz: b00888c384d18a8f1654b11b26e53a6024c8da9127139ee4f6e3f906c1c0a03f7a5f85982f2af28f494b6097d4ddcdce86401f6bf532ead9a7bda2084418c01c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.9.0 (August 3, 2014)
2
+
3
+ * Add Definition class to allow to define standalone state machine
4
+ * Upgrade RSpec dependency and refactor specs
5
+ * Change initial helper to simply state name with options
6
+ * Change HookEvent to be immutable and extend comparison
7
+ * Change Event to be immutable and extend comparison
8
+ * Add #build method to HookEvent
9
+ * Change finished? to terminated? and allow for multiple terminal states
10
+ * Change to require explicit context to call target methods
11
+
1
12
  0.8.1 (July 5, 2014)
2
13
 
3
14
  * Add EventsChain to handle internal events logic
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ gemspec
4
4
 
5
5
  group :development do
6
6
  gem 'rake', '~> 10.1.0'
7
- gem 'rspec', '~> 2.14.1'
7
+ gem 'rspec', '~> 3.0.0'
8
8
  gem 'yard', '~> 0.8.7'
9
9
  end
10
10
 
data/README.md CHANGED
@@ -17,7 +17,7 @@ A minimal finite state machine with a straightforward and intuitive syntax. You
17
17
 
18
18
  * plain object state machine
19
19
  * easy custom object integration
20
- * natural DSL for declaring events, exceptions and callbacks
20
+ * natural DSL for declaring events, callbacks and exceptions
21
21
  * observers (pub/sub) for state changes
22
22
  * ability to check reachable states
23
23
  * ability to check for terminal state
@@ -57,6 +57,7 @@ Or install it yourself as:
57
57
  * [2.3 Asynchronous transitions](#23-asynchronous-transitions)
58
58
  * [2.4 Single event with multiple from states](#24-single-event-with-multiple-from-states)
59
59
  * [2.5 Grouping states under single event](#25-grouping-states-under-single-event)
60
+ * [2.6 Silent transitions](#26-silent-transitions)
60
61
  * [3. Conditional transitions](#3-conditional-transitions)
61
62
  * [3.1 Using a Proc](#31-using-a-proc)
62
63
  * [3.2 Using a Symbol](#32-using-a-symbol)
@@ -80,10 +81,14 @@ Or install it yourself as:
80
81
  * [5.14 Cancelling inside callbacks](#514-cancelling-inside-callbacks)
81
82
  * [6. Errors](#6-errors)
82
83
  * [6.1 Using target](#61-using-target)
83
- * [7. Integration](#7-integration)
84
- * [7.1 Plain Ruby Objects](#71-plain-ruby-objects)
85
- * [7.2 ActiveRecord](#72-activerecord)
86
- * [8. Tips](#7-tips)
84
+ * [7. Stand-Alone FiniteMachine](#7-stand-alone-finitemachine)
85
+ * [7.1 Creating a Definition](#71-creating-a-definition)
86
+ * [7.2 Targeting definition](#72-targeting-definition)
87
+ * [8. Integration](#8-integration)
88
+ * [8.1 Plain Ruby Objects](#81-plain-ruby-objects)
89
+ * [8.2 ActiveRecord](#82-activerecord)
90
+ * [8.3 Transactions](#83-transactions)
91
+ * [9. Tips](#9-tips)
87
92
 
88
93
  ## 1 Usage
89
94
 
@@ -176,7 +181,7 @@ If you want to defer setting the initial state, pass the `:defer` option to the
176
181
 
177
182
  ```ruby
178
183
  fm = FiniteMachine.define do
179
- initial state: :green, defer: true
184
+ initial :green, defer: true # Defer calling :init event
180
185
 
181
186
  events {
182
187
  event :slow, :green => :yellow
@@ -184,7 +189,7 @@ fm = FiniteMachine.define do
184
189
  }
185
190
  end
186
191
  fm.current # => :none
187
- fm.init
192
+ fm.init # execute initial transition
188
193
  fm.current # => :green
189
194
  ```
190
195
 
@@ -192,7 +197,7 @@ If your target object already has `init` method or one of the events names redef
192
197
 
193
198
  ```ruby
194
199
  fm = FiniteMachine.define do
195
- initial state: :green, event: :start, defer: true
200
+ initial :green, event: :start, defer: true # Rename event from :init to :start
196
201
 
197
202
  events {
198
203
  event :slow, :green => :yellow
@@ -201,7 +206,7 @@ fm = FiniteMachine.define do
201
206
  end
202
207
 
203
208
  fm.current # => :none
204
- fm.start
209
+ fm.start # => call the renamed event
205
210
  fm.current # => :green
206
211
  ```
207
212
 
@@ -209,7 +214,7 @@ By default the `initial` does not trigger any callbacks. If you need to fire cal
209
214
 
210
215
  ```ruby
211
216
  fm = FiniteMachine.define do
212
- initial state: :green, silent: false # => callbacks are triggered
217
+ initial :green, silent: false # callbacks are triggered
213
218
 
214
219
  events {
215
220
  event :slow, :green => :yellow
@@ -220,28 +225,30 @@ end
220
225
 
221
226
  ### 1.3 terminal
222
227
 
223
- To specify a final state **FiniteMachine** uses the `terminal` method.
228
+ To specify a final state **FiniteMachine** uses the `terminal` method. The `terminal` can accpet more than one state.
224
229
 
225
230
  ```ruby
226
231
  fm = FiniteMachine.define do
227
232
  initial :green
233
+
228
234
  terminal :red
229
235
 
230
236
  events {
231
237
  event :slow, :green => :yellow
232
238
  event :stop, :yellow => :red
239
+ event :go, :red => :green
233
240
  }
234
241
  end
235
242
  ```
236
243
 
237
- When the terminal state has been specified, you can use `finished?` method on the state machine instance to verify if the terminal state has been reached or not.
244
+ When the terminal state has been specified, you can use `terminated?` method on the state machine instance to verify if the terminal state has been reached or not.
238
245
 
239
246
  ```ruby
240
- fm.finished? # => false
247
+ fm.terminated? # => false
241
248
  fm.slow
242
- fm.finished? # => false
249
+ fm.terminated? # => false
243
250
  fm.stop
244
- fm.finished? # => true
251
+ fm.terminated? # => true
245
252
  ```
246
253
 
247
254
  ### 1.4 is?
@@ -317,7 +324,7 @@ fm = FiniteMachine.define do
317
324
  end
318
325
  ```
319
326
 
320
- Furthermore, the context created through `target` helper will allow you to reference and call methods from another object.
327
+ Furthermore, the context created through `target` helper will allow you to reference and call methods from another object inside your callbacks. You can reference external context by calling `target`.
321
328
 
322
329
  ```ruby
323
330
  car = Car.new
@@ -332,13 +339,13 @@ fm = FiniteMachine.define do
332
339
  }
333
340
 
334
341
  callbacks {
335
- on_enter_start do |event| turn_engine_on end
336
- on_exit_start do |event| turn_engine_off end
342
+ on_enter_start do |event| target.turn_engine_on end
343
+ on_exit_start do |event| target.turn_engine_off end
337
344
  }
338
345
  end
339
346
  ```
340
347
 
341
- For more complex example see [Integration](#6-integration) section.
348
+ For more complex example see [Integration](#8-integration) section.
342
349
 
343
350
  Finally, you can always reference an external context inside the **FiniteMachine** by simply calling `target`, for instance, to reference it inside a callback:
344
351
 
@@ -476,6 +483,24 @@ fm = FiniteMachine.define do
476
483
  }
477
484
  ```
478
485
 
486
+ ### 2.6 Silent transitions
487
+
488
+ The **FiniteMachine** allows to selectively silence events and thus prevent any callbacks from firing. Using the `silent` option passed to event definition like so:
489
+
490
+ ```ruby
491
+ fm = FiniteMachine.define do
492
+ initial :yellow
493
+
494
+ events {
495
+ event :go :yellow => :green, silent: true
496
+ event :stop, :green => :red
497
+ }
498
+ end
499
+
500
+ fsm.go # no callbacks
501
+ fms.stop # callbacks are fired
502
+ ```
503
+
479
504
  ## 3 Conditional transitions
480
505
 
481
506
  Each event takes an optional `:if` and `:unless` options which act as a predicate for the transition. The `:if` and `:unless` can take a symbol, a string, a Proc or an array. Use `:if` option when you want to specify when the transition **should** happen. If you want to specify when the transition **should not** happen then use `:unless` option.
@@ -524,9 +549,9 @@ fm = FiniteMachine.define do
524
549
  target car
525
550
 
526
551
  events {
527
- event :start, :neutral => :one, if: -> (_car, state) {
528
- _car.engine_on = state
529
- _car.engine_on?
552
+ event :start, :neutral => :one, if: -> (target, state) {
553
+ target.engine_on = state
554
+ target.engine_on?
530
555
  }
531
556
  }
532
557
  end
@@ -838,8 +863,8 @@ fm = FiniteMachine.define do
838
863
  }
839
864
 
840
865
  callbacks {
841
- on_enter_reverse { |event| turn_reverse_lights_on }
842
- on_exit_reverse { |event| turn_reverse_lights_off }
866
+ on_enter_reverse { |event| target.turn_reverse_lights_on }
867
+ on_exit_reverse { |event| target.turn_reverse_lights_off }
843
868
  }
844
869
  end
845
870
  ```
@@ -850,8 +875,6 @@ Note that you can also fire events from callbacks.
850
875
  fm = FiniteMachine.define do
851
876
  initial :neutral
852
877
 
853
- target car
854
-
855
878
  events {
856
879
  event :forward, [:reverse, :neutral] => :one
857
880
  event :back, [:neutral, :one] => :reverse
@@ -865,7 +888,7 @@ end
865
888
  fm.back # => Go Piotr!
866
889
  ```
867
890
 
868
- For more complex example see [Integration](#7-integration) section.
891
+ For more complex example see [Integration](#8-integration) section.
869
892
 
870
893
  ### 5.12 Defining callbacks
871
894
 
@@ -987,11 +1010,89 @@ fm = FiniteMachine.define do
987
1010
  end
988
1011
  ```
989
1012
 
990
- ## 7 Integration
1013
+ ## 7 Stand-Alone FiniteMachine
1014
+
1015
+ **FiniteMachine** allows you to seperate your state machine from the target class so that you can keep your concerns broken in small maintainable pieces.
1016
+
1017
+ ### 7.1 Creating a Definition
1018
+
1019
+ You can turn a class into a **FiniteMachine** by simply subclassing `FiniteMachine::Definition`. As a rule of thumb, every single public method of the **FiniteMachine** is available inside your class:
1020
+
1021
+ ```ruby
1022
+ class Engine < FiniteMachine::Definition
1023
+ initial :neutral
1024
+
1025
+ events {
1026
+ event :forward, [:reverse, :neutral] => :one
1027
+ event :shift, :one => :two
1028
+ event :back, [:neutral, :one] => :reverse
1029
+ }
1030
+
1031
+ callbacks {
1032
+ on_enter :reverse do |event|
1033
+ target.turn_reverse_lights_on
1034
+ end
1035
+
1036
+ on_exit :reverse do |event|
1037
+ target.turn_reverse_lights_off
1038
+ end
1039
+ }
1040
+
1041
+ handlers {
1042
+ handle FiniteMachine::InvalidStateError do |exception| ... end
1043
+ }
1044
+ end
1045
+ ```
1046
+
1047
+ ### 7.2 Targeting definition
1048
+
1049
+ The next step is to instantiate your state machine and use `target` to load specific context.
1050
+
1051
+ ```ruby
1052
+ class Car
1053
+ def turn_reverse_lights_off
1054
+ @reverse_lights = false
1055
+ end
1056
+
1057
+ def turn_reverse_lights_on
1058
+ @reverse_lights = true
1059
+ end
1060
+
1061
+ def reverse_lights?
1062
+ @reverse_lights ||= false
1063
+ end
1064
+ end
1065
+ ```
1066
+ Thus, to associate `Engine` to `Car` do:
1067
+
1068
+ ```ruby
1069
+ car = Car.new
1070
+ engine = Engine.new
1071
+ engine.target car
1072
+
1073
+ car.reverse_lignts? # => false
1074
+ engine.back
1075
+ car.reverse_lights? # => true
1076
+ ```
1077
+
1078
+ Alternatively, create method inside the `Car` that will do the integration like so
1079
+
1080
+ ```ruby
1081
+ class Car
1082
+ ... # as above
1083
+ def engine
1084
+ @engine ||= Engine.new
1085
+ @engine.target(self)
1086
+ @engine
1087
+ end
1088
+ end
1089
+ ```
1090
+
1091
+ ## 8 Integration
991
1092
 
992
1093
  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
1094
 
994
- ### 7.1 Plain Ruby Objects
1095
+ ### 8.1 Plain Ruby Objects
995
1096
 
996
1097
  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:
997
1098
 
@@ -1025,11 +1126,11 @@ class Car
1025
1126
 
1026
1127
  callbacks {
1027
1128
  on_enter :reverse do |event|
1028
- turn_reverse_lights_on
1129
+ target.turn_reverse_lights_on
1029
1130
  end
1030
1131
 
1031
1132
  on_exit :reverse do |event|
1032
- turn_reverse_lights_off
1133
+ target.turn_reverse_lights_off
1033
1134
  end
1034
1135
 
1035
1136
  on_transition do |event|
@@ -1055,7 +1156,7 @@ car.gears.current # => :reverse
1055
1156
  car.reverse_lights_on? # => true
1056
1157
  ```
1057
1158
 
1058
- ### 7.2 ActiveRecord
1159
+ ### 8.2 ActiveRecord
1059
1160
 
1060
1161
  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.
1061
1162
 
@@ -1082,8 +1183,8 @@ class Account < ActiveRecord::Base
1082
1183
 
1083
1184
  callbacks {
1084
1185
  on_enter_state do |event|
1085
- self.state = event.to
1086
- save
1186
+ target.state = event.to
1187
+ target.save
1087
1188
  end
1088
1189
  }
1089
1190
  end
@@ -1098,7 +1199,21 @@ account.manage.authorize
1098
1199
  account.state # => :access
1099
1200
  ```
1100
1201
 
1101
- ## 8 Tips
1202
+ ### 8.3 Transactions
1203
+
1204
+ When using **FiniteMachine** with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section 8.2 one can run event in transaction in the following way:
1205
+
1206
+ ```ruby
1207
+ ActiveRecord::Base.transaction do
1208
+ account.manage.enqueue
1209
+ end
1210
+ ```
1211
+
1212
+ If the transition fails it will raise `TransitionError` which will cause the transaction to rollback.
1213
+
1214
+ Please check the ORM of your choice if it supports database transactions.
1215
+
1216
+ ## 9 Tips
1102
1217
 
1103
1218
  Creating a standalone **FiniteMachine** brings a number of benefits, one of them being easier testing. This is especially true if the state machine is extremely complex itself. Ideally, you would test the machine in isolation and then integrate it with other objects or ORMs.
1104
1219
 
@@ -24,6 +24,7 @@ require "finite_machine/logger"
24
24
  require "finite_machine/transition"
25
25
  require "finite_machine/transition_event"
26
26
  require "finite_machine/dsl"
27
+ require "finite_machine/definition"
27
28
  require "finite_machine/state_machine"
28
29
  require "finite_machine/subscribers"
29
30
  require "finite_machine/state_parser"
@@ -77,7 +77,7 @@ module FiniteMachine
77
77
  def evaluate_handler(handler)
78
78
  case handler
79
79
  when Symbol
80
- method(handler)
80
+ target.method(handler)
81
81
  when Proc
82
82
  if handler.arity.zero?
83
83
  proc { instance_exec(&handler) }
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # A class responsible for defining standalone state machine
5
+ class Definition
6
+ # The machine deferreds
7
+ #
8
+ # @return [Array[Proc]]
9
+ #
10
+ # @api private
11
+ def self.deferreds
12
+ @deferreds ||= []
13
+ end
14
+
15
+ # Add deferred
16
+ #
17
+ # @param [Proc] deferred
18
+ # the deferred execution
19
+ #
20
+ # @return [Array[Proc]]
21
+ #
22
+ # @api private
23
+ def self.add_deferred(deferred)
24
+ deferreds << deferred
25
+ end
26
+
27
+ # Instantiate a new Definition
28
+ #
29
+ # @example
30
+ # class Engine < FiniteMachine::Definition
31
+ # ...
32
+ # end
33
+ #
34
+ # engine = Engine.new
35
+ #
36
+ # @return [FiniteMachine::StateMachine]
37
+ #
38
+ # @api public
39
+ def self.new(*args)
40
+ context = self
41
+ FiniteMachine.define(*args) do
42
+ context.deferreds.each { |d| d.call(self) }
43
+ end
44
+ end
45
+
46
+ # Delay lookup of DSL method
47
+ #
48
+ # @param [Symbol] method_name
49
+ #
50
+ # @return [nil]
51
+ #
52
+ # @api private
53
+ def self.method_missing(method_name, *arguments, &block)
54
+ deferred = proc do |name, args, blok, object|
55
+ object.send(name, *args, &blok)
56
+ end
57
+ deferred = deferred.curry(4)[method_name][arguments][block]
58
+ add_deferred(deferred)
59
+ end
60
+ end # Definition
61
+ end # FiniteMachine