decide.rb 0.2.0 → 0.4.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 +12 -0
- data/README.md +17 -13
- data/examples/decide.md +81 -0
- data/examples/evolve.md +62 -0
- data/examples/state.md +65 -0
- data/lib/decider/version.rb +1 -1
- data/lib/decider.rb +71 -26
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73423e6911201b60ed5a3333ef636880cc93751425e95b39e522bed75d9ca4a5
|
4
|
+
data.tar.gz: 2b352d0c34587f8dfe90e819504e3d07093de738ddde24cea4e96a152f09ce0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f2e2449fac23e3ae81014a6fd0cfefd5a2a888089c3c89b2072cb52592f6ba5bb659c9f2a816c6d8b13eb6082b463f42594dbffedb2add06469857b7bfe07d0
|
7
|
+
data.tar.gz: 1a310ac9f050f40ba09df6605860fc62b348e093133fd0237618726753d25f5c669f72b723b9072664862d1d80a01898dd963dbd323c97ef30c8533ed9f523e7
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# Unreleased
|
2
|
+
|
3
|
+
* Accept more data structures for commands and events
|
4
|
+
|
5
|
+
# 0.3.0
|
6
|
+
|
7
|
+
* Rename `Decider.state` to `Decider.initial_state`
|
8
|
+
* Allow to use anything as state
|
9
|
+
* Use tuple-like array for composition
|
10
|
+
* Do not raise error when deciding unknown command
|
11
|
+
* Do not raise error when evolving unknown event
|
12
|
+
|
1
13
|
# 0.2.0
|
2
14
|
|
3
15
|
* Added `terminal?` function
|
data/README.md
CHANGED
@@ -21,6 +21,16 @@ gem "decide.rb"
|
|
21
21
|
```ruby
|
22
22
|
require "decider"
|
23
23
|
|
24
|
+
State = Data.define(:value) do
|
25
|
+
def max?
|
26
|
+
value >= MAX
|
27
|
+
end
|
28
|
+
|
29
|
+
def min?
|
30
|
+
value <= MIN
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
24
34
|
module Commands
|
25
35
|
Increase = Data.define
|
26
36
|
Decrease = Data.define
|
@@ -36,16 +46,7 @@ MAX = 100
|
|
36
46
|
|
37
47
|
ValueDecider = Decider.define do
|
38
48
|
# define intial state
|
39
|
-
|
40
|
-
# you can define custom methods on state
|
41
|
-
def max?
|
42
|
-
value >= MAX
|
43
|
-
end
|
44
|
-
|
45
|
-
def min?
|
46
|
-
value <= MIN
|
47
|
-
end
|
48
|
-
end
|
49
|
+
initial_state State.new(value: 0)
|
49
50
|
|
50
51
|
# decide command with state
|
51
52
|
decide Commands::Increase do |command, state|
|
@@ -89,8 +90,11 @@ new_state = events.reduce(state) { |state, event| ValueDecider.evolve(state, eve
|
|
89
90
|
You can also compose deciders:
|
90
91
|
|
91
92
|
```ruby
|
93
|
+
Left = Data.define(:value)
|
94
|
+
Right = Data.define(:value)
|
95
|
+
|
92
96
|
left = Decider.define do
|
93
|
-
|
97
|
+
initial_state Left.new(value: 0)
|
94
98
|
|
95
99
|
decide Commands::LeftCommand do |command, state|
|
96
100
|
[Events::LeftEvent.new(value: command.value)]
|
@@ -106,7 +110,7 @@ left = Decider.define do
|
|
106
110
|
end
|
107
111
|
|
108
112
|
right = Decider.define do
|
109
|
-
|
113
|
+
initial_state Right.new(value: 0)
|
110
114
|
|
111
115
|
decide Commands::RightCommand do |command, state|
|
112
116
|
[Events::RightEvent.new(value: command.value)]
|
@@ -124,7 +128,7 @@ end
|
|
124
128
|
Composition = Decider.compose(left, right)
|
125
129
|
|
126
130
|
state = Composition.initial_state
|
127
|
-
#> #<data left=#<data value=0>, right=#<data value=0>>
|
131
|
+
#> #<data left=#<data Left value=0>, right=#<data Right value=0>>
|
128
132
|
|
129
133
|
events = Composition.decide(Commands::LeftCommand.new(value: 1), state)
|
130
134
|
#> [#<data value=1>]
|
data/examples/decide.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Decide
|
2
|
+
|
3
|
+
## Handling unknown commands
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
decider = Decider.define do
|
7
|
+
initial_state :initial
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
Return empty list of events (nothing changed) by default:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
decider.decide :unknown, decider.initial_state
|
15
|
+
#> []
|
16
|
+
```
|
17
|
+
|
18
|
+
Raise error when using `decide!`:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
decider.decide! :unknown, decider.initial_state
|
22
|
+
#> raise ArgumentError, Unknown command
|
23
|
+
```
|
24
|
+
|
25
|
+
## Commands
|
26
|
+
|
27
|
+
Commands can be primitives like symbols:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
decider = Decider.define do
|
31
|
+
initial_state :initial
|
32
|
+
|
33
|
+
decide :start do |command, state|
|
34
|
+
case state
|
35
|
+
in :initial, :stopped
|
36
|
+
[:started]
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
decide :stop do |command, state|
|
43
|
+
case state
|
44
|
+
in :started
|
45
|
+
[:stopped]
|
46
|
+
else
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Or any classes like [Dry::Struct](https://dry-rb.org/gems/dry-struct/) or [Data](https://rubyapi.org/3.3/o/data):
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Start = Data.define(:value)
|
57
|
+
Stop = Data.define
|
58
|
+
|
59
|
+
decider = Decider.define do
|
60
|
+
initial_state :initial
|
61
|
+
|
62
|
+
decide Start do |command, state|
|
63
|
+
case state
|
64
|
+
in :initial, :stopped
|
65
|
+
[:started, command.value]
|
66
|
+
else
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
decide Stop do |command, state|
|
72
|
+
case state
|
73
|
+
in :started
|
74
|
+
[:stopped]
|
75
|
+
else
|
76
|
+
[]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
data/examples/evolve.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Evolve
|
2
|
+
|
3
|
+
## Handling unknown events
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
decider = Decider.define do
|
7
|
+
initial_state :initial
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
Return state (nothing changed) by default:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
decider.evolve decider.initial_state, :unknown
|
15
|
+
#> :initial
|
16
|
+
```
|
17
|
+
|
18
|
+
Raise error when using `evolve!`:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
decider.evolve! decider.initial_state, :unknown
|
22
|
+
#> raise ArgumentError, Unknown event
|
23
|
+
```
|
24
|
+
|
25
|
+
## Events
|
26
|
+
|
27
|
+
Events can be primitives like symbols:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
decider = Decider.define do
|
31
|
+
initial_state :initial
|
32
|
+
|
33
|
+
evolve :started do |state, event|
|
34
|
+
:started
|
35
|
+
end
|
36
|
+
|
37
|
+
evolve :stopped do |state, event|
|
38
|
+
:stopped
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
Or any classes like [Dry::Struct](https://dry-rb.org/gems/dry-struct/) or [Data](https://rubyapi.org/3.3/o/data):
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
State = Data.define(:speed)
|
47
|
+
Started = Data.define(:speed)
|
48
|
+
Stopped = Data.define
|
49
|
+
|
50
|
+
decider = Decider.define do
|
51
|
+
initial_state State.new(speed: 0)
|
52
|
+
|
53
|
+
evolve Started do |state, event|
|
54
|
+
State.new(speed: event.speed)
|
55
|
+
end
|
56
|
+
|
57
|
+
evolve Stopped do |state, event|
|
58
|
+
State.new(speed: 0)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
data/examples/state.md
ADDED
@@ -0,0 +1,65 @@
|
|
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 |command, state|
|
12
|
+
[Events::Event.new(value: command.value)]
|
13
|
+
end
|
14
|
+
|
15
|
+
evolve Events::Event do |state, event|
|
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 do |_command, state|
|
28
|
+
case state
|
29
|
+
in :turned_off
|
30
|
+
[Events::TurnedOn.new]
|
31
|
+
else
|
32
|
+
[]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
decide Commands::TurnOff do |_command, state|
|
37
|
+
case state
|
38
|
+
in :turned_on
|
39
|
+
[Events::TurnedOn.new]
|
40
|
+
else
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
evolve Events::TurnedOn do |_state, _event|
|
46
|
+
:turned_off
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
## List
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
decider = Decider.define do
|
55
|
+
initial_state []
|
56
|
+
|
57
|
+
decide Commands::Command do |command, _state|
|
58
|
+
[Events::Event.new(value: command.value)]
|
59
|
+
end
|
60
|
+
|
61
|
+
evolve Events::Event do |state, event|
|
62
|
+
state = state + [event]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
data/lib/decider/version.rb
CHANGED
data/lib/decider.rb
CHANGED
@@ -4,10 +4,34 @@ module Decider
|
|
4
4
|
StateAlreadyDefined = Class.new(StandardError)
|
5
5
|
StateNotDefined = Class.new(StandardError)
|
6
6
|
|
7
|
+
class Composition < Array
|
8
|
+
BLANK = Object.new
|
9
|
+
private_constant :BLANK
|
10
|
+
|
11
|
+
def initialize(left, right)
|
12
|
+
super([left, right]).freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def with(left: BLANK, right: BLANK)
|
16
|
+
Composition.new(
|
17
|
+
(left == BLANK) ? self.left : left,
|
18
|
+
(right == BLANK) ? self.right : right
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def left
|
23
|
+
self[0]
|
24
|
+
end
|
25
|
+
|
26
|
+
def right
|
27
|
+
self[1]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
7
31
|
class Module < ::Module
|
8
|
-
def initialize(
|
32
|
+
def initialize(initial_state:, deciders:, evolutions:, terminal:)
|
9
33
|
define_method(:initial_state) do
|
10
|
-
|
34
|
+
initial_state
|
11
35
|
end
|
12
36
|
|
13
37
|
define_method(:commands) do
|
@@ -15,23 +39,43 @@ module Decider
|
|
15
39
|
end
|
16
40
|
|
17
41
|
define_method(:events) do
|
18
|
-
|
42
|
+
evolutions.keys
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method(:decide!) do |command, state|
|
46
|
+
case deciders.find { |key, _| key === command }
|
47
|
+
in [_, handler]
|
48
|
+
handler.call(command, state)
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Unknown command: #{command.inspect}"
|
51
|
+
end
|
19
52
|
end
|
20
53
|
|
21
54
|
define_method(:decide) do |command, state|
|
22
|
-
|
23
|
-
|
24
|
-
|
55
|
+
case deciders.find { |key, _| key === command }
|
56
|
+
in [_, handler]
|
57
|
+
handler.call(command, state)
|
58
|
+
else
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
end
|
25
62
|
|
26
|
-
|
63
|
+
define_method(:evolve!) do |state, event|
|
64
|
+
case evolutions.find { |key, _| key === event }
|
65
|
+
in [_, handler]
|
66
|
+
handler.call(state, event)
|
67
|
+
else
|
68
|
+
raise ArgumentError, "Unknown event: #{event.inspect}"
|
69
|
+
end
|
27
70
|
end
|
28
71
|
|
29
72
|
define_method(:evolve) do |state, event|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
73
|
+
case evolutions.find { |key, _| key === event }
|
74
|
+
in [_, handler]
|
75
|
+
handler.call(state, event)
|
76
|
+
else
|
77
|
+
state
|
78
|
+
end
|
35
79
|
end
|
36
80
|
|
37
81
|
define_method(:terminal?) do |state|
|
@@ -46,38 +90,39 @@ module Decider
|
|
46
90
|
attr_reader :module
|
47
91
|
|
48
92
|
def initialize
|
49
|
-
@
|
93
|
+
@initial_state = DEFAULT
|
50
94
|
@deciders = {}
|
51
|
-
@
|
95
|
+
@evolutions = {}
|
52
96
|
@terminal = ->(_state) { false }
|
53
97
|
end
|
54
98
|
|
55
99
|
def build(&block)
|
56
100
|
instance_exec(&block) if block_given?
|
57
101
|
|
58
|
-
raise StateNotDefined if @
|
102
|
+
raise StateNotDefined if @initial_state == DEFAULT
|
103
|
+
|
104
|
+
decider = Object.new
|
59
105
|
|
60
106
|
@module = Module.new(
|
61
|
-
|
107
|
+
initial_state: @initial_state,
|
62
108
|
deciders: deciders,
|
63
|
-
|
109
|
+
evolutions: evolutions,
|
64
110
|
terminal: terminal
|
65
111
|
)
|
66
112
|
|
67
|
-
|
113
|
+
decider.extend(@module)
|
68
114
|
|
69
|
-
|
115
|
+
decider
|
70
116
|
end
|
71
117
|
|
72
118
|
private
|
73
119
|
|
74
|
-
attr_reader :
|
120
|
+
attr_reader :deciders, :evolutions, :terminal
|
75
121
|
|
76
|
-
def state
|
77
|
-
raise StateAlreadyDefined if @
|
122
|
+
def initial_state(state)
|
123
|
+
raise StateAlreadyDefined if @initial_state != DEFAULT
|
78
124
|
|
79
|
-
@
|
80
|
-
@initial_state_args = kwargs
|
125
|
+
@initial_state = state
|
81
126
|
end
|
82
127
|
|
83
128
|
def decide(command, &block)
|
@@ -85,7 +130,7 @@ module Decider
|
|
85
130
|
end
|
86
131
|
|
87
132
|
def evolve(event, &block)
|
88
|
-
|
133
|
+
evolutions[event] = block
|
89
134
|
end
|
90
135
|
|
91
136
|
def terminal?(&block)
|
@@ -101,7 +146,7 @@ module Decider
|
|
101
146
|
|
102
147
|
def self.compose(left, right)
|
103
148
|
define do
|
104
|
-
|
149
|
+
initial_state Composition.new(left.initial_state, right.initial_state)
|
105
150
|
|
106
151
|
left.commands.each do |klass|
|
107
152
|
decide klass do |command, state|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decide.rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Dudulski
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Functional Event Sourcing Decider in Ruby
|
14
14
|
email:
|
@@ -23,6 +23,9 @@ files:
|
|
23
23
|
- README.md
|
24
24
|
- Rakefile
|
25
25
|
- Steepfile
|
26
|
+
- examples/decide.md
|
27
|
+
- examples/evolve.md
|
28
|
+
- examples/state.md
|
26
29
|
- lib/decider.rb
|
27
30
|
- lib/decider/version.rb
|
28
31
|
- sig/decider.rbs
|