synapse-core 0.1.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 (155) hide show
  1. data/lib/synapse.rb +351 -0
  2. data/lib/synapse/command/command_bus.rb +45 -0
  3. data/lib/synapse/command/command_callback.rb +18 -0
  4. data/lib/synapse/command/command_filter.rb +17 -0
  5. data/lib/synapse/command/command_handler.rb +13 -0
  6. data/lib/synapse/command/dispatch_interceptor.rb +16 -0
  7. data/lib/synapse/command/duplication.rb +43 -0
  8. data/lib/synapse/command/errors.rb +27 -0
  9. data/lib/synapse/command/filters/validation.rb +32 -0
  10. data/lib/synapse/command/gateway.rb +34 -0
  11. data/lib/synapse/command/interceptor_chain.rb +31 -0
  12. data/lib/synapse/command/interceptors/serialization.rb +35 -0
  13. data/lib/synapse/command/message.rb +19 -0
  14. data/lib/synapse/command/rollback_policy.rb +22 -0
  15. data/lib/synapse/command/simple_command_bus.rb +138 -0
  16. data/lib/synapse/command/wiring.rb +47 -0
  17. data/lib/synapse/domain/aggregate_root.rb +121 -0
  18. data/lib/synapse/domain/event_container.rb +127 -0
  19. data/lib/synapse/domain/message.rb +82 -0
  20. data/lib/synapse/domain/message_builder.rb +34 -0
  21. data/lib/synapse/domain/stream.rb +108 -0
  22. data/lib/synapse/duplication.rb +60 -0
  23. data/lib/synapse/errors.rb +13 -0
  24. data/lib/synapse/event_bus/event_bus.rb +40 -0
  25. data/lib/synapse/event_bus/event_listener.rb +16 -0
  26. data/lib/synapse/event_bus/event_listener_proxy.rb +12 -0
  27. data/lib/synapse/event_bus/simple_event_bus.rb +69 -0
  28. data/lib/synapse/event_bus/wiring.rb +23 -0
  29. data/lib/synapse/event_sourcing/aggregate_factory.rb +69 -0
  30. data/lib/synapse/event_sourcing/aggregate_root.rb +104 -0
  31. data/lib/synapse/event_sourcing/conflict_resolver.rb +80 -0
  32. data/lib/synapse/event_sourcing/entity.rb +64 -0
  33. data/lib/synapse/event_sourcing/member.rb +72 -0
  34. data/lib/synapse/event_sourcing/repository.rb +119 -0
  35. data/lib/synapse/event_sourcing/snapshot/count_stream.rb +86 -0
  36. data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +91 -0
  37. data/lib/synapse/event_sourcing/snapshot/taker.rb +73 -0
  38. data/lib/synapse/event_sourcing/storage_listener.rb +34 -0
  39. data/lib/synapse/event_sourcing/stream_decorator.rb +25 -0
  40. data/lib/synapse/event_store/errors.rb +16 -0
  41. data/lib/synapse/event_store/event_store.rb +43 -0
  42. data/lib/synapse/event_store/in_memory.rb +59 -0
  43. data/lib/synapse/event_store/mongo/cursor_event_stream.rb +63 -0
  44. data/lib/synapse/event_store/mongo/event_store.rb +86 -0
  45. data/lib/synapse/event_store/mongo/per_commit_strategy.rb +253 -0
  46. data/lib/synapse/event_store/mongo/per_event_strategy.rb +143 -0
  47. data/lib/synapse/event_store/mongo/storage_strategy.rb +113 -0
  48. data/lib/synapse/event_store/mongo/template.rb +73 -0
  49. data/lib/synapse/identifier.rb +23 -0
  50. data/lib/synapse/message.rb +101 -0
  51. data/lib/synapse/message_builder.rb +38 -0
  52. data/lib/synapse/process_manager/correlation.rb +32 -0
  53. data/lib/synapse/process_manager/correlation_resolver.rb +14 -0
  54. data/lib/synapse/process_manager/correlation_set.rb +58 -0
  55. data/lib/synapse/process_manager/process.rb +71 -0
  56. data/lib/synapse/repository/errors.rb +26 -0
  57. data/lib/synapse/repository/lock_manager.rb +40 -0
  58. data/lib/synapse/repository/locking.rb +97 -0
  59. data/lib/synapse/repository/pessimistic_lock_manager.rb +61 -0
  60. data/lib/synapse/repository/repository.rb +109 -0
  61. data/lib/synapse/serialization/converter.rb +39 -0
  62. data/lib/synapse/serialization/converter/chain.rb +45 -0
  63. data/lib/synapse/serialization/converter/factory.rb +68 -0
  64. data/lib/synapse/serialization/converter/identity.rb +29 -0
  65. data/lib/synapse/serialization/converter/json.rb +31 -0
  66. data/lib/synapse/serialization/converter/ox.rb +31 -0
  67. data/lib/synapse/serialization/errors.rb +12 -0
  68. data/lib/synapse/serialization/lazy_object.rb +61 -0
  69. data/lib/synapse/serialization/message/data.rb +25 -0
  70. data/lib/synapse/serialization/message/metadata.rb +13 -0
  71. data/lib/synapse/serialization/message/serialization_aware.rb +17 -0
  72. data/lib/synapse/serialization/message/serialization_aware_message.rb +66 -0
  73. data/lib/synapse/serialization/message/serialized_message.rb +201 -0
  74. data/lib/synapse/serialization/message/serialized_message_builder.rb +64 -0
  75. data/lib/synapse/serialization/message/serialized_object_cache.rb +50 -0
  76. data/lib/synapse/serialization/message/serializer.rb +47 -0
  77. data/lib/synapse/serialization/revision_resolver.rb +30 -0
  78. data/lib/synapse/serialization/serialized_object.rb +37 -0
  79. data/lib/synapse/serialization/serialized_type.rb +31 -0
  80. data/lib/synapse/serialization/serializer.rb +98 -0
  81. data/lib/synapse/serialization/serializer/marshal.rb +32 -0
  82. data/lib/synapse/serialization/serializer/oj.rb +34 -0
  83. data/lib/synapse/serialization/serializer/ox.rb +31 -0
  84. data/lib/synapse/uow/factory.rb +28 -0
  85. data/lib/synapse/uow/listener.rb +79 -0
  86. data/lib/synapse/uow/listener_collection.rb +93 -0
  87. data/lib/synapse/uow/nesting.rb +262 -0
  88. data/lib/synapse/uow/provider.rb +71 -0
  89. data/lib/synapse/uow/storage_listener.rb +14 -0
  90. data/lib/synapse/uow/transaction_manager.rb +27 -0
  91. data/lib/synapse/uow/uow.rb +178 -0
  92. data/lib/synapse/upcasting/chain.rb +78 -0
  93. data/lib/synapse/upcasting/context.rb +58 -0
  94. data/lib/synapse/upcasting/data.rb +30 -0
  95. data/lib/synapse/upcasting/single_upcaster.rb +57 -0
  96. data/lib/synapse/upcasting/upcaster.rb +55 -0
  97. data/lib/synapse/version.rb +3 -0
  98. data/lib/synapse/wiring/message_wiring.rb +41 -0
  99. data/lib/synapse/wiring/wire.rb +55 -0
  100. data/lib/synapse/wiring/wire_registry.rb +61 -0
  101. data/test/command/duplication_test.rb +54 -0
  102. data/test/command/gateway_test.rb +25 -0
  103. data/test/command/interceptor_chain_test.rb +26 -0
  104. data/test/command/serialization_test.rb +37 -0
  105. data/test/command/simple_command_bus_test.rb +141 -0
  106. data/test/command/validation_test.rb +42 -0
  107. data/test/command/wiring_test.rb +73 -0
  108. data/test/domain/aggregate_root_test.rb +57 -0
  109. data/test/domain/fixtures.rb +31 -0
  110. data/test/domain/message_test.rb +61 -0
  111. data/test/domain/stream_test.rb +35 -0
  112. data/test/duplication_test.rb +40 -0
  113. data/test/event_bus/wiring_test.rb +46 -0
  114. data/test/event_sourcing/aggregate_factory_test.rb +28 -0
  115. data/test/event_sourcing/aggregate_root_test.rb +76 -0
  116. data/test/event_sourcing/entity_test.rb +34 -0
  117. data/test/event_sourcing/fixtures.rb +85 -0
  118. data/test/event_sourcing/repository_test.rb +102 -0
  119. data/test/event_sourcing/snapshot/aggregate_taker_test.rb +39 -0
  120. data/test/event_sourcing/snapshot/deferred_taker_test.rb +19 -0
  121. data/test/event_sourcing/snapshot/integration_test.rb +65 -0
  122. data/test/event_sourcing/storage_listener_test.rb +77 -0
  123. data/test/event_store/in_memory_test.rb +47 -0
  124. data/test/process_manager/correlation_set_test.rb +49 -0
  125. data/test/process_manager/correlation_test.rb +24 -0
  126. data/test/process_manager/process_test.rb +52 -0
  127. data/test/repository/locking_test.rb +101 -0
  128. data/test/serialization/converter/factory_test.rb +33 -0
  129. data/test/serialization/converter/identity_test.rb +17 -0
  130. data/test/serialization/converter/json_test.rb +31 -0
  131. data/test/serialization/converter/ox_test.rb +40 -0
  132. data/test/serialization/fixtures.rb +17 -0
  133. data/test/serialization/lazy_object_test.rb +32 -0
  134. data/test/serialization/message/metadata_test.rb +19 -0
  135. data/test/serialization/message/serialization_aware_message_test.rb +88 -0
  136. data/test/serialization/message/serialized_message_builder_test.rb +41 -0
  137. data/test/serialization/message/serialized_message_test.rb +140 -0
  138. data/test/serialization/message/serializer_test.rb +50 -0
  139. data/test/serialization/revision_resolver_test.rb +12 -0
  140. data/test/serialization/serialized_object_test.rb +36 -0
  141. data/test/serialization/serialized_type_test.rb +27 -0
  142. data/test/serialization/serializer/marshal_test.rb +22 -0
  143. data/test/serialization/serializer/oj_test.rb +24 -0
  144. data/test/serialization/serializer/ox_test.rb +36 -0
  145. data/test/serialization/serializer_test.rb +20 -0
  146. data/test/test_helper.rb +19 -0
  147. data/test/uow/factory_test.rb +23 -0
  148. data/test/uow/outer_commit_listener_test.rb +50 -0
  149. data/test/uow/provider_test.rb +70 -0
  150. data/test/uow/uow_test.rb +337 -0
  151. data/test/upcasting/chain_test.rb +29 -0
  152. data/test/upcasting/fixtures.rb +66 -0
  153. data/test/wiring/wire_registry_test.rb +60 -0
  154. data/test/wiring/wire_test.rb +51 -0
  155. metadata +263 -0
