event_sourcing 0.0.2

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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +7 -0
  5. data/Guardfile +12 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +13 -0
  9. data/event_sourcing.gemspec +28 -0
  10. data/features/steps/whole_stack.rb +29 -0
  11. data/features/support/env.rb +5 -0
  12. data/features/support/logger.log +1 -0
  13. data/features/support/sample_app.rb +40 -0
  14. data/features/support/spinach.log +0 -0
  15. data/features/whole_stack.feature +6 -0
  16. data/lib/event_sourcing.rb +3 -0
  17. data/lib/event_sourcing/aggregate.rb +30 -0
  18. data/lib/event_sourcing/aggregate/actor.rb +29 -0
  19. data/lib/event_sourcing/aggregate/manager.rb +33 -0
  20. data/lib/event_sourcing/aggregate/manager/cache.rb +17 -0
  21. data/lib/event_sourcing/aggregate/manager/instance_of.rb +11 -0
  22. data/lib/event_sourcing/aggregate/manager/reference.rb +14 -0
  23. data/lib/event_sourcing/aggregate/message.rb +7 -0
  24. data/lib/event_sourcing/aggregate/wrapper.rb +18 -0
  25. data/lib/event_sourcing/application.rb +37 -0
  26. data/lib/event_sourcing/application/actor.rb +40 -0
  27. data/lib/event_sourcing/application/actor/reference.rb +23 -0
  28. data/lib/event_sourcing/command.rb +30 -0
  29. data/lib/event_sourcing/command/bus.rb +16 -0
  30. data/lib/event_sourcing/event.rb +29 -0
  31. data/lib/event_sourcing/event/bus.rb +29 -0
  32. data/lib/event_sourcing/event/bus/reference.rb +28 -0
  33. data/lib/event_sourcing/event/publisher.rb +47 -0
  34. data/lib/event_sourcing/event/publisher/reference.rb +11 -0
  35. data/lib/event_sourcing/event/store.rb +8 -0
  36. data/lib/event_sourcing/event/store/memory.rb +43 -0
  37. data/lib/event_sourcing/event/stream.rb +23 -0
  38. data/lib/event_sourcing/event/subscriber.rb +16 -0
  39. data/lib/event_sourcing/version.rb +3 -0
  40. data/spec/concurrent_logging.rb +6 -0
  41. data/spec/spec_helper.rb +8 -0
  42. data/spec/support/actor_helpers.rb +11 -0
  43. data/spec/support/shared_examples/a_store_implementation.rb +70 -0
  44. data/spec/unit/aggregate/actor_spec.rb +59 -0
  45. data/spec/unit/aggregate/manager/cache_spec.rb +26 -0
  46. data/spec/unit/aggregate/manager/reference_spec.rb +14 -0
  47. data/spec/unit/aggregate/manager_spec.rb +32 -0
  48. data/spec/unit/aggregate/wrapper_spec.rb +22 -0
  49. data/spec/unit/aggregate_spec.rb +75 -0
  50. data/spec/unit/application/actor/reference_spec.rb +25 -0
  51. data/spec/unit/application/actor_spec.rb +36 -0
  52. data/spec/unit/application_spec.rb +41 -0
  53. data/spec/unit/command/bus_spec.rb +15 -0
  54. data/spec/unit/command_spec.rb +38 -0
  55. data/spec/unit/event/bus/reference_spec.rb +48 -0
  56. data/spec/unit/event/bus_spec.rb +41 -0
  57. data/spec/unit/event/publisher_spec.rb +28 -0
  58. data/spec/unit/event/store/memory_spec.rb +6 -0
  59. data/spec/unit/event/stream_spec.rb +30 -0
  60. data/spec/unit/event/subscriber_spec.rb +27 -0
  61. data/spec/unit/event_spec.rb +27 -0
  62. data/spec/unit_helper.rb +14 -0
  63. metadata +233 -0
