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 +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
|
+
...
|