decide.rb 0.5.0 → 0.5.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41af414c2f92dc8e43dad1ac61cf6ffb939fa65bc7217223b29e9780067ce5d3
4
- data.tar.gz: 96342dea929121644eca8d924fbcd1112fbb21ec34fb666c00e87261cf7d4dc1
3
+ metadata.gz: ef8af0a9aa14ef5a19fce005eba64d4b4504277493434c4091510e49f1378a39
4
+ data.tar.gz: 4ca62248b625d9bc3926749565c0ac4ef066a7fb46bd3cf0ec605f0c72d3e4b1
5
5
  SHA512:
6
- metadata.gz: acec8f3699e15b22ee30780b2b2e30cc95950c9d18701dbcaddc5788723780e0d6c09e16066c000db8a24ec87e62eadf20469c391c5140af662b9b951b61e41b
7
- data.tar.gz: b088dec6fb91fe9417f83b73ebe58dd638d31ffb78f1245e62afa826a5f416d1bede83c41cc0d612afa29d50afeb119703a42e5ec3570daad08003b1bb304eb0
6
+ metadata.gz: 2326979a7350bf881484052c80cba5147f160a923f41b39c80420da5424e9239b62b6758460efeef81198fb83aec4e026a933a503661cf2ced329bb83dbbd12d
7
+ data.tar.gz: 07d38ce7613facb15a5e553482cf7b43d05f8b2743c3cac9b26f1fc9ad909cd87e24c03b20fc0258b554a4d9c08fa03123c6f27a36f661d31db054cef52c5221
data/examples/decide.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Decide
2
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
+ #> [Event.new(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
+
3
74
  ## Handling unknown commands
4
75
 
5
76
  ```ruby
@@ -15,11 +86,18 @@ decider.decide :unknown, decider.initial_state
15
86
  #> []
16
87
  ```
17
88
 
18
- Raise error when using `decide!`:
89
+ If you want to raise error, define a catch-all as last one:
19
90
 
20
91
  ```ruby
21
- decider.decide! :unknown, decider.initial_state
22
- #> raise ArgumentError, Unknown command
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)
23
101
  ```
24
102
 
25
103
  ## Commands
@@ -30,24 +108,21 @@ Commands can be primitives like symbols:
30
108
  decider = Decider.define do
31
109
  initial_state :initial
32
110
 
33
- decide :start do |command, state|
34
- case state
35
- in :initial, :stopped
36
- [:started]
37
- else
38
- []
39
- end
111
+ decide proc { [command, state] in [:start, :initial | :stopped] } do
112
+ emit :started
40
113
  end
41
114
 
42
- decide :stop do |command, state|
43
- case state
44
- in :started
45
- [:stopped]
46
- else
47
- []
48
- end
115
+ decide proc { [command, state] in [:stop, :started] } do
116
+ emit :stopped
49
117
  end
50
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
+ #> []
51
126
  ```
52
127
 
53
128
  Or any classes like [Dry::Struct](https://dry-rb.org/gems/dry-struct/) or [Data](https://rubyapi.org/3.3/o/data):
@@ -59,23 +134,47 @@ Stop = Data.define
59
134
  decider = Decider.define do
60
135
  initial_state :initial
61
136
 
62
- decide Start do |command, state|
63
- case state
64
- in :initial, :stopped
65
- [:started, command.value]
66
- else
67
- []
68
- end
137
+ decide proc { [command, state] in [Start, :initial | :stopped] } do
138
+ emit [:started, command.value]
69
139
  end
70
140
 
71
- decide Stop do |command, state|
72
- case state
73
- in :started
74
- [:stopped]
75
- else
76
- []
77
- end
141
+ decide proc { [command, state] in [Stop, [:started, _]] } do
142
+ emit :stopped
78
143
  end
79
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]
80
150
  ```
81
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/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
+ ```
@@ -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,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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Decider
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.2"
5
5
  end
data/lib/decider.rb CHANGED
@@ -99,6 +99,14 @@ module Decider
99
99
 
100
100
  context.instance_exec(&terminal)
101
101
  end
102
+
103
+ define_method(:dimap_on_state) do |fl:, fr:|
104
+ Decider.dimap_on_state(self, fl: fl, fr: fr)
105
+ end
106
+
107
+ define_method(:dimap_on_event) do |fl:, fr:|
108
+ Decider.dimap_on_event(self, fl: fl, fr: fr)
109
+ end
102
110
  end
103
111
  end
104
112
 
@@ -194,4 +202,42 @@ module Decider
194
202
  end
195
203
  end
196
204
  end
205
+
206
+ def self.dimap_on_state(decider, fl:, fr:)
207
+ define do
208
+ initial_state fr.call(decider.initial_state)
209
+
210
+ decide proc { true } do
211
+ decider.decide(command, fl.call(state)).each(&method(:emit))
212
+ end
213
+
214
+ evolve proc { true } do
215
+ fr.call(decider.evolve(fl.call(state), event))
216
+ end
217
+
218
+ terminal? do
219
+ decider.terminal?(fl.call(state))
220
+ end
221
+ end
222
+ end
223
+
224
+ def self.dimap_on_event(decider, fl:, fr:)
225
+ define do
226
+ initial_state decider.initial_state
227
+
228
+ decide proc { true } do
229
+ decider.decide(command, state).each do |event|
230
+ emit fr.call(event)
231
+ end
232
+ end
233
+
234
+ evolve proc { true } do
235
+ decider.evolve(state, fl.call(event))
236
+ end
237
+
238
+ terminal? do
239
+ decider.terminal?(state)
240
+ end
241
+ end
242
+ end
197
243
  end
metadata CHANGED
@@ -1,15 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decide.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Dudulski
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-12-15 00:00:00.000000000 Z
12
- dependencies: []
10
+ date: 2025-04-12 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sqlite3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.5.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 2.5.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: ruby_event_store
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.15'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.15'
13
54
  description: Functional Event Sourcing Decider in Ruby
14
55
  email:
15
56
  - jan@dudulski.pl
@@ -25,8 +66,12 @@ files:
25
66
  - Steepfile
26
67
  - examples/decide.md
27
68
  - examples/evolve.md
69
+ - examples/infra.md
28
70
  - examples/state.md
29
71
  - lib/decider.rb
72
+ - lib/decider/event_sourcing.rb
73
+ - lib/decider/in_memory.rb
74
+ - lib/decider/state.rb
30
75
  - lib/decider/version.rb
31
76
  - sig/decider.rbs
32
77
  homepage: https://github.com/jandudulski/decide.rb
@@ -39,7 +84,6 @@ metadata:
39
84
  documentation_uri: https://github.com/jandudulski/decide.rb
40
85
  homepage_uri: https://github.com/jandudulski/decide.rb
41
86
  source_code_uri: https://github.com/jandudulski/decide.rb
42
- post_install_message:
43
87
  rdoc_options: []
44
88
  require_paths:
45
89
  - lib
@@ -54,8 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
98
  - !ruby/object:Gem::Version
55
99
  version: '0'
56
100
  requirements: []
57
- rubygems_version: 3.5.16
58
- signing_key:
101
+ rubygems_version: 3.6.2
59
102
  specification_version: 4
60
103
  summary: Functional Event Sourcing Decider in Ruby
61
104
  test_files: []