just_state_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 +4 -4
- data/README.md +109 -2
- data/examples/active_record_state_machine.rb +8 -0
- data/examples/simple_state_machine.rb +9 -1
- data/lib/jsm.rb +13 -7
- data/lib/jsm/base.rb +9 -4
- data/lib/jsm/callbacks.rb +38 -0
- data/lib/jsm/callbacks/callback.rb +25 -0
- data/lib/jsm/callbacks/chain.rb +54 -0
- data/lib/jsm/callbacks/chain_collection.rb +15 -0
- data/lib/jsm/client_extension.rb +2 -1
- data/lib/jsm/event_executor/active_record.rb +1 -1
- data/lib/jsm/event_executor/base.rb +20 -6
- data/lib/jsm/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8088d7c136243526d1a99b0353c9b5068efb6e3
|
4
|
+
data.tar.gz: f88ff999ee19e0c5fa41516999fe1a8e5fcd2f6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1200277a06f2a0da8313ff057f5237dfa71f1a3a6d5c4064ca7f9303cb655b44e545b8dc10e704b88b45b93742fdef3f498d206ab4c0fd73beaef57646d45b9
|
7
|
+
data.tar.gz: 1cd99d024ea0f880460e103ed6c8b8f0f0f3c3ad91d3fa4db37cca3672c217679c6327bdafa39677c0526380d6b9bf22a442c189e53de74a4890b884333c5e80
|
data/README.md
CHANGED
@@ -148,6 +148,19 @@ class UserStateMachine < Jsm::Base
|
|
148
148
|
transition from: :intermediate, to: :beginner
|
149
149
|
transition from: :master, to: :intermediate
|
150
150
|
end
|
151
|
+
|
152
|
+
|
153
|
+
before :upgrade_title do |user|
|
154
|
+
user.name = 'before'
|
155
|
+
end
|
156
|
+
|
157
|
+
after :upgrade_title do |result, user|
|
158
|
+
if result
|
159
|
+
user.name += ' after success'
|
160
|
+
else
|
161
|
+
user.name += 'after failed'
|
162
|
+
end
|
163
|
+
end
|
151
164
|
end
|
152
165
|
|
153
166
|
# Client Class
|
@@ -155,7 +168,7 @@ class User
|
|
155
168
|
include Jsm::Client
|
156
169
|
jsm_use UserStateMachine # your state machine class here
|
157
170
|
|
158
|
-
attr_accessor :title # same with attribute_name in UserStateMachine
|
171
|
+
attr_accessor :title, :name # same with attribute_name in UserStateMachine
|
159
172
|
def initialize
|
160
173
|
@title = :beginner
|
161
174
|
@level = 1
|
@@ -177,6 +190,62 @@ transition from: :intermediate, to: :beginner #one `from` states
|
|
177
190
|
transition from: [:intermediate, :master], to: :beginner #multiple `from` states
|
178
191
|
```
|
179
192
|
What above code means for multiple `from` states is the current state either `:intermediate` or `:master`, transform it into `:beginner` state
|
193
|
+
|
194
|
+
### Callbacks
|
195
|
+
`Jsm` now support callbacks. It provide 2 API: `before` and `after`. basically `before` callbacks is run before do event. Meanwhile, after callbacks is run after event. For **note**, after event first argument is `result` of the event
|
196
|
+
```ruby
|
197
|
+
class UserStateMachine < Jsm::Base
|
198
|
+
attribute_name :level
|
199
|
+
|
200
|
+
state :beginner
|
201
|
+
state :intermediate
|
202
|
+
state :master
|
203
|
+
|
204
|
+
event :upgrade_title do
|
205
|
+
transition from: [:beginner], to: :intermediate
|
206
|
+
transition from: [:intermediate], to: :master
|
207
|
+
end
|
208
|
+
|
209
|
+
event :downgrade_title do
|
210
|
+
transition from: :intermediate, to: :beginner
|
211
|
+
transition from: :master, to: :intermediate
|
212
|
+
end
|
213
|
+
|
214
|
+
before :upgrade_title do |user|
|
215
|
+
user.name = 'before'
|
216
|
+
end
|
217
|
+
|
218
|
+
after :upgrade_title do |result, user| # the first parameters is result of the event
|
219
|
+
if result
|
220
|
+
# execute if transition in event is success
|
221
|
+
user.name += ' after success'
|
222
|
+
else
|
223
|
+
# execute if transition in event is failed
|
224
|
+
user.name += 'after failed'
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Client Class
|
230
|
+
class User
|
231
|
+
include Jsm::Client
|
232
|
+
jsm_use UserStateMachine # your state machine class here
|
233
|
+
|
234
|
+
attr_accessor :title, :name # same with attribute_name in UserStateMachine
|
235
|
+
def initialize
|
236
|
+
@title = :beginner
|
237
|
+
@level = 1
|
238
|
+
end
|
239
|
+
#your code here
|
240
|
+
end
|
241
|
+
|
242
|
+
user = User.new
|
243
|
+
user.title # :beginner
|
244
|
+
user.name # nil
|
245
|
+
user.upgrade_title # true(it still return the original return value of the event)
|
246
|
+
user.name # before after success
|
247
|
+
```
|
248
|
+
|
180
249
|
## Active Model Integration
|
181
250
|
```ruby
|
182
251
|
class UserStateMachine < Jsm::Base
|
@@ -231,7 +300,7 @@ end
|
|
231
300
|
|
232
301
|
### Validation
|
233
302
|
It also support validation from `ActiveModel` . Validation checked based on `errors` value in the `instance`. you can add an error to the errors object. This will prevent the state from being changed
|
234
|
-
```
|
303
|
+
```ruby
|
235
304
|
user = User.new
|
236
305
|
user.level # 1
|
237
306
|
user.level = 18
|
@@ -288,6 +357,44 @@ end
|
|
288
357
|
```
|
289
358
|
`Jsm` support `ActiveRecord`. In the `client` class include the `Jsm::Client::ActiveRecord`. It also support `ActiveRecord` Validation. The behavior is same with `ActiveModel` client.
|
290
359
|
|
360
|
+
## StateMachine Visualization
|
361
|
+
When you want to visualize the state machine that has been defined. You can use the `Jsm::Drawer:Graphviz`. Basically it will generate an url for images of states diagram with theirs events. Basically the image is generated by `google chart`. They provide a really good API to generate state diagram.
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
class UserStateMachine < Jsm::Base
|
365
|
+
attribute_name :relationship
|
366
|
+
|
367
|
+
state :single, initial: true
|
368
|
+
state :in_relationship
|
369
|
+
state :married
|
370
|
+
state :divorced
|
371
|
+
state :widowed
|
372
|
+
|
373
|
+
event :start_dating do
|
374
|
+
transition from: :single, to: :in_relationship
|
375
|
+
end
|
376
|
+
|
377
|
+
event :marry do
|
378
|
+
transition from: [:single, :in_relationship], to: :married
|
379
|
+
end
|
380
|
+
|
381
|
+
event :cheating do
|
382
|
+
transition from: :in_relationship, to: :single
|
383
|
+
transition from: :married, to: :divorced
|
384
|
+
end
|
385
|
+
|
386
|
+
event :divorce do
|
387
|
+
transition from: :married, to: :divorced
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
Jsm::Drawer::Graphviz.generate_url(UserStateMachine) #the arguments is class name
|
392
|
+
```
|
393
|
+
the result will be:
|
394
|
+
|
395
|
+
![image](https://chart.googleapis.com/chart?cht=gv&chl=digraph{single-%3Ein_relationship[label=start_dating];single-%3Emarried[label=marry];in_relationship-%3Emarried[label=marry];in_relationship-%3Esingle[label=cheating];married-%3Edivorced[label=cheating];married-%3Edivorced[label=divorce]})
|
396
|
+
|
397
|
+
[source]( https://chart.googleapis.com/chart?cht=gv&chl=digraph{single-%3Ein_relationship[label=start_dating];single-%3Emarried[label=marry];in_relationship-%3Emarried[label=marry];in_relationship-%3Esingle[label=cheating];married-%3Edivorced[label=cheating];married-%3Edivorced[label=divorce]}Â)
|
291
398
|
## Development
|
292
399
|
|
293
400
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -25,6 +25,14 @@ class ActiveRecordSM < Jsm::Base
|
|
25
25
|
transition from: "married", to: "divorced"
|
26
26
|
end
|
27
27
|
|
28
|
+
before :marry do |user|
|
29
|
+
user.name = 'testBefore'
|
30
|
+
end
|
31
|
+
|
32
|
+
after :marry do |result, user|
|
33
|
+
user.name = ' testAfter'
|
34
|
+
end
|
35
|
+
|
28
36
|
validate "married" do |user|
|
29
37
|
unless user.approved_by_parents
|
30
38
|
user.errors.add(:approved_by_parents, 'can not marry, you havent been approved')
|
@@ -23,6 +23,14 @@ class UserStateMachine < Jsm::Base
|
|
23
23
|
event :divorce do
|
24
24
|
transition from: :married, to: :divorced
|
25
25
|
end
|
26
|
+
|
27
|
+
before :marry do |obj|
|
28
|
+
obj.name = 'testBefore'
|
29
|
+
end
|
30
|
+
|
31
|
+
after :marry do |result, obj|
|
32
|
+
obj.name += ' testAfterthis'
|
33
|
+
end
|
26
34
|
end
|
27
35
|
|
28
36
|
|
@@ -30,7 +38,7 @@ class UserBasic
|
|
30
38
|
include Jsm::Client
|
31
39
|
jsm_use UserStateMachine
|
32
40
|
|
33
|
-
attr_accessor :relationship
|
41
|
+
attr_accessor :relationship, :name
|
34
42
|
def initialize(relationship = nil)
|
35
43
|
@relationship = relationship
|
36
44
|
end
|
data/lib/jsm.rb
CHANGED
@@ -3,21 +3,27 @@ module Jsm
|
|
3
3
|
require "jsm/states"
|
4
4
|
require "jsm/event"
|
5
5
|
require "jsm/exceptions"
|
6
|
-
|
6
|
+
module EventExecutor
|
7
|
+
require "jsm/event_executor/base"
|
8
|
+
require "jsm/event_executor/active_model"
|
9
|
+
require "jsm/event_executor/active_record"
|
10
|
+
end
|
11
|
+
|
12
|
+
require "jsm/callbacks"
|
13
|
+
module Callbacks
|
14
|
+
require 'jsm/callbacks/callback'
|
15
|
+
require "jsm/callbacks/chain"
|
16
|
+
require "jsm/callbacks/chain_collection"
|
17
|
+
end
|
7
18
|
require "jsm/base"
|
8
19
|
require "jsm/client"
|
9
20
|
require "jsm/client/active_model"
|
10
21
|
require "jsm/client/active_record"
|
22
|
+
require "jsm/machines"
|
11
23
|
require "jsm/client_extension"
|
12
24
|
require "jsm/validator"
|
13
25
|
require "jsm/validators"
|
14
26
|
|
15
|
-
module EventExecutor
|
16
|
-
require "jsm/event_executor/base"
|
17
|
-
require "jsm/event_executor/active_model"
|
18
|
-
require "jsm/event_executor/active_record"
|
19
|
-
end
|
20
|
-
|
21
27
|
module Drawer
|
22
28
|
require "jsm/drawer/node"
|
23
29
|
require "jsm/drawer/digraph"
|
data/lib/jsm/base.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# this module used as extension for state machine class
|
2
2
|
# The DSL is built to define the state, event, and transition that happen
|
3
3
|
class Jsm::Base
|
4
|
-
|
4
|
+
include Jsm::Callbacks
|
5
5
|
# define attribute name of state attribute in the client class
|
6
6
|
def self.attribute_name(attribute_name = nil)
|
7
7
|
if attribute_name.nil?
|
@@ -67,13 +67,18 @@ class Jsm::Base
|
|
67
67
|
raise Jsm::InvalidStateError, "there is no state y"
|
68
68
|
end
|
69
69
|
|
70
|
-
|
71
|
-
@validators.add_validator(state_name, Jsm::Validator.new(:state, state_name, &block))
|
70
|
+
validators.add_validator(state_name, Jsm::Validator.new(:state, state_name, &block))
|
72
71
|
end
|
73
72
|
|
74
73
|
# list all validators that exist
|
75
74
|
def self.validators
|
76
|
-
@validators
|
75
|
+
@validators ||= Jsm::Validators.new
|
76
|
+
end
|
77
|
+
# set a before callback for an event
|
78
|
+
def self.pre_before(event_name, &block)
|
79
|
+
if !events[event_name]
|
80
|
+
raise Jsm::InvalidEventError, "event #{event_name} has not been registered"
|
81
|
+
end
|
77
82
|
end
|
78
83
|
|
79
84
|
def initialize(klass)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ::Jsm::Callbacks
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def jsm_callbacks
|
8
|
+
@jsm_callbacks ||= Jsm::Callbacks::ChainCollection.new(self)
|
9
|
+
end
|
10
|
+
#override this to do something before method `before`
|
11
|
+
def pre_before(context, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
#override this to do something before method `after`
|
15
|
+
def pre_after(context, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# this method is to register a `before callback` to a jsm_callbacks
|
19
|
+
def before(context, &block)
|
20
|
+
|
21
|
+
pre_before(context, &block)
|
22
|
+
callback = Jsm::Callbacks::Callback.new(:before, &block)
|
23
|
+
jsm_callbacks[context] ||= Jsm::Callbacks::Chain.new(context)
|
24
|
+
jsm_callbacks[context].insert_callback(callback)
|
25
|
+
end
|
26
|
+
|
27
|
+
# this method is to register a `after callback` to a jsm_callbacks
|
28
|
+
def after(context, &block)
|
29
|
+
pre_after(context, &block)
|
30
|
+
callback = Jsm::Callbacks::Callback.new(:after, &block)
|
31
|
+
self.jsm_callbacks[context].insert_callback(callback)
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_callback(context, *args, &block)
|
35
|
+
self.jsm_callbacks[context].compile(*args, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# the purpose of this class is to store the block that will be used as callback
|
2
|
+
# e.g:
|
3
|
+
# Jsm::Callbacks::Callback.new(:before) do
|
4
|
+
# put 'me awesome'
|
5
|
+
# end
|
6
|
+
|
7
|
+
class Jsm::Callbacks::Callback
|
8
|
+
FILTER_TYPES = [:before, :after]
|
9
|
+
attr_reader :filter_type
|
10
|
+
|
11
|
+
# the allowed filter_type: :before, :after
|
12
|
+
def initialize(filter_type, &block)
|
13
|
+
if FILTER_TYPES.include?(filter_type)
|
14
|
+
@filter_type = filter_type
|
15
|
+
else
|
16
|
+
raise ArgumentError, "invalid type #{filter_type}, allowed: #{FILTER_TYPES.join(', ')}"
|
17
|
+
end
|
18
|
+
@block = block
|
19
|
+
end
|
20
|
+
|
21
|
+
# run callback
|
22
|
+
def execute(*obj)
|
23
|
+
@block.call(*obj)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# chain is a repository to store list of callbacks of an event, for example
|
2
|
+
# store before and after callback of an event called `run`
|
3
|
+
# e.g:
|
4
|
+
# before_callback = Jsm::Callbacks::Callback.new(:before) do |user|
|
5
|
+
# log.info('before callback')
|
6
|
+
# end
|
7
|
+
# before_callback2 = Jsm::Callbacks::Callback.new(:before) do |user|
|
8
|
+
# user.age = 20
|
9
|
+
# end
|
10
|
+
# after_callback = Jsm::Callbacks::Callback.new(:after) do |result, user|
|
11
|
+
# log.info('after callback')
|
12
|
+
# end
|
13
|
+
# chain = Jsm::Callbacks::Chain.new(:run)
|
14
|
+
# chain.insert_callback(before_callback)
|
15
|
+
# chain.insert_callback(before_callback2)
|
16
|
+
# chain.insert_callback(after_callback)
|
17
|
+
# user = User.new
|
18
|
+
# result = chain.compile user do |user|
|
19
|
+
# user.address = 'Indonesia'
|
20
|
+
# end
|
21
|
+
# result -> true
|
22
|
+
# log -> before callback after callback
|
23
|
+
# user.address -> 'Indonesia'
|
24
|
+
|
25
|
+
class Jsm::Callbacks::Chain
|
26
|
+
attr_reader :context, :callbacks
|
27
|
+
FILTER_TYPES_POSITION = [:before, :after]
|
28
|
+
def initialize(context)
|
29
|
+
@context = context
|
30
|
+
@callbacks = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def insert_callback(callback)
|
34
|
+
@callbacks.push(callback)
|
35
|
+
end
|
36
|
+
|
37
|
+
def compile(*args, &block)
|
38
|
+
arrange_callbacks(*args, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
#run callbacks from before, given block then after callback
|
43
|
+
# the return value is the original given block result
|
44
|
+
# before callback get arguments from *args
|
45
|
+
# after callback get result of block execution and *args
|
46
|
+
def arrange_callbacks(*args, &block)
|
47
|
+
before = @callbacks.select { |callback| callback.filter_type == :before }
|
48
|
+
after = @callbacks.select { |callback| callback.filter_type == :after }
|
49
|
+
before.each {|callback| callback.execute(*args) }
|
50
|
+
return_value = block.call(*args)
|
51
|
+
after.each { |callback| callback.execute(return_value, *args) }
|
52
|
+
return_value
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# collection of chain
|
2
|
+
class Jsm::Callbacks::ChainCollection
|
3
|
+
def initialize(klass)
|
4
|
+
@klass = klass
|
5
|
+
@chains = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_chain(context,chain)
|
9
|
+
@chains[context] = chain
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](context)
|
13
|
+
@chains[context] ||= Jsm::Callbacks::Chain.new(context)
|
14
|
+
end
|
15
|
+
end
|
data/lib/jsm/client_extension.rb
CHANGED
@@ -8,10 +8,11 @@ class Jsm::ClientExtension
|
|
8
8
|
end
|
9
9
|
|
10
10
|
attr_reader :klass, :state_machine, :event_executor
|
11
|
+
# @param options with options state_machine. It contain state_machine class( class inherited from Jsm::Base)
|
11
12
|
def initialize(klass, params = {})
|
12
13
|
@klass = klass
|
13
14
|
@state_machine = params[:state_machine]
|
14
|
-
@event_executor = klass.jsm_event_executor.new(
|
15
|
+
@event_executor = klass.jsm_event_executor.new(state_machine: @state_machine)
|
15
16
|
unless @state_machine.attribute_name
|
16
17
|
raise Jsm::NoAttributeError, "please assign the attribute_name first in class #{@state_machine.name}"
|
17
18
|
end
|
@@ -1,16 +1,21 @@
|
|
1
1
|
# this class is the base for adapter
|
2
2
|
# it can be extended for ActiveRecord adapter
|
3
3
|
class Jsm::EventExecutor::Base
|
4
|
-
attr_reader :validators
|
4
|
+
attr_reader :validators, :state_machine
|
5
5
|
def initialize(params = {})
|
6
|
-
@
|
6
|
+
@state_machine = params[:state_machine]
|
7
|
+
@validators = @state_machine.validators
|
8
|
+
|
7
9
|
end
|
8
10
|
|
9
11
|
# it execute event for the object.
|
10
12
|
# If transition failed or invalid by validation toward the object,
|
11
13
|
# then it will return false
|
14
|
+
# it also run callbacks of the event
|
12
15
|
def execute(event, obj)
|
13
|
-
|
16
|
+
state_machine.run_callback event.name, obj do |obj|
|
17
|
+
execute_action(event, obj)
|
18
|
+
end
|
14
19
|
end
|
15
20
|
|
16
21
|
# check if the obj possible to execute the event(passed the validation and can do transition)
|
@@ -20,10 +25,19 @@ class Jsm::EventExecutor::Base
|
|
20
25
|
end
|
21
26
|
|
22
27
|
# same with execute, but if its failed raise error
|
28
|
+
# it also run callbacks of the event
|
29
|
+
# however after callbacks only run when success because if its failed will raise error
|
23
30
|
def execute!(event, obj)
|
24
|
-
|
25
|
-
|
31
|
+
state_machine.run_callback event.name, obj do |obj|
|
32
|
+
unless execute_action(event, obj)
|
33
|
+
raise Jsm::IllegalTransitionError, "there is no matching transitions or invalid, Cant do event #{event.name}"
|
34
|
+
end
|
35
|
+
true
|
26
36
|
end
|
27
|
-
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def execute_action(event, obj)
|
41
|
+
can_be_executed?(event, obj) ? event.execute(obj) : false
|
28
42
|
end
|
29
43
|
end
|
data/lib/jsm/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: just_state_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- wendy0402
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -144,6 +144,10 @@ files:
|
|
144
144
|
- jsm.gemspec
|
145
145
|
- lib/jsm.rb
|
146
146
|
- lib/jsm/base.rb
|
147
|
+
- lib/jsm/callbacks.rb
|
148
|
+
- lib/jsm/callbacks/callback.rb
|
149
|
+
- lib/jsm/callbacks/chain.rb
|
150
|
+
- lib/jsm/callbacks/chain_collection.rb
|
147
151
|
- lib/jsm/client.rb
|
148
152
|
- lib/jsm/client/active_model.rb
|
149
153
|
- lib/jsm/client/active_record.rb
|