Skip to content

Commit 7a2b523

Browse files
authored
Events mappers / serialisers proposal (#168)
* wip: extracted creating and building of the event to mapper * fix for accidential change. * small refactor to avoid code duplicity * moved back responsibility of Event record creation back to repository * removed DI from YAML mapper, following the code review feedback * moved nil check back to repository * refactored hash to event factorization * cleanup of code style * rename methods to show intent * moving condition to specific use case method * cleanup * SerializedRecord contract introduced * fixed remapping mapper example * added serialized record * move of mapper to ruby event store * added missing spec, enforced contract of configuration * cleanup in contract validation * added constantize via activesupport gem * cleanup all constantizer * improved specs regards to mutant feedback * refactored based on code review feedback * refactored based on code review feedback * refactored based on code review feedback * refactored based on code review feedback * adjusted styling in rspec * adjusted styling in rspec * fixed spec following mutant feedback * fix for specs following mutant feedback
1 parent 2e2c08b commit 7a2b523

File tree

9 files changed

+218
-25
lines changed

9 files changed

+218
-25
lines changed

rails_event_store_active_record/lib/rails_event_store_active_record/event.rb

-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ module RailsEventStoreActiveRecord
44
class Event < ::ActiveRecord::Base
55
self.primary_key = :id
66
self.table_name = 'event_store_events'
7-
serialize :metadata
8-
serialize :data
97
end
108

119
class EventInStream < ::ActiveRecord::Base

rails_event_store_active_record/lib/rails_event_store_active_record/event_repository.rb

+30-22
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ class EventRepository
66

77
POSITION_SHIFT = 1
88

9-
def initialize
9+
def initialize(mapper: RubyEventStore::Mappers::Default.new)
1010
verify_correct_schema_present
11+
@mapper = mapper
1112
end
1213

1314
def append_to_stream(events, stream_name, expected_version)
@@ -32,12 +33,7 @@ def append_to_stream(events, stream_name, expected_version)
3233
position = unless expected_version.equal?(:any)
3334
expected_version + index + POSITION_SHIFT
3435
end
35-
Event.create!(
36-
id: event.event_id,
37-
data: event.data,
38-
metadata: event.metadata,
39-
event_type: event.class,
40-
)
36+
build_event_record(event).save!
4137
events = [{
4238
stream: RubyEventStore::GLOBAL_STREAM,
4339
position: nil,
@@ -69,9 +65,8 @@ def has_event?(event_id)
6965
end
7066

7167
def last_stream_event(stream_name)
72-
build_event_entity(
73-
EventInStream.where(stream: stream_name).order('position DESC, id DESC').first
74-
)
68+
record = EventInStream.where(stream: stream_name).order('position DESC, id DESC').first
69+
record && build_event_instance(record)
7570
end
7671

7772
def read_events_forward(stream_name, after_event_id, count)
@@ -82,7 +77,7 @@ def read_events_forward(stream_name, after_event_id, count)
8277
end
8378

8479
stream.preload(:event).order('position ASC, id ASC').limit(count)
85-
.map(&method(:build_event_entity))
80+
.map(&method(:build_event_instance))
8681
end
8782

8883
def read_events_backward(stream_name, before_event_id, count)
@@ -93,17 +88,17 @@ def read_events_backward(stream_name, before_event_id, count)
9388
end
9489

9590
stream.preload(:event).order('position DESC, id DESC').limit(count)
96-
.map(&method(:build_event_entity))
91+
.map(&method(:build_event_instance))
9792
end
9893

9994
def read_stream_events_forward(stream_name)
10095
EventInStream.preload(:event).where(stream: stream_name).order('position ASC, id ASC')
101-
.map(&method(:build_event_entity))
96+
.map(&method(:build_event_instance))
10297
end
10398

10499
def read_stream_events_backward(stream_name)
105100
EventInStream.preload(:event).where(stream: stream_name).order('position DESC, id DESC')
106-
.map(&method(:build_event_entity))
101+
.map(&method(:build_event_instance))
107102
end
108103

109104
def read_all_streams_forward(after_event_id, count)
@@ -114,7 +109,7 @@ def read_all_streams_forward(after_event_id, count)
114109
end
115110

116111
stream.preload(:event).order('id ASC').limit(count)
117-
.map(&method(:build_event_entity))
112+
.map(&method(:build_event_instance))
118113
end
119114

120115
def read_all_streams_backward(before_event_id, count)
@@ -125,24 +120,37 @@ def read_all_streams_backward(before_event_id, count)
125120
end
126121

127122
stream.preload(:event).order('id DESC').limit(count)
128-
.map(&method(:build_event_entity))
123+
.map(&method(:build_event_instance))
129124
end
130125

131126
private
132127

128+
attr_reader :mapper
129+
133130
def detect_pkey_index_violated(e)
134131
e.message.include?("for key 'PRIMARY'") || # MySQL
135132
e.message.include?("event_store_events_pkey") || # Postgresql
136133
e.message.include?("event_store_events.id") # Sqlite3
137134
end
138135

139-
def build_event_entity(record)
140-
return nil unless record
141-
record.event.event_type.constantize.new(
142-
event_id: record.event.id,
143-
metadata: record.event.metadata,
144-
data: record.event.data
136+
def build_event_record(event)
137+
serialized_record = mapper.event_to_serialized_record(event)
138+
Event.new(
139+
id: serialized_record.event_id,
140+
data: serialized_record.data,
141+
metadata: serialized_record.metadata,
142+
event_type: serialized_record.event_type
143+
)
144+
end
145+
146+
def build_event_instance(record)
147+
serialized_record = RubyEventStore::SerializedRecord.new(
148+
event_id: record.event.id,
149+
metadata: record.event.metadata,
150+
data: record.event.data,
151+
event_type: record.event.event_type
145152
)
153+
mapper.serialized_record_to_event(serialized_record)
146154
end
147155

148156
def normalize_to_array(events)

ruby_event_store/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test: ## Run tests
1010

1111
mutate: test ## Run mutation tests
1212
@echo "Running mutation tests - only 100% free mutation will be accepted"
13-
@bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*" --ignore-subject "RubyEventStore.const_missing" --ignore-subject "RubyEventStore::InMemoryRepository#append_with_synchronize" --ignore-subject "RubyEventStore::InMemoryRepository#normalize_to_array" --ignore-subject "RubyEventStore::Client#normalize_to_array"
13+
@bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*" --ignore-subject "RubyEventStore.const_missing" --ignore-subject "RubyEventStore::InMemoryRepository#append_with_synchronize" --ignore-subject "RubyEventStore::InMemoryRepository#normalize_to_array" --ignore-subject "RubyEventStore::Client#normalize_to_array" --ignore-subject "RubyEventStore::SerializedRecord"
1414

1515
build:
1616
@gem build -V ruby_event_store.gemspec

ruby_event_store/lib/ruby_event_store.rb

+2
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@
77
require 'ruby_event_store/client'
88
require 'ruby_event_store/event'
99
require 'ruby_event_store/deprecations'
10+
require 'ruby_event_store/serialized_record'
11+
require 'ruby_event_store/mappers/default'
1012
require 'ruby_event_store/version'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require 'yaml'
2+
require 'active_support'
3+
4+
module RubyEventStore
5+
module Mappers
6+
class Default
7+
def initialize(serializer: YAML, events_class_remapping: {})
8+
@serializer = serializer
9+
@events_class_remapping = events_class_remapping
10+
end
11+
12+
def event_to_serialized_record(domain_event)
13+
SerializedRecord.new(
14+
event_id: domain_event.event_id,
15+
metadata: serializer.dump(domain_event.metadata),
16+
data: serializer.dump(domain_event.data),
17+
event_type: domain_event.class.name
18+
)
19+
end
20+
21+
def serialized_record_to_event(record)
22+
event_type = events_class_remapping.fetch(record.event_type) { record.event_type }
23+
ActiveSupport::Inflector.constantize(event_type).new(
24+
event_id: record.event_id,
25+
metadata: serializer.load(record.metadata),
26+
data: serializer.load(record.data)
27+
)
28+
end
29+
30+
private
31+
32+
attr_reader :serializer, :events_class_remapping
33+
end
34+
end
35+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module RubyEventStore
2+
class SerializedRecord
3+
StringsRequired = Class.new(StandardError)
4+
def initialize(event_id:, data:, metadata:, event_type:)
5+
raise StringsRequired unless [event_id, data, metadata, event_type].all? { |v| v.instance_of?(String) }
6+
@event_id = event_id
7+
@data = data
8+
@metadata = metadata
9+
@event_type = event_type
10+
freeze
11+
end
12+
13+
attr_reader :event_id, :data, :metadata, :event_type
14+
end
15+
end

ruby_event_store/ruby_event_store.gemspec

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
1919
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
2020
spec.require_paths = ['lib']
2121

22+
23+
spec.add_dependency 'activesupport'
2224
spec.add_development_dependency 'bundler', '~> 1.15'
2325
spec.add_development_dependency 'rake', '~> 10.0'
2426
spec.add_development_dependency 'rspec', '~> 3.6'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
SomethingHappened = Class.new(RubyEventStore::Event)
2+
3+
module RubyEventStore
4+
module Mappers
5+
RSpec.describe Default do
6+
let(:data) { {some_attribute: 5} }
7+
let(:metadata) { {some_meta: 1} }
8+
let(:event_id) { SecureRandom.uuid }
9+
let(:domain_event) { SomethingHappened.new(data: data, metadata: metadata, event_id: event_id) }
10+
11+
specify '#event_to_serialized_record returns YAML serialized record' do
12+
record = subject.event_to_serialized_record(domain_event)
13+
expect(record).to be_a SerializedRecord
14+
expect(record.event_id).to eq event_id
15+
expect(record.data).to eq "---\n:some_attribute: 5\n"
16+
expect(record.metadata).to eq "---\n:some_meta: 1\n"
17+
expect(record.event_type).to eq "SomethingHappened"
18+
end
19+
20+
specify '#serialized_record_to_event returns event instance' do
21+
record = SerializedRecord.new(
22+
event_id: domain_event.event_id,
23+
data: "---\n:some_attribute: 5\n",
24+
metadata: "---\n:some_meta: 1\n",
25+
event_type: SomethingHappened.name
26+
)
27+
event = subject.serialized_record_to_event(record)
28+
expect(event).to eq(domain_event)
29+
expect(event.event_id).to eq domain_event.event_id
30+
expect(event.data).to eq(data)
31+
expect(event.metadata).to eq(metadata)
32+
end
33+
34+
specify '#serialized_record_to_event its using events class remapping' do
35+
subject = described_class.new(events_class_remapping: {'EventNameBeforeRefactor' => 'SomethingHappened'})
36+
record = SerializedRecord.new(
37+
event_id: domain_event.event_id,
38+
data: "---\n:some_attribute: 5\n",
39+
metadata: "---\n:some_meta: 1\n",
40+
event_type: "EventNameBeforeRefactor"
41+
)
42+
event = subject.serialized_record_to_event(record)
43+
expect(event).to eq(domain_event)
44+
end
45+
46+
context 'when custom serializer is provided' do
47+
class ExampleYamlSerializer
48+
def self.load(value)
49+
YAML.load(decrypt(value))
50+
end
51+
52+
def self.dump(value)
53+
encrypt(YAML.dump(value))
54+
end
55+
56+
private
57+
58+
def self.encrypt(value)
59+
value.reverse
60+
end
61+
62+
def self.decrypt(value)
63+
value.reverse
64+
end
65+
end
66+
67+
let(:custom_serializer) { ExampleYamlSerializer }
68+
subject { described_class.new(serializer: custom_serializer) }
69+
70+
specify '#event_to_serialized_record returns serialized record' do
71+
record = subject.event_to_serialized_record(domain_event)
72+
expect(record).to be_a SerializedRecord
73+
expect(record.event_id).to eq event_id
74+
expect(record.data).to eq "\n5 :etubirtta_emos:\n---"
75+
expect(record.metadata).to eq "\n1 :atem_emos:\n---"
76+
expect(record.event_type).to eq "SomethingHappened"
77+
end
78+
79+
specify '#serialized_record_to_event returns event instance' do
80+
record = SerializedRecord.new(
81+
event_id: domain_event.event_id,
82+
data: "\n5 :etubirtta_emos:\n---",
83+
metadata: "\n1 :atem_emos:\n---",
84+
event_type: SomethingHappened.name
85+
)
86+
event = subject.serialized_record_to_event(record)
87+
expect(event).to eq(domain_event)
88+
expect(event.event_id).to eq domain_event.event_id
89+
expect(event.data).to eq(data)
90+
expect(event.metadata).to eq(metadata)
91+
end
92+
end
93+
end
94+
end
95+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module RubyEventStore
2+
3+
RSpec.describe SerializedRecord do
4+
let(:event_id) { "event_id" }
5+
let(:data) { "data" }
6+
let(:metadata) { "metadata" }
7+
let(:event_type) { "event_type" }
8+
9+
specify 'constructor accept all arguments and returns frozen instance' do
10+
record = described_class.new(event_id: event_id, data: data, metadata: metadata, event_type: event_type)
11+
expect(record.event_id).to be event_id
12+
expect(record.metadata).to be metadata
13+
expect(record.data).to be data
14+
expect(record.event_type).to be event_type
15+
expect(record.frozen?).to be true
16+
end
17+
18+
specify 'constructor raised SerializedRecord::StringsRequired when argument is not a String' do
19+
[[1, 1, 1, 1],
20+
[1, "string", "string", "string"],
21+
["string", 1, "string", "string"],
22+
["string", "string", 1, "string"],
23+
["string", "string", "string", 1]].each do |sample|
24+
event_id, data, metadata, event_type = sample
25+
expect do
26+
described_class.new(event_id: event_id, data: data, metadata: metadata, event_type: event_type)
27+
end.to raise_error SerializedRecord::StringsRequired
28+
end
29+
end
30+
31+
specify 'constructor raised when required args are missing' do
32+
expect do
33+
described_class.new
34+
end.to raise_error ArgumentError
35+
end
36+
end
37+
end
38+

0 commit comments

Comments
 (0)