euston 1.0.1-java
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/Rakefile +155 -0
- data/euston.gemspec +44 -0
- data/lib/euston/aggregate_command_map.rb +91 -0
- data/lib/euston/aggregate_root.rb +137 -0
- data/lib/euston/command_bus.rb +10 -0
- data/lib/euston/command_handler.rb +25 -0
- data/lib/euston/command_headers.rb +25 -0
- data/lib/euston/event_handler.rb +13 -0
- data/lib/euston/event_headers.rb +17 -0
- data/lib/euston/null_logger.rb +16 -0
- data/lib/euston/repository.rb +20 -0
- data/lib/euston/version.rb +3 -0
- data/lib/euston.rb +25 -0
- data/spec/aggregate_command_map_spec.rb +101 -0
- data/spec/aggregate_root_samples.rb +60 -0
- data/spec/aggregate_root_spec.rb +22 -0
- data/spec/spec_helper.rb +2 -0
- metadata +134 -0
data/Gemfile
ADDED
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
|
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
|
data/spec/spec_helper.rb
ADDED
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
|
+
...
|