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 CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'euston'
3
- s.version = '1.1.0'
4
- s.date = '2011-10-03'
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 committed_commands
33
- @committed_commands ||= []
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(headers, command)
41
- headers = Euston::CommandHeaders.from_hash(headers) if headers.is_a?(Hash)
42
- return if committed_commands.include? headers.id
47
+ def consume_command headers, command
48
+ consume_message headers, command, :command, Euston::CommandHeaders, :send_command_to_method
49
+ end
43
50
 
44
- @current_headers = headers
45
- @current_command = command
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
- handle_command headers, command
48
- self
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 @current_headers.nil?
87
- event.headers.merge! :command => @current_headers.to_hash.merge(:body => @current_command)
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
- handle_event Euston::EventHeaders.from_hash(event.headers), event.body
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 Euston::EventHeaders.from_hash(event.headers), event.body
121
+ replay_event event.headers, event.body
114
122
  end
115
123
  end
116
124
 
117
- def handle_command headers, command
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 handle_event headers, event
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 name_method, headers.type, headers.version
163
+ name = self.class.send(name_method, headers.type, headers.version).to_sym
129
164
 
130
- if respond_to? name.to_sym
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
@@ -11,6 +11,10 @@ module Euston
11
11
  "__consume__#{command}__v#{version}__"
12
12
  end
13
13
 
14
+ def consumes_regex
15
+ /__consume__(\w+)__v(\d+)__/
16
+ end
17
+
14
18
  def id_from_event_method_name type, version
15
19
  "__id_from_event_#{type}__v#{version}__"
16
20
  end
@@ -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 => @headers.merge(:version => version), :body => @body }
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
@@ -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
@@ -0,0 +1,5 @@
1
+ module Euston
2
+ module Errors
3
+ class InvalidCommandError < StandardError; end
4
+ end
5
+ end
@@ -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
@@ -1,17 +1,37 @@
1
1
  module Euston
2
2
  class EventHeaders
3
- attr_reader :id, :type, :version, :timestamp, :command
3
+ attr_reader :id, :type, :version, :timestamp, :source_message, :source_message_type
4
4
 
5
- def initialize id, type, version, timestamp = Time.now, command = nil
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
- @command = command
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
- self.new hash[:id], hash[:type].to_sym, hash[:version], hash[:timestamp], hash[:command]
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
@@ -1,3 +1,3 @@
1
1
  module Euston
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
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.committed_commands.should have(0).items
29
- aggregate2.committed_commands.should have(0).items
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.1.0
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-03 00:00:00.000000000 Z
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: &80875570 !ruby/object:Gem::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: *80875570
25
+ version_requirements: *83067690
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activesupport
28
- requirement: &80874680 !ruby/object:Gem::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: *80874680
36
+ version_requirements: *83067260
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: fuubar
39
- requirement: &80874230 !ruby/object:Gem::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: *80874230
47
+ version_requirements: *83066570
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: rspec
50
- requirement: &80873750 !ruby/object:Gem::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: *80873750
58
+ version_requirements: *83065520
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: uuid
61
- requirement: &80873120 !ruby/object:Gem::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: *80873120
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