Skip to content

Commit 286a806

Browse files
jandudulskimostlyobvious
authored andcommitted
decide.rb example
1 parent 8cdac34 commit 286a806

File tree

11 files changed

+277
-0
lines changed

11 files changed

+277
-0
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,18 @@ More: https://blog.arkency.com/make-your-ruby-code-more-modular-and-functional-w
9393
- aggregate not aware of events
9494
- aggregate object is still responsible for holding invariants
9595
- no id in domain class
96+
97+
### Decider
98+
99+
[source](examples/decider)
100+
101+
- clear separation of state sourcing (with projection)
102+
- aggregate with decider pattern
103+
104+
### Decider with decide.rb gem
105+
106+
[source](examples/decide.rb)
107+
108+
- clear separation of state sourcing (with projection) with expected version
109+
- aggregate with decider pattern and decide.rb DSL
110+
- mapping between infra (RES) events and domain events used inside decider

examples/decide.rb/.mutant.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
integration:
2+
name: minitest
3+
includes:
4+
- lib
5+
requires:
6+
- project_management
7+
matcher:
8+
subjects:
9+
- ProjectManagement*
10+
ignore:
11+
- ProjectManagement::Test*
12+
coverage_criteria:
13+
process_abort: true
14+
usage: opensource

examples/decide.rb/Gemfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
5+
gem "ruby_event_store"
6+
gem "decide.rb", require: "decider"
7+
gem "minitest"
8+
gem "mutant"
9+
gem "mutant-minitest"

examples/decide.rb/Gemfile.lock

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
ast (2.4.2)
5+
concurrent-ruby (1.3.4)
6+
decide.rb (0.6.2)
7+
concurrent-ruby (~> 1.3)
8+
diff-lcs (1.5.1)
9+
minitest (5.25.1)
10+
mutant (0.12.4)
11+
diff-lcs (~> 1.3)
12+
parser (~> 3.3.0)
13+
regexp_parser (~> 2.9.0)
14+
sorbet-runtime (~> 0.5.0)
15+
unparser (~> 0.6.14)
16+
mutant-minitest (0.12.4)
17+
minitest (~> 5.11)
18+
mutant (= 0.12.4)
19+
parser (3.3.6.0)
20+
ast (~> 2.4.1)
21+
racc
22+
racc (1.8.1)
23+
regexp_parser (2.9.2)
24+
ruby_event_store (2.15.0)
25+
concurrent-ruby (~> 1.0, >= 1.1.6)
26+
sorbet-runtime (0.5.11647)
27+
unparser (0.6.15)
28+
diff-lcs (~> 1.3)
29+
parser (>= 3.3.0)
30+
31+
PLATFORMS
32+
arm64-darwin
33+
x86_64-linux
34+
35+
DEPENDENCIES
36+
decide.rb
37+
minitest
38+
mutant
39+
mutant-minitest
40+
ruby_event_store
41+
42+
BUNDLED WITH
43+
2.5.23

