statum 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.hound.yml +2 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +6 -0
- data/README.md +84 -0
- data/lib/statum.rb +37 -12
- data/lib/statum/event.rb +23 -15
- data/lib/statum/hook.rb +30 -0
- data/lib/statum/machine.rb +8 -4
- data/lib/statum/state_definer.rb +7 -0
- data/lib/statum/version.rb +1 -1
- data/statum.gemspec +0 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8faf2ca3f2b51f152887aa12e747d8089b3aef9c
|
4
|
+
data.tar.gz: b1312cb2bdf56cd71d32780faf14dc0699409327
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f469b8e8ecad0b292ca837fdb4289d329b5b9dec87591ea8cfcb32c9f65f3eefb4baacaa423acb5734faec5a4fb04d1aa84da74525183336f7e7b5c2dc18301f
|
7
|
+
data.tar.gz: bfe730ae1c8ad58cc939464139480278c06ac84f9b1379a23793cf8eed5ad4e7182053618ccac0f16730edd211b801d5e1536b455a3d68ffd78cf63f57fa88ee
|
data/.hound.yml
ADDED
data/.rubocop.yml
CHANGED
@@ -35,6 +35,9 @@ Metrics/AbcSize:
|
|
35
35
|
Enabled: false
|
36
36
|
|
37
37
|
# Style
|
38
|
+
Style/MixinUsage:
|
39
|
+
Enabled: false
|
40
|
+
|
38
41
|
Style/StringLiterals:
|
39
42
|
Enabled: false
|
40
43
|
|
@@ -56,3 +59,7 @@ Lint/AmbiguousOperator:
|
|
56
59
|
|
57
60
|
Lint/AmbiguousBlockAssociation:
|
58
61
|
Enabled: false
|
62
|
+
|
63
|
+
# Layout
|
64
|
+
Layout/SpaceInLambdaLiteral:
|
65
|
+
EnforcedStyle: require_space
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -4,6 +4,16 @@
|
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/github/nulldef/statum/badge.svg?branch=master)](https://coveralls.io/github/nulldef/statum?branch=master&v=1)
|
5
5
|
|
6
6
|
Finite state machine for your objects
|
7
|
+
|
8
|
+
Tested on Ruby 2.2.0 +
|
9
|
+
|
10
|
+
- [Installation](#installation)
|
11
|
+
- [Usage](#usage)
|
12
|
+
- [Basic usage](#basic-usage)
|
13
|
+
- [Multiple state machines](#multiple-state-machines)
|
14
|
+
- [Hooks](#hooks)
|
15
|
+
- [Contributing](#contributing)
|
16
|
+
- [License](#license)
|
7
17
|
|
8
18
|
## Installation
|
9
19
|
|
@@ -49,6 +59,72 @@ car.riding? # => false
|
|
49
59
|
car.ride! # changes idle state to riding
|
50
60
|
```
|
51
61
|
|
62
|
+
You can define an array of states which will be able to fire event:
|
63
|
+
```ruby
|
64
|
+
class Car
|
65
|
+
include Statum
|
66
|
+
|
67
|
+
attr_accessor :state
|
68
|
+
|
69
|
+
statum :state, initial: :idle do
|
70
|
+
state :idle
|
71
|
+
state :parked
|
72
|
+
state :riding
|
73
|
+
|
74
|
+
event :ride, %i[idle parked] => :riding
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
Also you can use `any_state` helper to say, that event can be fired from any of defined states
|
80
|
+
```ruby
|
81
|
+
class Car
|
82
|
+
include Statum
|
83
|
+
|
84
|
+
attr_accessor :state
|
85
|
+
|
86
|
+
statum :state, initial: :idle do
|
87
|
+
state :idle
|
88
|
+
state :parked
|
89
|
+
state :riding
|
90
|
+
|
91
|
+
event :ride, any_state => :riding
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
### Multiple state machines
|
97
|
+
You can define more than one state machine on your object.
|
98
|
+
|
99
|
+
**IMPORTANT** use unique fields to work with two or more states
|
100
|
+
```ruby
|
101
|
+
class Car
|
102
|
+
include Statum
|
103
|
+
|
104
|
+
attr_accessor :state, :engine
|
105
|
+
|
106
|
+
statum :state do
|
107
|
+
state :riding
|
108
|
+
state :idle
|
109
|
+
|
110
|
+
event :ride, idle: :riding
|
111
|
+
end
|
112
|
+
|
113
|
+
statum :engine do
|
114
|
+
state :stopped
|
115
|
+
state :started
|
116
|
+
|
117
|
+
event :start, stopped: :started
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
car = Car.new
|
124
|
+
car.start! # changes engine to started
|
125
|
+
car.ride! # changes state to riding
|
126
|
+
```
|
127
|
+
|
52
128
|
### Hooks
|
53
129
|
You can be able to execute some procs before and after event will be fired
|
54
130
|
|
@@ -76,6 +152,14 @@ end
|
|
76
152
|
And then before state changes will be executed `before` proc, and after
|
77
153
|
changing - `after` proc (in instance context).
|
78
154
|
|
155
|
+
If you will wait for argument in hook - the instance will be passed.
|
156
|
+
```ruby
|
157
|
+
...
|
158
|
+
event :ride, idle: :riding,
|
159
|
+
before: -> (instance) { instance.started = true}
|
160
|
+
...
|
161
|
+
```
|
162
|
+
|
79
163
|
## Contributing
|
80
164
|
|
81
165
|
Bug reports and pull requests are welcome on GitHub at https://github.com/nulldef/statum.
|
data/lib/statum.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
require "statum/version"
|
2
|
-
require "statum/state_definer"
|
3
2
|
require "statum/machine"
|
3
|
+
require "statum/hook"
|
4
4
|
require "statum/event"
|
5
|
+
require "statum/state_definer"
|
5
6
|
|
6
7
|
module Statum
|
7
8
|
UnknownEventError = Class.new(ArgumentError)
|
8
9
|
ErrorTransitionError = Class.new(StandardError)
|
10
|
+
ExistingMachineError = Class.new(ArgumentError)
|
11
|
+
|
12
|
+
STATE_MACHINES_VARIABLE = '@__statum_machines'.freeze
|
13
|
+
|
14
|
+
ANY_STATE_NAME = :__statum_any_state
|
9
15
|
|
10
16
|
class << self
|
11
17
|
def included(base)
|
@@ -18,20 +24,29 @@ module Statum
|
|
18
24
|
def statum(field, options = {}, &block)
|
19
25
|
definer = Statum::StateDefiner.new(self, field, options)
|
20
26
|
definer.instance_eval(&block) if block_given?
|
21
|
-
|
27
|
+
add_machine(definer.state_machine)
|
22
28
|
end
|
23
29
|
|
24
|
-
def
|
25
|
-
instance_variable_get(
|
30
|
+
def state_machines
|
31
|
+
instance_variable_get(STATE_MACHINES_VARIABLE) || []
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_machine(machine)
|
37
|
+
if state_machines.any? { |m| m.name == machine.name }
|
38
|
+
raise ExistingMachineError, "State machine for #{machine.name} already exists"
|
39
|
+
end
|
40
|
+
instance_variable_set(STATE_MACHINES_VARIABLE, state_machines + [machine])
|
26
41
|
end
|
27
42
|
end
|
28
43
|
|
29
44
|
module InstanceMethods
|
30
45
|
def method_missing(meth, *args)
|
31
|
-
if meth.to_s.end_with?('?') &&
|
32
|
-
|
33
|
-
elsif meth.to_s.end_with?('!') &&
|
34
|
-
|
46
|
+
if meth.to_s.end_with?('?') && (machine = find_machine_by_state(meth[0...-1]))
|
47
|
+
machine.current(self) == meth[0...-1].to_sym
|
48
|
+
elsif meth.to_s.end_with?('!') && (machine = find_machine_by_event(meth[0...-1]))
|
49
|
+
machine.fire!(self, meth[0...-1])
|
35
50
|
else
|
36
51
|
super
|
37
52
|
end
|
@@ -39,16 +54,26 @@ module Statum
|
|
39
54
|
|
40
55
|
def respond_to_missing?(meth, *args)
|
41
56
|
if meth.to_s.end_with?('?')
|
42
|
-
|
57
|
+
!find_machine_by_state(meth[0...-1]).nil?
|
43
58
|
elsif meth.to_s.end_with?('!')
|
44
|
-
|
59
|
+
!find_machine_by_event(meth[0...-1]).nil?
|
45
60
|
else
|
46
61
|
super
|
47
62
|
end
|
48
63
|
end
|
49
64
|
|
50
|
-
|
51
|
-
|
65
|
+
private
|
66
|
+
|
67
|
+
def find_machine_by_event(name)
|
68
|
+
state_machines.select { |machine| machine.event?(name) }.first
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_machine_by_state(name)
|
72
|
+
state_machines.select { |machine| machine.state?(name) }.first
|
73
|
+
end
|
74
|
+
|
75
|
+
def state_machines
|
76
|
+
self.class.state_machines
|
52
77
|
end
|
53
78
|
end
|
54
79
|
end
|
data/lib/statum/event.rb
CHANGED
@@ -1,30 +1,38 @@
|
|
1
1
|
module Statum
|
2
2
|
# Class for storing event info
|
3
|
+
#
|
4
|
+
# @attr [Statum::Hook] before Before hook object
|
5
|
+
# @attr [Statum::Hook] after After hook object
|
6
|
+
# @attr [Symbol|Array] from From state name (or names)
|
7
|
+
# @attr [Symbol] to To state name
|
3
8
|
class Event
|
4
9
|
attr_reader :from, :to, :before, :after
|
5
10
|
|
6
11
|
# Creates an event class
|
7
12
|
#
|
8
|
-
# @param [String|Symbol] from From state name
|
13
|
+
# @param [String|Symbol|Array] from From state name
|
9
14
|
# @param [String|Symbol] to To state name
|
10
15
|
# @param [Hash] options Options for event
|
11
16
|
def initialize(from, to, options = {})
|
12
|
-
@from
|
13
|
-
@to
|
14
|
-
@before = options.fetch(:before, nil)
|
15
|
-
@after = options.fetch(:after, nil)
|
17
|
+
@from = from.is_a?(Array) ? from : from.to_sym
|
18
|
+
@to = to.to_sym
|
19
|
+
@before = Statum::Hook.new(options.fetch(:before, nil))
|
20
|
+
@after = Statum::Hook.new(options.fetch(:after, nil))
|
16
21
|
end
|
17
22
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
# Returns true if event can be fired from current state
|
24
|
+
#
|
25
|
+
# @param [String|Symbol] current_state Current state
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
def can_fire?(current_state)
|
29
|
+
if from.is_a?(Array)
|
30
|
+
from.include?(current_state.to_sym)
|
31
|
+
elsif from == Statum::ANY_STATE_NAME
|
32
|
+
true
|
33
|
+
else
|
34
|
+
from == current_state.to_sym
|
35
|
+
end
|
28
36
|
end
|
29
37
|
end
|
30
38
|
end
|
data/lib/statum/hook.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Statum
|
2
|
+
# Hook wrapper for Statum::Event
|
3
|
+
class Hook
|
4
|
+
# Creates new Hook instance
|
5
|
+
#
|
6
|
+
# @param [Symbol|Proc|Lambda] hook Callable object or symbol that represents instance method
|
7
|
+
def initialize(hook)
|
8
|
+
@hook = hook
|
9
|
+
end
|
10
|
+
|
11
|
+
# Execute hook on instane
|
12
|
+
#
|
13
|
+
# @param [Object] instance Class instance
|
14
|
+
def evaluate(instance)
|
15
|
+
return if @hook.nil?
|
16
|
+
hook = find_hook(instance)
|
17
|
+
if hook.arity.zero?
|
18
|
+
instance.instance_exec(&hook)
|
19
|
+
else
|
20
|
+
instance.instance_exec(instance, &hook)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def find_hook(instance)
|
27
|
+
@hook.respond_to?(:call) ? @hook : instance.method(@hook)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/statum/machine.rb
CHANGED
@@ -3,6 +3,9 @@ module Statum
|
|
3
3
|
class Machine
|
4
4
|
attr_reader :events, :states, :field
|
5
5
|
|
6
|
+
# Use state field for name of machine
|
7
|
+
alias name field
|
8
|
+
|
6
9
|
# Creates machine instance
|
7
10
|
#
|
8
11
|
# @param [Hash] options options hash
|
@@ -41,13 +44,13 @@ module Statum
|
|
41
44
|
current_state = current(instance)
|
42
45
|
event = events[name.to_sym]
|
43
46
|
|
44
|
-
|
47
|
+
unless event.can_fire?(current_state)
|
45
48
|
raise Statum::ErrorTransitionError, "Cannot transition from #{current_state} to #{event.to}"
|
46
49
|
end
|
47
50
|
|
48
|
-
|
51
|
+
event.before.evaluate(instance)
|
49
52
|
instance.send("#{field}=", event.to)
|
50
|
-
|
53
|
+
event.after.evaluate(instance)
|
51
54
|
end
|
52
55
|
|
53
56
|
# Returns current state of instance
|
@@ -56,7 +59,8 @@ module Statum
|
|
56
59
|
#
|
57
60
|
# @return [Symbol] Current instance's state
|
58
61
|
def current(instance)
|
59
|
-
instance.send(field)
|
62
|
+
value = instance.send(field)
|
63
|
+
value.nil? ? @initial : value.to_sym
|
60
64
|
end
|
61
65
|
end
|
62
66
|
end
|
data/lib/statum/state_definer.rb
CHANGED
@@ -35,6 +35,13 @@ module Statum
|
|
35
35
|
@states << name.to_sym unless @states.include?(name.to_sym)
|
36
36
|
end
|
37
37
|
|
38
|
+
# Returns any state identifier
|
39
|
+
#
|
40
|
+
# @return [Symbol]
|
41
|
+
def any_state
|
42
|
+
Statum::ANY_STATE_NAME
|
43
|
+
end
|
44
|
+
|
38
45
|
# Define a new event
|
39
46
|
#
|
40
47
|
# @param [String|Symbol] name Event name
|
data/lib/statum/version.rb
CHANGED
data/statum.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: statum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexey Bespalov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-12-
|
11
|
+
date: 2017-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -88,6 +88,7 @@ extensions: []
|
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
90
|
- ".gitignore"
|
91
|
+
- ".hound.yml"
|
91
92
|
- ".rspec"
|
92
93
|
- ".rubocop.yml"
|
93
94
|
- ".travis.yml"
|
@@ -99,6 +100,7 @@ files:
|
|
99
100
|
- bin/setup
|
100
101
|
- lib/statum.rb
|
101
102
|
- lib/statum/event.rb
|
103
|
+
- lib/statum/hook.rb
|
102
104
|
- lib/statum/machine.rb
|
103
105
|
- lib/statum/state_definer.rb
|
104
106
|
- lib/statum/version.rb
|
@@ -123,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
125
|
version: '0'
|
124
126
|
requirements: []
|
125
127
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.
|
128
|
+
rubygems_version: 2.5.1
|
127
129
|
signing_key:
|
128
130
|
specification_version: 4
|
129
131
|
summary: Ruby gem to control your states
|