rcqrs 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.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.md +4 -0
- data/Rakefile +1 -0
- data/lib/rcqrs.rb +30 -0
- data/lib/rcqrs/aggregate_root_base.rb +70 -0
- data/lib/rcqrs/base_command.rb +22 -0
- data/lib/rcqrs/base_command_handler.rb +27 -0
- data/lib/rcqrs/base_event.rb +57 -0
- data/lib/rcqrs/command_executor.rb +16 -0
- data/lib/rcqrs/command_not_valid_exception.rb +13 -0
- data/lib/rcqrs/command_source.rb +72 -0
- data/lib/rcqrs/configuration.rb +15 -0
- data/lib/rcqrs/convention_command.rb +32 -0
- data/lib/rcqrs/convention_command_handler.rb +33 -0
- data/lib/rcqrs/denormalizer.rb +79 -0
- data/lib/rcqrs/domain_exception.rb +11 -0
- data/lib/rcqrs/event_bus.rb +59 -0
- data/lib/rcqrs/event_collection.rb +18 -0
- data/lib/rcqrs/event_configuration_base.rb +43 -0
- data/lib/rcqrs/event_handler.rb +55 -0
- data/lib/rcqrs/event_type_registry.rb +16 -0
- data/lib/rcqrs/not_an_event_exception.rb +7 -0
- data/lib/rcqrs/published_event.rb +15 -0
- data/lib/rcqrs/repository.rb +27 -0
- data/lib/rcqrs/unknown_event_exception.rb +3 -0
- data/lib/rcqrs/version.rb +3 -0
- data/rcqrs.gemspec +29 -0
- data/spec/aggregate_root_base_spec.rb +69 -0
- data/spec/base_commandhandler_spec.rb +38 -0
- data/spec/base_event_spec.rb +26 -0
- data/spec/command_executor_spec.rb +12 -0
- data/spec/command_handler_spec.rb +40 -0
- data/spec/configuration_spec.rb +13 -0
- data/spec/convention_command_spec.rb +33 -0
- data/spec/denormalizer_spec.rb +107 -0
- data/spec/eventbus_spec.rb +109 -0
- data/spec/eventhandler_spec.rb +74 -0
- data/spec/repository_spec.rb +75 -0
- data/spec/ricko.rb +181 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/test_aggregate.rb +36 -0
- data/spec/test_command_handler.rb +33 -0
- data/spec/test_command_source.rb +4 -0
- data/spec/test_event.rb +13 -0
- data/spec/test_event_bus.rb +18 -0
- metadata +154 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
module Rcqrs
|
2
|
+
|
3
|
+
class Eventbus
|
4
|
+
|
5
|
+
def initialize eventstore
|
6
|
+
@eventstore = eventstore
|
7
|
+
@eventhandlers = Hash.new
|
8
|
+
@published_events = Array.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_events aggregate_id
|
12
|
+
@eventstore.load_events(aggregate_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def register eventtype, eventhandler
|
16
|
+
@eventhandlers[eventtype] = Array.new unless @eventhandlers.has_key? eventtype
|
17
|
+
unless @eventhandlers[eventtype].include? eventhandler
|
18
|
+
@eventhandlers[eventtype] << eventhandler
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def publish id, event
|
23
|
+
unless event.is_a? BaseEvent then
|
24
|
+
raise NotAnEventException, "Given event has to inherit from BaseEvent"
|
25
|
+
end
|
26
|
+
@published_events << PublishedEvent.new(id,event)
|
27
|
+
end
|
28
|
+
|
29
|
+
def commit
|
30
|
+
store_published_time_for_events
|
31
|
+
save_events
|
32
|
+
publish_events
|
33
|
+
flush
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def store_published_time_for_events
|
39
|
+
@published_events.each{|published_event| published_event.event.store_publish_time}
|
40
|
+
end
|
41
|
+
|
42
|
+
def save_events
|
43
|
+
@eventstore.store(@published_events)
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish_events
|
47
|
+
@published_events.each do |event_tuple|
|
48
|
+
if @eventhandlers[event_tuple.event.class] != nil
|
49
|
+
@eventhandlers[event_tuple.event.class].each {|handler| handler.handle event_tuple.aggregate_id, event_tuple.event }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def flush
|
55
|
+
@published_events = Array.new
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rcqrs
|
2
|
+
|
3
|
+
class EventCollection
|
4
|
+
|
5
|
+
def initialize aggregateroot_id, events
|
6
|
+
@events = events
|
7
|
+
@aggregateroot_id = aggregateroot_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def commit_to eventstore
|
11
|
+
@events.each do |event|
|
12
|
+
eventstore.publish @aggregateroot_id, event
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rcqrs
|
2
|
+
|
3
|
+
class EventConfigurationBase
|
4
|
+
|
5
|
+
@configs = Array.new
|
6
|
+
|
7
|
+
def self.register_handler(handler)
|
8
|
+
handler.handled_events.each do |event_name|
|
9
|
+
handle(event_name).with(handler)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.register_all_at(eventbus)
|
14
|
+
if(@configs != nil)
|
15
|
+
@configs.each do |config|
|
16
|
+
eventbus.register config.eventtype, config.eventhandler
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.handle(eventtype)
|
22
|
+
@configs = Array.new if (@configs == nil)
|
23
|
+
config = EventConfig.new(eval(eventtype.to_s))
|
24
|
+
@configs << config
|
25
|
+
config
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
class EventConfig
|
31
|
+
|
32
|
+
attr_reader :eventtype, :eventhandler
|
33
|
+
|
34
|
+
def initialize eventtype
|
35
|
+
@eventtype = eventtype
|
36
|
+
end
|
37
|
+
|
38
|
+
def with eventhandler
|
39
|
+
@eventhandler = eventhandler
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rcqrs
|
2
|
+
|
3
|
+
module EventHandler
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def handled_events
|
12
|
+
methods.select {|m| m.to_s.match(/^handle_/)}.map{|m| convert_to_event_name(m[6..-1])}
|
13
|
+
end
|
14
|
+
|
15
|
+
def convert_to_event_name method
|
16
|
+
full_event_name(method)
|
17
|
+
end
|
18
|
+
|
19
|
+
def handle aggregate_id, event
|
20
|
+
methodname = handler_method_name(event)
|
21
|
+
send(methodname, aggregate_id,event)
|
22
|
+
end
|
23
|
+
|
24
|
+
def handler_method_name(event)
|
25
|
+
"handle" + event.class.name.split("::").last.gsub(/[A-Z]/){|s| "_" + s.downcase}
|
26
|
+
end
|
27
|
+
|
28
|
+
def namespace namespace
|
29
|
+
@namespace = namespace.to_s + "::"
|
30
|
+
end
|
31
|
+
|
32
|
+
def handler(event, &handlercode)
|
33
|
+
create_handler_method(event,handlercode)
|
34
|
+
EventConfigurationBase.handle(full_event_name(event)).with(self)
|
35
|
+
end
|
36
|
+
|
37
|
+
def full_event_name event_name
|
38
|
+
(@namespace || "") + symbol_to_event_name(event_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def symbol_to_event_name event
|
42
|
+
name = event.to_s.gsub(/_[a-z]/){|s| s[1].upcase}
|
43
|
+
name[0] = name[0].upcase
|
44
|
+
name
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_handler_method(event,handlercode)
|
48
|
+
itself = (class << self; self; end)
|
49
|
+
itself.instance_eval do
|
50
|
+
define_method "handle_"+event.to_s.gsub(/[A-Z]/){|s| "_" + s.downcase}, &handlercode
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rcqrs
|
2
|
+
|
3
|
+
class PublishedEvent
|
4
|
+
attr_accessor :aggregate_id, :event
|
5
|
+
|
6
|
+
def initialize aggregate_id, event
|
7
|
+
@aggregate_id, @event = aggregate_id, event
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
@aggregate_id == other.aggregate_id && @event == other.event
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rcqrs
|
2
|
+
class Repository
|
3
|
+
|
4
|
+
def initialize event_store
|
5
|
+
@event_store = event_store
|
6
|
+
end
|
7
|
+
|
8
|
+
def load(aggregate_class, aggregate_id)
|
9
|
+
aggregate = aggregate_class.new aggregate_id
|
10
|
+
events = @event_store.load_events(aggregate_id)
|
11
|
+
aggregate.load_from(events)
|
12
|
+
aggregate
|
13
|
+
end
|
14
|
+
|
15
|
+
def save aggregate_root
|
16
|
+
aggregate_root.pending_events.commit_to @event_store
|
17
|
+
end
|
18
|
+
|
19
|
+
def fire aggregate_id, event
|
20
|
+
unless (event.is_a? BaseEvent) then
|
21
|
+
raise NotAnEventException, "Given event has to inherit from BaseEvent"
|
22
|
+
end
|
23
|
+
@event_store.publish(aggregate_id,event)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/rcqrs.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rcqrs/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rcqrs"
|
7
|
+
s.version = Rcqrs::VERSION
|
8
|
+
s.authors = ["Tim de Putter"]
|
9
|
+
s.email = ["tim.de.putter83@googlemail.com"]
|
10
|
+
s.summary = %q{Framework to provide cqrs + eventsourcing functionality in ruby.}
|
11
|
+
s.description = %q{Framework to provide cqrs + eventsourcing functionality in ruby.}
|
12
|
+
|
13
|
+
s.rubyforge_project = "rcqrs"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# specify any dependencies here; for example:
|
21
|
+
# s.add_runtime_dependency "rest-client"
|
22
|
+
|
23
|
+
s.add_development_dependency "rspec"
|
24
|
+
s.add_development_dependency "rspec-spies"
|
25
|
+
s.add_development_dependency "activemodel"
|
26
|
+
s.add_runtime_dependency "uuid"
|
27
|
+
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rcqrs::AggregateRootBase do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@idgenerator = UUID.new
|
7
|
+
@id = @idgenerator.generate
|
8
|
+
@testAggregate = TestAggregateModule::TestAggregate.new @id
|
9
|
+
@mockEventStore = mockup
|
10
|
+
@events = [TestEventModule::TestEvent.new, TestEventModule::TestEvent.new]
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should handle Events" do
|
14
|
+
@testAggregate.doSomething
|
15
|
+
@testAggregate.handledEvents.size.should == 1
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should events when there is no eventhandler method" do
|
19
|
+
class UnknownEvent < Rcqrs::BaseEvent; end
|
20
|
+
@testAggregate.load_from [TestEventModule::TestEvent.new,UnknownEvent.new]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should collect Events in a EventCollection" do
|
24
|
+
@testAggregate.doSomething
|
25
|
+
@testAggregate.pending_events.should be_an_instance_of(Rcqrs::EventCollection)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have recorded one event to be committed" do
|
29
|
+
@testAggregate.doSomething
|
30
|
+
@testAggregate.pending_events.commit_to @mockEventStore
|
31
|
+
@mockEventStore.message(:publish).with(any, any(TestEventModule::TestEvent)).should_be_received
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should send its uuid to eventstore to be able to assoziate the events with the aggregate" do
|
35
|
+
@testAggregate.doSomething
|
36
|
+
@testAggregate.pending_events.commit_to @mockEventStore
|
37
|
+
@mockEventStore.message(:publish).with(@id, any()).should_be_received
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should be able to load from existing events in the eventstore" do
|
41
|
+
@testAggregate.load_from @events
|
42
|
+
@testAggregate.handledEvents[0] == @events[0]
|
43
|
+
@testAggregate.handledEvents[1] == @events[1]
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should only pass the new events to the eventstore, not the ones loaded previously" do
|
47
|
+
@testAggregate.load_from @events
|
48
|
+
@testAggregate.doSomething
|
49
|
+
@testAggregate.pending_events.commit_to @mockEventStore
|
50
|
+
@mockEventStore.message(:publish).with(@id,any(TestEventModule::TestEvent)).should_be_received
|
51
|
+
end
|
52
|
+
|
53
|
+
it "events not inherited from BaseEvent are not allowed to be fired" do
|
54
|
+
expect {@testAggregate.doSomethingStupid}.to raise_error(Rcqrs::NotAnEventException)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should allow to define eventhandlers via a class macro" do
|
58
|
+
@testAggregate.handleAnotherTestEvent TestEventModule::AnotherTestEvent.new
|
59
|
+
@testAggregate.handledEvents[0].class.should == TestEventModule::AnotherTestEvent
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should handle events with eventhandlers defined by class macros" do
|
63
|
+
@testAggregate.do_something_else
|
64
|
+
@testAggregate.handledEvents[0].class.should == TestEventModule::AnotherTestEvent
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rcqrs::BaseCommandHandler do
|
4
|
+
|
5
|
+
it "should call execute on inherited class when executeCommand is called" do
|
6
|
+
commandHandler = TestCommandHandler.new nil
|
7
|
+
commandHandler.executeCommand TestCommand.new("param")
|
8
|
+
commandHandler.execute_called?.should == true
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should validate the given command if it provides validation" do
|
12
|
+
commandHandler = TestCommandHandler.new nil
|
13
|
+
testCommand = TestCommand.new("param")
|
14
|
+
def testCommand.invalid?; true; end
|
15
|
+
expect {commandHandler.executeCommand testCommand}.to raise_error Rcqrs::CommandNotValidException
|
16
|
+
end
|
17
|
+
|
18
|
+
it "the validation does not fail the command is executed" do
|
19
|
+
commandHandler = TestCommandHandler.new nil
|
20
|
+
testCommand = TestCommand.new("param")
|
21
|
+
def testCommand.invalid?; false; end
|
22
|
+
commandHandler.executeCommand testCommand
|
23
|
+
commandHandler.execute_called?.should == true
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should put all validationerrors in the Exception if the validation fails" do
|
27
|
+
begin
|
28
|
+
commandHandler = TestCommandHandler.new nil
|
29
|
+
testCommand = TestCommand.new("param")
|
30
|
+
def testCommand.invalid?; true; end
|
31
|
+
def testCommand.errormessages; ["Error1", "Error2"]; end
|
32
|
+
commandHandler.executeCommand testCommand
|
33
|
+
rescue Rcqrs::CommandNotValidException => exception
|
34
|
+
exception.errors.should == ["Error1", "Error2"]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
describe Rcqrs::BaseEvent do
|
2
|
+
|
3
|
+
class TestEvent < Rcqrs::BaseEvent
|
4
|
+
property :name
|
5
|
+
end
|
6
|
+
|
7
|
+
subject{TestEvent}
|
8
|
+
|
9
|
+
it "should allow to define properties on an event which can be initalized in the constructor" do
|
10
|
+
TestEvent.new(name: "Timbo").name.should == "Timbo"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should allow to use strings as keys" do
|
14
|
+
TestEvent.new("name" => "Timbo").name.should == "Timbo"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should reject properties not defined" do
|
18
|
+
lambda {TestEvent.new(age: 23)}.should raise_error
|
19
|
+
lambda {TestEvent.new("age" => 23)}.should raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not allow to define allready used methods as properties" do
|
23
|
+
lambda {class TestEvent; property :data ;end}.should raise_error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|