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.
@@ -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
+ ```
@@ -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
@@ -0,0 +1,15 @@
1
+ # Infra
2
+
3
+ ## In memory
4
+
5
+ ```ruby
6
+ require "decider/in_memory"
7
+
8
+ GLOBAL = Decider::InMemory.new(MyDecider)
9
+
10
+ # returns list of events
11
+ GLOBAL.handle(command)
12
+
13
+ # returns current state
14
+ GLOBAL.state
15
+ ```
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decider
4
+ VERSION = "0.9.0"
5
+ end