decide.rb 0.1.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/CHANGELOG.md +13 -0
- data/README.md +69 -14
- data/Steepfile +26 -0
- data/examples/decide.md +23 -0
- data/examples/evolve.md +23 -0
- data/examples/state.md +65 -0
- data/lib/decider/version.rb +1 -1
- data/lib/decider.rb +112 -15
- data/sig/decider.rbs +13 -0
- metadata +12 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da945c90e06dab8eb7c391135319bf4f9e126fd2e1904ac4481aabfc4df8943a
|
4
|
+
data.tar.gz: 9e70f93d1ee072b555f7a44b8babc2c0fb0bef50df27949d3bd45654b8c05ed8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67a5f37bf266ce6ca5ca35820cc384fa502d33ca0dae794527d160dc7b247eb7e376c96f6dedfe204c348c25436d64db6c934d9fc407daf8e8421c38e23758bd
|
7
|
+
data.tar.gz: aea4f6f4de5a4509ce7b908fb04c59e6546f96c01502b762b564e5c8f44b7b235f380bf98631cf2f906d08eeaf210cde02564ce6060b5fbf9677fcbdd24b675f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
# 0.3.0
|
2
|
+
|
3
|
+
* Rename `Decider.state` to `Decider.initial_state`
|
4
|
+
* Allow to use anything as state
|
5
|
+
* Use tuple-like array for composition
|
6
|
+
* Do not raise error when deciding unknown command
|
7
|
+
* Do not raise error when evolving unknown event
|
8
|
+
|
9
|
+
# 0.2.0
|
10
|
+
|
11
|
+
* Added `terminal?` function
|
12
|
+
* `Decider.compose(left, right)`
|
13
|
+
|
1
14
|
# 0.1.0
|
2
15
|
|
3
16
|
* Initial release
|
data/README.md
CHANGED
@@ -19,7 +19,17 @@ gem "decide.rb"
|
|
19
19
|
## Usage
|
20
20
|
|
21
21
|
```ruby
|
22
|
-
require "
|
22
|
+
require "decider"
|
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
|
23
33
|
|
24
34
|
module Commands
|
25
35
|
Increase = Data.define
|
@@ -31,21 +41,12 @@ module Events
|
|
31
41
|
ValueDecreased = Data.define
|
32
42
|
end
|
33
43
|
|
34
|
-
|
35
|
-
|
36
|
-
MAX = 100
|
44
|
+
MIN = 0
|
45
|
+
MAX = 100
|
37
46
|
|
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|
|
@@ -75,6 +76,10 @@ ValueDecider = Decider.define do
|
|
75
76
|
# state is immutable Data object
|
76
77
|
state.with(value: state.value - 1)
|
77
78
|
end
|
79
|
+
|
80
|
+
terminal? do |state|
|
81
|
+
state <= 0
|
82
|
+
end
|
78
83
|
end
|
79
84
|
|
80
85
|
state = ValueDecider.initial_state
|
@@ -82,6 +87,56 @@ events = ValueDecider.decide(Commands::Increase.new, state)
|
|
82
87
|
new_state = events.reduce(state) { |state, event| ValueDecider.evolve(state, events)
|
83
88
|
```
|
84
89
|
|
90
|
+
You can also compose deciders:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
Left = Data.define(:value)
|
94
|
+
Right = Data.define(:value)
|
95
|
+
|
96
|
+
left = Decider.define do
|
97
|
+
initial_state Left.new(value: 0)
|
98
|
+
|
99
|
+
decide Commands::LeftCommand do |command, state|
|
100
|
+
[Events::LeftEvent.new(value: command.value)]
|
101
|
+
end
|
102
|
+
|
103
|
+
evolve Events::LeftEvent do |state, event|
|
104
|
+
state.with(value: state.value + 1)
|
105
|
+
end
|
106
|
+
|
107
|
+
terminal? do |state|
|
108
|
+
state <= 0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
right = Decider.define do
|
113
|
+
initial_state Right.new(value: 0)
|
114
|
+
|
115
|
+
decide Commands::RightCommand do |command, state|
|
116
|
+
[Events::RightEvent.new(value: command.value)]
|
117
|
+
end
|
118
|
+
|
119
|
+
evolve Events::RightEvent do |state, event|
|
120
|
+
state.with(value: state.value + 1)
|
121
|
+
end
|
122
|
+
|
123
|
+
terminal? do |state|
|
124
|
+
state <= 0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
Composition = Decider.compose(left, right)
|
129
|
+
|
130
|
+
state = Composition.initial_state
|
131
|
+
#> #<data left=#<data Left value=0>, right=#<data Right value=0>>
|
132
|
+
|
133
|
+
events = Composition.decide(Commands::LeftCommand.new(value: 1), state)
|
134
|
+
#> [#<data value=1>]
|
135
|
+
|
136
|
+
state = events.reduce(state, &Composition.method(:evolve))
|
137
|
+
#> #<data left=#<data value=1>, right=#<data value=0>>
|
138
|
+
```
|
139
|
+
|
85
140
|
## Development
|
86
141
|
|
87
142
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Steepfile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
D = Steep::Diagnostic
|
2
|
+
|
3
|
+
target :lib do
|
4
|
+
signature "sig"
|
5
|
+
|
6
|
+
check "lib"
|
7
|
+
check "Gemfile"
|
8
|
+
|
9
|
+
# library "pathname" # Standard libraries
|
10
|
+
# library "strong_json" # Gems
|
11
|
+
|
12
|
+
# configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
|
13
|
+
configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
|
14
|
+
# configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
|
15
|
+
# configure_code_diagnostics do |hash| # You can setup everything yourself
|
16
|
+
# hash[D::Ruby::NoMethod] = :information
|
17
|
+
# end
|
18
|
+
end
|
19
|
+
|
20
|
+
# target :test do
|
21
|
+
# signature "sig", "sig-private"
|
22
|
+
|
23
|
+
# check "test"
|
24
|
+
|
25
|
+
# # library "pathname" # Standard libraries
|
26
|
+
# end
|
data/examples/decide.md
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
```
|
data/examples/evolve.md
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
```
|
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,13 +4,45 @@ 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:, evolvers:, terminal:)
|
9
33
|
define_method(:initial_state) do
|
10
|
-
|
34
|
+
initial_state
|
11
35
|
end
|
12
36
|
|
13
|
-
define_method(:
|
37
|
+
define_method(:commands) do
|
38
|
+
deciders.keys
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method(:events) do
|
42
|
+
evolvers.keys
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method(:decide!) do |command, state|
|
14
46
|
handler = deciders.fetch(command.class) {
|
15
47
|
raise ArgumentError, "Unknown command: #{command.class}"
|
16
48
|
}
|
@@ -18,13 +50,33 @@ module Decider
|
|
18
50
|
handler.call(command, state)
|
19
51
|
end
|
20
52
|
|
21
|
-
define_method(:
|
53
|
+
define_method(:decide) do |command, state|
|
54
|
+
handler = deciders.fetch(command.class) {
|
55
|
+
return []
|
56
|
+
}
|
57
|
+
|
58
|
+
handler.call(command, state)
|
59
|
+
end
|
60
|
+
|
61
|
+
define_method(:evolve!) do |state, event|
|
22
62
|
handler = evolvers.fetch(event.class) {
|
23
63
|
raise ArgumentError, "Unknown event: #{event.class}"
|
24
64
|
}
|
25
65
|
|
26
66
|
handler.call(state, event)
|
27
67
|
end
|
68
|
+
|
69
|
+
define_method(:evolve) do |state, event|
|
70
|
+
handler = evolvers.fetch(event.class) {
|
71
|
+
return state
|
72
|
+
}
|
73
|
+
|
74
|
+
handler.call(state, event)
|
75
|
+
end
|
76
|
+
|
77
|
+
define_method(:terminal?) do |state|
|
78
|
+
terminal.call(state)
|
79
|
+
end
|
28
80
|
end
|
29
81
|
end
|
30
82
|
|
@@ -34,36 +86,39 @@ module Decider
|
|
34
86
|
attr_reader :module
|
35
87
|
|
36
88
|
def initialize
|
37
|
-
@
|
89
|
+
@initial_state = DEFAULT
|
38
90
|
@deciders = {}
|
39
91
|
@evolvers = {}
|
92
|
+
@terminal = ->(_state) { false }
|
40
93
|
end
|
41
94
|
|
42
95
|
def build(&block)
|
43
96
|
instance_exec(&block) if block_given?
|
44
97
|
|
45
|
-
raise StateNotDefined if @
|
98
|
+
raise StateNotDefined if @initial_state == DEFAULT
|
99
|
+
|
100
|
+
decider = Object.new
|
46
101
|
|
47
102
|
@module = Module.new(
|
48
|
-
|
103
|
+
initial_state: @initial_state,
|
49
104
|
deciders: deciders,
|
50
|
-
evolvers: evolvers
|
105
|
+
evolvers: evolvers,
|
106
|
+
terminal: terminal
|
51
107
|
)
|
52
108
|
|
53
|
-
|
109
|
+
decider.extend(@module)
|
54
110
|
|
55
|
-
|
111
|
+
decider
|
56
112
|
end
|
57
113
|
|
58
114
|
private
|
59
115
|
|
60
|
-
attr_reader :
|
116
|
+
attr_reader :deciders, :evolvers, :terminal
|
61
117
|
|
62
|
-
def state
|
63
|
-
raise StateAlreadyDefined if @
|
118
|
+
def initial_state(state)
|
119
|
+
raise StateAlreadyDefined if @initial_state != DEFAULT
|
64
120
|
|
65
|
-
@
|
66
|
-
@initial_state_args = kwargs
|
121
|
+
@initial_state = state
|
67
122
|
end
|
68
123
|
|
69
124
|
def decide(command, &block)
|
@@ -73,6 +128,10 @@ module Decider
|
|
73
128
|
def evolve(event, &block)
|
74
129
|
evolvers[event] = block
|
75
130
|
end
|
131
|
+
|
132
|
+
def terminal?(&block)
|
133
|
+
@terminal = block
|
134
|
+
end
|
76
135
|
end
|
77
136
|
private_constant :Builder
|
78
137
|
|
@@ -80,4 +139,42 @@ module Decider
|
|
80
139
|
builder = Builder.new
|
81
140
|
builder.build(&block)
|
82
141
|
end
|
142
|
+
|
143
|
+
def self.compose(left, right)
|
144
|
+
define do
|
145
|
+
initial_state Composition.new(left.initial_state, right.initial_state)
|
146
|
+
|
147
|
+
left.commands.each do |klass|
|
148
|
+
decide klass do |command, state|
|
149
|
+
left.decide(command, state.left)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
right.commands.each do |klass|
|
154
|
+
decide klass do |command, state|
|
155
|
+
right.decide(command, state.right)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
left.events.each do |klass|
|
160
|
+
evolve klass do |state, event|
|
161
|
+
state.with(
|
162
|
+
left: left.evolve(state.left, event)
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
right.events.each do |klass|
|
168
|
+
evolve klass do |state, event|
|
169
|
+
state.with(
|
170
|
+
right: right.evolve(state.right, event)
|
171
|
+
)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
terminal? do |state|
|
176
|
+
left.terminal?(state.left) && right.terminal?(state.right)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
83
180
|
end
|
data/sig/decider.rbs
CHANGED
@@ -1,3 +1,16 @@
|
|
1
1
|
module Decider
|
2
2
|
VERSION: String
|
3
|
+
|
4
|
+
interface _Decider[C, S, E]
|
5
|
+
def decide: (C, S) -> Array[E]
|
6
|
+
|
7
|
+
def evolve: (S, E) -> S
|
8
|
+
|
9
|
+
def initial_state: () -> S
|
10
|
+
|
11
|
+
def terminal?: (S) -> bool
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.compose: [C1, S1, E1, C2, S2, E2] (_Decider[C1, S1, E1], _Decider[C2, S2, E2]) -> _Decider[C1 | C2, S1 & S2, E1 | E2]
|
15
|
+
def self.define: [C, S, E] () -> _Decider[C, S, E]
|
3
16
|
end
|
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.3.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
|
+
date: 2024-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Functional Event Sourcing Decider in Ruby
|
14
14
|
email:
|
@@ -22,19 +22,23 @@ files:
|
|
22
22
|
- LICENSE.txt
|
23
23
|
- README.md
|
24
24
|
- Rakefile
|
25
|
+
- Steepfile
|
26
|
+
- examples/decide.md
|
27
|
+
- examples/evolve.md
|
28
|
+
- examples/state.md
|
25
29
|
- lib/decider.rb
|
26
30
|
- lib/decider/version.rb
|
27
31
|
- sig/decider.rbs
|
28
|
-
homepage: https://github.com/jandudulski/
|
32
|
+
homepage: https://github.com/jandudulski/decide.rb
|
29
33
|
licenses:
|
30
34
|
- MIT
|
31
35
|
metadata:
|
32
36
|
allowed_push_host: https://rubygems.org
|
33
|
-
bug_tracker_uri: https://github.com/jandudulski/
|
34
|
-
changelog_uri: https://github.com/jandudulski/
|
35
|
-
documentation_uri: https://github.com/jandudulski/
|
36
|
-
homepage_uri: https://github.com/jandudulski/
|
37
|
-
source_code_uri: https://github.com/jandudulski/
|
37
|
+
bug_tracker_uri: https://github.com/jandudulski/decide.rb/issues
|
38
|
+
changelog_uri: https://github.com/jandudulski/decide.rb/CHANGELOG.md
|
39
|
+
documentation_uri: https://github.com/jandudulski/decide.rb
|
40
|
+
homepage_uri: https://github.com/jandudulski/decide.rb
|
41
|
+
source_code_uri: https://github.com/jandudulski/decide.rb
|
38
42
|
post_install_message:
|
39
43
|
rdoc_options: []
|
40
44
|
require_paths:
|