decide.rb 0.1.0 → 0.3.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: 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: