euston 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,155 @@
1
+ require 'date'
2
+ require 'rspec/core/rake_task'
3
+
4
+ #############################################################################
5
+ #
6
+ # Helper functions
7
+ #
8
+ #############################################################################
9
+
10
+ def name
11
+ @name ||= Dir['*.gemspec'].first.split('.').first
12
+ end
13
+
14
+ def version
15
+ line = File.read("lib/#{name}/version.rb")[/^\s*VERSION\s*=\s*.*/]
16
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
17
+ end
18
+
19
+ def date
20
+ Date.today.to_s
21
+ end
22
+
23
+ def rubyforge_project
24
+ name
25
+ end
26
+
27
+ def gemspec_file
28
+ "#{name}.gemspec"
29
+ end
30
+
31
+ def gem_file
32
+ "#{name}-#{version}.gem"
33
+ end
34
+
35
+ def replace_header(head, header_name, provider = nil)
36
+ if provider
37
+ value = send(provider)
38
+ else
39
+ value = "'#{send(header_name)}'"
40
+ end
41
+
42
+ provider ||= header_name
43
+ head.sub!(/(\.#{header_name}\s*= ).*/) { "#{$1}#{value}"}
44
+ end
45
+
46
+ def platform
47
+ jruby? ? '-java' : ''
48
+ end
49
+
50
+ def platform_dependant_gem_file
51
+ "#{name}-#{version}#{platform}.gem"
52
+ end
53
+
54
+ def platform_dependent_version
55
+ "'#{version}#{platform}'"
56
+ end
57
+
58
+ def jruby?
59
+ RUBY_PLATFORM.to_s == 'java'
60
+ end
61
+
62
+ #############################################################################
63
+ #
64
+ # Custom tasks
65
+ #
66
+ #############################################################################
67
+
68
+ default_rspec_opts = %w[--colour --format Fuubar]
69
+
70
+ desc "Run all examples"
71
+ RSpec::Core::RakeTask.new(:spec) do |t|
72
+ t.rspec_opts = default_rspec_opts
73
+ end
74
+
75
+ #############################################################################
76
+ #
77
+ # Packaging tasks
78
+ #
79
+ #############################################################################
80
+
81
+ def built_gem
82
+ @built_gem ||= Dir["#{name}*.gem"].first
83
+ end
84
+
85
+ desc "Create tag v#{platform_dependent_version} and build and push #{platform_dependant_gem_file} to Rubygems"
86
+ task :release => :build do
87
+ unless `git branch` =~ /^\* master$/
88
+ puts "You must be on the master branch to release!"
89
+ exit!
90
+ end
91
+
92
+ sh "git commit --allow-empty -a -m 'Release #{platform_dependent_version}'"
93
+ sh "git tag v#{platform_dependent_version}"
94
+ sh "git push origin master"
95
+ sh "git push origin v#{platform_dependent_version}"
96
+
97
+ command = "gem push pkg/#{platform_dependant_gem_file}"
98
+
99
+ if jruby?
100
+ puts "--------------------------------------------------------------------------------------"
101
+ puts "can't push to rubygems using jruby at the moment, so switch to mri and run: #{command}"
102
+ puts "--------------------------------------------------------------------------------------"
103
+ else
104
+ sh command
105
+ end
106
+ end
107
+
108
+ desc "Build #{platform_dependant_gem_file} into the pkg directory"
109
+ task :build => :gemspec do
110
+ sh "mkdir -p pkg"
111
+ sh "gem build #{gemspec_file}"
112
+ sh "mv #{built_gem} pkg"
113
+ end
114
+
115
+ desc "Generate #{gemspec_file}"
116
+ task :gemspec => :validate do
117
+ # read spec file and split out manifest section
118
+ spec = File.read(gemspec_file)
119
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
120
+
121
+ # replace name version and date
122
+ replace_header(head, :name)
123
+ replace_header(head, :version)
124
+ replace_header(head, :date)
125
+ #comment this out if your rubyforge_project has a different name
126
+ #replace_header(head, :rubyforge_project)
127
+
128
+ # determine file list from git ls-files
129
+ files = `git ls-files`.
130
+ split("\n").
131
+ sort.
132
+ reject { |file| file =~ /^\./ }.
133
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
134
+ map { |file| " #{file}" }.
135
+ join("\n")
136
+
137
+ # piece file back together and write
138
+ manifest = " s.files = %w[\n#{files}\n ]\n"
139
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
140
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
141
+ puts "Updated #{gemspec_file}"
142
+ end
143
+
144
+ desc "Validate #{gemspec_file}"
145
+ task :validate do
146
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
147
+ unless libfiles.empty?
148
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
149
+ exit!
150
+ end
151
+ unless Dir['VERSION*'].empty?
152
+ puts "A `VERSION` file at root level violates Gem best practices."
153
+ exit!
154
+ end
155
+ end
data/euston.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'euston'
3
+ s.version = '1.0.0'
4
+ s.date = '2011-09-15'
5
+ s.platform = RUBY_PLATFORM.to_s == 'java' ? 'java' : Gem::Platform::RUBY
6
+ s.authors = ['Lee Henson', 'Guy Boertje']
7
+ s.email = ['lee.m.henson@gmail.com', 'guyboertje@gmail.com']
8
+ s.summary = %q{Cqrs tooling.}
9
+ s.description = ''
10
+ s.homepage = 'http://github.com/leemhenson/euston'
11
+
12
+ # = MANIFEST =
13
+ s.files = %w[
14
+ Rakefile
15
+ euston.gemspec
16
+ lib/euston.rb
17
+ lib/euston/aggregate_command_map.rb
18
+ lib/euston/aggregate_root.rb
19
+ lib/euston/command_bus.rb
20
+ lib/euston/command_handler.rb
21
+ lib/euston/command_headers.rb
22
+ lib/euston/event_handler.rb
23
+ lib/euston/event_headers.rb
24
+ lib/euston/repository.rb
25
+ lib/euston/version.rb
26
+ spec/aggregate_command_map_spec.rb
27
+ spec/aggregate_root_samples.rb
28
+ spec/aggregate_root_spec.rb
29
+ spec/spec_helper.rb
30
+ ]
31
+ # = MANIFEST =
32
+
33
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
34
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
35
+
36
+ s.add_dependency 'activesupport', '~> 3.0.9'
37
+ s.add_dependency 'euston-eventstore', '~> 1.0.0'
38
+ s.add_dependency 'require_all', '~> 1.2.0'
39
+ s.add_development_dependency 'fuubar', '~> 0.0.0'
40
+ s.add_development_dependency 'rspec', '~> 2.6.0'
41
+ s.add_development_dependency 'uuid', '~> 2.3.0'
42
+ end
@@ -0,0 +1,91 @@
1
+ module Euston
2
+ class AggregateMap < Array
3
+ def find_entry_by_type(type)
4
+ find { |a| a[:type] == type }
5
+ end
6
+
7
+ def find_entry_with_mapping_match(spec)
8
+ find { |m| m[:mappings].any_mapping_matching?(spec) }
9
+ end
10
+ end
11
+
12
+ class AggregateEntry < Hash
13
+ def find_identifier_by_type(type)
14
+ mapping = self[:mappings].find_mapping_for_type(type)
15
+ return mapping[:identifier] if mapping
16
+ nil
17
+ end
18
+ end
19
+
20
+ class MappingMap < Array
21
+ def has_mapping?(value, key = :type)
22
+ self.any? { |m| m[key] == value }
23
+ end
24
+
25
+ def find_mapping_for_type(type)
26
+ find { |c| c[:type] == type }
27
+ end
28
+
29
+ def any_mapping_matching?(spec)
30
+ any? { |c| (c.keys & spec.keys).all? {|k| c[k] == spec[k]} }
31
+ end
32
+
33
+ def find_mapping_matching(spec)
34
+ find {|c| (c.keys & spec.keys).all? {|k| c[k] == spec[k]} }
35
+ end
36
+
37
+ def push_if_unique(mapping, type)
38
+ return if has_mapping?(type)
39
+ push(mapping)
40
+ end
41
+ end
42
+
43
+ class AggregateCommandMap
44
+ class << self
45
+ attr_reader :map #for tests
46
+
47
+ def map_command_as_aggregate_constructor(type, command, identifier, to_i = [])
48
+ @map ||= AggregateMap.new
49
+ mapping = { :kind => :construct, :type => command, :identifier => identifier, :to_i => to_i }
50
+ if ( aggregate_entry = @map.find_entry_by_type(type) )
51
+ aggregate_entry[:mappings].push_if_unique(mapping, command)
52
+ else
53
+ @map << AggregateEntry.new.merge!( :type => type, :mappings => MappingMap.new.push(mapping) )
54
+ end
55
+ end
56
+
57
+ def map_command_as_aggregate_method(type, command, identifier, to_i = [])
58
+ mapping = { :kind => :consume, :type => command, :identifier => identifier, :to_i => to_i }
59
+ aggregate_entry = @map.find_entry_by_type(type)
60
+ aggregate_entry[:mappings].push_if_unique(mapping, command)
61
+ end
62
+
63
+ def deliver_command(headers, command)
64
+ args = [headers, command]
65
+ query = {:kind => :construct, :type => headers.type}
66
+ if (entry = @map.find_entry_with_mapping_match( query ))
67
+ aggregate = load_aggregate(entry, *args) || create_aggregate(entry, *args)
68
+ else
69
+ query[:kind] = :consume
70
+ entry = @map.find_entry_with_mapping_match( query )
71
+ return unless entry
72
+ aggregate = load_aggregate(entry, *args)
73
+ end
74
+ aggregate.consume_command( headers, command )
75
+ end
76
+
77
+ private
78
+
79
+ def create_aggregate(map_entry, headers, command)
80
+ identifier = map_entry.find_identifier_by_type(headers.type)
81
+ aggregate_id = command[identifier] || Euston.uuid.generate
82
+ map_entry[:type].new(aggregate_id)
83
+ end
84
+
85
+ def load_aggregate(map_entry, headers, command)
86
+ identifier = map_entry.find_identifier_by_type(headers.type)
87
+ Repository.find(map_entry[:type], command[identifier])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,137 @@
1
+ module Euston
2
+ module AggregateRoot
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def applies event, version, &consumer
7
+ define_method "__consume__#{event}__v#{version}" do |*args| instance_exec *args, &consumer end
8
+ end
9
+
10
+ def consumes *arguments, &consumer #*args is an array of symbols plus an optional options hash at the end
11
+ commands, options = [], {}
12
+ while (arg = arguments.shift) do
13
+ commands << arg if arg.is_a?(Symbol)
14
+ options = arg if arg.is_a?(Hash)
15
+ end
16
+ commands.each do |command|
17
+ define_method "__consume__#{command}" do |*args| instance_exec *args, &consumer end
18
+
19
+ map_command :map_command_as_aggregate_method, self, command, options
20
+ end
21
+ end
22
+
23
+ def created_by command, options = {}, &consumer
24
+ define_method "__consume__#{command}" do |*args| instance_exec *args, &consumer end
25
+
26
+ map_command :map_command_as_aggregate_constructor, self, command, options
27
+ end
28
+
29
+ def hydrate(stream)
30
+ instance = self.new
31
+ instance.send :reconstitute_from_history, stream
32
+ instance
33
+ end
34
+
35
+ private
36
+
37
+ def map_command(entry_point, type, command, opts)
38
+ id = opts.has_key?(:id) ? opts[:id] : :id
39
+ to_i = opts.key?(:to_i) ? opts[:to_i] : []
40
+
41
+ Euston::AggregateCommandMap.send entry_point, type, command, id, to_i
42
+ end
43
+ end
44
+
45
+ module InstanceMethods
46
+ def initialize aggregate_id = nil
47
+ @aggregate_id = aggregate_id unless aggregate_id.nil?
48
+ end
49
+
50
+ attr_reader :aggregate_id
51
+
52
+ def initial_version
53
+ @initial_version ||= 0
54
+ end
55
+
56
+ def has_uncommitted_changes?
57
+ !uncommitted_events.empty?
58
+ end
59
+
60
+ def committed_commands
61
+ @committed_commands ||= []
62
+ end
63
+
64
+ def uncommitted_events
65
+ @uncommitted_events ||= []
66
+ end
67
+
68
+ def consume_command(headers, command)
69
+ headers = Euston::CommandHeaders.from_hash(headers) if headers.is_a?(Hash)
70
+ return if committed_commands.include? headers.id
71
+
72
+ @current_headers = headers
73
+ @current_command = command
74
+
75
+ handle_command headers, command
76
+ self
77
+ end
78
+
79
+ def replay_event(headers, event)
80
+ headers = Euston::EventHeaders.from_hash(headers) if headers.is_a?(Hash)
81
+ command = headers.command
82
+ committed_commands << command[:id] unless command.nil? || committed_commands.include?(command[:id])
83
+
84
+ handle_event headers, event
85
+ @initial_version = initial_version + 1
86
+ end
87
+
88
+ def version
89
+ initial_version + uncommitted_events.length
90
+ end
91
+
92
+ protected
93
+
94
+ def apply_event(type, version, body = {})
95
+ event = Euston::EventStore::EventMessage.new(body.is_a?(Hash) ? body : body.marshal_dump)
96
+ event.headers.merge! :id => Euston.uuid.generate,
97
+ :type => type,
98
+ :version => version,
99
+ :timestamp => Time.now.to_f
100
+
101
+ unless @current_headers.nil?
102
+ event.headers.merge! :command => @current_headers.to_hash.merge(:body => @current_command)
103
+ end
104
+
105
+ handle_event Euston::EventHeaders.from_hash(event.headers), event.body
106
+ uncommitted_events << event
107
+ end
108
+
109
+ def handle_command(headers, command)
110
+ name = "__consume__#{headers.type}"
111
+ method(name).call OpenStruct.new(command).freeze
112
+ end
113
+
114
+ def handle_event(headers, event)
115
+ name = "__consume__#{headers.type}__v#{headers.version}"
116
+ if respond_to? name.to_sym
117
+ method(name).call OpenStruct.new(event).freeze
118
+ else
119
+ raise "Couldn't find an event handler for #{headers.type} (v#{headers.version}) on #{self.class}. Did you forget an 'applies' block?"
120
+ end
121
+ end
122
+
123
+ def reconstitute_from_history(stream)
124
+ events = stream.committed_events
125
+ return if events.empty?
126
+
127
+ raise "This aggregate cannot apply a historical event stream because it is not empty." unless uncommitted_events.empty? && initial_version == 0
128
+
129
+ @aggregate_id = stream.stream_id
130
+
131
+ events.each_with_index do |event, i|
132
+ replay_event Euston::EventHeaders.from_hash(event.headers), event.body
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,10 @@
1
+ module Euston
2
+ module CommandBus
3
+ def self.publish headers, command
4
+ aggregate = AggregateCommandMap.deliver_command headers, command
5
+ raise "No aggregate found to handle command: #{headers} #{command}" if aggregate.nil?
6
+
7
+ Repository.save aggregate
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ module Euston
2
+ module CommandHandler
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def version number, &consumer
7
+ define_method "__version__#{number}" do |*args|
8
+ if block_given?
9
+ instance_exec *args, &consumer
10
+ else
11
+ publish args.shift, args.shift
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ protected
19
+
20
+ def publish headers, command
21
+ Euston::CommandBus.publish headers, command
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module Euston
2
+ class CommandHeaders
3
+ attr_reader :id, :type, :version, :log_completion
4
+
5
+ def initialize id, type, version, log_completion = false
6
+ @id = id
7
+ @type = type
8
+ @version = version
9
+ @log_completion = log_completion
10
+ end
11
+
12
+ def to_hash
13
+ {
14
+ :id => id,
15
+ :type => type,
16
+ :version => version,
17
+ :log_completion => log_completion
18
+ }
19
+ end
20
+
21
+ def self.from_hash hash
22
+ self.new hash[:id], hash[:type].to_sym, hash[:version], ( hash[:log_completion] || false )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module Euston
2
+ module EventHandler
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def consumes type, version, &consumer
7
+ define_method "__event_handler__#{type}__#{version}" do |*args|
8
+ instance_exec *args, &consumer
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Euston
2
+ class EventHeaders
3
+ attr_reader :id, :type, :version, :timestamp, :command
4
+
5
+ def initialize id, type, version, timestamp = Time.now, command = nil
6
+ @id = id
7
+ @type = type
8
+ @version = version
9
+ @timestamp = Time.at(timestamp).utc
10
+ @command = command
11
+ end
12
+
13
+ def self.from_hash hash
14
+ self.new hash[:id], hash[:type].to_sym, hash[:version], hash[:timestamp], hash[:command]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Euston
2
+ module Repository
3
+ class << self
4
+ attr_accessor :event_store
5
+
6
+ def find type, id
7
+ stream = event_store.open_stream :stream_id => id
8
+ return nil if stream.committed_events.empty?
9
+
10
+ type.hydrate stream
11
+ end
12
+
13
+ def save aggregate
14
+ stream = event_store.open_stream :stream_id => aggregate.aggregate_id
15
+ aggregate.uncommitted_events.each { |e| stream << e }
16
+ stream.commit_changes Euston.uuid.generate
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Euston
2
+ VERSION = "1.0.0"
3
+ end
data/lib/euston.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'active_support/concern'
2
+ require 'require_all'
3
+ require 'ostruct'
4
+
5
+ module Euston
6
+ class << self
7
+ attr_accessor :uuid, :logger
8
+ end
9
+ end
10
+
11
+ if RUBY_PLATFORM.to_s == 'java'
12
+ module Uuid
13
+ def self.generate
14
+ Java::JavaUtil::UUID.randomUUID().toString()
15
+ end
16
+ end
17
+ else
18
+ require 'uuid'
19
+ Uuid = UUID.new
20
+ end
21
+
22
+ Euston.uuid = Uuid
23
+
24
+ require 'euston-eventstore'
25
+ require_rel 'euston'
@@ -0,0 +1,101 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ module Euston
4
+
5
+ describe "AggregateCommandMap" do
6
+ let(:guid1) {Euston.uuid.generate}
7
+ let(:guid2) {Euston.uuid.generate}
8
+
9
+ let(:command_cw) { { :headers => CommandHeaders.new(Euston.uuid.generate, :create_widget, 1),
10
+ :body => { :id => guid1} } }
11
+ let(:command_iw) { { :headers => CommandHeaders.new(Euston.uuid.generate, :import_widget, 1),
12
+ :body => { :id => guid1, :imported_count => 5 } } }
13
+ let(:command_aw) { { :headers => CommandHeaders.new(Euston.uuid.generate, :log_access_to_widget, 1),
14
+ :body => { :widget_id => guid1 } } }
15
+ let(:command_cp) { { :headers => CommandHeaders.new(Euston.uuid.generate, :create_product, 1),
16
+ :body => { :id => guid2} } }
17
+ let(:command_ip) { { :headers => CommandHeaders.new(Euston.uuid.generate, :import_product, 1),
18
+ :body => { :id => guid2, :imported_count => 5 } } }
19
+ let(:command_ap) { { :headers => CommandHeaders.new(Euston.uuid.generate, :log_access_to_product, 1),
20
+ :body => { :product_id => guid2 } } }
21
+
22
+ describe "when creating new Aggregates" do
23
+ let(:aggregate) { Sample::Widget.new( Euston.uuid.generate ) }
24
+ let(:aggregate2) { Sample::Product.new( Euston.uuid.generate ) }
25
+
26
+ it "then side effects are seen" do
27
+
28
+ aggregate.committed_commands.should have(0).items
29
+ aggregate2.committed_commands.should have(0).items
30
+
31
+ Sample::Widget.new( Euston.uuid.generate )
32
+
33
+ AggregateCommandMap.map.should have(2).items
34
+
35
+ #ap AggregateCommandMap.map
36
+
37
+ entry1 = AggregateCommandMap.map[0]
38
+ entry1[:type].should eql(Euston::Sample::Widget)
39
+ entry1[:mappings].should have(3).items
40
+
41
+ entry2 = AggregateCommandMap.map[1]
42
+ entry2[:type].should eql(Euston::Sample::Product)
43
+ entry2[:mappings].should have(3).items
44
+
45
+ end
46
+ end
47
+
48
+ describe "when consuming commands with first constructor" do
49
+ it "is followed by a consumes command" do
50
+
51
+ results = {}
52
+ Repository.stub(:find) do |type, id|
53
+ results[id]
54
+ end
55
+
56
+ aggregate = AggregateCommandMap.deliver_command command_cw[:headers], command_cw[:body]
57
+ results[aggregate.aggregate_id] = aggregate
58
+ aggregate.uncommitted_events.should have(1).item
59
+
60
+ aggregate2 = AggregateCommandMap.deliver_command command_aw[:headers], command_aw[:body]
61
+ aggregate2.uncommitted_events.should have(2).items
62
+
63
+ end
64
+ end
65
+
66
+ describe "when consuming commands with the second constructor" do
67
+ it "is followed by a consumes command" do
68
+
69
+ results = {}
70
+ Repository.stub(:find) do |type, id|
71
+ results[id]
72
+ end
73
+
74
+ aggregate = AggregateCommandMap.deliver_command command_iw[:headers], command_iw[:body]
75
+ results[aggregate.aggregate_id] = aggregate
76
+ aggregate.uncommitted_events.should have(1).item
77
+
78
+ aggregate2 = AggregateCommandMap.deliver_command command_aw[:headers], command_aw[:body]
79
+ aggregate2.uncommitted_events.should have(2).items
80
+ end
81
+ end
82
+
83
+ describe "when consuming commands with the two constructs" do
84
+ it "is followed by a consumes command" do
85
+
86
+ results = {}
87
+ Repository.stub(:find) do |type, id|
88
+ results[id]
89
+ end
90
+
91
+ aggregate = AggregateCommandMap.deliver_command command_iw[:headers], command_iw[:body]
92
+ results[aggregate.aggregate_id] = aggregate
93
+ aggregate.uncommitted_events.should have(1).item
94
+ AggregateCommandMap.deliver_command command_iw[:headers], command_iw[:body]
95
+ AggregateCommandMap.deliver_command command_aw[:headers], command_aw[:body]
96
+ aggregate.uncommitted_events.should have(3).items
97
+ end
98
+ end
99
+ end
100
+
101
+ end
@@ -0,0 +1,60 @@
1
+ module Euston
2
+ module Sample
3
+ class Widget
4
+ include Euston::AggregateRoot
5
+
6
+ created_by :create_widget do |command|
7
+ apply_event :widget_created, 1, command
8
+ end
9
+
10
+ created_by :import_widget do |command|
11
+ apply_event :widget_imported, 1, :access_count => (@access_count || 0) + command.imported_count
12
+ end
13
+
14
+ consumes :log_access_to_widget, :id => :widget_id do |command|
15
+ apply_event :widget_access_logged, 1, :widget_id => command.widget_id,
16
+ :access_count => @access_count + 1
17
+ end
18
+
19
+ applies :widget_created, 1 do |event|
20
+ @access_count = 0
21
+ end
22
+
23
+ applies :widget_imported, 1 do |event|
24
+ @access_count = event.access_count
25
+ end
26
+
27
+ applies :widget_access_logged, 1 do |event|
28
+ @access_count = event.access_count
29
+ end
30
+ end
31
+
32
+ class Product
33
+ include Euston::AggregateRoot
34
+
35
+ created_by :create_product do |command|
36
+ apply_event :product_created, 1, command
37
+ end
38
+
39
+ created_by :import_product do |command|
40
+ apply_event :product_imported, 1, :access_count => command.imported_count
41
+ end
42
+
43
+ consumes :log_access_to_product, :id => :product_id do |command|
44
+ apply_event :product_access_logged, 1, :product_id => command.product_id,
45
+ :access_count => @access_count + 1
46
+ end
47
+ applies :product_created, 1 do |event|
48
+ @access_count = 0
49
+ end
50
+
51
+ applies :product_imported, 1 do |event|
52
+ @access_count = event.access_count
53
+ end
54
+
55
+ applies :product_access_logged, 1 do |event|
56
+ @access_count = event.access_count
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ module Euston
4
+ describe 'aggregate root' do
5
+ context 'duplicate command consumption' do
6
+ let(:aggregate) { Sample::Widget.new }
7
+ let(:aggregate2) { Sample::Widget.new }
8
+ let(:command) { { :headers => CommandHeaders.new(Euston.uuid.generate, :create_widget, 1),
9
+ :body => { :id => Euston.uuid.generate } } }
10
+
11
+ it 'does not handle the same command twice' do
12
+ aggregate.consume_command command[:headers], command[:body]
13
+ aggregate.uncommitted_events.should have(1).item
14
+
15
+ aggregate.uncommitted_events.each { |e| aggregate2.replay_event e.headers, e.body }
16
+
17
+ aggregate2.consume_command command[:headers], command[:body]
18
+ aggregate2.uncommitted_events.should have(0).items
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,2 @@
1
+ require 'euston'
2
+ require 'aggregate_root_samples'
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: euston
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lee Henson
9
+ - Guy Boertje
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-09-15 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: &69591450 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.9
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *69591450
26
+ - !ruby/object:Gem::Dependency
27
+ name: euston-eventstore
28
+ requirement: &69591000 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *69591000
37
+ - !ruby/object:Gem::Dependency
38
+ name: require_all
39
+ requirement: &69584600 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 1.2.0
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *69584600
48
+ - !ruby/object:Gem::Dependency
49
+ name: fuubar
50
+ requirement: &69584040 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 0.0.0
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *69584040
59
+ - !ruby/object:Gem::Dependency
60
+ name: rspec
61
+ requirement: &69583690 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 2.6.0
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *69583690
70
+ - !ruby/object:Gem::Dependency
71
+ name: uuid
72
+ requirement: &69583330 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 2.3.0
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: *69583330
81
+ description: ''
82
+ email:
83
+ - lee.m.henson@gmail.com
84
+ - guyboertje@gmail.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - Rakefile
90
+ - euston.gemspec
91
+ - lib/euston.rb
92
+ - lib/euston/aggregate_command_map.rb
93
+ - lib/euston/aggregate_root.rb
94
+ - lib/euston/command_bus.rb
95
+ - lib/euston/command_handler.rb
96
+ - lib/euston/command_headers.rb
97
+ - lib/euston/event_handler.rb
98
+ - lib/euston/event_headers.rb
99
+ - lib/euston/repository.rb
100
+ - lib/euston/version.rb
101
+ - spec/aggregate_command_map_spec.rb
102
+ - spec/aggregate_root_samples.rb
103
+ - spec/aggregate_root_spec.rb
104
+ - spec/spec_helper.rb
105
+ homepage: http://github.com/leemhenson/euston
106
+ licenses: []
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ! '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 1.8.10
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: Cqrs tooling.
129
+ test_files: []