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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b7afded6cbe7cf6fd572d9a7779875e901dbe81b25292847eea0377a549538c
4
- data.tar.gz: 713abc6c3365c06b611a254859bb83fe0a642c26bef2ee51c8c53eb075c47e72
3
+ metadata.gz: da945c90e06dab8eb7c391135319bf4f9e126fd2e1904ac4481aabfc4df8943a
4
+ data.tar.gz: 9e70f93d1ee072b555f7a44b8babc2c0fb0bef50df27949d3bd45654b8c05ed8
5
5
  SHA512:
6
- metadata.gz: 969092990e15943086821f2821d44d73e6bff46e77aa865904cf8152dd4cdadc65a979bfa6edd241583f2aee2748fd229b86f5e2a188fd04c9306c1b5209c963
7
- data.tar.gz: 29b205b6af520c32e971c6e6a5691f9d4cd4e745a0290fd932ee96217018a69a7429722eb7fa3c66b74e204f10529406075cf6e3e2aa8e89af053139f86c9838
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 "decide.rb"
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
- ValueDecider = Decider.define do
35
- MIN = 0
36
- MAX = 100
44
+ MIN = 0
45
+ MAX = 100
37
46
 
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|
@@ -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
@@ -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
+ ```
@@ -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
+ ```
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Decider
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
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(initial_state_args:, deciders:, evolvers:)
32
+ def initialize(initial_state:, deciders:, evolvers:, terminal:)
9
33
  define_method(:initial_state) do
10
- new(**initial_state_args)
34
+ initial_state
11
35
  end
12
36
 
13
- define_method(:decide) do |command, state|
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(:evolve) do |state, event|
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
- @state = DEFAULT
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 @state == DEFAULT
98
+ raise StateNotDefined if @initial_state == DEFAULT
99
+
100
+ decider = Object.new
46
101
 
47
102
  @module = Module.new(
48
- initial_state_args: initial_state_args,
103
+ initial_state: @initial_state,
49
104
  deciders: deciders,
50
- evolvers: evolvers
105
+ evolvers: evolvers,
106
+ terminal: terminal
51
107
  )
52
108
 
53
- @state.extend(@module)
109
+ decider.extend(@module)
54
110
 
55
- @state
111
+ decider
56
112
  end
57
113
 
58
114
  private
59
115
 
60
- attr_reader :initial_state_args, :deciders, :evolvers
116
+ attr_reader :deciders, :evolvers, :terminal
61
117
 
62
- def state(**kwargs, &block)
63
- raise StateAlreadyDefined if @state != DEFAULT
118
+ def initial_state(state)
119
+ raise StateAlreadyDefined if @initial_state != DEFAULT
64
120
 
65
- @state = Data.define(*kwargs.keys, &block)
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.1.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-10-20 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:
@@ -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/decider.rb
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/decider.rb/issues
34
- changelog_uri: https://github.com/jandudulski/decider.rb/CHANGELOG.md
35
- documentation_uri: https://github.com/jandudulski/decider.rb
36
- homepage_uri: https://github.com/jandudulski/decider.rb
37
- source_code_uri: https://github.com/jandudulski/decider.rb
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: