finite_machine 0.3.0 → 0.4.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: 29fd461f4bd19a780bac89bc363026b214db867f
4
- data.tar.gz: 70e229f5f28433618f2408d35136aad7a0a1bb8e
3
+ metadata.gz: b69fcd7446203267dc8130e4c986ea334e4b26ec
4
+ data.tar.gz: 3a4bca63a58bf4a3c0a509cc99778d66771a535e
5
5
  SHA512:
6
- metadata.gz: 00f1f997a94d5f5cb8571921d088a5bc4245001a45463f3b0fc57538094623d94e97c52df76780e957f16279c1bd43d8d0c004595a55a60005f70993fe4873d2
7
- data.tar.gz: c62a75b03d74891a0f83bc91ef3bac4e22090f29b0adb9edc398f0290f39dfbcde36513d994373698be17554aa4618916bcd15018275d2afc197f93b0e6944b7
6
+ metadata.gz: d5c2636ea7607b138737e263771b5269d950f1403079e67dfd41d4940241f41cf62d1118cfe7ae177cfe3a716985cad9f33f76313ada9ce4424ba3fcbbd89511
7
+ data.tar.gz: c40fda5ae25615ca3d2ee239dc59a952596c3860f309c22b8d01e8b4b8acd10f0d7aeb7ddb2f1dc80aecd46c9bd4b3b6e0fd456b28b14dfc9f34144b7ef80fac
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ 0.4.0 (April 13, 2014)
2
+
3
+ * Change initial state to stop firing event notification
4
+ * Fix initial to accept any state object
5
+ * Add logger
6
+ * Add ability to cancel transitions inside callbacks
7
+ * Fix proc conditions to accept aditional arguments
8
+ * Increase test coverage to 97%
9
+ * Add ability to force transitions
10
+
1
11
  0.3.0 (March 30, 2014)
2
12
 
3
13
  * Move development dependencies to Gemfile
data/README.md CHANGED
@@ -43,11 +43,38 @@ Or install it yourself as:
43
43
  ## Contents
44
44
 
