just_state_machine 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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