statum 0.2.0 → 0.3.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 +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
|
[](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
|