@@ -0,0 +1,40 @@
1
+ require "concurrent/actor"
2
+ require "event_sourcing/aggregate/manager"
3
+ require "event_sourcing/command/bus"
4
+ require "event_sourcing/event/bus"
5
+
6
+ module EventSourcing
7
+ module Application
8
+ class Actor < Concurrent::Actor::RestartingContext
9
+ require "event_sourcing/application/actor/reference"
10
+
11
+ def self.to_str #TODO Remove this. It's needed for specs passing under jruby O_o
12
+ to_s
13
+ end
14
+
15
+ def default_reference_class
16
+ Reference
17
+ end
18
+
19
+ def initialize(event_store)
20
+ @event_store = event_store
21
+ @event_bus = EventSourcing::Event::Bus.spawn!(name: :event_bus, supervise: true, args: [@event_store])
22
+ @aggregate_manager = EventSourcing::Aggregate::Manager.spawn!(name: :aggregate_manager, supervise: true, args: [@event_bus])
23
+ @command_bus = EventSourcing::Command::Bus.spawn!(name: :command_bus, supervise: true, args: [@aggregate_manager])
24
+ end
25
+
26
+ def on_message(message)
27
+ case message
28
+ when :get_command_bus
29
+ @command_bus
30
+ when :get_event_bus
31
+ @event_bus
32
+ when :get_aggregate_manager
33
+ @aggregate_manager
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ Concurrent::Actor.i_know_it_is_experimental!
@@ -0,0 +1,23 @@
1
+ require "concurrent/actor"
2
+ require "event_sourcing/application/actor"
3
+
4
+ module EventSourcing
5
+ module Application
6
+ class Actor
7
+ class Reference < Concurrent::Actor::Reference
8
+ def execute_command(command)
9
+ command_bus.tell(command)
10
+ end
11
+
12
+ def terminate!
13
+ tell(:terminate!)
14
+ end
15
+
16
+ private
17
+ def command_bus
18
+ @command_bus ||= ask!(:get_command_bus)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ module EventSourcing
2
+ class Command
3
+ private_class_method :new
4
+
5
+ def self.define(*fields, &block)
6
+ raise "Commands require an execution block" unless block_given?
7
+
8
+ Class.new(self) do
9
+ public_class_method :new
10
+ attr_reader(*fields)
11
+
12
+ define_method :initialize do |properties = {}|
13
+ missing_keys = fields - properties.keys
14
+
15
+ if missing_keys.any?
16
+ raise ArgumentError, "missing keyword: #{missing_keys.first}"
17
+ end
18
+
19
+ fields.each do |field|
20
+ instance_variable_set("@#{field}", properties[field])
21
+ end
22
+ end
23
+
24
+ define_method :execute do |*args|
25
+ instance_exec(*args, &block)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ require "concurrent/actor"
2
+
3
+ module EventSourcing
4
+ class Command
5
+ class Bus < Concurrent::Actor::RestartingContext
6
+
7
+ def initialize(aggregate_manager)
8
+ @aggregate_manager = aggregate_manager
9
+ end
10
+
11
+ def on_message(command)
12
+ command.execute(@aggregate_manager)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ module EventSourcing
2
+ class Event
3
+
4
+ private_class_method :new
5
+
6
+ def self.define(*fields)
7
+ Class.new(self) do
8
+ attr_reader(*fields)
9
+ public_class_method :new
10
+
11
+ define_method :initialize do |properties = {}|
12
+ missing_keys = fields - properties.keys
13
+
14
+ if missing_keys.any?
15
+ raise ArgumentError, "missing keyword: #{missing_keys.first}"
16
+ end
17
+
18
+ fields.each do |field|
19
+ instance_variable_set("@#{field}", properties[field])
20
+ end
21
+ end
22
+
23
+ def to_s
24
+ self.class.to_s
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ require "concurrent/actor"
2
+ require "event_sourcing/event/publisher"
3
+
4
+ module EventSourcing
5
+ class Event
6
+ class Bus < Concurrent::Actor::RestartingContext
7
+
8
+ require "event_sourcing/event/bus/reference"
9
+
10
+ def initialize(event_store)
11
+ @store = event_store
12
+ @publisher = Publisher.spawn!(name: :event_publisher, supervise: true)
13
+ end
14
+
15
+ def on_message(message)
16
+ case message
17
+ when :get_event_publisher
18
+ @publisher
19
+ when :get_event_store
20
+ @store
21
+ end
22
+ end
23
+
24
+ def default_reference_class
25
+ Reference
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require "concurrent/actor"
2
+ require "event_sourcing/event/bus"
3
+
4
+ module EventSourcing
5
+ class Event
6
+ class Bus
7
+ class Reference < Concurrent::Actor::Reference
8
+
9
+ def publish(events)
10
+ publisher.publish(events)
11
+ end
12
+
13
+ def get_stream(id)
14
+ store.get_stream(id)
15
+ end
16
+
17
+ private
18
+ def publisher
19
+ @publisher ||= ask!(:get_event_publisher)
20
+ end
21
+
22
+ def store
23
+ @store ||= ask!(:get_event_store)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ require "concurrent/actor"
2
+
3
+ module EventSourcing
4
+ class Event
5
+ class Publisher < Concurrent::Actor::RestartingContext
6
+
7
+ require "event_sourcing/event/publisher/reference"
8
+
9
+ @subscribers = {}
10
+
11
+ class << self
12
+ attr_reader :subscribers
13
+
14
+ def subscribe(klass, event)
15
+ @subscribers[event.to_s] ||= []
16
+ @subscribers[event.to_s] << klass
17
+ end
18
+ end
19
+
20
+ def initialize
21
+ @subscribed_actors = {}
22
+ self.class.subscribers.each do |event,subscribers|
23
+ @subscribed_actors[event] = subscribers.map { |s| actor_for(s) }
24
+ end
25
+ end
26
+
27
+ def on_message(event)
28
+ subscribers_for(event).each do |subscriber|
29
+ subscriber.tell(event) # TODO: Add support for some kind of ACK + recovery
30
+ end
31
+ end
32
+
33
+ def default_reference_class
34
+ Reference
35
+ end
36
+
37
+ private
38
+ def subscribers_for(event)
39
+ @subscribed_actors[event.to_s] || []
40
+ end
41
+
42
+ def actor_for(subscriber)
43
+ @actors ||= subscriber.spawn!(name: subscriber.to_s, supervise: true)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,11 @@
1
+ module EventSourcing
2
+ class Event
3
+ class Publisher
4
+ class Reference < Concurrent::Actor::Reference
5
+ def publish(event)
6
+ tell(event)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module EventSourcing
2
+ class Event
3
+ module Store
4
+ class ConcurrencyError < RuntimeError
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,43 @@
1
+ require "event_sourcing/event"
2
+ require "event_sourcing/event/store"
3
+ require "event_sourcing/event/stream"
4
+
5
+ module EventSourcing
6
+ class Event
7
+ module Store
8
+ class Memory
9
+ def initialize
10
+ @events_with_stream_id = []
11
+ end
12
+
13
+ def events
14
+ @events_with_stream_id.map { |e| e[:event] }
15
+ end
16
+
17
+ def get_stream(id)
18
+ events = events_for(id)
19
+ EventSourcing::Event::Stream.new(id, events, events.count, self)
20
+ end
21
+
22
+ def append(stream_id, expected_version, events)
23
+ raise EventSourcing::Event::Store::ConcurrencyError if get_stream(stream_id).version != expected_version
24
+
25
+ Array(events).tap do |events|
26
+ events.each do |event|
27
+ @events_with_stream_id.push(stream_id: stream_id, event: event)
28
+ end
29
+ end
30
+ nil
31
+ end
32
+
33
+ alias_method :<<, :append
34
+ alias_method :push, :append
35
+
36
+ protected
37
+ def events_for(stream_id)
38
+ @events_with_stream_id.select { |event| event[:stream_id] == stream_id }.map { |event| event[:event] }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module EventSourcing
2
+ class Event
3
+ class Stream
4
+ attr_reader :version
5
+ include Enumerable
6
+
7
+ def initialize(id, events, version, store)
8
+ @id = id
9
+ @events = events
10
+ @version = version
11
+ @store = store
12
+ end
13
+
14
+ def each(&block)
15
+ @events.each(&block)
16
+ end
17
+
18
+ def append(events)
19
+ @store.append(@id, @version, events)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require "event_sourcing/event/publisher"
2
+
3
+ module EventSourcing
4
+ class Event
5
+ class Subscriber < Concurrent::Actor::RestartingContext #TODO: Should be a plain Context?
6
+ def self.subscribe_to(event, &block)
7
+ define_method "handle_#{event}", &block
8
+ Publisher.subscribe(self, event)
9
+ end
10
+
11
+ def on_message(event)
12
+ send("handle_#{event}", event)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module EventSourcing
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "concurrent"
2
+ require "logger"
3
+
4
+ Concurrent.configuration.logger = lambda do |level, progname, message = nil, &block|
5
+ Logger.new(File.dirname(__FILE__) + '/../log/test.log').add Logger::INFO, message, progname, &block
6
+ end
@@ -0,0 +1,8 @@
1
+ require "unit_helper"
2
+
3
+ RSpec.configure do |config|
4
+ config.mock_with :rspec do |mocks|
5
+ mocks.verify_doubled_constant_names = true
6
+ mocks.verify_partial_doubles = true
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module ActorHelpers
2
+ def actor_reference(ref_class)
3
+ ref_class.new(instance_double("Concurrent::Actor::Core", is_a?: true, path: "Path for #{ref_class} core", context_class: ref_class))
4
+ end
5
+
6
+ def event_double(name)
7
+ instance_double("EventSourcing::Event", to_s: name)
8
+ end
9
+ end
10
+
11
+ RSpec.configuration.include ActorHelpers
@@ -0,0 +1,70 @@
1
+ require "event_sourcing/event"
2
+
3
+ RSpec.shared_examples "a store implementation" do
4
+ let(:event) { double("Some event") }
5
+ let(:another_event) { double("Anothe event") }
6
+
7
+ it "is empty when created" do
8
+ expect(subject.events).to be_empty
9
+ end
10
+
11
+ context "append" do
12
+ it "returns nil, since it's a command" do
13
+ expect(subject.append("stream-id", 0, event)).to be_nil
14
+ end
15
+
16
+ it "does nothing if no events are passed to it" do
17
+ expect { subject.append("stream-id", 0, []) }.not_to change { subject.events.count }
18
+ end
19
+ end
20
+
21
+ context "when events have been appended" do
22
+ before do
23
+ subject.append("stream-id", 0, event)
24
+ end
25
+
26
+ it "they can be retrieved later" do
27
+ expect(subject.events).to eq([event])
28
+ end
29
+
30
+ it "they can be retrieved by stream" do
31
+ expect(subject.get_stream("stream-id").to_a).to eq([event])
32
+ end
33
+ end
34
+
35
+ context "when multiple events are appended" do
36
+ let(:further_event) { double("Further event")}
37
+
38
+ before do
39
+ subject.append("stream-id", 0, [event, another_event])
40
+ subject.append("stream-id", 2, [further_event])
41
+ end
42
+
43
+ it "they can be retrieved later" do
44
+ expect(subject.events).to eq([event, another_event, further_event])
45
+ end
46
+
47
+ it "they can be retrieved by stream" do
48
+ expect(subject.get_stream("stream-id").to_a).to eq([event, another_event, further_event])
49
+ end
50
+ end
51
+
52
+ it "locks optimistically per stream" do
53
+ expect {
54
+ subject.append("stream-id", 0, event)
55
+ subject.append("stream-id", 0, another_event)
56
+ }.to raise_error(EventSourcing::Event::Store::ConcurrencyError)
57
+ end
58
+
59
+ context "with multiple streams" do
60
+ before do
61
+ subject.append("stream-id", 0, event)
62
+ subject.append("another-stream-id", 0, [event, another_event])
63
+ end
64
+
65
+ it "allows retrieval of events by stream id" do
66
+ expect(subject.get_stream("stream-id").version).to eq(1)
67
+ expect(subject.get_stream("another-stream-id").version).to eq(2)
68
+ end
69
+ end
70
+ end