finite_machine 0.7.1 → 0.8.0

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: d0521fc82d97ac5139abe2fcea0ed72532af05fe
4
- data.tar.gz: 260b8c31a86e2c3f80018aad2640c62222cea2af
3
+ metadata.gz: f31937c5d49cee8c974c90b8339c1e3aa57f4866
4
+ data.tar.gz: 5386d3a848f6dd01d05d174d165447366c80ed19
5
5
  SHA512:
6
- metadata.gz: f443039daa14568b5bc2424d6346b89da989494bde20c416e6c7186d1692dd878a1a1bae3e9dcd9a6271c4bfa2bed42d7c795b24c53883a692ffc672a9ec05bc
7
- data.tar.gz: 4cc03728f4c4694f3ebac12bb3f112622b84f877efcbea2ef0f783a0005ee93d889b9317922e09e87f2f5a0ddc189d5618baa17bf498024c8263d783f1e77e83
6
+ metadata.gz: d8ca32da8076f07a43c53650ebdb99a22ba4e0fb4f721db972caeed7df91fb70feecde074b022e79798b5318c07ab59fddfadc91ddf131d4a4ea4d32b477e946
7
+ data.tar.gz: 674cc1ae00eb4fafe21adc37721a2bc92f7a482c2755599dbf1c884bb8e1e7f7591bdcaaf09922e54c2204fd3cd64042f43db5b3c9a939c7a2e0c93321ab940a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ 0.8.0 (June 22, 2014)
2
+
3
+ * Add silent option for state machine events to allow turning on/off
4
+ selectively callbacks
5
+ * Ensure that can? & cannot? take into account conditionl logic applied
6
+ to transitions
7
+ * Add restore! method to allow to set the state directly without callbacks
8
+ * Add ability to do dynamic conditional branching using the choice DSL or
9
+ grouped events with different outgoing transitions [solves #13 and #6 issue]
10
+
1
11
  0.7.1 (June 8, 2014)
2
12
 
3
13
  * Change to relax callback name checks to allow for duplicate state and event names
data/README.md CHANGED
@@ -21,10 +21,10 @@ A minimal finite state machine with a straightforward and intuitive syntax. You
21
21
  * observers (pub/sub) for state changes
22
22
  * ability to check reachable states
23
23
  * ability to check for terminal state
24
- * conditional transitions
24
+ * transition guard conditions
25
+ * dynamic conditional branching
25
26
  * sync and async transitions
26
27
  * sync and async callbacks
27
- * nested/composable states (TODO)
28
28
 
29
29
  ## Installation
30
30
 
@@ -50,6 +50,7 @@ Or install it yourself as:
50
50
  * [1.5 can? and cannot?](#15-can-and-cannot)
51
51
  * [1.6 states](#16-states)
52
52
  * [1.7 target](#17-target)
53
+ * [1.8 restore!](#18-restore)
53
54
  * [2. Transitions](#2-transitions)
54
55
  * [2.1 Performing transitions](#21-performing-transitions)
55
56
  * [2.2 Forcing transitions](#22-forcing-transitions)
@@ -61,26 +62,27 @@ Or install it yourself as:
61
62
  * [3.2 Using a Symbol](#32-using-a-symbol)
62
63
  * [3.3 Using a String](#33-using-a-string)
63
64
  * [3.4 Combining transition conditions](#34-combining-transition-conditions)
64
- * [4. Callbacks](#4-callbacks)
65
- * [4.1 on_enter](#41-on_enter)
66
- * [4.2 on_transition](#42-on_transition)
67
- * [4.3 on_exit](#43-on_exit)
68
- * [4.4 on_before](#44-on_before)
69
- * [4.5 on_after](#45-on_after)
70
- * [4.6 once_on](#46-once_on)
71
- * [4.7 Execution sequence](#47-execution-sequence)
72
- * [4.8 Parameters](#48-parameters)
73
- * [4.9 Same kind of callbacks](#49-same-kind-of-callbacks)
74
- * [4.10 Fluid callbacks](#410-fluid-callbacks)
75
- * [4.11 Executing methods inside callbacks](#411-executing-methods-inside-callbacks)
76
- * [4.12 Defining callbacks](#412-defining-callbacks)
77
- * [4.13 Asynchronous callbacks](#413-asynchronous-callbacks)
78
- * [4.14 Cancelling inside callbacks](#414-cancelling-inside-callbacks)
79
- * [5. Errors](#5-errors)
80
- * [5.1 Using target](#51-using-target)
81
- * [6. Integration](#6-integration)
82
- * [6.1 ActiveRecord](#61-activerecord)
83
- * [7. Tips](#7-tips)
65
+ * [4. Choice pseudostates](#4-choice-pseudostates)
66
+ * [5. Callbacks](#5-callbacks)
67
+ * [5.1 on_enter](#51-on_enter)
68
+ * [5.2 on_transition](#52-on_transition)
69
+ * [5.3 on_exit](#53-on_exit)
70
+ * [5.4 on_before](#54-on_before)
71
+ * [5.5 on_after](#55-on_after)
72
+ * [5.6 once_on](#56-once_on)
73
+ * [5.7 Execution sequence](#57-execution-sequence)
74
+ * [5.8 Parameters](#58-parameters)
75
+ * [5.9 Same kind of callbacks](#59-same-kind-of-callbacks)
76
+ * [5.10 Fluid callbacks](#510-fluid-callbacks)
77
+ * [5.11 Executing methods inside callbacks](#511-executing-methods-inside-callbacks)
78
+ * [5.12 Defining callbacks](#512-defining-callbacks)
79
+ * [5.13 Asynchronous callbacks](#513-asynchronous-callbacks)
80
+ * [5.14 Cancelling inside callbacks](#514-cancelling-inside-callbacks)
81
+ * [6. Errors](#6-errors)
82
+ * [6.1 Using target](#61-using-target)
83
+ * [7. Integration](#7-integration)
84
+ * [7.1 ActiveRecord](#71-activerecord)
85
+ * [8. Tips](#7-tips)
84
86
 
85
87
  ## 1 Usage
86
88
 
@@ -104,7 +106,7 @@ fm = FiniteMachine.define do
104
106
  end
105
107
  ```
106
108
 
107
- As the example demonstrates, by calling the `define` method on **FiniteMachine** you create an instance of finite state machine. The `events` and `callbacks` scopes help to define the behaviour of the machine. Read [Transitions](#2-transitions) and [Callbacks](#4-callbacks) sections for more details.
109
+ As the example demonstrates, by calling the `define` method on **FiniteMachine** you create an instance of finite state machine. The `events` and `callbacks` scopes help to define the behaviour of the machine. Read [Transitions](#2-transitions) and [Callbacks](#5-callbacks) sections for more details.
108
110
 
109
111
  Alternatively, you can construct the machine like a regular object without using the DSL. The same machine could be reimplemented as follows:
110
112
 
@@ -128,7 +130,7 @@ The **FiniteMachine** allows you to query the current state by calling the `curr
128
130
 
129
131
  ### 1.2 initial
130
132
 
131
- There are number of ways to provide the initial state **FiniteMachine** depending on your requirements.
133
+ There are number of ways to provide the initial state in **FiniteMachine** depending on your requirements.
132
134
 
133
135
  By default the **FiniteMachine** will be in the `:none` state and you will need to provide an event to transition out of this state.
134
136
 
@@ -202,6 +204,19 @@ fm.start
202
204
  fm.current # => :green
203
205
  ```
204
206
 
207
+ By default the `initial` does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the `silent` option set to `false` like so
208
+
209
+ ```ruby
210
+ fm = FiniteMachine.define do
211
+ initial state: :green, silent: false # => callbacks are triggered
212
+
213
+ events {
214
+ event :slow, :green => :yellow
215
+ event :stop, :yellow => :red
216
+ }
217
+ end
218
+ ```
219
+
205
220
  ### 1.3 terminal
206
221
 
207
222
  To specify a final state **FiniteMachine** uses the `terminal` method.
@@ -255,6 +270,25 @@ fm.cannot?(:ready) # => false
255
270
  fm.cannot?(:go) # => true
256
271
  ```
257
272
 
273
+ The `can?` and `cannot?` helper methods take into account the `:if` and `:unless` conditions applied to events. The set of values that `:if` or `:unless` condition takes as block parameter can be passed in directly via `can?` and `cannot?` methods' arguments, after the name of the event. For instance,
274
+
275
+ ```ruby
276
+ fm = FiniteMachine.define do
277
+ initial :green
278
+
279
+ events {
280
+ event :slow, :green => :yellow
281
+ event :stop, :yellow => :red, if: proc { |_, param| :breaks == param }
282
+ }
283
+ end
284
+ fm.can?(:slow) # => true
285
+ fm.can?(:stop) # => false
286
+
287
+ fm.slow
288
+ fm.can?(:stop, :breaks) # => true
289
+ fm.can?(:stop, :no_breaks) # => false
290
+ ```
291
+
258
292
  ### 1.6 states
259
293
 
260
294
  You can use the `states` method to return an array of all the states for a given state machine.
@@ -307,6 +341,7 @@ For more complex example see [Integration](#6-integration) section.
307
341
 
308
342
  Finally, you can always reference an external context inside the **FiniteMachine** by simply calling `target`, for instance, to reference it inside a callback:
309
343
 
344
+ ```ruby
310
345
  car = Car.new
311
346
 
312
347
  fm = FiniteMachine.define do
@@ -323,6 +358,17 @@ fm = FiniteMachine.define do
323
358
  end
324
359
  }
325
360
  end
361
+ ```
362
+
363
+ ### 1.8 restore!
364
+
365
+ In order to set the machine to a given state and thus skip triggering callbacks use the `restore!` method:
366
+
367
+ ```ruby
368
+ fm.restore!(:neutral)
369
+ ```
370
+
371
+ This method may be suitable when used testing your state machine or in restoring the state from datastore.
326
372
 
327
373
  ## 2 Transitions
328
374
 
@@ -490,7 +536,7 @@ fm.start(true)
490
536
  fm.current # => :one
491
537
  ```
492
538
 
493
- When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [4.10 Cancelling inside callbacks](#410-cancelling-inside-callbacks)
539
+ When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [5.10 Cancelling inside callbacks](#510-cancelling-inside-callbacks)
494
540
 
495
541
  ### 3.2 Using a Symbol
496
542
 
@@ -543,7 +589,69 @@ end
543
589
 
544
590
  The transition only runs when all the `:if` conditions and none of the `unless` conditions are evaluated to `true`.
545
591
 
546
- ## 4 Callbacks
592
+ ## 4 Choice pseudostates
593
+
594
+ Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to to select only one outgoing transition.
595
+
596
+ You can implement the conditional branch as ordinary events grouped under the same name and use familiar `:if/:unless` conditions:
597
+
598
+ ```ruby
599
+ fsm = FiniteMachine.define do
600
+ initial :green
601
+
602
+ events {
603
+ event :next, :green => :yellow, if: -> { false }
604
+ event :next, :green => :red, if: -> { true }
605
+ }
606
+ end
607
+
608
+ fsm.current # => :green
609
+ fsm.next
610
+ fsm.current # => :red
611
+ ```
612
+
613
+ The same conditional logic can be implemented using much shorter and more descriptive style using `choice` method:
614
+
615
+ ```ruby
616
+ fsm = FiniteMachine.define do
617
+ initial :green
618
+
619
+ events {
620
+ event :next, from: :green do
621
+ choice :yellow, if: -> { false }
622
+ choice :red, if: -> { true }
623
+ end
624
+ }
625
+ end
626
+
627
+ fsm.current # => :green
628
+ fsm.next
629
+ fsm.current # => :red
630
+ ```
631
+
632
+ Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:
633
+
634
+ ```ruby
635
+ fsm = FiniteMachine.define do
636
+ initial :green
637
+
638
+ events {
639
+ event :next, from: :green do
640
+ choice :yellow, if: -> (context, a) { a < 1 }
641
+ choice :red, if: -> (context, a) { a > 1 }
642
+ default :red
643
+ end
644
+ }
645
+ end
646
+
647
+ fsm.current # => :green
648
+ fsm.next(0)
649
+ fsm.current # => :yellow
650
+ ```
651
+
652
+ If more than one of the conditions evaluates to true, a first matching one is chosen. If none of the conditions evaluate to true, then the `default` state is matched. However if default state is not present and non of the conditions match, no transition is performed. To avoid such situation always specify `default` choice.
653
+
654
+ ## 5 Callbacks
547
655
 
548
656
  You can watch state machine events and the information they provide by registering a callback. The following 5 types of callbacks are available in **FiniteMachine**:
549
657
 
@@ -578,27 +686,27 @@ fm.ready(1, 2, 3)
578
686
  fm.go('Piotr!')
579
687
  ```
580
688
 
581
- ### 4.1 on_enter
689
+ ### 5.1 on_enter
582
690
 
583
691
  The `on_enter` callback is executed before given state change is fired. By passing state name you can narrow down the listener to only watch out for enter state changes. Otherwise, all enter state changes will be watched.
584
692
 
585
- ### 4.2 on_transition
693
+ ### 5.2 on_transition
586
694
 
587
695
  The `on_transition` callback is executed when given state change happens. By passing state name you can narrow down the listener to only watch out for transition state changes. Otherwise, all transition state changes will be watched.
588
696
 
589
- ### 4.3 on_exit
697
+ ### 5.3 on_exit
590
698
 
591
699
  The `on_exit` callback is executed after a given state change happens. By passing state name you can narrow down the listener to only watch out for exit state changes. Otherwise, all exit state changes will be watched.
592
700
 
593
- ### 4.4 on_before
701
+ ### 5.4 on_before
594
702
 
595
703
  The `on_before` callback is executed before a given event happens. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
596
704
 
597
- ### 4.5 on_after
705
+ ### 5.5 on_after
598
706
 
599
707
  This callback is executed after a given event happened. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
600
708
 
601
- ### 4.6 once_on
709
+ ### 5.6 once_on
602
710
 
603
711
  **FiniteMachine** allows you to listen on initial state change or when the event is fired first time by using the following 5 types of callbacks:
604
712
 
@@ -608,7 +716,7 @@ This callback is executed after a given event happened. By default it will liste
608
716
  * `once_before`
609
717
  * `once_after`
610
718
 
611
- ### 4.7 Execution sequence
719
+ ### 5.7 Execution sequence
612
720
 
613
721
  Assuming we have the following event specified:
614
722
 
@@ -629,7 +737,7 @@ then by calling `go` event the following callbacks in the following sequence wil
629
737
  * `on_after :go` - callback after the `go` event
630
738
  * `on_after` - generic callback after `any` event
631
739
 
632
- ### 4.8 Parameters
740
+ ### 5.8 Parameters
633
741
 
634
742
  All callbacks get the `TransitionEvent` object with the following attributes.
635
743
 
@@ -657,7 +765,7 @@ end
657
765
  fm.ready(3) # => 'lights switching from red to yellow in 3 seconds'
658
766
  ```
659
767
 
660
- ### 4.9 Same kind of callbacks
768
+ ### 5.9 Same kind of callbacks
661
769
 
662
770
  You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
663
771
 
@@ -677,7 +785,7 @@ end
677
785
  fm.slow # => will invoke both callbacks
678
786
  ```
679
787
 
680
- ### 4.10 Fluid callbacks
788
+ ### 5.10 Fluid callbacks
681
789
 
682
790
  Callbacks can also be specified as full method calls.
683
791
 
@@ -699,7 +807,7 @@ fm = FiniteMachine.define do
699
807
  end
700
808
  ```
701
809
 
702
- ### 4.11 Executing methods inside callbacks
810
+ ### 5.11 Executing methods inside callbacks
703
811
 
704
812
  In order to execute method from another object use `target` helper.
705
813
 
@@ -756,9 +864,9 @@ end
756
864
  fm.back # => Go Piotr!
757
865
  ```
758
866
 
759
- For more complex example see [Integration](#6-integration) section.
867
+ For more complex example see [Integration](#7-integration) section.
760
868
 
761
- ### 4.12 Defining callbacks
869
+ ### 5.12 Defining callbacks
762
870
 
763
871
  When defining callbacks you are not limited to the `callbacks` helper. After **FiniteMachine** instance is created you can register callbacks the same way as before by calling `on` and supplying the type of notification and state/event you are interested in.
764
872
 
@@ -778,7 +886,7 @@ fm.on_enter_yellow do |event|
778
886
  end
779
887
  ```
780
888
 
781
- ### 4.13 Asynchronous callbacks
889
+ ### 5.13 Asynchronous callbacks
782
890
 
783
891
  By default all callbacks are run synchronosuly. In order to add a callback that runs asynchronously, you need to pass second `:async` argument like so:
784
892
 
@@ -794,7 +902,7 @@ or
794
902
 
795
903
  This will ensure that when the callback is fired it will run in seperate thread outside of the main execution thread.
796
904
 
797
- ### 4.14 Cancelling inside callbacks
905
+ ### 5.14 Cancelling inside callbacks
798
906
 
799
907
  Preferred way to handle cancelling transitions is to use [3 Conditional transitions](#3-conditional-transitions). However if the logic is more than one liner you can cancel the event, hence the transition by returning `FiniteMachine::CANCELLED` constant from the callback scope. The two ways you can affect the event are
800
908
 
@@ -821,7 +929,7 @@ fm.ready
821
929
  fm.current # => :red
822
930
  ```
823
931
 
824
- ## 5 Errors
932
+ ## 6 Errors
825
933
 
826
934
  By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
827
935
 
@@ -851,7 +959,7 @@ fm = FiniteMachine.define do
851
959
  end
852
960
  ```
853
961
 
854
- ### 5.1 Using target
962
+ ### 6.1 Using target
855
963
 
856
964
  You can pass an external context via `target` helper that will be the receiver for the handler. The handler method needs to take one argument that will be called with the exception.
857
965
 
@@ -878,7 +986,7 @@ fm = FiniteMachine.define do
878
986
  end
879
987
  ```
880
988
 
881
- ## 6 Integration
989
+ ## 7 Integration
882
990
 
883
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.
884
992
 
@@ -926,7 +1034,7 @@ class Car
926
1034
  end
927
1035
  ```
928
1036
 
929
- ### 6.1 ActiveRecord
1037
+ ### 7.1 ActiveRecord
930
1038
 
931
1039
  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.
932
1040
 
@@ -968,7 +1076,7 @@ account.manage.authorize
968
1076
  account.state # => :access
969
1077
  ```
970
1078
 
971
- ## 7 Tips
1079
+ ## 8 Tips
972
1080
 
973
1081
  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.
974
1082
 
@@ -10,6 +10,7 @@ require "finite_machine/safety"
10
10
  require "finite_machine/thread_context"
11
11
  require "finite_machine/callable"
12
12
  require "finite_machine/catchable"
13
+ require "finite_machine/choice_merger"
13
14
  require "finite_machine/async_proxy"
14
15
  require "finite_machine/async_call"
15
16
  require "finite_machine/hook_event"
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # A class responsible for merging choice options
5
+ class ChoiceMerger
6
+ include Threadable
7
+
8
+ # The context where choice is executed
9
+ attr_threadsafe :context
10
+
11
+ # The options passed in to the context
12
+ attr_threadsafe :options
13
+
14
+ # Initialize a ChoiceMerger
15
+ #
16
+ # @api private
17
+ def initialize(context, options)
18
+ self.context = context
19
+ self.options = options
20
+ end
21
+
22
+ # Create choice transition
23
+ #
24
+ # @example
25
+ # event from: :green do
26
+ # choice :yellow
27
+ # end
28
+ #
29
+ # @param [Symbol] to
30
+ # the to state
31
+ # @param [Hash] attrs
32
+ #
33
+ # @return [FiniteMachine::Transition]
34
+ #
35
+ # @api public
36
+ def choice(to, attrs = {})
37
+ opts = options.dup
38
+ opts.merge!(attrs)
39
+ opts.merge!(parsed_states: { options[:from] => to })
40
+ Transition.create(context.machine, opts)
41
+ end
42
+ alias_method :default, :choice
43
+ end # ChoiceMerger
44
+ end # FiniteMachine