@@ -0,0 +1,16 @@
1
+ module Synapse
2
+ module EventBus
3
+ # Represents a listener that can be notified of events from an event bus. Implementations are
4
+ # highly discouraged from throwing exceptions.
5
+ #
6
+ # @abstract
7
+ module EventListener
8
+ # Called when an event is published to the event bus
9
+ #
10
+ # @abstract
11
+ # @param [EventMessage] event
12
+ # @return [undefined]
13
+ def notify(event); end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Synapse
2
+ module EventBus
3
+ # @abstract
4
+ module EventListenerProxy
5
+ extend EventListener
6
+
7
+ # @abstract
8
+ # @return [Class]
9
+ def target_type; end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,69 @@
1
+ module Synapse
2
+ module EventBus
3
+ # Implementation of an event bus that notifies any subscribed event listeners in the calling
4
+ # thread. Listeners are expected to implement asynchronous handing themselves, if desired.
5
+ class SimpleEventBus < EventBus
6
+ def initialize
7
+ @listeners = Set.new
8
+ @logger = Logging.logger.new self.class
9
+ end
10
+
11
+ # @param [EventMessage...] events
12
+ # @return [undefined]
13
+ def publish(*events)
14
+ if @listeners.empty?
15
+ return
16
+ end
17
+
18
+ events.flatten!
19
+ events.each do |event|
20
+ @listeners.each do |listener|
21
+ if @logger.debug?
22
+ listener_type = actual_type listener
23
+ @logger.debug 'Dispatching event [%s] to listener [%s]' %
24
+ [event.payload_type, listener_type]
25
+ end
26
+
27
+ listener.notify event
28
+ end
29
+ end
30
+ end
31
+
32
+ # @param [EventListener] listener
33
+ # @return [undefined]
34
+ def subscribe(listener)
35
+ listener_type = actual_type listener
36
+
37
+ if @listeners.add? listener
38
+ @logger.debug 'Event listener [%s] subscribed' % listener_type
39
+ else
40
+ @logger.info 'Event listener [%s] not added, was already subscribed' % listener_type
41
+ end
42
+ end
43
+
44
+ # @param [EventListener] listener
45
+ # @return [undefined]
46
+ def unsubscribe(listener)
47
+ listener_type = actual_type listener
48
+
49
+ if @listeners.delete? listener
50
+ @logger.debug 'Event listener [%s] unsubscribed' % listener_type
51
+ else
52
+ @logger.info 'Event listener [%s] not removed, was not subscribed' % listener_type
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # @param [EventListener] listener
59
+ # @return [Class]
60
+ def actual_type(listener)
61
+ if listener.respond_to? :proxied_type
62
+ listener.proxied_type
63
+ else
64
+ listener.class
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ module Synapse
2
+ module EventBus
3
+ # Mixin for an event listener that wishes to use the wiring DSL
4
+ module WiringEventListener
5
+ extend ActiveSupport::Concern
6
+ include EventListener
7
+ include Wiring::MessageWiring
8
+
9
+ included do
10
+ self.wire_registry = Wiring::WireRegistry.new true
11
+ end
12
+
13
+ # @param [EventMessage] event
14
+ # @return [undefined]
15
+ def notify(event)
16
+ wire = self.wire_registry.wire_for event.payload_type
17
+ if wire
18
+ invoke_wire event, wire
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,69 @@
1
+ module Synapse
2
+ module EventSourcing
3
+ # Represents a mechanism for creating aggregates to be initialized by an event stream
4
+ # @abstract
5
+ class AggregateFactory
6
+ # Instantiates an aggregate using the given aggregate identifier and first event
7
+ #
8
+ # The first event is either the event used to create the aggregate or the most recent
9
+ # snapshot event for the aggregate.
10
+ #
11
+ # @abstract
12
+ # @param [Object] aggregate_id
13
+ # @param [DomainEventMessage] first_event
14
+ # @return [AggregateRoot]
15
+ def create_aggregate(aggregate_id, first_event); end
16
+
17
+ # @abstract
18
+ # @return [Class] Type of aggregate being created by this factory
19
+ def aggregate_type; end
20
+
21
+ # @abstract
22
+ # @return [String] Type identifier used to store the aggregate in the event store
23
+ def type_identifier; end
24
+ end
25
+
26
+ # Aggregate factory that uses a convention to create instances of aggregates
27
+ class GenericAggregateFactory < AggregateFactory
28
+ # @return [Class]
29
+ attr_reader :aggregate_type
30
+
31
+ # @return [String]
32
+ attr_reader :type_identifier
33
+
34
+ # @param [Class] aggregate_type
35
+ # @return [undefined]
36
+ def initialize(aggregate_type)
37
+ @aggregate_type = aggregate_type
38
+ @type_identifier = aggregate_type.to_s.demodulize
39
+ end
40
+
41
+ # @param [Object] aggregate_id
42
+ # @param [DomainEventMessage] first_event
43
+ # @return [AggregateRoot]
44
+ def create_aggregate(aggregate_id, first_event)
45
+ payload = first_event.payload
46
+
47
+ if payload.is_a? AggregateRoot
48
+ aggregate = payload
49
+ else
50
+ aggregate = @aggregate_type.allocate
51
+ end
52
+
53
+ post_process aggregate
54
+ end
55
+
56
+ protected
57
+
58
+ # Performs any processing that must be done on an aggregate instance that was reconstructed
59
+ # from a snapshot event. Implementations may choose to modify the existing instance or return
60
+ # a new instance.
61
+ #
62
+ # @param [AggregateRoot] aggregate
63
+ # @return [AggregateRoot]
64
+ def post_process(aggregate)
65
+ aggregate
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,104 @@
1
+ module Synapse
2
+ module EventSourcing
3
+ # Mixin for the root entity of an aggregate that is initialized from a historical event stream
4
+ module AggregateRoot
5
+ extend ActiveSupport::Concern
6
+ include Domain::AggregateRoot
7
+ include Member
8
+
9
+ module ClassMethods
10
+ # Creates a new instance of this aggregate root without calling its initializer and
11
+ # initializes the state of the aggregate from the given event stream.
12
+ #
13
+ # @param [DomainEventStream] stream
14
+ # @return [AggregateRoot]
15
+ def new_from_stream(stream)
16
+ aggregate = allocate
17
+ aggregate.initialize_from_stream stream
18
+ aggregate
19
+ end
20
+ end
21
+
22
+ # @return [Integer] The sequence number of the last committed event
23
+ def version
24
+ last_committed_sequence_number
25
+ end
26
+
27
+ # Initializes the state of this aggregate from the given domain event stream
28
+ #
29
+ # @raise [RuntimeError] If aggregate has already been initialized
30
+ # @param [DomainEventStream] stream
31
+ # @return [undefined]
32
+ def initialize_from_stream(stream)
33
+ if uncommitted_event_count > 0
34
+ raise 'Aggregate has already been initialized'
35
+ end
36
+
37
+ pre_initialize
38
+
39
+ last_sequence_number = nil
40
+
41
+ stream.each do |event|
42
+ last_sequence_number = event.sequence_number
43
+ handle_recursively event
44
+ end
45
+
46
+ initialize_event_container last_sequence_number
47
+ end
48
+
49
+ # Called when a member of the aggregate publishes an event
50
+ #
51
+ # This is only meant to be invoked by entities that are members of this aggregate
52
+ #
53
+ # @api private
54
+ # @param [Object] payload
55
+ # @param [Hash] metadata
56
+ # @return [undefined]
57
+ def handle_member_event(payload, metadata = nil)
58
+ apply payload, metadata
59
+ end
60
+
61
+ protected
62
+
63
+ # Hook that is called before the aggregate is initialized
64
+ # @return [undefined]
65
+ def pre_initialize; end
66
+
67
+ # Creates an event with the given metadata and payload, publishes it using the event
68
+ # container, and finally handles it locally and recursively down the aggregate.
69
+ #
70
+ # @param [Object] payload
71
+ # @param [Hash] metadata
72
+ # @return [undefined]
73
+ def apply(payload, metadata = nil)
74
+ if id
75
+ event = publish_event payload, metadata
76
+ handle_recursively event
77
+ else
78
+ # This is a workaround for aggregates that set the aggregate identifier in an event handler
79
+ event = Domain::DomainEventMessage.build do |b|
80
+ b.metadata = metadata
81
+ b.payload = payload
82
+ b.sequence_number = 0
83
+ end
84
+
85
+ handle_recursively event
86
+ publish_event payload, metadata
87
+ end
88
+ end
89
+
90
+ # Handles the event locally and then cascades to any registered child entities
91
+ #
92
+ # @param [DomainEventMessage] event
93
+ # @return [undefined]
94
+ def handle_recursively(event)
95
+ handle_event event
96
+
97
+ child_entities.each do |entity|
98
+ entity.aggregate_root = self
99
+ entity.handle_aggregate_event event
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,80 @@
1
+ module Synapse
2
+ module EventSourcing
3
+ # Represents a mechanism that is capable of detecting conflicts between applied changes
4
+ # to the aggregate and unseen changes made to the aggregate.
5
+ class ConflictResolver
6
+ # Checks the list of changes applied to the aggregate and compares it to the list of
7
+ # events already applied to the aggregate. If a conflict is detected, this should throw
8
+ # an exception. Otherwise, the changes will be applied.
9
+ #
10
+ # @raise [ConflictingModificationException] If any conflicts were detected
11
+ # @param [Array] applied_changes List of changes applied to the aggregate
12
+ # @param [Array] committed_changes List of events that were unexpected by the command handler
13
+ # @return [undefined]
14
+ def resolve_conflicts(applied_changes, committed_changes); end
15
+ end
16
+
17
+ # Unit of work listener that detects if there is a conflict before an aggregate is committed
18
+ # If there is a potential conflict, a conflict resolver determines how to resolve the conflict.
19
+ class ConflictResolvingUnitOfWorkListener < UnitOfWork::UnitOfWorkListener
20
+ # @param [AggregateRoot] aggregate
21
+ # @param [Array] unseen_events
22
+ # @param [ConflictResolver] conflict_resolver
23
+ # @return [undefined]
24
+ def initialize(aggregate, unseen_events, conflict_resolver)
25
+ @aggregate = aggregate
26
+ @unseen_events = unseen_events
27
+ @conflict_resolver = conflict_resolver
28
+ end
29
+
30
+ # @param [UnitOfWork] unit
31
+ # @param [Array<AggregateRoot>] aggregates
32
+ # @param [Hash<EventBus, Array>] events
33
+ # @return [undefined]
34
+ def on_prepare_commit(unit, aggregates, events)
35
+ if potential_conflicts?
36
+ @conflict_resolver.resolve_conflicts @aggregate.uncommitted_events.to_a, @unseen_events
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # @return [Boolean]
43
+ def potential_conflicts?
44
+ @aggregate.uncommitted_event_count > 0 and
45
+ @aggregate.version and
46
+ @unseen_events.size > 0
47
+ end
48
+ end
49
+
50
+ # Event stream decorator that captures any events that have been applied after the expected
51
+ # version of an aggregate
52
+ class CapturingEventStream < Domain::DomainEventStream
53
+ extend Forwardable
54
+
55
+ # @param [DomainEventStream] delegate
56
+ # @param [Array] unseen_events
57
+ # @param [Integer] expected_version
58
+ # @return [undefined]
59
+ def initialize(delegate, unseen_events, expected_version)
60
+ @delegate = delegate
61
+ @unseen_events = unseen_events
62
+ @expected_version = expected_version
63
+ end
64
+
65
+ # @return [DomainEventMessage]
66
+ def next_event
67
+ event = @delegate.next_event
68
+
69
+ if @expected_version and event.sequence_number > @expected_version
70
+ @unseen_events.push event
71
+ end
72
+
73
+ event
74
+ end
75
+
76
+ # Delegators for domain event stream
77
+ def_delegators :@delegate, :end?, :peek
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,64 @@
1
+ module Synapse
2
+ module EventSourcing
3
+ # Mixin for an entity that is part of an event-sourced aggregate
4
+ #
5
+ # Instead managing its own published events, the entity relies on being registered to the
6
+ # aggregate root and using its event container. Events applied to child entities will be
7
+ # cascaded throughout the entire aggregate.
8
+ module Entity
9
+ extend ActiveSupport::Concern
10
+ include Member
11
+
12
+ # Handles an aggregate event locally and then cascades to any registered child entities
13
+ #
14
+ # @api private
15
+ # @param [DomainEventMessage] event
16
+ # @return [undefined]
17
+ def handle_aggregate_event(event)
18
+ handle_recursively event
19
+ end
20
+
21
+ # Registers this entity to the aggregate root
22
+ #
23
+ # @api private
24
+ # @raise [RuntimeError] If entity is registered to a different aggregate root
25
+ # @param [AggregateRoot] aggregate_root
26
+ # @return [undefined]
27
+ def aggregate_root=(aggregate_root)
28
+ if @aggregate_root and !@aggregate_root.equal? aggregate_root
29
+ raise 'Entity is registered to a different aggregate root'
30
+ end
31
+
32
+ @aggregate_root = aggregate_root
33
+ end
34
+
35
+ protected
36
+
37
+ # Handles the event locally and then cascades to any registered child entities
38
+ #
39
+ # @param [DomainEventMessage] event
40
+ # @return [undefined]
41
+ def handle_recursively(event)
42
+ handle_event event
43
+
44
+ child_entities.each do |entity|
45
+ entity.aggregate_root = @aggregate_root
46
+ entity.handle_aggregate_event event
47
+ end
48
+ end
49
+
50
+ # Applies the given event to the aggregate and publishes it to the event container
51
+ #
52
+ # @param [Object] payload
53
+ # @param [Hash] metadata
54
+ # @return [undefined]
55
+ def apply(payload, metadata = nil)
56
+ unless @aggregate_root
57
+ raise 'Entity has not been registered to an aggregate root'
58
+ end
59
+
60
+ @aggregate_root.handle_member_event payload, metadata
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,72 @@
1
+ module Synapse
2
+ module EventSourcing
3
+ # Base mixin for a member of an aggregate which has its state mutated by events that are
4
+ # applied to the aggregate
5
+ module Member
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # Registers an instance variable as a child entity
10
+ #
11
+ # @param [Symbol...] fields
12
+ # @return [undefined]
13
+ def child_entity(*fields)
14
+ fields.each do |field|
15
+ child_entities.add field.to_s
16
+ end
17
+ end
18
+
19
+ # Returns a set of symbols referring to child entities
20
+ # @return [Set]
21
+ def child_entities
22
+ @child_entities ||= Set.new
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ # Returns an array of the child entities of this aggregate member
29
+ # @return [Array]
30
+ def child_entities
31
+ entities = Array.new
32
+
33
+ self.class.child_entities.each do |field|
34
+ value = instance_variable_get '@' + field
35
+
36
+ if value.is_a? Entity
37
+ entities.push value
38
+
39
+ # Hashes
40
+ elsif value.is_a? Hash
41
+ entities.concat filter_entities value.each_key
42
+ entities.concat filter_entities value.each_value
43
+
44
+ # Sets, arrays
45
+ elsif value.is_a? Enumerable
46
+ entities.concat filter_entities value
47
+ end
48
+ end
49
+
50
+ entities
51
+ end
52
+
53
+ # If the event is relative to this member, its parameters will be used to change
54
+ # the state of this member
55
+ #
56
+ # @abstract
57
+ # @param [EventMessage] event
58
+ # @return [undefined]
59
+ def handle_event(event); end
60
+
61
+ private
62
+
63
+ # @param [Array] entities
64
+ # @return [Array]
65
+ def filter_entities(entities)
66
+ entities.select do |entity|
67
+ entity.is_a? Member
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end