examples/decide.rb/Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
install:
2+
@bundle install
3+
4+
test:
5+
@bundle exec ruby -Ilib -rproject_management test/issue_test.rb
6+
7+
mutate:
8+
@bundle exec mutant run
9+
10+
.PHONY: install test mutate
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../../shared/lib/project_management"
4+
require_relative "project_management/handler"
5+
require_relative "project_management/issue"
6+
require_relative "project_management/repository"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
module ProjectManagement
4+
class Handler
5+
def initialize(event_store)
6+
@decider = Issue::Decider.dimap_on_event(
7+
fl: ->(event) { infra_to_domain(event) },
8+
fr: ->(event) { domain_to_infra(event) }
9+
)
10+
@repository = Repository.new(event_store)
11+
end
12+
13+
def call(cmd)
14+
state = @repository.load(cmd.id, @decider)
15+
events = @decider.decide(cmd, state)
16+
@repository.store(cmd.id, events)
17+
end
18+
19+
private
20+
21+
def infra_to_domain(event)
22+
case event
23+
in IssueOpened
24+
Issue::IssueOpened.new(issue_id: event.data[:issue_id])
25+
in IssueResolved
26+
Issue::IssueResolved.new(issue_id: event.data[:issue_id])
27+
in IssueClosed
28+
Issue::IssueClosed.new(issue_id: event.data[:issue_id])
29+
in IssueReopened
30+
Issue::IssueReopened.new(issue_id: event.data[:issue_id])
31+
in IssueProgressStarted
32+
Issue::IssueProgressStarted.new(issue_id: event.data[:issue_id])
33+
in IssueProgressStopped
34+
Issue::IssueProgressStopped.new(issue_id: event.data[:issue_id])
35+
end
36+
end
37+
38+
def domain_to_infra(event)
39+
case event
40+
in Issue::IssueOpened
41+
IssueOpened.new(data: { issue_id: event.issue_id })
42+
in Issue::IssueResolved
43+
IssueResolved.new(data: { issue_id: event.issue_id })
44+
in Issue::IssueClosed
45+
IssueClosed.new(data: { issue_id: event.issue_id })
46+
in Issue::IssueReopened
47+
IssueReopened.new(data: { issue_id: event.issue_id })
48+
in Issue::IssueProgressStarted
49+
IssueProgressStarted.new(data: { issue_id: event.issue_id })
50+
in Issue::IssueProgressStopped
51+
IssueProgressStopped.new(data: { issue_id: event.issue_id })
52+
in Issue::InvalidTransition
53+
raise Error
54+
end
55+
end
56+
end
57+
end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
require "decider"
4+
5+
module ProjectManagement
6+
module Issue
7+
IssueOpened = Data.define(:issue_id)
8+
IssueResolved = Data.define(:issue_id)
9+
IssueClosed = Data.define(:issue_id)
10+
IssueReopened = Data.define(:issue_id)
11+
IssueProgressStarted = Data.define(:issue_id)
12+
IssueProgressStopped = Data.define(:issue_id)
13+
InvalidTransition = Data.define
14+
15+
Decider = Decider.define do
16+
initial_state :none
17+
18+
decide CreateIssue, :none do
19+
emit IssueOpened.new(issue_id: command.id)
20+
end
21+
22+
decide proc { [command, state] in [ResolveIssue, :open | :in_progress | :reopened] } do
23+
emit IssueResolved.new(issue_id: command.id)
24+
end
25+
26+
decide proc { [command, state] in [CloseIssue, :open | :in_progress | :resolved | :reopened] } do
27+
emit IssueClosed.new(issue_id: command.id)
28+
end
29+
30+
decide proc { [command, state] in [ReopenIssue, :resolved | :closed] } do
31+
emit IssueReopened.new(issue_id: command.id)
32+
end
33+
34+
decide proc { [command, state] in [StartIssueProgress, :open | :reopened] } do
35+
emit IssueProgressStarted.new(issue_id: command.id)
36+
end
37+
38+
decide StopIssueProgress, :in_progress do
39+
emit IssueProgressStopped.new(issue_id: command.id)
40+
end
41+
42+
decide proc { true } do
43+
emit InvalidTransition.new
44+
end
45+
46+
evolve IssueOpened do
47+
:open
48+
end
49+
50+
evolve IssueResolved do
51+
:resolved
52+
end
53+
54+
evolve IssueClosed do
55+
:closed
56+
end
57+
58+
evolve IssueReopened do
59+
:reopened
60+
end
61+
62+
evolve IssueProgressStarted do
63+
:in_progress
64+
end
65+
66+
evolve IssueProgressStopped do
67+
:open
68+
end
69+
end
70+
end
71+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module ProjectManagement
4+
class Repository
5+
def initialize(event_store)
6+
@event_store = event_store
7+
end
8+
9+
def load(id, decider)
10+
stream = @event_store
11+
.read
12+
.stream(stream_name(id))
13+
14+
@expected_version = stream.count - 1
15+
16+
stream.reduce(decider.initial_state, &decider.evolve)
17+
end
18+
19+
def store(id, events)
20+
@event_store.append(
21+
events,
22+
stream_name: stream_name(id),
23+
expected_version: @expected_version
24+
)
25+
end
26+
27+
private
28+
29+
def stream_name(id) = "Issue$#{id}"
30+
end
31+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
require "minitest/autorun"
4+
require "minitest/mock"
5+
require "mutant/minitest/coverage"
6+
require "ruby_event_store"
7+
8+
require_relative "../lib/project_management"
9+
10+
module ProjectManagement
11+
class IssueTest < Minitest::Test
12+
include Test.with(
13+
handler: ->(event_store) { Handler.new(event_store) },
14+
event_store: -> { RubyEventStore::Client.new }
15+
)
16+
17+
cover "ProjectManagement::Issue*"
18+
end
19+
end

0 commit comments

Comments
 (0)