euston 1.0.1-java

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/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+ gemspec
3
+
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,44 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'euston'
3
+ s.version = '1.0.1'
4
+ s.date = '2011-09-27'
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
+ Gemfile
15
+ Rakefile
16
+ euston.gemspec
17
+ lib/euston.rb
18
+ lib/euston/aggregate_command_map.rb
19
+ lib/euston/aggregate_root.rb
20
+ lib/euston/command_bus.rb
21
+ lib/euston/command_handler.rb
22
+ lib/euston/command_headers.rb
23
+ lib/euston/event_handler.rb
24
+ lib/euston/event_headers.rb
25
+ lib/euston/null_logger.rb
26
+ lib/euston/repository.rb
27
+ lib/euston/version.rb
28
+ spec/aggregate_command_map_spec.rb
29
+ spec/aggregate_root_samples.rb
30
+ spec/aggregate_root_spec.rb
31
+ spec/spec_helper.rb
32
+ ]
33
+ # = MANIFEST =
34
+
35
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
36
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
37
+
38
+ s.add_dependency 'activesupport', '~> 3.0.9'
39
+ s.add_dependency 'euston-eventstore', '~> 1.0.0'
40
+ s.add_dependency 'require_all', '~> 1.2.0'
41
+ s.add_development_dependency 'fuubar', '~> 0.0.0'
42
+ s.add_development_dependency 'rspec', '~> 2.6.0'
43
+ s.add_development_dependency 'uuid', '~> 2.3.0'
44
+ 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,16 @@
1
+ # extracted from Rack
2
+ # https://github.com/rack/rack/blob/master/lib/rack/nulllogger.rb
3
+
4
+ require 'singleton'
5
+
6
+ module Euston
7
+ class NullLogger
8
+ include Singleton
9
+
10
+ def info(progname = nil, &block); end
11
+ def debug(progname = nil, &block); end
12
+ def warn(progname = nil, &block); end
13
+ def error(progname = nil, &block); end
14
+ def fatal(progname = nil, &block); end
15
+ end
16
+ 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.1"
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,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: euston
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.0.1
6
+ platform: java
7
+ authors:
8
+ - Lee Henson
9
+ - Guy Boertje
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-09-27 00:00:00.000000000 +01:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activesupport
18
+ version_requirements: &2164 !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.9
23
+ none: false
24
+ requirement: *2164
25
+ prerelease: false
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ name: euston-eventstore
29
+ version_requirements: &2182 !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ none: false
35
+ requirement: *2182
36
+ prerelease: false
37
+ type: :runtime
38
+ - !ruby/object:Gem::Dependency
39
+ name: require_all
40
+ version_requirements: &2198 !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 1.2.0
45
+ none: false
46
+ requirement: *2198
47
+ prerelease: false
48
+ type: :runtime
49
+ - !ruby/object:Gem::Dependency
50
+ name: fuubar
51
+ version_requirements: &2214 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 0.0.0
56
+ none: false
57
+ requirement: *2214
58
+ prerelease: false
59
+ type: :development
60
+ - !ruby/object:Gem::Dependency
61
+ name: rspec
62
+ version_requirements: &2232 !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 2.6.0
67
+ none: false
68
+ requirement: *2232
69
+ prerelease: false
70
+ type: :development
71
+ - !ruby/object:Gem::Dependency
72
+ name: uuid
73
+ version_requirements: &2248 !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 2.3.0
78
+ none: false
79
+ requirement: *2248
80
+ prerelease: false
81
+ type: :development
82
+ description: ''
83
+ email:
84
+ - lee.m.henson@gmail.com
85
+ - guyboertje@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - Gemfile
91
+ - Rakefile
92
+ - euston.gemspec
93
+ - lib/euston.rb
94
+ - lib/euston/aggregate_command_map.rb
95
+ - lib/euston/aggregate_root.rb
96
+ - lib/euston/command_bus.rb
97
+ - lib/euston/command_handler.rb
98
+ - lib/euston/command_headers.rb
99
+ - lib/euston/event_handler.rb
100
+ - lib/euston/event_headers.rb
101
+ - lib/euston/null_logger.rb
102
+ - lib/euston/repository.rb
103
+ - lib/euston/version.rb
104
+ - spec/aggregate_command_map_spec.rb
105
+ - spec/aggregate_root_samples.rb
106
+ - spec/aggregate_root_spec.rb
107
+ - spec/spec_helper.rb
108
+ has_rdoc: true
109
+ homepage: http://github.com/leemhenson/euston
110
+ licenses: []
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ none: false
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ none: false
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 1.5.1
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: Cqrs tooling.
133
+ test_files: []
134
+ ...