lieutenant 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +23 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/README.md +108 -0
- data/Rakefile +20 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/lieutenant/aggregate.rb +80 -0
- data/lib/lieutenant/aggregate_repository.rb +68 -0
- data/lib/lieutenant/command.rb +10 -0
- data/lib/lieutenant/command_handler.rb +23 -0
- data/lib/lieutenant/command_sender.rb +38 -0
- data/lib/lieutenant/config.rb +26 -0
- data/lib/lieutenant/event.rb +17 -0
- data/lib/lieutenant/event_bus/in_memory.rb +31 -0
- data/lib/lieutenant/event_bus.rb +8 -0
- data/lib/lieutenant/event_store/in_memory.rb +36 -0
- data/lib/lieutenant/event_store.rb +44 -0
- data/lib/lieutenant/exception/aggregate_not_found.rb +9 -0
- data/lib/lieutenant/exception/concurrency_conflict.rb +10 -0
- data/lib/lieutenant/exception/no_registered_handler.rb +9 -0
- data/lib/lieutenant/exception.rb +11 -0
- data/lib/lieutenant/message.rb +26 -0
- data/lib/lieutenant/version.rb +5 -0
- data/lib/lieutenant.rb +28 -0
- data/lieutenant.gemspec +33 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4b4bd4305e270917371083e9ad6138b7a45f6473
|
4
|
+
data.tar.gz: 5bd3c11a3945578b07e45226dc95efdd4388ba3a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 11e225acbc456c2e8093eb467a1537df64c98400686d295a1ecfbd1c96f2d7463f896cc1c2363d38b8d37700a30db74f7cb731c4b967d11562a1ad0e461b9f30
|
7
|
+
data.tar.gz: 95136b8ad7cd14e41d4bc2eb7b2a3c8e4a76f2fad74b684574ef76d63e962422585377d2b044eb6ee1d7ec813269c5970c6ba4888ad57cf57276ffea4704e467
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.4
|
3
|
+
|
4
|
+
Metrics/LineLength:
|
5
|
+
Max: 120
|
6
|
+
|
7
|
+
Metrics/ClassLength:
|
8
|
+
Max: 150
|
9
|
+
|
10
|
+
Metrics/MethodLength:
|
11
|
+
Max: 15
|
12
|
+
|
13
|
+
Metrics/BlockLength:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Style/RegexpLiteral:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Layout/IndentArray:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Layout/IndentHash:
|
23
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# **Lieutenant**
|
2
|
+
|
3
|
+
## **CQRS/ES Toolkit to command them all**
|
4
|
+
|
5
|
+
Lieutenant is a toolkit that implements various of the components of Command & Query Responsability Segregation (CQRS) and Event Sourcing (ES). It means that your application can get rid of the "current" state of the entities you choose and store all the *changes* that led them to it.
|
6
|
+
|
7
|
+
This gem aims to be most independent as possible of your tecnological choices, it means that it should work with Rails, Sinatra, pure Rack apps or whatever you want.
|
8
|
+
|
9
|
+
If you are not familiarized, you may check this references:
|
10
|
+
|
11
|
+
- [CQRS Journey](https://msdn.microsoft.com/en-us/library/jj554200.aspx)
|
12
|
+
- [crqs.nu](http://cqrs.nu/)
|
13
|
+
- [Event Sourcing, by Martin Fowler](https://martinfowler.com/eaaDev/EventSourcing.html)
|
14
|
+
- [CQRS Documents, by Greg Young](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
|
15
|
+
- [Choosing an architecture, from TrustBK](https://blog.trustbk.com/choosing-an-architecture-85750e1e5a03)
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'lieutenant'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install lieutenant
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
By now, Lieutenant offer the components listed below. With each one, there's a description and example usage. If you cannot understand it, feel free to open an issue. Or if you think that it's not sufficient to other people, pull requests are welcome!
|
36
|
+
|
37
|
+
- [Commands](#commands)
|
38
|
+
- [Command Sender](#command-sender)
|
39
|
+
- [Command Handlers](#command-handlers)
|
40
|
+
- [Aggregate Repositories](#aggregate-repositories)
|
41
|
+
- [Aggregates](#aggregates)
|
42
|
+
- [Events](#events)
|
43
|
+
- [Event Store](#event-store)
|
44
|
+
- [Event Bus](#event-bus)
|
45
|
+
|
46
|
+
### Commands
|
47
|
+
|
48
|
+
TODO
|
49
|
+
|
50
|
+
|
51
|
+
### Command Sender
|
52
|
+
|
53
|
+
TODO
|
54
|
+
|
55
|
+
|
56
|
+
### Command Handlers
|
57
|
+
|
58
|
+
TODO
|
59
|
+
|
60
|
+
|
61
|
+
### Aggregate Repositories
|
62
|
+
|
63
|
+
TODO
|
64
|
+
|
65
|
+
|
66
|
+
### Aggregates
|
67
|
+
|
68
|
+
TODO
|
69
|
+
|
70
|
+
|
71
|
+
### Events
|
72
|
+
|
73
|
+
TODO
|
74
|
+
|
75
|
+
|
76
|
+
### Event Store
|
77
|
+
|
78
|
+
TODO
|
79
|
+
|
80
|
+
|
81
|
+
### Event Bus
|
82
|
+
|
83
|
+
TODO
|
84
|
+
|
85
|
+
|
86
|
+
## Roadmap
|
87
|
+
|
88
|
+
In order to give some directions to the development of this gem, the roadmap below presents in a large picture of the plans to the future (more or less ordered).
|
89
|
+
|
90
|
+
- Projections
|
91
|
+
- Better documentation
|
92
|
+
- Command filters
|
93
|
+
- Command retry policies
|
94
|
+
- Sagas
|
95
|
+
- More implementations of event store
|
96
|
+
- More implementations of event bus
|
97
|
+
|
98
|
+
## Development
|
99
|
+
|
100
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
101
|
+
|
102
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
103
|
+
|
104
|
+
You can also use `bundle exec rake lint` to be sure that your code follows our policies. We currently use [rubocop](https://github.com/bbatsov/rubocop) and [reek](https://github.com/troessner/reek).
|
105
|
+
|
106
|
+
## Contributing
|
107
|
+
|
108
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/gabteles/lieutenant.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
require 'reek/rake/task'
|
7
|
+
require 'yard'
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
|
+
RuboCop::RakeTask.new(:rubocop) do |t|
|
11
|
+
t.options = ['--display-cop-names']
|
12
|
+
end
|
13
|
+
Reek::Rake::Task.new do |t|
|
14
|
+
t.fail_on_error = false
|
15
|
+
end
|
16
|
+
YARD::Rake::YardocTask.new
|
17
|
+
|
18
|
+
task default: :spec
|
19
|
+
task lint: %i[rubocop reek]
|
20
|
+
task fulltest: %i[spec lint]
|
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Representation of an aggregate root
|
5
|
+
module Aggregate
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Define common class methods to aggregates
|
11
|
+
module ClassMethods
|
12
|
+
def load_from_history(id, history)
|
13
|
+
allocate.send(:load_from_history, id, history)
|
14
|
+
end
|
15
|
+
|
16
|
+
def on(*event_classes, &handler)
|
17
|
+
event_classes.each do |event_class|
|
18
|
+
unless event_class < Event
|
19
|
+
raise(Lieutenant::Exception, "Expected #{event_class} to include Lieutenant::Event")
|
20
|
+
end
|
21
|
+
|
22
|
+
handlers[event_class] = handlers[event_class].push(handler)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def handlers_for(event_class)
|
27
|
+
handlers[event_class]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def handlers
|
33
|
+
@handlers ||= Hash.new { [] }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :id
|
38
|
+
attr_reader :uncommitted_events
|
39
|
+
attr_reader :version
|
40
|
+
|
41
|
+
def mark_as_committed
|
42
|
+
self.version += uncommitted_events.size
|
43
|
+
uncommitted_events.clear
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def apply(event_class, **params)
|
49
|
+
event = event_class.with(**params)
|
50
|
+
internal_apply(event)
|
51
|
+
uncommitted_events << event
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_writer :version
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def setup(id)
|
59
|
+
@id = id
|
60
|
+
@uncommitted_events = []
|
61
|
+
@version = -1
|
62
|
+
end
|
63
|
+
|
64
|
+
def load_from_history(id, history)
|
65
|
+
setup(id)
|
66
|
+
|
67
|
+
history.each do |event|
|
68
|
+
internal_apply(event)
|
69
|
+
self.version += 1
|
70
|
+
end
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def internal_apply(event)
|
76
|
+
raise(Lieutenant::Exception, "Invalid event: #{event.inspect}") unless event.valid?
|
77
|
+
self.class.handlers_for(event.class).each { |handler| instance_exec(event, &handler) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Manages the repository logic to persist and retrieve aggregates
|
5
|
+
class AggregateRepository
|
6
|
+
def initialize(store)
|
7
|
+
@store = store
|
8
|
+
end
|
9
|
+
|
10
|
+
def unit_of_work
|
11
|
+
AggregateRepositoryUnit.new(store)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :store
|
17
|
+
|
18
|
+
# Represents one unit of work of the repository to grant independence
|
19
|
+
# between multiple concurrent commands being handled
|
20
|
+
class AggregateRepositoryUnit
|
21
|
+
def initialize(store)
|
22
|
+
@aggregates = {}
|
23
|
+
@store = store
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_aggregate(aggregate)
|
27
|
+
aggregates[aggregate.id] = aggregate
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_aggregate(aggregate_type, aggregate_id)
|
31
|
+
aggregates[aggregate_id] ||= begin
|
32
|
+
history = store.event_stream_for(aggregate_id)
|
33
|
+
aggregate_type.load_from_history(aggregate_id, history)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def execute
|
38
|
+
yield(self)
|
39
|
+
commit
|
40
|
+
ensure
|
41
|
+
clean
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def commit
|
47
|
+
return if aggregates.empty?
|
48
|
+
|
49
|
+
store.transaction do
|
50
|
+
aggregates.each_value(&method(:commit_aggregate))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def clean
|
55
|
+
aggregates.clear
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :aggregates
|
59
|
+
attr_reader :store
|
60
|
+
|
61
|
+
# :reek:FeatureEnvy
|
62
|
+
def commit_aggregate(aggregate)
|
63
|
+
store.save_events(aggregate.id, aggregate.uncommitted_events, aggregate.version)
|
64
|
+
aggregate.mark_as_committed
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Command handler helper. Allows clean syntax to register handlers:
|
5
|
+
#
|
6
|
+
# module FooCommandHandler
|
7
|
+
# include Lieutenant::CommandHandler
|
8
|
+
#
|
9
|
+
# on(BarCommand) do |repository, command|
|
10
|
+
# # ...
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
module CommandHandler
|
14
|
+
def self.included(base)
|
15
|
+
base.extend(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
# :reek:UtilityFunction
|
19
|
+
def on(command_class, &block)
|
20
|
+
Lieutenant.config.command_sender.register(command_class, block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Command bus dispatch commands to the appropriate handler and manages the repository commit/clean
|
5
|
+
class CommandSender
|
6
|
+
def initialize(aggregate_repository)
|
7
|
+
@aggregate_repository = aggregate_repository
|
8
|
+
@handlers = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register(command_class, handler)
|
12
|
+
raise(Lieutenant::Exception, "Handler for #{command_class} already registered") if handlers.key?(command_class)
|
13
|
+
handlers[command_class] = handler
|
14
|
+
end
|
15
|
+
|
16
|
+
def dispatch(command)
|
17
|
+
handler = handler_for(command.class)
|
18
|
+
# TODO: Filters
|
19
|
+
raise(Lieutenant::Exception, "Invalid command: #{command.inspect}") unless command.valid?
|
20
|
+
aggregate_repository.unit_of_work.execute { |repository| handler.call(repository, command) }
|
21
|
+
# rescue Exception::ConcurrencyConflict
|
22
|
+
# TODO: implement command retry policy
|
23
|
+
end
|
24
|
+
|
25
|
+
alias call dispatch
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :aggregate_repository
|
30
|
+
attr_reader :handlers
|
31
|
+
|
32
|
+
def handler_for(command_class)
|
33
|
+
handlers.fetch(command_class) do
|
34
|
+
raise(Exception::NoRegisteredHandler, "No registered handler for #{command_class}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Manages configuration
|
5
|
+
class Config
|
6
|
+
# :reek:BooleanParameter
|
7
|
+
def event_bus(implementation = false)
|
8
|
+
return @event_bus = implementation if implementation
|
9
|
+
@event_bus ||= EventBus::InMemory.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# :reek:BooleanParameter
|
13
|
+
def event_store(implementation = false)
|
14
|
+
return @event_store_implementation = implementation if implementation
|
15
|
+
@event_store ||= EventStore.new(@event_store_implementation, event_bus)
|
16
|
+
end
|
17
|
+
|
18
|
+
def aggregate_repository
|
19
|
+
@aggregate_repository ||= AggregateRepository.new(event_store)
|
20
|
+
end
|
21
|
+
|
22
|
+
def command_sender
|
23
|
+
@command_sender ||= CommandSender.new(aggregate_repository)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# The basic interface to register the aggregates events
|
5
|
+
module Event
|
6
|
+
def self.included(base)
|
7
|
+
base.include(Message)
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :aggregate_id, :sequence_number
|
11
|
+
|
12
|
+
def prepare(aggregate_id, sequence_number)
|
13
|
+
@aggregate_id = aggregate_id
|
14
|
+
@sequence_number = sequence_number
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
module EventBus
|
5
|
+
# Memory implementation of the event bus. Publishes and notifies on the same memory space.
|
6
|
+
class InMemory
|
7
|
+
def initialize
|
8
|
+
@handlers = Hash.new { [] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def subscribe(*event_classes, &handler)
|
12
|
+
event_classes.each do |event_class|
|
13
|
+
handlers[event_class] = handlers[event_class].push(handler)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def publish(event)
|
18
|
+
block = CALL_HANDLER_WITH_EVENT[event]
|
19
|
+
handlers[:all].each(&block)
|
20
|
+
handlers[event.class].each(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :handlers
|
26
|
+
|
27
|
+
CALL_HANDLER_WITH_EVENT = ->(event) { ->(handler) { handler.call(event) } }
|
28
|
+
private_constant :CALL_HANDLER_WITH_EVENT
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
class EventStore
|
5
|
+
# Memory implementation of the event store. Stores events while the application is running
|
6
|
+
class InMemory
|
7
|
+
def initialize
|
8
|
+
@store = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def persist(events)
|
12
|
+
events.each { |event| (store[event.aggregate_id] ||= []).push(event) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def event_stream_for(aggregate_id)
|
16
|
+
events = store[aggregate_id]
|
17
|
+
return nil unless events
|
18
|
+
Enumerator.new { |yielder| events.each(&yielder.method(:<<)) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def aggregate_sequence_number(aggregate_id)
|
22
|
+
return -1 unless store.key?(aggregate_id)
|
23
|
+
store[aggregate_id].last.sequence_number
|
24
|
+
end
|
25
|
+
|
26
|
+
def transaction
|
27
|
+
# In memory event store currently does not support transactions.
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :store
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Event stores handles pushing and pulling events from the event store.
|
5
|
+
class EventStore
|
6
|
+
autoload :InMemory, 'lieutenant/event_store/in_memory'
|
7
|
+
|
8
|
+
def initialize(store, event_bus)
|
9
|
+
@store = store
|
10
|
+
@event_bus = event_bus
|
11
|
+
end
|
12
|
+
|
13
|
+
def save_events(aggregate_id, events, expected_version)
|
14
|
+
raise(Exception::ConcurrencyConflict) if store.aggregate_sequence_number(aggregate_id) != expected_version
|
15
|
+
|
16
|
+
PREPARE_EVENTS[aggregate_id, events, expected_version].tap do |final_events|
|
17
|
+
store.persist(final_events)
|
18
|
+
final_events.each(&event_bus.method(:publish))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def event_stream_for(aggregate_id)
|
23
|
+
store.event_stream_for(aggregate_id) || raise(Exception::AggregateNotFound, aggregate_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def transaction(&blk)
|
27
|
+
store.transaction(&blk)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :store
|
33
|
+
attr_reader :event_bus
|
34
|
+
|
35
|
+
PREPARE_EVENTS = lambda do |aggregate_id, events, sequence_number|
|
36
|
+
events.lazy.with_index.map do |event, idx|
|
37
|
+
event.prepare(aggregate_id, sequence_number + idx + 1)
|
38
|
+
event
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private_constant :PREPARE_EVENTS
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Generic class to all Lieutenant exceptions. Anything is
|
5
|
+
# rescue-able with Lieutenant::Exception
|
6
|
+
class Exception < StandardError
|
7
|
+
autoload :AggregateNotFound, 'lieutenant/exception/aggregate_not_found'
|
8
|
+
autoload :ConcurrencyConflict, 'lieutenant/exception/concurrency_conflict'
|
9
|
+
autoload :NoRegisteredHandler, 'lieutenant/exception/no_registered_handler'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Helper to define messages with validation
|
5
|
+
module Message
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.include(ActiveModel::Validations)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Define common class methods to commands
|
12
|
+
module ClassMethods
|
13
|
+
def with(params)
|
14
|
+
new.tap do |command|
|
15
|
+
params.each_pair do |key, value|
|
16
|
+
begin
|
17
|
+
command.send("#{key}=", value)
|
18
|
+
rescue NoMethodError # rubocop:disable Lint/HandleExceptions
|
19
|
+
# DO NOTHING
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/lieutenant.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'active_model'
|
5
|
+
|
6
|
+
# Lieutenant namespace
|
7
|
+
module Lieutenant
|
8
|
+
autoload :Aggregate, 'lieutenant/aggregate'
|
9
|
+
autoload :AggregateRepository, 'lieutenant/aggregate_repository'
|
10
|
+
autoload :Command, 'lieutenant/command'
|
11
|
+
autoload :CommandHandler, 'lieutenant/command_handler'
|
12
|
+
autoload :CommandSender, 'lieutenant/command_sender'
|
13
|
+
autoload :Config, 'lieutenant/config'
|
14
|
+
autoload :Event, 'lieutenant/event'
|
15
|
+
autoload :EventBus, 'lieutenant/event_bus'
|
16
|
+
autoload :EventStore, 'lieutenant/event_store'
|
17
|
+
autoload :Exception, 'lieutenant/exception'
|
18
|
+
autoload :Message, 'lieutenant/message'
|
19
|
+
autoload :VERSION, 'lieutenant/version'
|
20
|
+
|
21
|
+
module_function
|
22
|
+
|
23
|
+
@config = Config.new
|
24
|
+
|
25
|
+
def config
|
26
|
+
block_given? ? yield(@config) : @config
|
27
|
+
end
|
28
|
+
end
|
data/lieutenant.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'lieutenant/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'lieutenant'
|
10
|
+
spec.version = Lieutenant::VERSION
|
11
|
+
spec.authors = ['Gabriel Teles']
|
12
|
+
spec.email = ['gab.teles@hotmail.com']
|
13
|
+
|
14
|
+
spec.summary = 'CQRS/ES Toolkit to command them all'
|
15
|
+
spec.homepage = 'https://github.com/gabteles/lieutenant'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler'
|
25
|
+
spec.add_development_dependency 'pry'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'reek'
|
28
|
+
spec.add_development_dependency 'rspec'
|
29
|
+
spec.add_development_dependency 'rubocop'
|
30
|
+
spec.add_development_dependency 'simplecov'
|
31
|
+
spec.add_development_dependency 'yard'
|
32
|
+
spec.add_dependency 'activemodel'
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lieutenant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gabriel Teles
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-12-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: reek
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: activemodel
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- gab.teles@hotmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".rspec"
|
148
|
+
- ".rubocop.yml"
|
149
|
+
- ".travis.yml"
|
150
|
+
- Gemfile
|
151
|
+
- README.md
|
152
|
+
- Rakefile
|
153
|
+
- bin/console
|
154
|
+
- bin/setup
|
155
|
+
- lib/lieutenant.rb
|
156
|
+
- lib/lieutenant/aggregate.rb
|
157
|
+
- lib/lieutenant/aggregate_repository.rb
|
158
|
+
- lib/lieutenant/command.rb
|
159
|
+
- lib/lieutenant/command_handler.rb
|
160
|
+
- lib/lieutenant/command_sender.rb
|
161
|
+
- lib/lieutenant/config.rb
|
162
|
+
- lib/lieutenant/event.rb
|
163
|
+
- lib/lieutenant/event_bus.rb
|
164
|
+
- lib/lieutenant/event_bus/in_memory.rb
|
165
|
+
- lib/lieutenant/event_store.rb
|
166
|
+
- lib/lieutenant/event_store/in_memory.rb
|
167
|
+
- lib/lieutenant/exception.rb
|
168
|
+
- lib/lieutenant/exception/aggregate_not_found.rb
|
169
|
+
- lib/lieutenant/exception/concurrency_conflict.rb
|
170
|
+
- lib/lieutenant/exception/no_registered_handler.rb
|
171
|
+
- lib/lieutenant/message.rb
|
172
|
+
- lib/lieutenant/version.rb
|
173
|
+
- lieutenant.gemspec
|
174
|
+
homepage: https://github.com/gabteles/lieutenant
|
175
|
+
licenses: []
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.6.11
|
194
|
+
signing_key:
|
195
|
+
specification_version: 4
|
196
|
+
summary: CQRS/ES Toolkit to command them all
|
197
|
+
test_files: []
|