euston 1.1.0 → 1.2.0

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