lieutenant 0.1.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 +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: []
|