event_sourcing 0.0.2

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