event_sorcerer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: