decider 0.9.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 +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +377 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +10 -0
- data/Steepfile +26 -0
- data/examples/decide.md +180 -0
- data/examples/evolve.md +150 -0
- data/examples/infra.md +15 -0
- data/examples/state.md +55 -0
- data/lib/decider/event_sourcing.rb +25 -0
- data/lib/decider/in_memory.rb +26 -0
- data/lib/decider/reactor.rb +142 -0
- data/lib/decider/state.rb +25 -0
- data/lib/decider/version.rb +5 -0
- data/lib/decider/view.rb +154 -0
- data/lib/decider.rb +380 -0
- data/mise.toml +17 -0
- data/sig/decider.rbs +16 -0
- metadata +107 -0
data/examples/decide.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Decide
|
|
2
|
+
|
|
3
|
+
## Match by command
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Command = Data.define(:value)
|
|
7
|
+
State = Data.define(:value)
|
|
8
|
+
|
|
9
|
+
decider = Decider.define do
|
|
10
|
+
initial_state State.new(value: 5)
|
|
11
|
+
|
|
12
|
+
decide Command do
|
|
13
|
+
# emit events
|
|
14
|
+
emit Event.new(
|
|
15
|
+
value: command.value + state.value
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
decider.decide Command.new(value: 10), decider.initial_state
|
|
21
|
+
#> [#<data Event value=15>]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Match by command and state
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
decider = Decider.define do
|
|
28
|
+
initial_state :turned_off
|
|
29
|
+
|
|
30
|
+
decide :turn_on, :turned_off do
|
|
31
|
+
emit :turned_on
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
decide :turn_off, :turned_on do
|
|
35
|
+
emit :turned_off
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
decider.decide :turn_on, decider.initial_state
|
|
40
|
+
#> [:turned_on]
|
|
41
|
+
decider.decide :turn_off, :turned_on
|
|
42
|
+
#> [:turned_off]
|
|
43
|
+
decider.decide :turn_on, :turned_on
|
|
44
|
+
#> []
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Match by pattern matching
|
|
48
|
+
|
|
49
|
+
State = Data.define(:status)
|
|
50
|
+
TurnOn = Data.define
|
|
51
|
+
TurnOff = Data.define
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
decider = Decider.define do
|
|
55
|
+
initial_state State.new(status: :turned_off)
|
|
56
|
+
|
|
57
|
+
decide proc { [command, state] in [TurnOn, State(status: :turned_off)] } do
|
|
58
|
+
emit :turned_on
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
decide proc { [command, state] in [TurnOff, State(status: :turned_on)] } do
|
|
62
|
+
emit :turned_off
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
decider.decide :turn_on, decider.initial_state
|
|
67
|
+
#> [:turned_on]
|
|
68
|
+
decider.decide :turn_off, :turned_on
|
|
69
|
+
#> [:turned_off]
|
|
70
|
+
decider.decide :turn_on, :turned_on
|
|
71
|
+
#> []
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Handling unknown commands
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
decider = Decider.define do
|
|
78
|
+
initial_state :initial
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Return empty list of events (nothing changed) by default:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
decider.decide :unknown, decider.initial_state
|
|
86
|
+
#> []
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If you want to raise error, define a catch-all as last one:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
decider = Decider.define do
|
|
93
|
+
initial_state :initial
|
|
94
|
+
|
|
95
|
+
decide proc { true } do
|
|
96
|
+
raise ArgumentError, "Unknown command #{command}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
decider.decide :unknown, decider.initial_state
|
|
100
|
+
#> Unknown command unknown (ArgumentError)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
Commands can be primitives like symbols:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
decider = Decider.define do
|
|
109
|
+
initial_state :initial
|
|
110
|
+
|
|
111
|
+
decide proc { [command, state] in [:start, :initial | :stopped] } do
|
|
112
|
+
emit :started
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
decide proc { [command, state] in [:stop, :started] } do
|
|
116
|
+
emit :stopped
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
decider.decide :start, decider.initial_state
|
|
121
|
+
#> [:started]
|
|
122
|
+
decider.decide :stop, :started
|
|
123
|
+
#> [:stopped]
|
|
124
|
+
decider.decider :start, :started
|
|
125
|
+
#> []
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or any classes like [Dry::Struct](https://dry-rb.org/gems/dry-struct/) or [Data](https://rubyapi.org/3.3/o/data):
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
Start = Data.define(:value)
|
|
132
|
+
Stop = Data.define
|
|
133
|
+
|
|
134
|
+
decider = Decider.define do
|
|
135
|
+
initial_state :initial
|
|
136
|
+
|
|
137
|
+
decide proc { [command, state] in [Start, :initial | :stopped] } do
|
|
138
|
+
emit [:started, command.value]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
decide proc { [command, state] in [Stop, :started] } do
|
|
142
|
+
emit :stopped
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
decider.decide Start.new(value: 10), decider.initial_state
|
|
147
|
+
#> [[:started, 10]]
|
|
148
|
+
decider.decide Stop.new, [:started, 10]
|
|
149
|
+
#> [:stopped]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Emitting Events
|
|
153
|
+
|
|
154
|
+
Decide can emit 0, 1 or more events:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
decider = Decider.define do
|
|
158
|
+
initial_state :initial
|
|
159
|
+
|
|
160
|
+
decide :none do
|
|
161
|
+
# noop
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
decide :one do
|
|
165
|
+
emit :event
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
decide :multiple do
|
|
169
|
+
emit :one
|
|
170
|
+
emit :two
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
decider.decide :none, decider.initial_state
|
|
175
|
+
#> []
|
|
176
|
+
decider.decide :one, decider.initial_state
|
|
177
|
+
#> [:event]
|
|
178
|
+
decider.decide :multiple, decider.initial_state
|
|
179
|
+
#> [:one, :two]
|
|
180
|
+
```
|
data/examples/evolve.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Evolve
|
|
2
|
+
|
|
3
|
+
## Match by event
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Event = Data.define(:value)
|
|
7
|
+
|
|
8
|
+
decider = Decider.define do
|
|
9
|
+
initial_state :initial
|
|
10
|
+
|
|
11
|
+
evolve Event do
|
|
12
|
+
event.value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
decider.evolve decider.initial_state, Event.new(value: :changed)
|
|
17
|
+
#> :changed
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Match by state and event
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
decider = Decider.define do
|
|
24
|
+
initial_state :turned_off
|
|
25
|
+
|
|
26
|
+
evolve :turned_on, :turned_off do
|
|
27
|
+
event
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
evolve :turned_off, :turned_on do
|
|
31
|
+
event
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
decider.evolve decider.initial_state, :turned_on
|
|
36
|
+
#> :turned_on
|
|
37
|
+
decider.evolve :turned_on, :turned_off
|
|
38
|
+
#> :turned_off
|
|
39
|
+
decider.evolve :turned_on, :unknown
|
|
40
|
+
#> :turned_on
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Match by pattern matching
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
State = Data.define(:value)
|
|
47
|
+
Event = Data.define(:value)
|
|
48
|
+
|
|
49
|
+
decider = Decider.define do
|
|
50
|
+
initial_state State.new(value: :turned_off)
|
|
51
|
+
|
|
52
|
+
evolve proc { [state, event] in [State(value: :turned_off), Event(value: :turned_on)] } do
|
|
53
|
+
state.with(value: event.value)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
decider.evolve decider.initial_state, Event.new(value: :turned_on)
|
|
58
|
+
#> #<data State value=:turned_on>
|
|
59
|
+
decider.evolve decider.initial_state, :unknown
|
|
60
|
+
#> #<data State value=:turned_off>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Handling unknown events
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
decider = Decider.define do
|
|
67
|
+
initial_state :initial
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
decider.decide decider.initial_state, :initial
|
|
71
|
+
#> :initial
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If you want to raise error, define a catch-all as last one:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
decider = Decider.define do
|
|
78
|
+
initial_state :initial
|
|
79
|
+
|
|
80
|
+
evolve proc { true } do
|
|
81
|
+
raise ArgumentError, "Unknown event #{event}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
decider.evolve decider.initial_state, :unknown
|
|
85
|
+
#> Unknown event unknown (ArgumentError)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Events
|
|
89
|
+
|
|
90
|
+
Events can be primitives like symbols:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
decider = Decider.define do
|
|
94
|
+
initial_state :initial
|
|
95
|
+
|
|
96
|
+
evolve :started do
|
|
97
|
+
:started
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
evolve :stopped do
|
|
101
|
+
:stopped
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or any classes like [Dry::Struct](https://dry-rb.org/gems/dry-struct/) or [Data](https://rubyapi.org/3.3/o/data):
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
State = Data.define(:speed)
|
|
110
|
+
Started = Data.define(:speed)
|
|
111
|
+
Stopped = Data.define
|
|
112
|
+
|
|
113
|
+
decider = Decider.define do
|
|
114
|
+
initial_state State.new(speed: 0)
|
|
115
|
+
|
|
116
|
+
evolve State, Started do
|
|
117
|
+
state.with(speed: event.speed)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
evolve State, Stopped do
|
|
121
|
+
State.new(speed: 0)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Calculate state
|
|
127
|
+
|
|
128
|
+
In most cases you want to take a collection of events and reduce them with `evolve` to calculate the state, like:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
decider = Decider.define do
|
|
132
|
+
initial_state 0
|
|
133
|
+
|
|
134
|
+
evolve :increased do
|
|
135
|
+
state + 1
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
[:increased, :increased].reduce(decider.initial_state) { |state, event| decider.evolve(state, event) }
|
|
140
|
+
#> 2
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You can shortcut that with `&`:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
[:increased, :increased].reduce(decider.initial_state, &decider.method(:evolve))
|
|
147
|
+
#> 2
|
|
148
|
+
[:increased, :increased].reduce(decider.initial_state, &decider.evolve)
|
|
149
|
+
#> 2
|
|
150
|
+
```
|
data/examples/infra.md
ADDED
data/examples/state.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# State examples
|
|
2
|
+
|
|
3
|
+
## Data
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
State = Data.define(:value)
|
|
7
|
+
|
|
8
|
+
decider = Decider.define do
|
|
9
|
+
initial_state State.new(value: 0)
|
|
10
|
+
|
|
11
|
+
decide Commands::Command do
|
|
12
|
+
emit Events::Event.new(value: command.value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
evolve Events::Event do
|
|
16
|
+
state.with(value: state.value + 1)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Primitive
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
decider = Decider.define do
|
|
25
|
+
initial_state :turned_off
|
|
26
|
+
|
|
27
|
+
decide Commands::TurnOn, :turned_off do
|
|
28
|
+
emit Events::TurnedOn.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
decide Commands::TurnOff, :turned_on do
|
|
32
|
+
emit Events::TurnedOff.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
evolve Events::TurnedOn do
|
|
36
|
+
:turned_on
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## List
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
decider = Decider.define do
|
|
45
|
+
initial_state []
|
|
46
|
+
|
|
47
|
+
decide Commands::Command, [] do
|
|
48
|
+
emit Events::Event.new(value: command.value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
evolve Events::Event do
|
|
52
|
+
state + [event]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decider
|
|
4
|
+
class EventSourcing
|
|
5
|
+
def initialize(decider:, event_store:)
|
|
6
|
+
@decider = decider
|
|
7
|
+
@event_store = event_store
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(command, stream_name:)
|
|
11
|
+
events = event_store.read.stream(stream_name)
|
|
12
|
+
state = events.reduce(decider.initial_state, &decider.method(:evolve))
|
|
13
|
+
|
|
14
|
+
new_events = decider.decide(command, state)
|
|
15
|
+
|
|
16
|
+
event_store.append(new_events, stream_name: stream_name, expected_version: events.count)
|
|
17
|
+
|
|
18
|
+
[new_events, events.count + new_events.count]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :decider, :event_store
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent-ruby"
|
|
4
|
+
|
|
5
|
+
module Decider
|
|
6
|
+
class InMemory
|
|
7
|
+
def initialize(decider)
|
|
8
|
+
@decider = decider
|
|
9
|
+
@atom = Concurrent::Atom.new(decider.initial_state)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(command)
|
|
13
|
+
events = decider.decide(command, state)
|
|
14
|
+
atom.swap { |state| events.reduce(state, &decider.method(:evolve)) }
|
|
15
|
+
events
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def state
|
|
19
|
+
atom.value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :decider, :atom
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decider
|
|
4
|
+
module Reactor
|
|
5
|
+
class Module < ::Module
|
|
6
|
+
REACT_FALLBACK = proc { [nil, proc {}] }
|
|
7
|
+
|
|
8
|
+
React = Data.define(:action_result, :_actions) do
|
|
9
|
+
def issue(*actions)
|
|
10
|
+
_actions.push(*actions)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(reactions:)
|
|
15
|
+
define_method(:react) do |action_result|
|
|
16
|
+
context = React.new(action_result: action_result, _actions: [])
|
|
17
|
+
|
|
18
|
+
reactions.find(REACT_FALLBACK) do |arg, _|
|
|
19
|
+
case arg
|
|
20
|
+
in Proc => fn
|
|
21
|
+
context.instance_exec(&fn)
|
|
22
|
+
in artype
|
|
23
|
+
action_result in ^artype
|
|
24
|
+
else
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end => [_, handler]
|
|
28
|
+
|
|
29
|
+
context.instance_exec(&handler)
|
|
30
|
+
context._actions
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method(:lmap_on_action_result) do |fn|
|
|
34
|
+
Decider::Reactor.lmap_on_action_result(fn, self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
define_method(:rmap_on_action) do |fn|
|
|
38
|
+
Decider::Reactor.rmap_on_action(fn, self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
define_method(:map_on_action) do |fn|
|
|
42
|
+
Decider::Reactor.rmap_on_action(fn, self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
define_method(:combine_with_decider) do |decider|
|
|
46
|
+
Decider::Reactor.combine_with_decider(self, decider)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Builder
|
|
52
|
+
DEFAULT = Object.new
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@reactions = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build(&block)
|
|
59
|
+
instance_exec(&block) if block_given?
|
|
60
|
+
|
|
61
|
+
reactor = Class.new
|
|
62
|
+
|
|
63
|
+
mod = Module.new(
|
|
64
|
+
reactions: reactions
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
reactor.extend(mod)
|
|
68
|
+
|
|
69
|
+
reactor
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :reactions
|
|
75
|
+
|
|
76
|
+
def react(arg, &block)
|
|
77
|
+
reactions[arg] = block
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
private_constant :Builder
|
|
81
|
+
|
|
82
|
+
def self.define(&block)
|
|
83
|
+
builder = Builder.new
|
|
84
|
+
builder.build(&block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.lmap_on_action_result(fn, reactor)
|
|
88
|
+
define do
|
|
89
|
+
react proc { true } do
|
|
90
|
+
reactor.react(fn.call(action_result)).each do |action|
|
|
91
|
+
issue action
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.rmap_on_action(fn, reactor)
|
|
98
|
+
define do
|
|
99
|
+
react proc { true } do
|
|
100
|
+
reactor.react(action_result).each do |action|
|
|
101
|
+
issue fn.call(action)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.map_on_action(fn, reactor)
|
|
108
|
+
rmap_on_action(fn, reactor)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.combine_with_decider(reactor, decider)
|
|
112
|
+
Decider.define do
|
|
113
|
+
initial_state decider.initial_state
|
|
114
|
+
|
|
115
|
+
decide proc { true } do
|
|
116
|
+
fn = ->(commands, events, ds) {
|
|
117
|
+
case commands
|
|
118
|
+
in []
|
|
119
|
+
events
|
|
120
|
+
in [head, *tail]
|
|
121
|
+
new_events = decider.decide(head, ds)
|
|
122
|
+
new_commands = new_events.flat_map { |action_result| reactor.react(action_result) }
|
|
123
|
+
new_state = new_events.reduce(ds, &decider.evolve)
|
|
124
|
+
|
|
125
|
+
fn.call(tail + new_commands, events + new_events, new_state)
|
|
126
|
+
end
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn.call([command], [], state).each { |event| emit event }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
evolve proc { true } do
|
|
133
|
+
decider.evolve(state, event)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
terminal? do
|
|
137
|
+
decider.terminal?(state)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decider
|
|
4
|
+
class State
|
|
5
|
+
def initialize(decider:, repository:)
|
|
6
|
+
@decider = decider
|
|
7
|
+
@repository = repository
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(command, key:, etag: nil)
|
|
11
|
+
state, etag = repository.try_load(key: key, etag: etag)
|
|
12
|
+
|
|
13
|
+
events = decider.decide(command, state)
|
|
14
|
+
new_state = events.reduce(state, &decider.method(:evolve))
|
|
15
|
+
|
|
16
|
+
new_etag = repository.save(new_state, key: key, etag: etag)
|
|
17
|
+
|
|
18
|
+
[events, new_etag]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :decider, :repository
|
|
24
|
+
end
|
|
25
|
+
end
|