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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ffaec8272814487f36a006b35a7fb4ba9d03e212
4
- data.tar.gz: 9e6c7df7b81ee6e55d79f47c9a8e44ac36234df1
3
+ metadata.gz: b8088d7c136243526d1a99b0353c9b5068efb6e3
4
+ data.tar.gz: f88ff999ee19e0c5fa41516999fe1a8e5fcd2f6d
5
5
  SHA512:
6
- metadata.gz: a8f00e1451d1fb713f8000027239c1de502093bb658be3ee7a2c1106103b39b4573053ae88445e4e399a9f2b1b5e455c9b95f789ac498d5e5e32a02253ad7dc0
7
- data.tar.gz: dd327c2a4857e9b2795ac11dbb081d31f2da958fa17ce0bcfb2de68288edb7936ccb39a666294ffc2b6bae79b2c8917f5768d990d421c1b79101a7f68104ae3d
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
- require "jsm/machines"
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"
@@ -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
- @validators ||= Jsm::Validators.new
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
@@ -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(validators: @state_machine.validators)
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,5 +1,5 @@
1
1
  class Jsm::EventExecutor::ActiveRecord < Jsm::EventExecutor::ActiveModel
2
- def execute(event, obj)
2
+ def execute_action(event, obj)
3
3
  if can_be_executed?(event, obj)
4
4
  result = false
5
5
  # do transaction to prevent shit happen
@@ -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
- @validators = params[:validators] || Jsm::Validators.new
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
- can_be_executed?(event, obj) ? event.execute(obj) : false
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
- unless execute(event, obj)
25
- raise Jsm::IllegalTransitionError, "there is no matching transitions or invalid, Cant do event #{event.name}"
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
- true
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
@@ -1,3 +1,3 @@
1
1
  module Jsm
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
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.3.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-22 00:00:00.000000000 Z
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