Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,18 @@ More: https://blog.arkency.com/make-your-ruby-code-more-modular-and-functional-w
- aggregate not aware of events
- aggregate object is still responsible for holding invariants
- no id in domain class

### Decider

[source](examples/decider)

- clear separation of state sourcing (with projection)
- aggregate with decider pattern

### Decider with decide.rb gem

[source](examples/decide.rb)

- clear separation of state sourcing (with projection) with expected version
- aggregate with decider pattern and decide.rb DSL
- mapping between infra (RES) events and domain events used inside decider
14 changes: 14 additions & 0 deletions examples/decide.rb/.mutant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
integration:
name: minitest
includes:
- lib
requires:
- project_management
matcher:
subjects:
- ProjectManagement*
ignore:
- ProjectManagement::Test*
coverage_criteria:
process_abort: true
usage: opensource
9 changes: 9 additions & 0 deletions examples/decide.rb/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem "ruby_event_store"
gem "decide.rb", require: "decider"
gem "minitest"
gem "mutant"
gem "mutant-minitest"
43 changes: 43 additions & 0 deletions examples/decide.rb/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
concurrent-ruby (1.3.4)
decide.rb (0.6.2)
concurrent-ruby (~> 1.3)
diff-lcs (1.5.1)
minitest (5.25.1)
mutant (0.12.4)
diff-lcs (~> 1.3)
parser (~> 3.3.0)
regexp_parser (~> 2.9.0)
sorbet-runtime (~> 0.5.0)
unparser (~> 0.6.14)
mutant-minitest (0.12.4)
minitest (~> 5.11)
mutant (= 0.12.4)
parser (3.3.6.0)
ast (~> 2.4.1)
racc
racc (1.8.1)
regexp_parser (2.9.2)
ruby_event_store (2.15.0)
concurrent-ruby (~> 1.0, >= 1.1.6)
sorbet-runtime (0.5.11647)
unparser (0.6.15)
diff-lcs (~> 1.3)
parser (>= 3.3.0)

PLATFORMS
arm64-darwin
x86_64-linux

DEPENDENCIES
decide.rb
minitest
mutant
mutant-minitest
ruby_event_store

BUNDLED WITH
2.5.23
10 changes: 10 additions & 0 deletions examples/decide.rb/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
install:
@bundle install

test:
@bundle exec ruby -Ilib -rproject_management test/issue_test.rb

mutate:
@bundle exec mutant run

.PHONY: install test mutate
6 changes: 6 additions & 0 deletions examples/decide.rb/lib/project_management.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

require_relative "../../../shared/lib/project_management"
require_relative "project_management/handler"
require_relative "project_management/issue"
require_relative "project_management/repository"
57 changes: 57 additions & 0 deletions examples/decide.rb/lib/project_management/handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module ProjectManagement
class Handler
def initialize(event_store)
@decider = Issue::Decider.dimap_on_event(
fl: ->(event) { infra_to_domain(event) },
fr: ->(event) { domain_to_infra(event) }
)
@repository = Repository.new(event_store)
end

def call(cmd)
state = @repository.load(cmd.id, @decider)
events = @decider.decide(cmd, state)
@repository.store(cmd.id, events)
end

private

def infra_to_domain(event)
case event
in IssueOpened
Issue::IssueOpened.new(issue_id: event.data[:issue_id])
in IssueResolved
Issue::IssueResolved.new(issue_id: event.data[:issue_id])
in IssueClosed
Issue::IssueClosed.new(issue_id: event.data[:issue_id])
in IssueReopened
Issue::IssueReopened.new(issue_id: event.data[:issue_id])
in IssueProgressStarted
Issue::IssueProgressStarted.new(issue_id: event.data[:issue_id])
in IssueProgressStopped
Issue::IssueProgressStopped.new(issue_id: event.data[:issue_id])
end
end

def domain_to_infra(event)
case event
in Issue::IssueOpened
IssueOpened.new(data: { issue_id: event.issue_id })
in Issue::IssueResolved
IssueResolved.new(data: { issue_id: event.issue_id })
in Issue::IssueClosed
IssueClosed.new(data: { issue_id: event.issue_id })
in Issue::IssueReopened
IssueReopened.new(data: { issue_id: event.issue_id })
in Issue::IssueProgressStarted
IssueProgressStarted.new(data: { issue_id: event.issue_id })
in Issue::IssueProgressStopped
IssueProgressStopped.new(data: { issue_id: event.issue_id })
in Issue::InvalidTransition
raise Error
end
end
end
end
71 changes: 71 additions & 0 deletions examples/decide.rb/lib/project_management/issue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require "decider"

module ProjectManagement
module Issue
IssueOpened = Data.define(:issue_id)
IssueResolved = Data.define(:issue_id)
IssueClosed = Data.define(:issue_id)
IssueReopened = Data.define(:issue_id)
IssueProgressStarted = Data.define(:issue_id)
IssueProgressStopped = Data.define(:issue_id)
InvalidTransition = Data.define

Decider = Decider.define do
initial_state :none

decide CreateIssue, :none do
emit IssueOpened.new(issue_id: command.id)
end

decide proc { [command, state] in [ResolveIssue, :open | :in_progress | :reopened] } do
emit IssueResolved.new(issue_id: command.id)
end

decide proc { [command, state] in [CloseIssue, :open | :in_progress | :resolved | :reopened] } do
emit IssueClosed.new(issue_id: command.id)
end

decide proc { [command, state] in [ReopenIssue, :resolved | :closed] } do
emit IssueReopened.new(issue_id: command.id)
end

decide proc { [command, state] in [StartIssueProgress, :open | :reopened] } do
emit IssueProgressStarted.new(issue_id: command.id)
end

decide StopIssueProgress, :in_progress do
emit IssueProgressStopped.new(issue_id: command.id)
end

decide proc { true } do
emit InvalidTransition.new
end

evolve IssueOpened do
:open
end

evolve IssueResolved do
:resolved
end

evolve IssueClosed do
:closed
end

evolve IssueReopened do
:reopened
end

evolve IssueProgressStarted do
:in_progress
end

evolve IssueProgressStopped do
:open
end
end
end
end
31 changes: 31 additions & 0 deletions examples/decide.rb/lib/project_management/repository.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module ProjectManagement
class Repository
def initialize(event_store)
@event_store = event_store
end

def load(id, decider)
stream = @event_store
.read
.stream(stream_name(id))

@expected_version = stream.count - 1

stream.reduce(decider.initial_state, &decider.evolve)
end

def store(id, events)
@event_store.append(
events,
stream_name: stream_name(id),
expected_version: @expected_version
)
end

private

def stream_name(id) = "Issue$#{id}"
end
end
19 changes: 19 additions & 0 deletions examples/decide.rb/test/issue_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "minitest/autorun"
require "minitest/mock"
require "mutant/minitest/coverage"
require "ruby_event_store"

require_relative "../lib/project_management"

module ProjectManagement
class IssueTest < Minitest::Test
include Test.with(
handler: ->(event_store) { Handler.new(event_store) },
event_store: -> { RubyEventStore::Client.new }
)

cover "ProjectManagement::Issue*"
end
end
2 changes: 2 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
ruby = "3.2.2"