euston 1.1.0 → 1.2.0
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.
- data/euston.gemspec +4 -2
- data/lib/euston/aggregate_command_map.rb +2 -1
- data/lib/euston/aggregate_root.rb +63 -27
- data/lib/euston/aggregate_root_dsl_methods.rb +5 -0
- data/lib/euston/aggregate_root_private_method_names.rb +4 -0
- data/lib/euston/command.rb +14 -6
- data/lib/euston/command_bus.rb +3 -3
- data/lib/euston/errors.rb +5 -0
- data/lib/euston/event_handler.rb +4 -0
- data/lib/euston/event_headers.rb +24 -4
- data/lib/euston/idempotence.rb +27 -0
- data/lib/euston/version.rb +1 -1
- data/lib/euston.rb +3 -0
- data/spec/aggregate_command_map_spec.rb +2 -2
- metadata +14 -12
data/euston.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'euston'
|
3
|
-
s.version = '1.
|
4
|
-
s.date = '2011-10-
|
3
|
+
s.version = '1.2.0'
|
4
|
+
s.date = '2011-10-12'
|
5
5
|
s.platform = RUBY_PLATFORM.to_s == 'java' ? 'java' : Gem::Platform::RUBY
|
6
6
|
s.authors = ['Lee Henson', 'Guy Boertje']
|
7
7
|
s.email = ['lee.m.henson@gmail.com', 'guyboertje@gmail.com']
|
@@ -24,10 +24,12 @@ Gem::Specification.new do |s|
|
|
24
24
|
lib/euston/command_handler.rb
|
25
25
|
lib/euston/command_handler_private_method_names.rb
|
26
26
|
lib/euston/command_headers.rb
|
27
|
+
lib/euston/errors.rb
|
27
28
|
lib/euston/event.rb
|
28
29
|
lib/euston/event_handler.rb
|
29
30
|
lib/euston/event_handler_private_method_names.rb
|
30
31
|
lib/euston/event_headers.rb
|
32
|
+
lib/euston/idempotence.rb
|
31
33
|
lib/euston/null_logger.rb
|
32
34
|
lib/euston/repository.rb
|
33
35
|
lib/euston/version.rb
|
@@ -60,7 +60,7 @@ module Euston
|
|
60
60
|
aggregate_entry[:mappings].push_if_unique(mapping, command)
|
61
61
|
end
|
62
62
|
|
63
|
-
def deliver_command(headers, command)
|
63
|
+
def deliver_command(headers, command, logger = Euston::NullLogger.instance)
|
64
64
|
args = [headers, command]
|
65
65
|
query = {:kind => :construct, :type => headers.type}
|
66
66
|
if (entry = @map.find_entry_with_mapping_match( query ))
|
@@ -71,6 +71,7 @@ module Euston
|
|
71
71
|
return unless entry
|
72
72
|
aggregate = load_aggregate(entry, *args)
|
73
73
|
end
|
74
|
+
aggregate.log = logger
|
74
75
|
aggregate.consume_command( headers, command )
|
75
76
|
end
|
76
77
|
|
@@ -6,8 +6,9 @@ module Euston
|
|
6
6
|
include Euston::EventHandler
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
-
def hydrate stream, snapshot = nil
|
9
|
+
def hydrate stream, snapshot = nil, log = nil
|
10
10
|
instance = self.new
|
11
|
+
instance.log = log unless log.nil?
|
11
12
|
instance.send :apply_snapshot, snapshot unless snapshot.nil?
|
12
13
|
instance.send :apply_stream, stream
|
13
14
|
instance
|
@@ -17,8 +18,10 @@ module Euston
|
|
17
18
|
module InstanceMethods
|
18
19
|
def initialize aggregate_id = nil
|
19
20
|
@aggregate_id = aggregate_id
|
21
|
+
@log = Euston::NullLogger.instance
|
20
22
|
end
|
21
23
|
|
24
|
+
attr_accessor :log
|
22
25
|
attr_reader :aggregate_id
|
23
26
|
|
24
27
|
def initial_version
|
@@ -29,23 +32,34 @@ module Euston
|
|
29
32
|
!uncommitted_events.empty?
|
30
33
|
end
|
31
34
|
|
32
|
-
def
|
33
|
-
@
|
35
|
+
def committed_messages
|
36
|
+
@committed_messages ||= Set.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def uncommitted_commands
|
40
|
+
@uncommitted_commands ||= []
|
34
41
|
end
|
35
42
|
|
36
43
|
def uncommitted_events
|
37
44
|
@uncommitted_events ||= []
|
38
45
|
end
|
39
46
|
|
40
|
-
def consume_command
|
41
|
-
headers
|
42
|
-
|
47
|
+
def consume_command headers, command
|
48
|
+
consume_message headers, command, :command, Euston::CommandHeaders, :send_command_to_method
|
49
|
+
end
|
43
50
|
|
44
|
-
|
45
|
-
|
51
|
+
def consume_event_subscription headers, event
|
52
|
+
consume_message headers, event, :event_subscription, Euston::EventHeaders, :send_event_subscription_to_method
|
53
|
+
end
|
46
54
|
|
47
|
-
|
48
|
-
|
55
|
+
def replay_event headers, event
|
56
|
+
headers = Euston::EventHeaders.from_hash(headers) if headers.is_a?(Hash)
|
57
|
+
|
58
|
+
source_message = headers.source_message
|
59
|
+
committed_messages << source_message[:headers][:id] unless source_message.nil?
|
60
|
+
|
61
|
+
send_event_to_method headers, event
|
62
|
+
@initial_version = initial_version + 1
|
49
63
|
end
|
50
64
|
|
51
65
|
def take_snapshot
|
@@ -61,15 +75,6 @@ module Euston
|
|
61
75
|
{ :version => version, :payload => send(name) }
|
62
76
|
end
|
63
77
|
|
64
|
-
def replay_event(headers, event)
|
65
|
-
headers = Euston::EventHeaders.from_hash(headers) if headers.is_a?(Hash)
|
66
|
-
command = headers.command
|
67
|
-
committed_commands << command[:id] unless command.nil? || committed_commands.include?(command[:id])
|
68
|
-
|
69
|
-
handle_event headers, event
|
70
|
-
@initial_version = initial_version + 1
|
71
|
-
end
|
72
|
-
|
73
78
|
def version
|
74
79
|
initial_version + uncommitted_events.length
|
75
80
|
end
|
@@ -83,11 +88,12 @@ module Euston
|
|
83
88
|
:version => version,
|
84
89
|
:timestamp => Time.now.to_f
|
85
90
|
|
86
|
-
unless @
|
87
|
-
event.headers
|
91
|
+
unless @current_message_headers.nil?
|
92
|
+
event.headers[@current_message_type] = { :headers => @current_message_headers.to_hash,
|
93
|
+
:body => @current_message_body }
|
88
94
|
end
|
89
95
|
|
90
|
-
|
96
|
+
send_event_to_method Euston::EventHeaders.from_hash(event.headers), event.body
|
91
97
|
uncommitted_events << event
|
92
98
|
end
|
93
99
|
|
@@ -97,6 +103,8 @@ module Euston
|
|
97
103
|
raise "Trying to load a snapshot of aggregate #{self.class.name} but it does not have a load_snapshot method for version #{version}!" unless respond_to? self.class.load_snapshot_method_name(version)
|
98
104
|
|
99
105
|
name = self.class.load_snapshot_method_name version
|
106
|
+
|
107
|
+
@log.debug "Applying snapshot: #{snapshot.inspect}"
|
100
108
|
self.send name, snapshot.payload
|
101
109
|
end
|
102
110
|
end
|
@@ -110,24 +118,52 @@ module Euston
|
|
110
118
|
raise "This aggregate cannot apply a historical event stream because it is not empty." unless uncommitted_events.empty? && initial_version == 0
|
111
119
|
|
112
120
|
events.each_with_index do |event, i|
|
113
|
-
replay_event
|
121
|
+
replay_event event.headers, event.body
|
114
122
|
end
|
115
123
|
end
|
116
124
|
|
117
|
-
def
|
125
|
+
def publish_command command, dispatch_at = Time.now.to_f
|
126
|
+
raise ArgumentError, 'Commands must subclass Euston::Command' unless command.is_a? Euston::Command
|
127
|
+
raise Euston::Errors::InvalidCommandError, "An attempt was made to publish an invalid command from an aggregate root.\n\nAggregate id: #{@aggregate_id}\nAggregate type: #{self.class.name}\nCommand: #{command.to_hash}\nErrors: #{command.errors}" unless command.valid?
|
128
|
+
|
129
|
+
command.headers[:dispatch_at] = dispatch_at
|
130
|
+
|
131
|
+
uncommitted_commands << command
|
132
|
+
end
|
133
|
+
|
134
|
+
def send_command_to_method headers, command
|
118
135
|
deliver_message headers, command, :consumes_method_name, 'a command', "a 'consumes' block"
|
119
136
|
end
|
120
137
|
|
121
|
-
def
|
138
|
+
def send_event_to_method headers, event
|
122
139
|
deliver_message headers, event, :applies_method_name, 'an event', "an 'applies' block"
|
123
140
|
end
|
124
141
|
|
142
|
+
def send_event_subscription_to_method headers, event
|
143
|
+
deliver_message headers, event, :event_handler_method_name, 'an event', "a 'subscribes' block"
|
144
|
+
end
|
145
|
+
|
125
146
|
private
|
126
147
|
|
148
|
+
def consume_message headers, body, message_type, headers_type, send_method
|
149
|
+
headers = headers_type.from_hash(headers) if headers.is_a?(Hash)
|
150
|
+
|
151
|
+
unless committed_messages.include? headers.id
|
152
|
+
@current_message_type = message_type
|
153
|
+
@current_message_headers = headers
|
154
|
+
@current_message_body = body
|
155
|
+
|
156
|
+
self.send send_method, headers, body
|
157
|
+
end
|
158
|
+
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
127
162
|
def deliver_message headers, message, name_method, message_kind, expected_block_kind
|
128
|
-
name = self.class.send
|
163
|
+
name = self.class.send(name_method, headers.type, headers.version).to_sym
|
129
164
|
|
130
|
-
if respond_to? name
|
165
|
+
if respond_to? name
|
166
|
+
@log.debug "Calling #{name} with: #{message.inspect}"
|
131
167
|
method(name).call OpenStruct.new(message).freeze
|
132
168
|
else
|
133
169
|
raise "Couldn't deliver #{message_kind} (#{headers.type} v#{headers.version}) to #{self.class}. Did you forget #{expected_block_kind}?"
|
@@ -40,6 +40,7 @@ module Euston
|
|
40
40
|
private
|
41
41
|
|
42
42
|
def define_private_method name, &block
|
43
|
+
block = method(:null_block) if block.nil?
|
43
44
|
define_method name do |*args| instance_exec *args, &block end
|
44
45
|
end
|
45
46
|
|
@@ -49,6 +50,10 @@ module Euston
|
|
49
50
|
|
50
51
|
Euston::AggregateCommandMap.send entry_point, type, command, id, to_i
|
51
52
|
end
|
53
|
+
|
54
|
+
def null_block *args
|
55
|
+
# block-less definitions get pointed here
|
56
|
+
end
|
52
57
|
end
|
53
58
|
end
|
54
59
|
end
|
data/lib/euston/command.rb
CHANGED
@@ -2,10 +2,20 @@ module Euston
|
|
2
2
|
class Command
|
3
3
|
include ActiveModel::Validations
|
4
4
|
|
5
|
-
def initialize body
|
5
|
+
def initialize body, dispatch_at = nil
|
6
6
|
@headers = { :id => Uuid.generate,
|
7
7
|
:type => self.class.to_s.split('::').pop.underscore.to_sym }
|
8
|
+
|
8
9
|
@body = body
|
10
|
+
@headers[:dispatch_at] = dispatch_at unless dispatch_at.nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
def headers
|
14
|
+
@headers.merge :version => version
|
15
|
+
end
|
16
|
+
|
17
|
+
def id
|
18
|
+
@headers[:id]
|
9
19
|
end
|
10
20
|
|
11
21
|
def read_attribute_for_validation key
|
@@ -13,15 +23,13 @@ module Euston
|
|
13
23
|
end
|
14
24
|
|
15
25
|
def to_hash
|
16
|
-
{ :headers =>
|
17
|
-
end
|
18
|
-
|
19
|
-
def id
|
20
|
-
@headers[:id]
|
26
|
+
{ :headers => headers, :body => @body }
|
21
27
|
end
|
22
28
|
|
23
29
|
def version
|
24
30
|
1
|
25
31
|
end
|
32
|
+
|
33
|
+
attr_reader :body
|
26
34
|
end
|
27
35
|
end
|
data/lib/euston/command_bus.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
module Euston
|
2
2
|
module CommandBus
|
3
|
-
def self.publish headers, command
|
4
|
-
aggregate = AggregateCommandMap.deliver_command headers, command
|
3
|
+
def self.publish headers, command, logger = Euston::NullLogger.instance
|
4
|
+
aggregate = AggregateCommandMap.deliver_command headers, command, logger
|
5
5
|
raise "No aggregate found to handle command: #{headers} #{command}" if aggregate.nil?
|
6
6
|
|
7
7
|
Repository.save aggregate
|
8
8
|
end
|
9
9
|
end
|
10
|
-
end
|
10
|
+
end
|
data/lib/euston/event_handler.rb
CHANGED
@@ -3,6 +3,10 @@ module Euston
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
include Euston::EventHandlerPrivateMethodNames
|
5
5
|
|
6
|
+
included do
|
7
|
+
attr_accessor :log unless public_method_defined? :log=
|
8
|
+
end
|
9
|
+
|
6
10
|
module ClassMethods
|
7
11
|
def subscribes type, version = 1, opts = nil, &consumer
|
8
12
|
if self.include? Euston::AggregateRoot
|
data/lib/euston/event_headers.rb
CHANGED
@@ -1,17 +1,37 @@
|
|
1
1
|
module Euston
|
2
2
|
class EventHeaders
|
3
|
-
attr_reader :id, :type, :version, :timestamp, :
|
3
|
+
attr_reader :id, :type, :version, :timestamp, :source_message, :source_message_type
|
4
4
|
|
5
|
-
def initialize id, type, version, timestamp = Time.now,
|
5
|
+
def initialize id, type, version, timestamp = Time.now, source_message = nil, source_message_type = nil
|
6
6
|
@id = id
|
7
7
|
@type = type
|
8
8
|
@version = version
|
9
9
|
@timestamp = Time.at(timestamp).utc
|
10
|
-
@
|
10
|
+
@source_message = source_message
|
11
|
+
@source_message_type = source_message_type
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
Hash[@source_message_type, @source_message].merge :id => id,
|
16
|
+
:type => type,
|
17
|
+
:version => version,
|
18
|
+
:timestamp => timestamp
|
11
19
|
end
|
12
20
|
|
13
21
|
def self.from_hash hash
|
14
|
-
|
22
|
+
if hash.has_key? :command
|
23
|
+
source_message = hash[:command]
|
24
|
+
source_message_type = :command
|
25
|
+
elsif hash.has_key? :event_subscription
|
26
|
+
source_message = hash[:event_subscription]
|
27
|
+
source_message_type = :event_subscription
|
28
|
+
end
|
29
|
+
|
30
|
+
self.new hash[:id], hash[:type].to_sym, hash[:version], hash[:timestamp], source_message, source_message_type
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"#{id} #{type} (v#{version})"
|
15
35
|
end
|
16
36
|
end
|
17
37
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Euston
|
2
|
+
module Idempotence
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module InstanceMethods
|
6
|
+
def if_unhandled obj, headers
|
7
|
+
return if obj._history.include? headers.id
|
8
|
+
|
9
|
+
obj._history << headers.id
|
10
|
+
|
11
|
+
unless headers.source_message.nil? || obj._history.include?(headers.source_message[:headers][:id])
|
12
|
+
obj._history << headers.source_message[:headers][:id]
|
13
|
+
end
|
14
|
+
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
|
18
|
+
def if_exists_and_unhandled document_type, id, headers
|
19
|
+
document = document_type.find id
|
20
|
+
|
21
|
+
if_unhandled document, headers do
|
22
|
+
yield document
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/euston/version.rb
CHANGED
data/lib/euston.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'active_support/concern'
|
2
2
|
require 'active_model'
|
3
3
|
require 'ostruct'
|
4
|
+
require 'set'
|
4
5
|
|
5
6
|
module Euston
|
6
7
|
class << self
|
@@ -21,6 +22,7 @@ end
|
|
21
22
|
|
22
23
|
Euston.uuid = Uuid
|
23
24
|
|
25
|
+
require 'euston/errors'
|
24
26
|
require 'euston/aggregate_command_map'
|
25
27
|
require 'euston/aggregate_root_private_method_names'
|
26
28
|
require 'euston/aggregate_root_dsl_methods'
|
@@ -33,6 +35,7 @@ require 'euston/event'
|
|
33
35
|
require 'euston/event_handler_private_method_names'
|
34
36
|
require 'euston/event_handler'
|
35
37
|
require 'euston/event_headers'
|
38
|
+
require 'euston/idempotence'
|
36
39
|
require 'euston/null_logger'
|
37
40
|
require 'euston/aggregate_root'
|
38
41
|
require 'euston/repository'
|
@@ -25,8 +25,8 @@ module Euston
|
|
25
25
|
|
26
26
|
it "then side effects are seen" do
|
27
27
|
|
28
|
-
aggregate.
|
29
|
-
aggregate2.
|
28
|
+
aggregate.committed_messages.should have(0).items
|
29
|
+
aggregate2.committed_messages.should have(0).items
|
30
30
|
|
31
31
|
Sample::Widget.new( Euston.uuid.generate )
|
32
32
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: euston
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2011-10-
|
13
|
+
date: 2011-10-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activemodel
|
17
|
-
requirement: &
|
17
|
+
requirement: &83067690 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ~>
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 3.0.9
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *83067690
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: activesupport
|
28
|
-
requirement: &
|
28
|
+
requirement: &83067260 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: 3.0.9
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *83067260
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: fuubar
|
39
|
-
requirement: &
|
39
|
+
requirement: &83066570 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ~>
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: 0.0.0
|
45
45
|
type: :development
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *83066570
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: rspec
|
50
|
-
requirement: &
|
50
|
+
requirement: &83065520 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ~>
|
@@ -55,10 +55,10 @@ dependencies:
|
|
55
55
|
version: 2.6.0
|
56
56
|
type: :development
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *83065520
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: uuid
|
61
|
-
requirement: &
|
61
|
+
requirement: &83064720 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ~>
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
version: 2.3.0
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
|
-
version_requirements: *
|
69
|
+
version_requirements: *83064720
|
70
70
|
description: ''
|
71
71
|
email:
|
72
72
|
- lee.m.henson@gmail.com
|
@@ -88,10 +88,12 @@ files:
|
|
88
88
|
- lib/euston/command_handler.rb
|
89
89
|
- lib/euston/command_handler_private_method_names.rb
|
90
90
|
- lib/euston/command_headers.rb
|
91
|
+
- lib/euston/errors.rb
|
91
92
|
- lib/euston/event.rb
|
92
93
|
- lib/euston/event_handler.rb
|
93
94
|
- lib/euston/event_handler_private_method_names.rb
|
94
95
|
- lib/euston/event_headers.rb
|
96
|
+
- lib/euston/idempotence.rb
|
95
97
|
- lib/euston/null_logger.rb
|
96
98
|
- lib/euston/repository.rb
|
97
99
|
- lib/euston/version.rb
|