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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c8cf547b60c57f5e6d7de4f9818c6422dd45b7508d7d547e0857596f12dc9dee
4
+ data.tar.gz: 567d203347522c6401e1fc724316ddec6f5d0c7625f59a44d2214a4dc0299bc9
5
+ SHA512:
6
+ metadata.gz: b47df204a4040a1377b33f6d3395fcca83e167d0c073a84c7555195b671c54464d3e29f6c0d27036260bcc949547d189e52acb2e5ea7ab6421c774dc1412971b
7
+ data.tar.gz: 4a808fd5cff184f84e7e6a3be391c431052e5e38106e4ac547b140c405eb1af9fcf2d225e3306f774376a114f75d7108ec837b759514409b6aaf366f5773de7f
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,377 @@
1
+ # 0.9.0
2
+
3
+ * Rename gem to `decider`
4
+ * Support `many` in `View`
5
+
6
+ Big Thanks to [skalnik](https://github.com/skalnik/) for transfering the name ❤️
7
+
8
+ # 0.8.1
9
+
10
+ * Fix support for Ruby > 3.4.5
11
+
12
+ # 0.8.0
13
+
14
+ * Add view that can evolve from initial state
15
+
16
+ ```ruby
17
+ view = Decider::View.define do
18
+ initial_state 0
19
+
20
+ evolve :increased do
21
+ state + 1
22
+ end
23
+
24
+ evolve :decreased do
25
+ state - 1
26
+ end
27
+ end
28
+ ```
29
+
30
+ * Add `lmap_on_event`, `dimap_on_state`, `lmap_on_state` and `rmap_on_state` extensions for view
31
+
32
+ # 0.7.0
33
+
34
+ * Add reactor that can react to action results and issue actions
35
+
36
+ ```ruby
37
+ ActionResult = Data.define(:value)
38
+ Action = Data.define(:value)
39
+
40
+ reactor = Decider::Reactor.define do
41
+ react :action_result do
42
+ issue :action
43
+ issue :another_action
44
+ end
45
+
46
+ react proc { action_result in ActionResult(value: 42) } do
47
+ issue Action.new(value: "the answer")
48
+ end
49
+
50
+ react ActionResult do
51
+ issue Action.new(value: action_result.value)
52
+ end
53
+ end
54
+
55
+ reactor.react(:action_result)
56
+ # => [:action, :another_action]
57
+ reactor.react(ActionResult.new(value: 42)
58
+ # => #<data Action value="the answer">
59
+ reactor.react(ActionResult.new(value: 1)
60
+ # => #<data Action value=1>
61
+ ```
62
+
63
+ * Add `lmap_on_action_result` extension to reactor
64
+ * Add `rmap_on_action` (aliased to `map_on_action`) extensions to reactor
65
+ * Add `combine_with_decider` extension to reactor
66
+
67
+ ```ruby
68
+ decider = Decider.define do
69
+ initial_state 0
70
+
71
+ decide :action do
72
+ emit :result
73
+ end
74
+
75
+ decide :another_action do
76
+ emit :another_result
77
+ end
78
+ end
79
+
80
+ reactor = Decider::Reactor.define do
81
+ react :result do
82
+ issue :another_action
83
+ end
84
+ end
85
+
86
+ decider = reactor.combine_with_decider(decider)
87
+ decider.decide(:action)
88
+ # => [:result, :another_result]
89
+ ```
90
+
91
+ # 0.6.2
92
+
93
+ * Add `many` extension that takes a decider and manage many instances
94
+
95
+ ```ruby
96
+ deciders = Decider.many(decider)
97
+ # or
98
+ deciders = decider.many
99
+
100
+ deciders.initial_state
101
+ # => {}
102
+
103
+ deciders.decide([id, command], state)
104
+ # => [[id, event], [id, event]]
105
+
106
+ deciders.evolve(state, [id, event])
107
+ # => state
108
+ ```
109
+
110
+ # 0.6.1
111
+
112
+ * Add `apply` extension for creating applicatives
113
+
114
+ ```ruby
115
+ decider = Decider.map(fn.curry, deciderx)
116
+ decider = Decider.apply(decider, decidery)
117
+ decider = Decider.apply(decider, deciderz)
118
+ # or
119
+ deciderx.map(fn.curry).apply(decidery).apply(deciderz)
120
+ ```
121
+
122
+ # 0.6.0
123
+
124
+ * All extensions takes decider as last argument
125
+
126
+ ```ruby
127
+ # Before
128
+ Decider.dimap_on_state(decider, fl:, fr:)
129
+ Decider.dimap_on_event(decider, fl:, fr:)
130
+ Decider.lmap_on_event(decider, fn)
131
+ Decider.lmap_on_command(decider, fn)
132
+ Decider.lmap_on_state(decider, fn)
133
+ Decider.rmap_on_event(decider, fn)
134
+ Decider.rmap_on_state(decider, fn)
135
+
136
+ # After
137
+ Decider.dimap_on_state(fl, fr, decider)
138
+ Decider.dimap_on_event(fl, fr, decider)
139
+ Decider.lmap_on_event(fn, decider)
140
+ Decider.lmap_on_command(fn, decider)
141
+ Decider.lmap_on_state(fn, decider)
142
+ Decider.rmap_on_event(fn, decider)
143
+ Decider.rmap_on_state(fn, decider)
144
+ ```
145
+
146
+ # 0.5.5
147
+
148
+ * Add `lmap_on_command` extension that takes proc that maps command and returns a new decider
149
+
150
+ ```ruby
151
+ decider = Decider.define do
152
+ initial_state 0
153
+
154
+ decide :increase do
155
+ emit :increased
156
+ end
157
+ end
158
+
159
+ lmap = decider.lmap_on_command(->(command) { command.to_sym })
160
+ lmap.decide("increase", 0)
161
+ # => [:increased]
162
+ ```
163
+
164
+ * Add `map` extension that works the same way as `rmap_on_state` but `Decider.map` takes function first
165
+
166
+ ```ruby
167
+ # equivalent
168
+ Decider.rmap_on_state(decider, fn)
169
+ Decider.map(fn, decider)
170
+ decider.rmap_on_state(fn)
171
+ decider.map(fn)
172
+ ```
173
+
174
+ * Add `map2` extension that takes function with two arguments and two deciders and returns a decider:
175
+
176
+ ```ruby
177
+ dx = Decider.define do
178
+ initial_state({score: 0})
179
+
180
+ decide :score do
181
+ emit :scored
182
+ end
183
+
184
+ evolve :scored do
185
+ {score: state[:score] + 1}
186
+ end
187
+ end
188
+
189
+ dy = Decider.define do
190
+ initial_state({time: 0})
191
+
192
+ decide :tick do
193
+ emit :ticked
194
+ end
195
+
196
+ evolve :ticked do
197
+ {time: state[:time] + 1}
198
+ end
199
+ end
200
+
201
+ decider = Decider.map2(
202
+ ->(sx, sy) { {score: sx[:score], time: sy[:time]} }, dx, dy
203
+ )
204
+
205
+ decider.initial_state
206
+ # => {score: 0, time: 0}
207
+ decider.decide(:score, decider.initial_state)
208
+ # => [:scored]
209
+ decider.decide(:tick, decider.initial_state)
210
+ # => [:ticked]
211
+ decider.evolve(decider.initial_state, :scored)
212
+ # => {score: 1, time: 0}
213
+ decider.evolve(decider.initial_state, :ticked)
214
+ # => {score: 0, time: 1}
215
+ ```
216
+
217
+ # 0.5.4
218
+
219
+ * Add `lmap_on_event` and `rmap_on_event` extensions that takes proc that maps event in or out and returns a new decider
220
+
221
+ ```ruby
222
+ decider = Decider.define do
223
+ initial_state 0
224
+
225
+ decide :increase do
226
+ emit :increased
227
+ end
228
+
229
+ evolve :increased do
230
+ state + 1
231
+ end
232
+ end
233
+
234
+ lmap = decider.lmap_on_event(->(event) { event.to_sym })
235
+ lmap.evolve(0, "increased")
236
+ # => 1
237
+
238
+ rmap = decider.rmap_on_event(->(event) { event.to_s })
239
+ rmap.decide(:increase, 0)
240
+ # => "increased"
241
+ ```
242
+
243
+ * Add `lmap_on_state` and `rmap_on_state` extensions that takes proc that maps state in or out and returns a new decider
244
+
245
+ ```ruby
246
+ decider = Decider.define do
247
+ initial_state :symbol
248
+
249
+ decide :command, :state do
250
+ emit :called
251
+ end
252
+
253
+ evolve :state, :called do
254
+ :new_state
255
+ end
256
+ end
257
+ decider.initial_state
258
+ # => :symbol
259
+
260
+ lmap = decider.lmap_on_state(
261
+ ->(state) { state.to_sym }
262
+ )
263
+ lmap.initial_state
264
+ # => :symbol
265
+ lmap.decide(:command, "symbol")
266
+ # => [:called]
267
+ lmap.evolve("symbol", :called)
268
+ # => :new_state
269
+
270
+ rmap = inner.rmap_on_state(
271
+ ->(state) { state.to_s }
272
+ )
273
+ rmap.initial_state
274
+ # => "symbol"
275
+ rmap.decide(:command, :symbol)
276
+ # => [:called]
277
+ rmap.evolve(:symbol, :called)
278
+ # => "new_state"
279
+ ```
280
+
281
+ # 0.5.3
282
+
283
+ * Add shortcut for evolving:
284
+
285
+ ```ruby
286
+ # this three are the same now
287
+ [event, event].reduce(state) { |s, e| decider.evolve(s, e) }
288
+ [event, event].reduce(state, &decider.method(:evolve))
289
+
290
+ # new
291
+ [event, event].reduce(state, &decider.evolve)
292
+ ```
293
+
294
+ # 0.5.2
295
+
296
+ * Add `dimap_on_event` extension that takes procs that maps event in and out and returns a new decider
297
+
298
+ ```ruby
299
+ inner = Decider.define do
300
+ initial_state 0
301
+
302
+ decide :increase do
303
+ emit :increased
304
+ end
305
+
306
+ evolve :increased do
307
+ state + 1
308
+ end
309
+ end
310
+
311
+ outer = inner.dimap_on_event(
312
+ fl: ->(event) { event.to_sym },
313
+ fr: ->(event) { event.to_s }
314
+ )
315
+
316
+ outer.decide(:increase, 0)
317
+ # => "increased"
318
+
319
+ outer.evolve(0, "increased")
320
+ # => 1
321
+ ```
322
+
323
+ # 0.5.1
324
+
325
+ * Add `dimap_on_state` extension that takes procs that maps state in and out and returns a new decider
326
+
327
+ ```ruby
328
+ inner = Decider.define do
329
+ initial_state :symbol
330
+ end
331
+ inner.initial_state
332
+ # => :symbol
333
+
334
+ outer = inner.dimap_on_state(
335
+ fl: ->(state) { state.to_sym },
336
+ fr: ->(state) { state.to_s }
337
+ )
338
+ outer.initial_state
339
+ # => "symbol"
340
+
341
+ # under the hood it will run inner.decide(:command, :symbol)
342
+ outer.decide(:command, "symbol")
343
+ ```
344
+
345
+ # 0.5.0
346
+
347
+ * Support pattern matching for commands and events
348
+ * Support passing state to decider and evolve matchers
349
+ * Remove explicit arguments for handlers
350
+ * Remove redundant bang methods - raise error in catch-all if needed
351
+ * Add Left|Right value wrappers for composition
352
+ * Use `emit` to return events in `decide`
353
+
354
+ # 0.4.1
355
+
356
+ * `define` returns new class, not object
357
+
358
+ # 0.4.0
359
+
360
+ * Accept more data structures for commands and events
361
+
362
+ # 0.3.0
363
+
364
+ * Rename `Decider.state` to `Decider.initial_state`
365
+ * Allow to use anything as state
366
+ * Use tuple-like array for composition
367
+ * Do not raise error when deciding unknown command
368
+ * Do not raise error when evolving unknown event
369
+
370
+ # 0.2.0
371
+
372
+ * Added `terminal?` function
373
+ * `Decider.compose(left, right)`
374
+
375
+ # 0.1.0
376
+
377
+ * Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Jan Dudulski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Decider
2
+
3
+ This gem provides simple DSL for building Functional Event Sourcing Decider in Ruby. To learn more about the pattern read the original [article by Jérémie Chassaing](https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider).
4
+
5
+ Special credits for [Ismael Celis for inspiration](https://ismaelcelis.com/posts/decide-evolve-react-pattern-in-ruby/).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gem install decider
11
+ ```
12
+
13
+ or add to Gemfile
14
+
15
+ ```ruby
16
+ gem "decider"
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ruby
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
33
+
34
+ module Commands
35
+ Increase = Data.define
36
+ Decrease = Data.define
37
+ end
38
+
39
+ module Events
40
+ ValueIncreased = Data.define
41
+ ValueDecreased = Data.define
42
+ end
43
+
44
+ MIN = 0
45
+ MAX = 100
46
+
47
+ ValueDecider = Decider.define do
48
+ # define intial state
49
+ initial_state State.new(value: 0)
50
+
51
+ # decide command with state
52
+ decide Commands::Increase do
53
+ # return collection of events
54
+ if state.max?
55
+ []
56
+ else
57
+ [Events::ValueIncreased.new]
58
+ end
59
+ end
60
+
61
+ decide Commands::Decrease do
62
+ if state.min?
63
+ []
64
+ else
65
+ [Events::ValueDecreased.new]
66
+ end
67
+ end
68
+
69
+ # evolve state with events
70
+ evolve Events::ValueIncreased do
71
+ # return new state
72
+ state.with(value: state.value + 1)
73
+ end
74
+
75
+ evolve Events::ValueDecreased do
76
+ # state is immutable Data object
77
+ state.with(value: state.value - 1)
78
+ end
79
+
80
+ terminal? do
81
+ state <= 0
82
+ end
83
+ end
84
+
85
+ state = ValueDecider.initial_state
86
+ events = ValueDecider.decide(Commands::Increase.new, state)
87
+ new_state = events.reduce(state) { |state, event| ValueDecider.evolve(state, events)
88
+ ```
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
100
+ [Events::LeftEvent.new(value: command.value)]
101
+ end
102
+
103
+ evolve Events::LeftEvent do
104
+ state.with(value: state.value + 1)
105
+ end
106
+
107
+ terminal? do
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
116
+ [Events::RightEvent.new(value: command.value)]
117
+ end
118
+
119
+ evolve Events::RightEvent do
120
+ state.with(value: state.value + 1)
121
+ end
122
+
123
+ terminal? do
124
+ state <= 0
125
+ end
126
+ end
127
+
128
+ Composition = Decider.compose(left, right)
129
+
130
+ state = Composition.initial_state
131
+ #> #<data Decider::Pair left=#<data Left value=0>, right=#<data Right value=0>>
132
+
133
+ events = Composition.decide(Decider::Left.new(Commands::LeftCommand.new(value: 1)), state)
134
+ #> [#<Decider::Left value=#<data value=1>]
135
+
136
+ state = events.reduce(state, &Composition.method(:evolve))
137
+ #> #<data Decider::Pair left=#<data value=1>, right=#<data value=0>>
138
+ ```
139
+
140
+ ## Development
141
+
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.
143
+
144
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
145
+
146
+ ## Contributing
147
+
148
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/decider.
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
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