event_sorcerer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +16 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/event_sorcerer.gemspec +28 -0
- data/lib/event_sorcerer/aggregate.rb +91 -0
- data/lib/event_sorcerer/aggregate_creator.rb +35 -0
- data/lib/event_sorcerer/aggregate_loader.rb +67 -0
- data/lib/event_sorcerer/aggregate_proxy.rb +122 -0
- data/lib/event_sorcerer/argument_hashifier.rb +30 -0
- data/lib/event_sorcerer/event.rb +5 -0
- data/lib/event_sorcerer/event_applicator.rb +22 -0
- data/lib/event_sorcerer/event_store.rb +55 -0
- data/lib/event_sorcerer/message_bus.rb +17 -0
- data/lib/event_sorcerer/no_unit_of_work.rb +35 -0
- data/lib/event_sorcerer/unit_of_work.rb +70 -0
- data/lib/event_sorcerer/version.rb +4 -0
- data/lib/event_sorcerer.rb +77 -0
- data/spec/lib/event_sorcerer_spec.rb +11 -0
- metadata +163 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8f1f0fac682006f02615146ad4d6997e9ceeddb5
|
4
|
+
data.tar.gz: ee90c449e367c4d6685f6fd7712b95a6236a939c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1903c49e3cae9c1b11225c04c2fc6e8469e9475c8050945f62b8d4237548770d7306638bb176790815984da437b882f411ccf1d15c2dc765511fc07900f35430
|
7
|
+
data.tar.gz: 6f2030bc699886797c125f21d3c304293723a9e79fc5eebd600ce5f93bacafbb08000627ea2c5ea71d71630e00f5f8a95a19d3187107d297160c626c167eba77
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Copyright 2014 Sebastian Edwards
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
14
|
+
|
15
|
+
All of the files in this project are under the project-wide license
|
16
|
+
unless they are otherwise marked.
|
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# EventSorcerer
|
2
|
+
|
3
|
+
Generic event-sourcing scaffold.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'event_sorcerer'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
## Contributing
|
16
|
+
|
17
|
+
1. Fork it ( http://github.com/sebastianedwards/event_sorcerer/fork )
|
18
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
19
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
20
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
21
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'event_sorcerer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'event_sorcerer'
|
8
|
+
spec.version = EventSorcerer::VERSION
|
9
|
+
spec.authors = ['Sebastian Edwards']
|
10
|
+
spec.email = ['me@sebastianedwards.co.nz']
|
11
|
+
spec.homepage = 'https://github.com/SebastianEdwards/event_sorcerer'
|
12
|
+
spec.summary = %w(Generic event-sourcing scaffold)
|
13
|
+
spec.description = spec.summary
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
21
|
+
spec.add_development_dependency 'inch'
|
22
|
+
spec.add_development_dependency 'rspec', '~> 3.0.0'
|
23
|
+
spec.add_development_dependency 'rubocop'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'invokr', '0.0.5'
|
27
|
+
spec.add_runtime_dependency 'timecop', '0.7.1'
|
28
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Mixin to turn a plain class into an event-store backed aggreagte.
|
3
|
+
module Aggregate
|
4
|
+
# Public: Returns the ID for the aggregate.
|
5
|
+
attr_reader :id
|
6
|
+
|
7
|
+
# Public: Returns the version number of the aggregate in memory.
|
8
|
+
attr_reader :local_version
|
9
|
+
|
10
|
+
# Public: Returns the version number of the aggregate in the database.
|
11
|
+
attr_reader :persisted_version
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.class.send :alias_method, :build, :new
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
base.class.extend(Forwardable)
|
17
|
+
base.class.send :def_delegators, :EventSorcerer, :unit_of_work
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Class methods to be extended onto the including class.
|
21
|
+
module ClassMethods
|
22
|
+
# Public: An array of symbols representing the names of the methods which
|
23
|
+
# are events.
|
24
|
+
def event_methods
|
25
|
+
@event_methods ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Methods defined within this block will have their method symbol
|
29
|
+
# added to the event_methods array.
|
30
|
+
#
|
31
|
+
# block - block containing the event method definitions.
|
32
|
+
#
|
33
|
+
# Returns self.
|
34
|
+
def events(&block)
|
35
|
+
test_class ||= Class.new(BasicObject)
|
36
|
+
starting_methods = test_class.instance_methods
|
37
|
+
test_class.class_eval(&block)
|
38
|
+
|
39
|
+
new_events = test_class.instance_methods.select do |method|
|
40
|
+
!starting_methods.include? method
|
41
|
+
end
|
42
|
+
|
43
|
+
event_methods.concat new_events
|
44
|
+
class_eval(&block)
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Load an aggregate out of the event store.
|
50
|
+
#
|
51
|
+
# id - the ID of the aggregate to load.
|
52
|
+
#
|
53
|
+
# Returns an AggregateProxy object.
|
54
|
+
def find(id)
|
55
|
+
if unit_of_work.fetch_aggregate(id)
|
56
|
+
return unit_of_work.fetch_aggregate(id)
|
57
|
+
end
|
58
|
+
|
59
|
+
AggregateLoader.new(self, id).load.tap do |aggregate|
|
60
|
+
unit_of_work.store_aggregate(aggregate)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public: Load an aggregate out of the event store or create new.
|
65
|
+
#
|
66
|
+
# id - the ID of the aggregate to load.
|
67
|
+
#
|
68
|
+
# Returns an AggregateProxy object.
|
69
|
+
def find_or_new(id)
|
70
|
+
if unit_of_work.fetch_aggregate(id)
|
71
|
+
return unit_of_work.fetch_aggregate(id)
|
72
|
+
end
|
73
|
+
|
74
|
+
AggregateLoader.new(self, id, false).load.tap do |aggregate|
|
75
|
+
unit_of_work.store_aggregate(aggregate)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Creates a new aggregate.
|
80
|
+
#
|
81
|
+
# id - the ID to set on the new aggregate.
|
82
|
+
#
|
83
|
+
# Returns an AggregateProxy object.
|
84
|
+
def new(id = EventSorcerer.generate_id)
|
85
|
+
AggregateCreator.new(self, id).create.tap do |aggregate|
|
86
|
+
unit_of_work.store_aggregate(aggregate)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Service class for building new aggregates.
|
3
|
+
class AggregateCreator
|
4
|
+
# Public: Creates a new AggregateCreator instance.
|
5
|
+
#
|
6
|
+
# klass - class for the aggregate to be created.
|
7
|
+
# id - desired id for the aggregate to be created.
|
8
|
+
def initialize(klass, id)
|
9
|
+
@id = id
|
10
|
+
@klass = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Wraps and returns aggregate in a new AggregateProxy.
|
14
|
+
def create
|
15
|
+
AggregateProxy.new(aggregate)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Private: Returns the desired id for the aggregate to be created.
|
21
|
+
attr_reader :id
|
22
|
+
|
23
|
+
# Private: Returns the class for the aggregate to be created.
|
24
|
+
attr_reader :klass
|
25
|
+
|
26
|
+
# Private: Memorizes and returns a new instance of an aggregate.
|
27
|
+
def aggregate
|
28
|
+
@aggregate ||= klass.build.tap do |aggregate|
|
29
|
+
aggregate.instance_variable_set(:@id, id)
|
30
|
+
aggregate.instance_variable_set(:@local_version, 0)
|
31
|
+
aggregate.instance_variable_set(:@persisted_version, 0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Service class for loading aggregates from the event store.
|
3
|
+
class AggregateLoader
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
# Public: Shortcut to access the global event_store.
|
7
|
+
def_delegators :EventSorcerer, :event_store
|
8
|
+
|
9
|
+
# Public: Creates a new AggregateLoader instance.
|
10
|
+
#
|
11
|
+
# klass - class for the aggregate to be loaded.
|
12
|
+
# id - id for the aggregate to be loaded.
|
13
|
+
# prohibit_new - whether or not to raise an error if aggregate not existing.
|
14
|
+
def initialize(klass, id, prohibit_new = true)
|
15
|
+
@id = id
|
16
|
+
@klass = klass
|
17
|
+
@prohibit_new = prohibit_new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Wraps and returns aggregate in a proxy.
|
21
|
+
#
|
22
|
+
# Returns an AggregateProxy.
|
23
|
+
# Raises AggregateNotFound if aggregate not found and prohibit_new is true.
|
24
|
+
def load
|
25
|
+
fail AggregateNotFound if prohibit_new && new_aggregate?
|
26
|
+
|
27
|
+
AggregateProxy.new(aggregate)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Private: Returns the id for the aggregate to be loaded.
|
33
|
+
attr_reader :id
|
34
|
+
|
35
|
+
# Private: Returns the class for the aggregate to be loaded.3
|
36
|
+
attr_reader :klass
|
37
|
+
|
38
|
+
# Private: Returns whether new aggregates will be allowed.
|
39
|
+
attr_reader :prohibit_new
|
40
|
+
|
41
|
+
# Private: Memorizes and returns a new instance of an aggregate. Applies
|
42
|
+
# existing events from the event store.
|
43
|
+
def aggregate
|
44
|
+
@aggregate ||= klass.build.tap do |aggregate|
|
45
|
+
aggregate.instance_variable_set(:@id, id)
|
46
|
+
aggregate.instance_variable_set(:@local_version, version)
|
47
|
+
aggregate.instance_variable_set(:@persisted_version, version)
|
48
|
+
events.each { |event| EventApplicator.apply_event!(aggregate, event) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Private: Memorizes and returns the existing events from the event store.
|
53
|
+
def events
|
54
|
+
@events ||= event_store.read_events(id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def new_aggregate?
|
58
|
+
!version || version == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
# Private: Memorizes and returns the current version number from the event
|
62
|
+
# store.
|
63
|
+
def version
|
64
|
+
@version ||= event_store.get_current_version(id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
class EventArgumentError < RuntimeError; end
|
3
|
+
|
4
|
+
# Public: Transparent wrapped around Aggregate objects which tracks dirty
|
5
|
+
# events and handles persisting these.
|
6
|
+
class AggregateProxy
|
7
|
+
# Public: Value object which is returned from a successful save.
|
8
|
+
class SaveReciept < Struct.new(:id, :klass, :events, :meta); end
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
instance_methods.each do |m|
|
13
|
+
undef_method(m) unless m =~ /(^__|^nil\?$|^send$|^object_id$|^tap$)/
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Shortcuts to access the global event_store and unit_of_work.
|
17
|
+
def_delegators :EventSorcerer, :event_store, :unit_of_work
|
18
|
+
|
19
|
+
# Public: Creates a new AggregateProxy instance.
|
20
|
+
#
|
21
|
+
# aggregate - aggregate to wrap.
|
22
|
+
def initialize(aggregate)
|
23
|
+
@_aggregate = aggregate
|
24
|
+
@_dirty_events = []
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Forwards messages to the aggregate. Saves the details of event
|
28
|
+
# messages to the _dirty_events array.
|
29
|
+
def method_missing(method_sym, *arguments, &block)
|
30
|
+
if event_method?(method_sym)
|
31
|
+
send_event_to_aggregate(method_sym, *arguments, &block)
|
32
|
+
else
|
33
|
+
@_aggregate.send method_sym, *arguments, &block
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Saves the current dirty events via the current unit_of_work.
|
38
|
+
#
|
39
|
+
# meta - Hash of extra data to publish on the message bus with the events
|
40
|
+
# after save.
|
41
|
+
#
|
42
|
+
# Returns self.
|
43
|
+
def save(meta = {})
|
44
|
+
dirty_events = @_dirty_events
|
45
|
+
version = persisted_version
|
46
|
+
|
47
|
+
unit_of_work.handle_save(proc do
|
48
|
+
event_store.append_events(id, self.class.name, dirty_events, version)
|
49
|
+
SaveReciept.new(id, self.class, dirty_events, meta)
|
50
|
+
end)
|
51
|
+
|
52
|
+
@_dirty_events = []
|
53
|
+
@_aggregate.instance_variable_set(:@persisted_version, local_version)
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Private: Handles the serialization of event arguments and pushes the
|
61
|
+
# Event object onto the _dirty_events array. Increments the local
|
62
|
+
# version number for the aggregate.
|
63
|
+
def add_dirty_event!(method_sym, *arguments)
|
64
|
+
increment_version!
|
65
|
+
|
66
|
+
method = @_aggregate.method(method_sym)
|
67
|
+
method.parameters.each.with_index.select { |(type, _), _| type == :req }
|
68
|
+
|
69
|
+
details = ArgumentHashifier.hashify(method.parameters, arguments.dup)
|
70
|
+
@_dirty_events << Event.new(method_sym, Time.now, details)
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Private: Decrements the wrapped aggregates local version by one.
|
76
|
+
def decrement_version!
|
77
|
+
@_aggregate.instance_variable_set(:@local_version, local_version - 1)
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Private: Checks whether the aggregate has an event method defined with
|
83
|
+
# a given name.
|
84
|
+
#
|
85
|
+
# method_sym - A symbol of the method name.
|
86
|
+
#
|
87
|
+
# Returns true if event method exists.
|
88
|
+
# Returns false if event method does not exist.
|
89
|
+
def event_method?(method_sym)
|
90
|
+
@_aggregate.class.event_methods.include? method_sym
|
91
|
+
end
|
92
|
+
|
93
|
+
# Private: Increments the wrapped aggregates local version by one.
|
94
|
+
def increment_version!
|
95
|
+
@_aggregate.instance_variable_set(:@local_version, local_version + 1)
|
96
|
+
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Private: Forwards an event message to the aggregate while adding the
|
101
|
+
# Event to the _dirty_events and incrementing the local version.
|
102
|
+
# Rolls everything back if a StandardError is caught.
|
103
|
+
def send_event_to_aggregate(method_sym, *arguments, &block)
|
104
|
+
fail EventArgumentError if block
|
105
|
+
|
106
|
+
add_dirty_event!(method_sym, *arguments)
|
107
|
+
|
108
|
+
@_aggregate.send method_sym, *arguments
|
109
|
+
rescue StandardError => e
|
110
|
+
undo_dirty_event!
|
111
|
+
raise e
|
112
|
+
end
|
113
|
+
|
114
|
+
# Private: Undoes the side-effects of the last event message.
|
115
|
+
def undo_dirty_event!
|
116
|
+
decrement_version!
|
117
|
+
@_dirty_events.pop
|
118
|
+
|
119
|
+
self
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Helper to hashify a method call.
|
3
|
+
module ArgumentHashifier
|
4
|
+
# Public: Creates a Hash representing a particular method call.
|
5
|
+
#
|
6
|
+
# parameters - an Array of the parameters for a method.
|
7
|
+
# arguments - an Array of the arguments for a particular method call.
|
8
|
+
#
|
9
|
+
# Returns a Hash where arguments are keyed by the corrosponding parameter
|
10
|
+
# name.
|
11
|
+
def self.hashify(parameters, arguments)
|
12
|
+
{}.tap do |hash|
|
13
|
+
required_positionals(parameters).each do |param|
|
14
|
+
hash[param] = arguments.shift
|
15
|
+
end
|
16
|
+
|
17
|
+
keyword_arguments = arguments.last.is_a?(Hash) ? arguments.pop : {}
|
18
|
+
hash.merge! keyword_arguments
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Private: Returns an Array of the required positional parameter names for
|
25
|
+
# a given set of parameters.
|
26
|
+
def self.required_positionals(parameters)
|
27
|
+
parameters.select { |type, _| type == :req }.map(&:last)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'invokr'
|
2
|
+
require 'timecop'
|
3
|
+
|
4
|
+
module EventSorcerer
|
5
|
+
# Public: Helper for applying events to an aggregate.
|
6
|
+
module EventApplicator
|
7
|
+
# Public: Sets the clock to the event time and calls the event method on
|
8
|
+
# the aggregate.
|
9
|
+
#
|
10
|
+
# aggregate - The Aggregate to apply the Event to.
|
11
|
+
# event - The Event to be applied.
|
12
|
+
#
|
13
|
+
# Returns self.
|
14
|
+
def self.apply_event!(aggregate, event)
|
15
|
+
Timecop.freeze(event.created_at) do
|
16
|
+
Invokr.invoke method: event.name, on: aggregate, with: event.details
|
17
|
+
end
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Abstract class for event store errors.
|
3
|
+
class EventStoreError < StandardError; end
|
4
|
+
|
5
|
+
# Public: Error raised when given uuid is in invalid format.
|
6
|
+
class InvalidUUID < EventStoreError; end
|
7
|
+
|
8
|
+
# Public: Error raised when expected version doesn't match current stored
|
9
|
+
# version.
|
10
|
+
class UnexpectedVersionNumber < EventStoreError; end
|
11
|
+
|
12
|
+
# Public: Abstract class for an event store implementation.
|
13
|
+
class EventStore
|
14
|
+
# Public: Append events to a specified aggregate. Should be defined in a
|
15
|
+
# subclass.
|
16
|
+
#
|
17
|
+
# _aggregate_id - UUID of the aggregate as a String.
|
18
|
+
# _klass - Text representation of aggregate class.
|
19
|
+
# _events - Array of JSON-serialized events.
|
20
|
+
# _expected_version - The current version of the aggregate.
|
21
|
+
#
|
22
|
+
# Raises a NotImplementedError
|
23
|
+
def append_events(_aggregate_id, _klass, _events, _expected_version)
|
24
|
+
fail NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Retrieve the current version for a specified aggregate. Should
|
28
|
+
# be defined in a subclass.
|
29
|
+
#
|
30
|
+
# _aggregate_id - UUID of the aggregate as a String.
|
31
|
+
#
|
32
|
+
# Raises a NotImplementedError
|
33
|
+
def get_current_version(_aggregate_id)
|
34
|
+
fail NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Retrieve the events for a specified aggregate. Should be
|
38
|
+
# defined in a subclass.
|
39
|
+
#
|
40
|
+
# _aggregate_id - UUID of the aggregate as a String.
|
41
|
+
#
|
42
|
+
# Raises a NotImplementedError
|
43
|
+
def read_events(_aggregate_id)
|
44
|
+
fail NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Ensures events appended within the given block are done so
|
48
|
+
# atomically. Should be defined in a subclass.
|
49
|
+
#
|
50
|
+
# Raises a NotImplementedError
|
51
|
+
def with_transaction
|
52
|
+
fail NotImplementedError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Abstract class for an message bus implementation.
|
3
|
+
class MessageBus
|
4
|
+
# Public: Publish events for a specified aggregate. Should be defined in a
|
5
|
+
# subclass.
|
6
|
+
#
|
7
|
+
# _aggregate_id - UUID of the aggregate as a String.
|
8
|
+
# _klass - Text representation of aggregate class.
|
9
|
+
# _events - Array of Event objects.
|
10
|
+
# _meta - Hash of extra data to publish on the bus.
|
11
|
+
#
|
12
|
+
# Raises a NotImplementedError
|
13
|
+
def publish_events(_aggregate_id, _klass, _events, _meta)
|
14
|
+
fail NotImplementedError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Handles the API of a UnitOfWork but does nothing.
|
3
|
+
module NoUnitOfWork
|
4
|
+
class << self
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
# Public: Shortcuts to access the global message_bus.
|
8
|
+
def_delegators :EventSorcerer, :message_bus
|
9
|
+
|
10
|
+
# Public: Returns nil.
|
11
|
+
def fetch_aggregate(_id)
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Immediately calls the save proc and publishes the reciepts via
|
16
|
+
# the message bus.
|
17
|
+
#
|
18
|
+
# save - the Proc object representing the save process.
|
19
|
+
#
|
20
|
+
# Returns self.
|
21
|
+
def handle_save(save)
|
22
|
+
reciept = save.call
|
23
|
+
message_bus.publish_events(reciept.id, reciept.klass, reciept.events,
|
24
|
+
reciept.meta)
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Returns self.
|
30
|
+
def store_aggregate(_aggregate)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module EventSorcerer
|
2
|
+
# Public: Provides transactional integrity across multiple aggregate saves.
|
3
|
+
# Also provides an indentity map.
|
4
|
+
class UnitOfWork
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
# Public: Shortcuts to access the global event_store and message_bus.
|
8
|
+
def_delegators :EventSorcerer, :event_store, :message_bus
|
9
|
+
|
10
|
+
# Public: Creates a new UnitOfWork instance.
|
11
|
+
def initialize
|
12
|
+
@identity_map = {}
|
13
|
+
@pending_saves = []
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Executes all pending saves within a transaction, clears the
|
17
|
+
# pending saves, and publishes the reciepts via the message bus.
|
18
|
+
#
|
19
|
+
# Returns self.
|
20
|
+
def execute_work!
|
21
|
+
save_receipts = event_store.with_transaction do
|
22
|
+
pending_saves.map(&:call)
|
23
|
+
end
|
24
|
+
|
25
|
+
@pending_saves = []
|
26
|
+
|
27
|
+
save_receipts.each do |reciept|
|
28
|
+
message_bus.publish_events(reciept.id, reciept.klass, reciept.events,
|
29
|
+
reciept.meta)
|
30
|
+
end
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Fetches an aggregate via it's ID from the identity map.
|
36
|
+
#
|
37
|
+
# id - the ID for the aggregate.
|
38
|
+
#
|
39
|
+
# Returns nil if not found.
|
40
|
+
# Returns Aggregate if found.
|
41
|
+
def fetch_aggregate(id)
|
42
|
+
identity_map[id]
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_save(save)
|
46
|
+
pending_saves << save
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Stores an aggregate via it's ID into the identity map.
|
50
|
+
#
|
51
|
+
# aggregate - the aggregate to store.
|
52
|
+
#
|
53
|
+
# Returns self.
|
54
|
+
def store_aggregate(aggregate)
|
55
|
+
return self if fetch_aggregate(aggregate.id)
|
56
|
+
|
57
|
+
identity_map[aggregate.id] = aggregate
|
58
|
+
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Private: Returns the identity map Hash.
|
65
|
+
attr_reader :identity_map
|
66
|
+
|
67
|
+
# Private: Returns the pending saves Array.
|
68
|
+
attr_reader :pending_saves
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'event_sorcerer/version'
|
2
|
+
|
3
|
+
require 'event_sorcerer/aggregate'
|
4
|
+
require 'event_sorcerer/aggregate_creator'
|
5
|
+
require 'event_sorcerer/aggregate_loader'
|
6
|
+
require 'event_sorcerer/aggregate_proxy'
|
7
|
+
require 'event_sorcerer/argument_hashifier'
|
8
|
+
require 'event_sorcerer/event'
|
9
|
+
require 'event_sorcerer/event_applicator'
|
10
|
+
require 'event_sorcerer/event_store'
|
11
|
+
require 'event_sorcerer/message_bus'
|
12
|
+
require 'event_sorcerer/no_unit_of_work'
|
13
|
+
require 'event_sorcerer/unit_of_work'
|
14
|
+
|
15
|
+
# Public: Top-level namespace.
|
16
|
+
module EventSorcerer
|
17
|
+
class AggregateNotFound < RuntimeError; end
|
18
|
+
class UnsetEventStore < RuntimeError; end
|
19
|
+
class UnsetMessageBus < RuntimeError; end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# Public: Writer method to set new event_store.
|
23
|
+
attr_writer :event_store
|
24
|
+
|
25
|
+
# Public: Writer method to set a new id_generation function.
|
26
|
+
attr_writer :id_generator
|
27
|
+
|
28
|
+
# Public: Writer method to set new message_bus.
|
29
|
+
attr_writer :message_bus
|
30
|
+
|
31
|
+
# Public: Returns the current event store. Raises UnsetEventStore if not
|
32
|
+
# set.
|
33
|
+
def event_store
|
34
|
+
@event_store || fail(UnsetEventStore)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Generates a new ID using the current id_generator.
|
38
|
+
def generate_id
|
39
|
+
id_generator.call
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Returns the current id_generator. Defaults to a SecureRandom.uuid
|
43
|
+
# based generator.
|
44
|
+
def id_generator
|
45
|
+
@id_generator ||= proc { SecureRandom.uuid }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Returns the current message bus. Raises UnsetMessageBus if not
|
49
|
+
# set.
|
50
|
+
def message_bus
|
51
|
+
@message_bus || fail(UnsetMessageBus)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Public: Returns the unit_of_work for the current thread. Defaults to
|
55
|
+
# NoUnitOfWork.
|
56
|
+
def unit_of_work
|
57
|
+
Thread.current[:unit_of_work] || NoUnitOfWork
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Creates a new UnitOfWork and sets it for the current thread
|
61
|
+
# within the block. Executes the work after block completion.
|
62
|
+
#
|
63
|
+
# Returns value of block.
|
64
|
+
def with_unit_of_work(unit_of_work = UnitOfWork.new, autosave = true)
|
65
|
+
old_unit_of_work = Thread.current[:unit_of_work]
|
66
|
+
new_unit_of_work = Thread.current[:unit_of_work] = unit_of_work
|
67
|
+
begin
|
68
|
+
result = yield
|
69
|
+
new_unit_of_work.execute_work! if autosave
|
70
|
+
ensure
|
71
|
+
Thread.current[:unit_of_work] = old_unit_of_work
|
72
|
+
end
|
73
|
+
|
74
|
+
result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'event_sorcerer'
|
2
|
+
|
3
|
+
describe EventSorcerer do
|
4
|
+
describe '.with_unit_of_work' do
|
5
|
+
it 'puts a new unit of work in the thread variables' do
|
6
|
+
EventSorcerer.with_unit_of_work do
|
7
|
+
expect(Thread.current[:unit_of_work]).to be_truthy
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: event_sorcerer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Edwards
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-29 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: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: inch
|
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: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.0.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
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: rake
|
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: invokr
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.0.5
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.0.5
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: timecop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.7.1
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.7.1
|
111
|
+
description: "[\"Generic\", \"event-sourcing\", \"scaffold\"]"
|
112
|
+
email:
|
113
|
+
- me@sebastianedwards.co.nz
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- event_sorcerer.gemspec
|
124
|
+
- lib/event_sorcerer.rb
|
125
|
+
- lib/event_sorcerer/aggregate.rb
|
126
|
+
- lib/event_sorcerer/aggregate_creator.rb
|
127
|
+
- lib/event_sorcerer/aggregate_loader.rb
|
128
|
+
- lib/event_sorcerer/aggregate_proxy.rb
|
129
|
+
- lib/event_sorcerer/argument_hashifier.rb
|
130
|
+
- lib/event_sorcerer/event.rb
|
131
|
+
- lib/event_sorcerer/event_applicator.rb
|
132
|
+
- lib/event_sorcerer/event_store.rb
|
133
|
+
- lib/event_sorcerer/message_bus.rb
|
134
|
+
- lib/event_sorcerer/no_unit_of_work.rb
|
135
|
+
- lib/event_sorcerer/unit_of_work.rb
|
136
|
+
- lib/event_sorcerer/version.rb
|
137
|
+
- spec/lib/event_sorcerer_spec.rb
|
138
|
+
homepage: https://github.com/SebastianEdwards/event_sorcerer
|
139
|
+
licenses: []
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.2.0
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: "[\"Generic\", \"event-sourcing\", \"scaffold\"]"
|
161
|
+
test_files:
|
162
|
+
- spec/lib/event_sorcerer_spec.rb
|
163
|
+
has_rdoc:
|