stateful.rb 2.1.0 → 2.2.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/CHANGELOG.md +137 -0
- data/Gemfile +2 -0
- data/README.md +309 -0
- data/lib/Stateful/Sequel/ClassMethods.rb +117 -0
- data/lib/Stateful/Sequel.rb +30 -0
- data/lib/Stateful/VERSION.rb +1 -1
- data/lib/Stateful.rb +3 -0
- data/stateful.rb.gemspec +42 -0
- data/test/ActiveRecord/WithColumnName.rb +159 -0
- data/test/ActiveRecord/WithExplicitDeterministicEventOrdering.rb +159 -0
- data/test/ActiveRecord/WithExplicitGlobalNonDeterministicEventOrdering.rb +165 -0
- data/test/ActiveRecord/WithExplicitGlobalNonDeterministicEventOrderingAndInitialStateBlock.rb +163 -0
- data/test/ActiveRecord/WithExplicitNonDeterministicEventOrdering.rb +161 -0
- data/test/ActiveRecord/WithInitialStateBlock.rb +157 -0
- data/test/ActiveRecord/WithMultipleFinalStates.rb +188 -0
- data/test/ActiveRecord/WithMultipleStateMachines.rb +221 -0
- data/test/ActiveRecord/WithMultipleStateMachinesAndStatefulBlock.rb +223 -0
- data/test/ActiveRecord/WithPersistenceNonSpecificExtend.rb +159 -0
- data/test/ActiveRecord/WithPersistenceNonSpecificExtendAndStatefulBlock.rb +161 -0
- data/test/ActiveRecord/WithPersistenceSpecificExtend.rb +159 -0
- data/test/ActiveRecord/WithPersistenceSpecificExtendAndStatefulBlock.rb +161 -0
- data/test/ActiveRecord/test_helper.rb +9 -0
- data/test/ActiveRecord.rb +5 -0
- data/test/Poro/WithExplicitDeterministicEventOrdering.rb +147 -0
- data/test/Poro/WithExplicitGlobalNonDeterministicEventOrdering.rb +153 -0
- data/test/Poro/WithExplicitGlobalNonDeterministicEventOrderingAndInitialStateBlock.rb +151 -0
- data/test/Poro/WithExplicitNonDeterministicEventOrdering.rb +149 -0
- data/test/Poro/WithInitialStateBlock.rb +145 -0
- data/test/Poro/WithMultipleFinalStates.rb +176 -0
- data/test/Poro/WithMultipleStateMachines.rb +192 -0
- data/test/Poro/WithMultipleStateMachinesAndStatefulBlock.rb +194 -0
- data/test/Poro/WithPersistenceNonSpecificExtend.rb +147 -0
- data/test/Poro/WithPersistenceNonSpecificExtendAndStatefulBlock.rb +149 -0
- data/test/Poro/WithPersistenceSpecificExtend.rb +147 -0
- data/test/Poro/WithPersistenceSpecificExtendAndStatefulBlock.rb +149 -0
- data/test/Poro/WithVariableName.rb +147 -0
- data/test/Poro.rb +5 -0
- data/test/Sequel/WithColumnName.rb +154 -0
- data/test/Sequel/WithExplicitDeterministicEventOrdering.rb +154 -0
- data/test/Sequel/WithExplicitGlobalNonDeterministicEventOrdering.rb +160 -0
- data/test/Sequel/WithExplicitGlobalNonDeterministicEventOrderingAndInitialStateBlock.rb +158 -0
- data/test/Sequel/WithExplicitNonDeterministicEventOrdering.rb +156 -0
- data/test/Sequel/WithInitialStateBlock.rb +152 -0
- data/test/Sequel/WithMultipleFinalStates.rb +183 -0
- data/test/Sequel/WithMultipleStateMachines.rb +216 -0
- data/test/Sequel/WithMultipleStateMachinesAndStatefulBlock.rb +218 -0
- data/test/Sequel/WithPersistenceNonSpecificExtend.rb +154 -0
- data/test/Sequel/WithPersistenceNonSpecificExtendAndStatefulBlock.rb +156 -0
- data/test/Sequel/WithPersistenceSpecificExtend.rb +154 -0
- data/test/Sequel/WithPersistenceSpecificExtendAndStatefulBlock.rb +156 -0
- data/test/Sequel/test_helper.rb +5 -0
- data/test/Sequel.rb +5 -0
- data/test/Stateful.rb +15 -0
- metadata +69 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19870edf42d6c87988172b8041d30496fd4f06c2c47ecb6cfd1d2903ba47f3e5
|
|
4
|
+
data.tar.gz: 05f29ff4ddeb5ffe4317b9bc94bf6ee1089fd9c27e88ebb1876512ff05fd3ff6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4126dfa52b171f819e7d9144e926c5ffcf8b80a9ad4ee0d2fbae09f61571a1955939a12ad9864d5c4639ab49fecbabc7bed4113a7e13adc1640ac54adf588104
|
|
7
|
+
data.tar.gz: cf3ec7be8218914f4f84d1f6a950e2ed42d46bfe5b70c6492d654ee884a2914add13da4cfb630446fd65a7088a2fe61e7cbb2951c470fd4e1bd0c120eda6675a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# stateful/CHANGELOG.md
|
|
2
|
+
|
|
3
|
+
## 2.2.0 (20260418): Add Sequel support.
|
|
4
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
### +
|
|
7
|
+
1. lib/Stateful/Sequel.rb: Sequel adapter module, mirroring Stateful::ActiveRecord.
|
|
8
|
+
2. lib/Stateful/Sequel/ClassMethods.rb: Sequel-specific class methods using self[:column] / self[:column]= for persistence.
|
|
9
|
+
3. test/Sequel/test\_helper.rb: Sequel test helper using in-memory SQLite.
|
|
10
|
+
4. test/Sequel.rb: Sequel test runner.
|
|
11
|
+
5. test/Sequel/WithPersistenceNonSpecificExtend.rb
|
|
12
|
+
6. test/Sequel/WithPersistenceNonSpecificExtendAndStatefulBlock.rb
|
|
13
|
+
7. test/Sequel/WithPersistenceSpecificExtend.rb
|
|
14
|
+
8. test/Sequel/WithPersistenceSpecificExtendAndStatefulBlock.rb
|
|
15
|
+
9. test/Sequel/WithInitialStateBlock.rb
|
|
16
|
+
10. test/Sequel/WithColumnName.rb
|
|
17
|
+
11. test/Sequel/WithMultipleFinalStates.rb
|
|
18
|
+
12. test/Sequel/WithMultipleStateMachines.rb
|
|
19
|
+
13. test/Sequel/WithMultipleStateMachinesAndStatefulBlock.rb
|
|
20
|
+
14. test/Sequel/WithExplicitDeterministicEventOrdering.rb
|
|
21
|
+
15. test/Sequel/WithExplicitNonDeterministicEventOrdering.rb
|
|
22
|
+
16. test/Sequel/WithExplicitGlobalNonDeterministicEventOrdering.rb
|
|
23
|
+
17. test/Sequel/WithExplicitGlobalNonDeterministicEventOrderingAndInitialStateBlock.rb
|
|
24
|
+
|
|
25
|
+
### ~
|
|
26
|
+
1. lib/Stateful.rb: Auto-detect Sequel::Model in Stateful.extended, between ActiveRecord and Poro.
|
|
27
|
+
2. test/Stateful.rb: + Sequel specs
|
|
28
|
+
3. stateful.rb.gemspec: + sequel dev dependency; update description to include Sequel.
|
|
29
|
+
4. lib/Stateful/VERSION.rb: /2.1.0/2.2.0/
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## 2.1.0 (20260412): Add mRuby support.
|
|
33
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
### +
|
|
36
|
+
1. mrbgem.rake: mrbgem specification with spec.rbfiles pointing at lib/ directly.
|
|
37
|
+
2. mruby/stub\_require\_relative.rb: Stub to silence require_relative calls at mRuby build time.
|
|
38
|
+
3. mruby/smoke_test.rb: Smoke test for mRuby compatibility.
|
|
39
|
+
4. TODO (See notes.txt.)
|
|
40
|
+
|
|
41
|
+
### -
|
|
42
|
+
1. CHANGELOG.txt as it was left empty after creating CHANGELOG.md.
|
|
43
|
+
2. notes.txt (Now TODO.)
|
|
44
|
+
|
|
45
|
+
### ~
|
|
46
|
+
1. lib/Stateful.rb: + Object.const_defined?(:ActiveRecord) for mRuby compatibility. In mRuby defined?() raises NameError instead of returning nil as with CRuby.
|
|
47
|
+
2. lib/Stateful/VERSION.rb: /2.0.0/2.1.0/
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## 2.0.0 (20260216): Allow multiple state machines per class.
|
|
51
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
### +
|
|
54
|
+
1. Stateful::ClassMethods#state_machines: For indexing named `StateMachine` instances.
|
|
55
|
+
2. Stateful::ClassMethods#state_machine
|
|
56
|
+
3. Poro::ClassMethods#define_named_machine_methods
|
|
57
|
+
4. ActiveRecord::ClassMethods#define_named_machine_methods
|
|
58
|
+
5. StateMachine accepts parameter for `global_non_deterministic_event_ordering
|
|
59
|
+
6. ~/test/ActiveRecord/test_helper.rb
|
|
60
|
+
|
|
61
|
+
### -
|
|
62
|
+
1. StateMachine#stateful
|
|
63
|
+
|
|
64
|
+
### ~
|
|
65
|
+
1. Stateful::ClassMethods#stateful: Use class_eval instead of delegating to StateMachine#stateful, which is now removed.
|
|
66
|
+
2. /Stateful::States/Stateful::StateMachine/
|
|
67
|
+
3. /stateful_states/stateful_state_machine/
|
|
68
|
+
4. Test infrastructure migrated from PostgreSQL to SQLite (in-memory)
|
|
69
|
+
5. stateful.rb.gemspec: /pg/sqlite3/
|
|
70
|
+
6. Stateful::StateMachine#final_states: final_state?, final_states? weren't being defined.
|
|
71
|
+
7. lib/Stateful/VERSION.rb: /1.0.2/2.1.0/
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## 1.0.2 (20260216): Fix non-deterministic event ordering.
|
|
75
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
### ~
|
|
78
|
+
1. Stateful::State#non_deterministic_event_ordering?: Fixed logic bug where (@options[:deterministic] && !@options[:deterministic]) always evaluated to false.
|
|
79
|
+
2. Stateful::States#global_non_deterministic_event_ordering?: Same fix as above.
|
|
80
|
+
3. Stateful::State: /shuffle/shuffle!/non-deterministic ordering actually mutates the transitions array.
|
|
81
|
+
4. Stateful::State: Removed require_relative for lib/Array/shuffle.rb.
|
|
82
|
+
5. test/ActiveRecord/WithPersistenceSpecificExtend.rb: Fixed delete_all guard to check 'active_record_machine2s' instead of 'active_record_machine1s'.
|
|
83
|
+
6. test/ActiveRecord/WithMultipleFinalStates.rb: Fixed comment header (was Poro/WithMultipleFinalStates.rb).
|
|
84
|
+
7. test/Poro/WithVariableName.rb: Fixed comment header (was ActiveRecord/WithVariableName.rb).
|
|
85
|
+
8. test/Poro/WithExplicitNonDeterministicEventOrdering.rb: Changed transition order assertions to use must_include instead of must_equal.
|
|
86
|
+
9. test/ActiveRecord/WithExplicitNonDeterministicEventOrdering.rb: Same as above.
|
|
87
|
+
10. test/ActiveRecord/WithExplicitGlobalNonDeterministicEventOrdering.rb: Same as above.
|
|
88
|
+
11. test/ActiveRecord/WithExplicitGlobalNonDeterministicEventOrderingAndInitialStateBlock.rb: Same as above.
|
|
89
|
+
12. lib/Stateful/VERSION.rb: /1.0.1/1.0.2/
|
|
90
|
+
|
|
91
|
+
### -
|
|
92
|
+
1. Array#shuffle: Use stdlib.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## 1.0.1 (20260216): Fix date and version.
|
|
96
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
### ~
|
|
99
|
+
1. stateful.rb.gemspec: Insert the correct date.
|
|
100
|
+
2. lib/Stateful/VERSION.rb: /1.0.0/1.0.1/
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## 1.0.0 (20260216): Version number bump to 1.0.
|
|
104
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
### ~
|
|
107
|
+
1. lib/Stateful/VERSION.rb: /0.14.12/1.0.0/
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## 0.14.12 (20260216): Change file mode on lib/Array/shuffle.rb.
|
|
111
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
### ~
|
|
114
|
+
1. lib/Array/shuffle.rb: File mode change to read-only from being executable.
|
|
115
|
+
2. lib/Stateful/VERSION.rb: /0.14.11/0.14.12/
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## 0.14.11 (20200419): Fix copypasta error in README.md.
|
|
119
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
### ~
|
|
122
|
+
1. README.md: Fixed copypasta error in Installation section (was 'git.rb', now 'stateful.rb')
|
|
123
|
+
2. lib/Stateful/VERSION.rb: /0.14.10/0.14.11/
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## 0.14.10 (20150207, 20200331): Allow Poro::ClassMethods to handle its own initialisation when extended.
|
|
127
|
+
-----------------------------------------------------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
### +
|
|
130
|
+
1. I finally implemented a change which I'd had sitting in a file never committed named shouldgetthisworking.rb since 2015-02-07 which entails moving the instance method generation methods from each of the application-specific files (lib/Poro.rb and lib/ActiveRecord.rb) to the application-specific class methods files (lib/Poro/ClassMethods.rb and lib/ActiveRecord/ClassMethods.rb).
|
|
131
|
+
2. README.md
|
|
132
|
+
3. CHANGES.txt
|
|
133
|
+
|
|
134
|
+
### ~
|
|
135
|
+
1. .gitignore: + *.gem
|
|
136
|
+
2. lib/Stateful/VERSION.rb: /0.14.9/0.14.10/
|
|
137
|
+
3. stateful.rb.gemspec to reflect the updated version
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Stateful
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
A Ruby state machine which allows you to easily add state to Poro and ActiveRecord objects. Works with both CRuby and mRuby.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### CRuby
|
|
10
|
+
|
|
11
|
+
Add this line to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'stateful.rb'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
And then execute:
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
$ bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install it yourself as:
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
$ gem install stateful.rb
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### mRuby
|
|
30
|
+
|
|
31
|
+
Add this to your `build_config.rb`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
conf.gem github: 'thoran/Stateful'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### 1. Poro with persistence-specific extend and a stateful block declaration
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class PoroMachine
|
|
43
|
+
extend Stateful::Poro
|
|
44
|
+
|
|
45
|
+
stateful do
|
|
46
|
+
initial_state :initial_state
|
|
47
|
+
|
|
48
|
+
state :initial_state do
|
|
49
|
+
on :an_event => :next_state
|
|
50
|
+
on :another_event => :final_state
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
state :next_state do
|
|
54
|
+
on :yet_another_event => :final_state
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
final_state :final_state
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
poro_machine = PoroMachine.new
|
|
62
|
+
poro_machine.initial_state.name
|
|
63
|
+
# => :initial_state
|
|
64
|
+
poro_machine.final_state.name
|
|
65
|
+
# => :final_state
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Poro with persistence non-specific extend, without a stateful block declaration, an initial_state block, and multiple final states
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class PoroMachine
|
|
72
|
+
extend Stateful
|
|
73
|
+
|
|
74
|
+
initial_state :initial_state do
|
|
75
|
+
on :an_event => :next_state
|
|
76
|
+
on :another_event => :final_state0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
state :next_state do
|
|
80
|
+
on :yet_another_event => :final_state1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
final_state :final_state0, :final_state1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
poro_machine = PoroMachine.new
|
|
87
|
+
poro_machine.current_state.name
|
|
88
|
+
# => :initial_state
|
|
89
|
+
poro_machine.an_event
|
|
90
|
+
poro_machine.current_state.name
|
|
91
|
+
# => :next_state
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. ActiveRecord with persistence-specific extend and a stateful block declaration
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class ActiveRecordMachine < ActiveRecord::Base
|
|
98
|
+
extend Stateful::ActiveRecord
|
|
99
|
+
|
|
100
|
+
stateful do
|
|
101
|
+
initial_state :initial_state
|
|
102
|
+
|
|
103
|
+
state :initial_state do
|
|
104
|
+
on :an_event => :next_state
|
|
105
|
+
on :another_event => :final_state
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
state :next_state do
|
|
109
|
+
on :yet_another_event => :final_state
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
final_state :final_state
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
active_record_machine = ActiveRecordMachine.new
|
|
117
|
+
active_record_machine.initial_state?
|
|
118
|
+
# => true
|
|
119
|
+
active_record_machine.an_event
|
|
120
|
+
active_record_machine.next_state?
|
|
121
|
+
# => true
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4. ActiveRecord with persistence non-specific extend, without a stateful block declaration, an initial_state block, and multiple final states
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class ActiveRecordMachine < ActiveRecord::Base
|
|
128
|
+
extend Stateful
|
|
129
|
+
|
|
130
|
+
initial_state :initial_state do
|
|
131
|
+
on :an_event => :next_state
|
|
132
|
+
on :another_event => :final_state0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
state :next_state do
|
|
136
|
+
on :yet_another_event => :final_state0
|
|
137
|
+
on :and_yet_another_event => :final_state1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
final_state :final_state0, :final_state1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
active_record_machine = ActiveRecordMachine.new
|
|
144
|
+
active_record_machine.an_event
|
|
145
|
+
active_record_machine.final_state?
|
|
146
|
+
# => false
|
|
147
|
+
active_record_machine.yet_another_event
|
|
148
|
+
active_record_machine.final_state?
|
|
149
|
+
# => true
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 5. Custom variable/column name and non-deterministic event ordering
|
|
153
|
+
|
|
154
|
+
It's also possible to define a state machine (either Poro or ActiveRecord) which has a custom variable (Poro) or column (ActiveRecord) name, which fires off the events in a random or non-deterministic order, and which has no final state.
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
class ActiveRecordMachine < ActiveRecord::Base
|
|
158
|
+
|
|
159
|
+
@stateful_column_name = 'status' # This needs to be set before the class is extended. The default is 'current_state'.
|
|
160
|
+
|
|
161
|
+
extend Stateful
|
|
162
|
+
|
|
163
|
+
initial_state :initial_state, non_deterministic: true do
|
|
164
|
+
on :an_event => :another_state
|
|
165
|
+
on :another_event => :yet_another_state
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
state :another_state do
|
|
169
|
+
on :another_event => :yet_another_state
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
state :yet_another_state do
|
|
173
|
+
on :yet_another_event => :another_state
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
active_record_machine = ActiveRecordMachine.new
|
|
178
|
+
# Supposing that both :an_event or :another_event messages are able to fire, then if the order of the evaluation of the transitions is non-deterministic, then the state change is also. Non-deterministic event ordering really only makes sense in the context of the sibling library thoran/Eventful, which will evaluate the transitions automatically and will do so in the order as presented by Stateful.
|
|
179
|
+
active_record_machine.another_event # With non-deterministic event firing it could check for :another_event before checking :an_event.
|
|
180
|
+
active_record_machine.current_state.name
|
|
181
|
+
# => :yet_another_state
|
|
182
|
+
active_record_machine.an_event
|
|
183
|
+
active_record_machine.current_state.name
|
|
184
|
+
# => :yet_another_state
|
|
185
|
+
active_record_machine.final_state?
|
|
186
|
+
# => false
|
|
187
|
+
active_record_machine.yet_another_event
|
|
188
|
+
active_record_machine.final_state?
|
|
189
|
+
# => false
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 6. Multiple state machines per class
|
|
193
|
+
|
|
194
|
+
A single class can have multiple independent (orthogonal) state machines. Each machine gets its own namespaced methods, while domain predicates and event methods remain unnamespaced.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
class Order
|
|
198
|
+
extend Stateful
|
|
199
|
+
|
|
200
|
+
state_machine :fulfilment do
|
|
201
|
+
initial_state :pending do
|
|
202
|
+
on :items_collected => :ready
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
state :ready do
|
|
206
|
+
on :dispatch => :dispatched
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
final_state :dispatched
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
state_machine :payment do
|
|
213
|
+
initial_state :unpaid do
|
|
214
|
+
on :pay => :paid
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
state :paid do
|
|
218
|
+
on :refund => :refunded
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
final_state :refunded
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
order = Order.new
|
|
226
|
+
|
|
227
|
+
order.fulfilment_state.name # state accessors are namespaced.
|
|
228
|
+
# => :pending
|
|
229
|
+
order.payment_state.name
|
|
230
|
+
# => :unpaid
|
|
231
|
+
|
|
232
|
+
order.fulfilment_initial_state? # General/overlapping predicates are namespaced.
|
|
233
|
+
# => true
|
|
234
|
+
order.payment_final_state?
|
|
235
|
+
# => false
|
|
236
|
+
|
|
237
|
+
order.fulfilment_transitions.first.event_name # Transitions are namespaced.
|
|
238
|
+
# => :items_collected
|
|
239
|
+
|
|
240
|
+
order.pending? # Domain predicates are not namespaced.
|
|
241
|
+
# => true
|
|
242
|
+
order.unpaid?
|
|
243
|
+
# => true
|
|
244
|
+
|
|
245
|
+
order.pay # Event methods are not namespaced.
|
|
246
|
+
order.paid?
|
|
247
|
+
# => true
|
|
248
|
+
|
|
249
|
+
# State machines are orthogonal
|
|
250
|
+
order.fulfilment_initial_state?
|
|
251
|
+
# => true
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Multiple state machines can also be declared inside a stateful block:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
class Order
|
|
258
|
+
extend Stateful
|
|
259
|
+
|
|
260
|
+
stateful do
|
|
261
|
+
state_machine :fulfilment do
|
|
262
|
+
initial_state :pending do
|
|
263
|
+
on :items_collected => :ready
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
state :ready do
|
|
267
|
+
on :dispatch => :dispatched
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
final_state :dispatched
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
state_machine :payment do
|
|
274
|
+
initial_state :unpaid do
|
|
275
|
+
on :pay => :paid
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
state :paid do
|
|
279
|
+
on :refund => :refunded
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
final_state :refunded
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
For ActiveRecord, each machine uses a column named `<machine_name>_state`:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
class CreateOrders < ActiveRecord::Migration[6.0]
|
|
292
|
+
def change
|
|
293
|
+
create_table :orders do |t|
|
|
294
|
+
t.string :fulfilment_state
|
|
295
|
+
t.string :payment_state
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
For Poro, each machine uses an instance variable named `@<machine_name>_state`.
|
|
302
|
+
|
|
303
|
+
## Contributing
|
|
304
|
+
|
|
305
|
+
1. Fork it: https://github.com/thoran/Stateful/fork
|
|
306
|
+
2. Create your feature branch: `git checkout -b my-new-feature`
|
|
307
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
|
308
|
+
4. Push to the branch: `git push origin my-new-feature`
|
|
309
|
+
5. Create a new pull request
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Stateful/Sequel/ClassMethods.rb
|
|
2
|
+
# Stateful::Sequel::ClassMethods
|
|
3
|
+
|
|
4
|
+
module Stateful
|
|
5
|
+
module Sequel
|
|
6
|
+
module ClassMethods
|
|
7
|
+
class << self
|
|
8
|
+
def extended(klass)
|
|
9
|
+
klass.define_stateful_column_name_setter_method
|
|
10
|
+
klass.define_stateful_column_name_getter_method
|
|
11
|
+
klass.define_next_state_method
|
|
12
|
+
klass.define_transitions_method
|
|
13
|
+
klass.define_initial_stateQ_method
|
|
14
|
+
klass.define_final_stateQ_method
|
|
15
|
+
end
|
|
16
|
+
end # class << self
|
|
17
|
+
|
|
18
|
+
def define_named_machine_methods(machine_name)
|
|
19
|
+
define_stateful_column_name_setter_method(machine_name: machine_name)
|
|
20
|
+
define_stateful_column_name_getter_method(machine_name: machine_name)
|
|
21
|
+
define_next_state_method(machine_name: machine_name)
|
|
22
|
+
define_transitions_method(machine_name: machine_name)
|
|
23
|
+
define_initial_stateQ_method(machine_name: machine_name)
|
|
24
|
+
define_final_stateQ_method(machine_name: machine_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stateful_column_name=(stateful_column_name)
|
|
28
|
+
@stateful_column_name = stateful_column_name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stateful_column_name
|
|
32
|
+
@stateful_column_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def column_name_for_machine(machine_name = nil)
|
|
36
|
+
if machine_name
|
|
37
|
+
"#{machine_name}_state"
|
|
38
|
+
else
|
|
39
|
+
instance_variable_get(:@stateful_column_name)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def define_event_method(transition, machine_name: nil)
|
|
44
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
45
|
+
define_method(transition.event_name) do
|
|
46
|
+
next_state_name = self.send(stateful_column_name).next_state_name(transition.event_name)
|
|
47
|
+
next_state = self.class.stateful_state_machine(machine_name).find(next_state_name)
|
|
48
|
+
self.send("#{stateful_column_name}=", next_state)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def define_status_predicate_method(state_name, machine_name: nil)
|
|
53
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
54
|
+
define_method("#{state_name}?") do
|
|
55
|
+
self.send(stateful_column_name).name == state_name
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def define_stateful_column_name_setter_method(machine_name: nil)
|
|
60
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
61
|
+
define_method("#{stateful_column_name}=") do |state|
|
|
62
|
+
instance_variable_set("@#{stateful_column_name}", self.class.stateful_state_machine(machine_name).find(state))
|
|
63
|
+
self[stateful_column_name.to_sym] = instance_variable_get("@#{stateful_column_name}").name.to_s
|
|
64
|
+
self.save
|
|
65
|
+
instance_variable_get("@#{stateful_column_name}")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def define_stateful_column_name_getter_method(machine_name: nil)
|
|
70
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
71
|
+
define_method(stateful_column_name) do
|
|
72
|
+
instance_variable_set("@#{stateful_column_name}", self.class.stateful_state_machine(machine_name).find(self[stateful_column_name.to_sym]))
|
|
73
|
+
if state = instance_variable_get("@#{stateful_column_name}")
|
|
74
|
+
state
|
|
75
|
+
else
|
|
76
|
+
initial_state = self.class.stateful_state_machine(machine_name).initial_state
|
|
77
|
+
self.send("#{stateful_column_name}=", initial_state.name)
|
|
78
|
+
initial_state
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def define_next_state_method(machine_name: nil)
|
|
84
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
85
|
+
method_name = machine_name ? "#{machine_name}_next_state" : :next_state
|
|
86
|
+
define_method(method_name) do |event_name|
|
|
87
|
+
next_state_name = self.send(stateful_column_name).next_state_name(event_name)
|
|
88
|
+
self.class.stateful_state_machine(machine_name).find(next_state_name)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def define_transitions_method(machine_name: nil)
|
|
93
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
94
|
+
method_name = machine_name ? "#{machine_name}_transitions" : :transitions
|
|
95
|
+
define_method(method_name) do
|
|
96
|
+
self.send(stateful_column_name).transitions
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def define_initial_stateQ_method(machine_name: nil)
|
|
101
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
102
|
+
method_name = machine_name ? "#{machine_name}_initial_state?" : :initial_state?
|
|
103
|
+
define_method(method_name) do
|
|
104
|
+
self.send(stateful_column_name) == self.class.stateful_state_machine(machine_name).initial_state
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def define_final_stateQ_method(machine_name: nil)
|
|
109
|
+
stateful_column_name = column_name_for_machine(machine_name)
|
|
110
|
+
method_name = machine_name ? "#{machine_name}_final_state?" : :final_state?
|
|
111
|
+
define_method(method_name) do
|
|
112
|
+
self.class.stateful_state_machine(machine_name).final_states.include?(self.send(stateful_column_name))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Stateful/Sequel.rb
|
|
2
|
+
# Stateful::Sequel
|
|
3
|
+
|
|
4
|
+
require_relative 'ClassMethods'
|
|
5
|
+
require_relative 'InstanceMethods'
|
|
6
|
+
require_relative File.join('Sequel', 'ClassMethods')
|
|
7
|
+
|
|
8
|
+
module Stateful
|
|
9
|
+
module Sequel
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
|
|
13
|
+
def set_column_name(klass)
|
|
14
|
+
unless klass.instance_variable_get(:@stateful_column_name)
|
|
15
|
+
klass.instance_variable_set(:@stateful_column_name, 'current_state')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def extended(klass)
|
|
20
|
+
klass.extend(Stateful::ClassMethods)
|
|
21
|
+
klass.send(:include, Stateful::InstanceMethods)
|
|
22
|
+
set_column_name(klass)
|
|
23
|
+
klass.extend(Stateful::Sequel::ClassMethods)
|
|
24
|
+
end
|
|
25
|
+
alias_method :included, :extended
|
|
26
|
+
|
|
27
|
+
end # class << self
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/Stateful/VERSION.rb
CHANGED
data/lib/Stateful.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Stateful
|
|
|
9
9
|
if Object.const_defined?(:ActiveRecord) && defined?(::ActiveRecord::Base) && klass < ::ActiveRecord::Base
|
|
10
10
|
require_relative File.join('Stateful', 'ActiveRecord')
|
|
11
11
|
klass.extend(Stateful::ActiveRecord)
|
|
12
|
+
elsif Object.const_defined?(:Sequel) && defined?(::Sequel::Model) && klass < ::Sequel::Model
|
|
13
|
+
require_relative File.join('Stateful', 'Sequel')
|
|
14
|
+
klass.extend(Stateful::Sequel)
|
|
12
15
|
else
|
|
13
16
|
require_relative File.join('Stateful', 'Poro')
|
|
14
17
|
klass.extend(Stateful::Poro)
|
data/stateful.rb.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative './lib/Stateful/VERSION'
|
|
2
|
+
|
|
3
|
+
class Gem::Specification
|
|
4
|
+
def development_dependencies=(gems)
|
|
5
|
+
gems.each{|gem| add_development_dependency(*gem)}
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
Gem::Specification.new do |spec|
|
|
10
|
+
spec.name = 'stateful.rb'
|
|
11
|
+
spec.version = Stateful::VERSION
|
|
12
|
+
|
|
13
|
+
spec.summary = "A Ruby state machine."
|
|
14
|
+
spec.description = "Easily add state to Poro, ActiveRecord, and Sequel objects."
|
|
15
|
+
|
|
16
|
+
spec.author = 'thoran'
|
|
17
|
+
spec.email = 'code@thoran.com'
|
|
18
|
+
spec.homepage = 'http://github.com/thoran/stateful'
|
|
19
|
+
spec.license = 'MIT'
|
|
20
|
+
|
|
21
|
+
# The required version of Ruby is this higher than it strictly needs to be solely because the migrations have been made to require AR 6.0, but otherwise this should work with much lower versions of Ruby and ActiveRecord, even down to 2.3 (or lower) and AR 3 (or lower).
|
|
22
|
+
spec.required_ruby_version = '>= 2.5'
|
|
23
|
+
|
|
24
|
+
spec.require_paths = ['lib']
|
|
25
|
+
|
|
26
|
+
spec.files = [
|
|
27
|
+
'stateful.rb.gemspec',
|
|
28
|
+
'CHANGELOG.md',
|
|
29
|
+
'Gemfile',
|
|
30
|
+
'README.md',
|
|
31
|
+
Dir['lib/**/*.rb'],
|
|
32
|
+
Dir['test/**/*.rb']
|
|
33
|
+
].flatten
|
|
34
|
+
|
|
35
|
+
spec.development_dependencies = %w{
|
|
36
|
+
minitest
|
|
37
|
+
minitest-spec-context
|
|
38
|
+
activerecord
|
|
39
|
+
sequel
|
|
40
|
+
sqlite3
|
|
41
|
+
}
|
|
42
|
+
end
|