decide.rb 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d042a0810111e7fcb0f9226ad109b20903a8305689df72d25a04caaa17deeff
4
- data.tar.gz: d45f4d9aa5edc832dbdf303c8b0ba2e5aa6b7293c8c00614f9f8dfc69186c5c3
3
+ metadata.gz: 73423e6911201b60ed5a3333ef636880cc93751425e95b39e522bed75d9ca4a5
4
+ data.tar.gz: 2b352d0c34587f8dfe90e819504e3d07093de738ddde24cea4e96a152f09ce0d
5
5
  SHA512:
6
- metadata.gz: bf2b770bdabf04def99cbb942d6de633b00a9be6637adec05e81ad52228022e92e0cfc8e71e2e9ac12501febcdb411386242eb341d25e4d0be8f52b62f761ff6
7
- data.tar.gz: 614461548e7859a8524cbd6e29961790443e89a10e69d49ec690448af7204afb04a71bf984e8825de3d7b57496753e836e98c0b7ff9c4475d2715838ffaa0075
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
- state value: 0 do
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
- state value: 0
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
- state value: 0
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>]
@@ -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
+
@@ -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
+ ```
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Decider
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
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(initial_state_args:, deciders:, evolvers:, terminal:)
32
+ def initialize(initial_state:, deciders:, evolutions:, terminal:)
9
33
  define_method(:initial_state) do
10
- new(**initial_state_args)
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
- evolvers.keys
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
- handler = deciders.fetch(command.class) {
23
- raise ArgumentError, "Unknown command: #{command.class}"
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
- handler.call(command, state)
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
- handler = evolvers.fetch(event.class) {
31
- raise ArgumentError, "Unknown event: #{event.class}"
32
- }
33
-
34
- handler.call(state, event)
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
- @state = DEFAULT
93
+ @initial_state = DEFAULT
50
94
  @deciders = {}
51
- @evolvers = {}
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 @state == DEFAULT
102
+ raise StateNotDefined if @initial_state == DEFAULT
103
+
104
+ decider = Object.new
59
105
 
60
106
  @module = Module.new(
61
- initial_state_args: initial_state_args,
107
+ initial_state: @initial_state,
62
108
  deciders: deciders,
63
- evolvers: evolvers,
109
+ evolutions: evolutions,
64
110
  terminal: terminal
65
111
  )
66
112
 
67
- @state.extend(@module)
113
+ decider.extend(@module)
68
114
 
69
- @state
115
+ decider
70
116
  end
71
117
 
72
118
  private
73
119
 
74
- attr_reader :initial_state_args, :deciders, :evolvers, :terminal
120
+ attr_reader :deciders, :evolutions, :terminal
75
121
 
76
- def state(**kwargs, &block)
77
- raise StateAlreadyDefined if @state != DEFAULT
122
+ def initial_state(state)
123
+ raise StateAlreadyDefined if @initial_state != DEFAULT
78
124
 
79
- @state = Data.define(*kwargs.keys, &block)
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
- evolvers[event] = block
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
- state left: left.initial_state, right: right.initial_state
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.2.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-04 00:00:00.000000000 Z
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