45
45
  * [1. Usage](#1-usage)
46
+ * [1.1 Current](#11-current)
47
+ * [1.2 Initial](#12-initial)
48
+ * [1.3 Terminal](#13-terminal)
49
+ * [1.4 is?](#14-is)
50
+ * [1.5 can? and cannot?](#15-can-and-cannot)
51
+ * [1.6 states](#16-states)
52
+ * [1.7 target](#17-target)
46
53
  * [2. Transitions](#2-transitions)
54
+ * [2.1 Performing transitions](#21-performing-transitions)
55
+ * [2.2 Forcing transitions](#22-forcing-transitions)
56
+ * [2.3 Asynchronous transitions](#23-asynchronous-transitions)
57
+ * [2.4 Single event with multiple from states](#24-single-event-with-multiple-from-states)
47
58
  * [3. Conditional transitions](#3-conditional-transitions)
59
+ * [3.1 Using a Proc](#31-using-a-proc)
60
+ * [3.2 Using a Symbol](#32-using-a-symbol)
61
+ * [3.3 Using a String](#33-using-a-string)
62
+ * [3.4 Combining transition conditions](#34-combining-transition-conditions)
48
63
  * [4. Callbacks](#4-callbacks)
64
+ * [4.1 on_enter](#41-on_enter)
65
+ * [4.2 on_transition](#42-on_transition)
66
+ * [4.3 on_exit](#43-on_exit)
67
+ * [4.4 once_on](#44-once_on)
68
+ * [4.5 parameters](#45-parameters)
69
+ * [4.6 Same kind of callbacks](#46-same-kind-of-callbacks)
70
+ * [4.7 Fluid callbacks](#47-fluid-callbacks)
71
+ * [4.8 Executing methods inside callbacks](#48-executing-methods-inside-callbacks)
72
+ * [4.9 Defining callbacks](#49-defining-callbacks)
73
+ * [4.10 Cancelling inside callbacks](#410-cancelling-inside-callbacks)
49
74
  * [5. Errors](#5-errors)
75
+ * [5.1 Using target](#51-using-target)
50
76
  * [6. Integration](#6-integration)
77
+ * [6.1 ActiveRecord](#61-activerecord)
51
78
  * [7. Tips](#7-tips)
52
79
 
53
80
  ## 1 Usage
@@ -98,9 +125,11 @@ fm = FiniteMachine.define do
98
125
  end
99
126
 
100
127
  fm.current # => :none
128
+ fm.start
129
+ fm.current # => :green
101
130
  ```
102
131
 
103
- If you specify initial state using the `initial` helper, an `init` event will be created and triggered when the state machine is created.
132
+ If you specify initial state using the `initial` helper, the state machine will be created already in that state.
104
133
 
105
134
  ```ruby
106
135
  fm = FiniteMachine.define do
@@ -115,34 +144,36 @@ end
115
144
  fm.current # => :green
116
145
  ```
117
146
 
118
- If your target object already has `init` method or one of the events names redefines `init`, you can use different name by passing `:event` option to `initial` helper.
147
+ If you want to defer setting the initial state, pass the `:defer` option to the `initial` helper. By default **FiniteMachine** will create `init` event that will allow to transition from `:none` state to the new state.
119
148
 
120
149
  ```ruby
121
150
  fm = FiniteMachine.define do
122
- initial :green, event: :start
151
+ initial state: :green, defer: true
123
152
 
124
153
  events {
125
154
  event :slow, :green => :yellow
126
155
  event :stop, :yellow => :red
127
156
  }
128
157
  end
129
-
158
+ fm.current # => :none
159
+ fm.init
130
160
  fm.current # => :green
131
161
  ```
132
162
 
133
- If you want to defer calling the initial state method pass the `:defer` option to the `initial` helper.
163
+ If your target object already has `init` method or one of the events names redefines `init`, you can use different name by passing `:event` option to `initial` helper.
134
164
 
135
165
  ```ruby
136
166
  fm = FiniteMachine.define do
137
- initial state: :green, defer: true
167
+ initial state: :green, event: :start, defer: true
138
168
 
139
169
  events {
140
170
  event :slow, :green => :yellow
141
171
  event :stop, :yellow => :red
142
172
  }
143
173
  end
174
+
144
175
  fm.current # => :none
145
- fm.init
176
+ fm.start
146
177
  fm.current # => :green
147
178
  ```
148
179
 
@@ -286,7 +317,11 @@ fm.go('Piotr!')
286
317
  fm.current # => :green
287
318
  ```
288
319
 
289
- ### 2.2 Asynchronous transitions
320
+ ### 2.2 Forcing transitions
321
+
322
+ When you declare event, for instance `ready`, the **FiniteMachine** will provide a dangerous version with a bang `ready!`. In the case when you need to perform transition disregarding current state reachable states, the `ready!` will transition without any validations or callbacks.
323
+
324
+ ### 2.3 Asynchronous transitions
290
325
 
291
326
  By default the transitions will be fired synchronosuly.
292
327
 
@@ -303,7 +338,7 @@ In order to fire the event transition asynchronously use the `async` scope like
303
338
  fm.async.ready # => executes in separate Thread
304
339
  ```
305
340
 
306
- ### 2.3 single event with multiple from states
341
+ ### 2.4 Single event with multiple from states
307
342
 
308
343
  If an event transitions from multiple states to the same state then all the states can be grouped into an array.
309
344
  Altenatively, you can create separte events under the same name for each transition that needs combining.
@@ -342,10 +377,12 @@ fm.slow # doesn't transition to :yellow state
342
377
  fm.current # => :green
343
378
  ```
344
379
 
345
- You can also execute methods on an associated object by passing it as an argument to `target` helper.
380
+ Provided your **FiniteMachine** is associated with another object through `target` helper. Then the target object together with event arguments will be passed to the `:if` or `:unless` condition scope.
346
381
 
347
382
  ```ruby
348
383
  class Car
384
+ attr_accessor :engine_on
385
+
349
386
  def turn_engine_on
350
387
  @engine_on = true
351
388
  end
@@ -368,14 +405,21 @@ fm = FiniteMachine.define do
368
405
  target car
369
406
 
370
407
  events {
371
- event :start, :neutral => :one, if: "engine_on?"
408
+ event :start, :neutral => :one, if: -> (_car, state) {
409
+ _car.engine_on = state
410
+ _car.engine_on?
411
+ }
372
412
  }
373
413
  end
374
414
 
375
- fm.start
376
- fm.current # => :one
415
+ fm.start(false)
416
+ fm.current # => :neutral
417
+ fm.start(true)
418
+ fm.current # => :one
377
419
  ```
378
420
 
421
+ 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)
422
+
379
423
  ### 3.2 Using a Symbol
380
424
 
381
425
  You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
@@ -643,6 +687,33 @@ fm.on_enter_yellow do |event|
643
687
  end
644
688
  ```
645
689
 
690
+ ### 4.10 Cancelling inside callbacks
691
+
692
+ 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
693
+
694
+ * `on_exit :state_name`
695
+ * `on_enter :event_name`
696
+
697
+ For example
698
+
699
+ ```ruby
700
+ fm = FiniteMachine.define do
701
+ initial :red
702
+
703
+ events {
704
+ event :ready, :red => :yellow
705
+ event :go, :yellow => :green
706
+ event :stop, :green => :red
707
+ }
708
+ callbacks {
709
+ on_exit :red do |event| FiniteMachine::CANCELLED end
710
+ }
711
+ end
712
+
713
+ fm.ready
714
+ fm.current # => :red
715
+ ```
716
+
646
717
  ## 5 Errors
647
718
 
648
719
  By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
@@ -792,7 +863,7 @@ account.state # => :access
792
863
 
793
864
  ## 7 Tips
794
865
 
795
- Creating a standalone **FiniteMachine** brings few 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.
866
+ 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.
796
867
 
797
868
  ## Contributing
798
869
 
data/examples/atm.rb ADDED
@@ -0,0 +1,45 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'finite_machine'
4
+
5
+ class Account
6
+ attr_accessor :number
7
+
8
+ def verify(account_number, pin)
9
+ return account_number == 123456 && pin == 666
10
+ end
11
+ end
12
+
13
+ account = Account.new
14
+
15
+ atm = FiniteMachine.define do
16
+ initial :unauthorized
17
+
18
+ target account
19
+
20
+ events {
21
+ event :authorize, :unauthorized => :authorized, if: -> (account, account_number, pin) {
22
+ account.verify(account_number, pin)
23
+ }
24
+ event :deauthorize, :authorized => :unauthorized
25
+ }
26
+
27
+ callbacks {
28
+ on_exit :unauthorized do |event, account_number, pin|
29
+ # if verify(account_number, pin)
30
+ self.number = account_number
31
+ # else
32
+ # puts "Invalid Account and/or PIN"
33
+ # FiniteMachine::CANCELLED
34
+ # end
35
+ end
36
+ }
37
+ end
38
+
39
+ atm.authorize(111222, 666)
40
+ puts "authorized: #{atm.authorized?}"
41
+ puts "Number: #{account.number}"
42
+
43
+ atm.authorize(123456, 666)
44
+ puts "authorized: #{atm.authorized?}"
45
+ puts "Number: #{account.number}"
@@ -0,0 +1,145 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'finite_machine'
4
+
5
+ class User
6
+ attr_accessor :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+ end
12
+
13
+ class Manager < User
14
+ attr_accessor :developers
15
+
16
+ def initialize(name)
17
+ super
18
+ @developers = []
19
+ end
20
+
21
+ def manages(developer)
22
+ @developers << developer
23
+ end
24
+
25
+ def assign(bug)
26
+ developer = @developers.first
27
+ bug.assign
28
+ developer.bug = bug
29
+ end
30
+ end
31
+
32
+ class Tester < User
33
+ def report(bug)
34
+ bug.report
35
+ end
36
+
37
+ def reopen(bug)
38
+ bug.reopen
39
+ end
40
+ end
41
+
42
+ class Developer < User
43
+ attr_accessor :bug
44
+
45
+ def work_on
46
+ bug.start
47
+ end
48
+
49
+ def resolve
50
+ bug.close
51
+ end
52
+ end
53
+
54
+ class BugSystem
55
+ attr_accessor :managers
56
+
57
+ def initialize(managers = [])
58
+ @managers = managers
59
+ end
60
+
61
+ def notify_manager(bug)
62
+ manager = @managers.first
63
+ manager.assign(bug)
64
+ end
65
+ end
66
+
67
+ class Bug
68
+ attr_accessor :name
69
+ attr_accessor :priority
70
+ # fake belongs_to relationship
71
+ attr_accessor :bug_system
72
+
73
+ def initialize(name, priority)
74
+ @name = name
75
+ @priority = priority
76
+ end
77
+
78
+ def report
79
+ status.report
80
+ end
81
+
82
+ def assign
83
+ status.assign
84
+ end
85
+
86
+ def start
87
+ status.start
88
+ end
89
+
90
+ def close
91
+ status.close
92
+ end
93
+
94
+ def reopen
95
+ status.reopen
96
+ end
97
+
98
+ def status
99
+ context = self
100
+ @status ||= FiniteMachine.define do
101
+ target context
102
+
103
+ events {
104
+ event :report, :none => :new
105
+ event :assign, :new => :assigned
106
+ event :start, :assigned => :in_progress
107
+ event :close, [:in_progress, :reopened] => :resolved
108
+ event :reopen, :resolved => :reopened
109
+ }
110
+
111
+ callbacks {
112
+ on_enter :new do |event|
113
+ bug_system.notify_manager(self)
114
+ end
115
+ }
116
+ end
117
+ end
118
+ end
119
+
120
+ tester = Tester.new("John")
121
+ manager = Manager.new("David")
122
+ developer = Developer.new("Piotr")
123
+ manager.manages(developer)
124
+
125
+ bug_system = BugSystem.new([manager])
126
+ bug = Bug.new(:trojan, :high)
127
+ bug.bug_system = bug_system
128
+
129
+ puts "A BUG's LIFE"
130
+ puts "#1 #{bug.status.current}"
131
+
132
+ tester.report(bug)
133
+ puts "#2 #{bug.status.current}"
134
+
135
+ developer.work_on
136
+ puts "#3 #{bug.status.current}"
137
+
138
+ developer.resolve
139
+ puts "#4 #{bug.status.current}"
140
+
141
+ tester.reopen(bug)
142
+ puts "#5 #{bug.status.current}"
143
+
144
+ developer.resolve
145
+ puts "#6 #{bug.status.current}"
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require "logger"
3
4
  require "thread"
4
5
  require "sync"
5
6
 
@@ -13,6 +14,7 @@ require "finite_machine/async_call"
13
14
  require "finite_machine/event"
14
15
  require "finite_machine/event_queue"
15
16
  require "finite_machine/hooks"
17
+ require "finite_machine/logger"
16
18
  require "finite_machine/transition"
17
19
  require "finite_machine/dsl"
18
20
  require "finite_machine/state_machine"
@@ -56,11 +58,21 @@ module FiniteMachine
56
58
  # Raised when event has no transitions
57
59
  NotEnoughTransitionsError = Class.new(::ArgumentError)
58
60
 
61
+ # Raised when initial event specified without state name
62
+ MissingInitialStateError = Class.new(::StandardError)
63
+
59
64
  Environment = Struct.new(:target)
60
65
 
61
- # TODO: this should instantiate system not the state machine
62
- # and then delegate calls to StateMachine instance etc...
63
- def self.define(*args, &block)
64
- StateMachine.new(*args, &block)
66
+ class << self
67
+ attr_accessor :logger
68
+
69
+ # TODO: this should instantiate system not the state machine
70
+ # and then delegate calls to StateMachine instance etc...
71
+ def define(*args, &block)
72
+ StateMachine.new(*args, &block)
73
+ end
65
74
  end
75
+
66
76
  end # FiniteMachine
77
+
78
+ FiniteMachine.logger = Logger.new(STDERR)