event_sorcerer 0.0.1

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 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
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .ruby-gemset
6
+ .ruby-version
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in event_sorcerer.gemspec
4
+ gemspec
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,5 @@
1
+ module EventSorcerer
2
+ # Public: Simple value object representing an event.
3
+ class Event < Struct.new(:name, :created_at, :details)
4
+ end
5
+ 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,4 @@
1
+ # Public: Defines a constant for the current version number.
2
+ module EventSorcerer
3
+ VERSION = '0.0.1'
4
+ 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: