ruby_event_store 2.0.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/lib/ruby_event_store/client.rb +37 -6
  4. data/lib/ruby_event_store/errors.rb +1 -0
  5. data/lib/ruby_event_store/event.rb +5 -2
  6. data/lib/ruby_event_store/in_memory_repository.rb +67 -15
  7. data/lib/ruby_event_store/instrumented_dispatcher.rb +12 -2
  8. data/lib/ruby_event_store/instrumented_repository.rb +17 -11
  9. data/lib/ruby_event_store/mappers/default.rb +1 -28
  10. data/lib/ruby_event_store/mappers/encryption_mapper.rb +2 -0
  11. data/lib/ruby_event_store/mappers/forgotten_data.rb +1 -1
  12. data/lib/ruby_event_store/mappers/in_memory_encryption_key_repository.rb +1 -1
  13. data/lib/ruby_event_store/mappers/instrumented_mapper.rb +0 -4
  14. data/lib/ruby_event_store/mappers/json_mapper.rb +7 -5
  15. data/lib/ruby_event_store/mappers/pipeline.rb +2 -26
  16. data/lib/ruby_event_store/mappers/pipeline_mapper.rb +0 -4
  17. data/lib/ruby_event_store/mappers/transformation/domain_event.rb +13 -3
  18. data/lib/ruby_event_store/mappers/transformation/upcast.rb +37 -0
  19. data/lib/ruby_event_store/metadata.rb +3 -3
  20. data/lib/ruby_event_store/projection.rb +2 -2
  21. data/lib/ruby_event_store/spec/broker_lint.rb +11 -11
  22. data/lib/ruby_event_store/spec/event_lint.rb +9 -9
  23. data/lib/ruby_event_store/spec/event_repository_lint.rb +338 -281
  24. data/lib/ruby_event_store/spec/subscriptions_lint.rb +41 -33
  25. data/lib/ruby_event_store/subscriptions.rb +24 -9
  26. data/lib/ruby_event_store/transform_keys.rb +5 -5
  27. data/lib/ruby_event_store/version.rb +1 -1
  28. data/lib/ruby_event_store.rb +43 -44
  29. metadata +26 -17
  30. data/.mutant.yml +0 -1
  31. data/CHANGELOG.md +0 -93
  32. data/Gemfile +0 -9
  33. data/Gemfile.lock +0 -118
  34. data/Makefile +0 -32
  35. data/lib/ruby_event_store/mappers/deprecated_wrapper.rb +0 -33
  36. data/lib/ruby_event_store/mappers/transformation/serialization.rb +0 -36
  37. data/ruby_event_store.gemspec +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3c88fc2b8bb6cc690ca8518b0d05e81e648969f21047aaa630c122b3d62d039
4
- data.tar.gz: 62de2a86056dfe64d42bcdb58b37054152bda5cec7e8614239ffab7ea06b14a8
3
+ metadata.gz: 9170662f3f550d541b79d403aeacfdf734a41a2c48c90752a7a35218913c0ead
4
+ data.tar.gz: 3fa3e5b0e60d839391da9669a74228847aa921903c5d5a7db35363945af5b0f9
5
5
  SHA512:
6
- metadata.gz: 5cd0b9296fdf313dd30ee7d5dc4ce3d09f83b10675da8cf735dbbab8043ee1725e13d3c29e574d3bc16f0e57b7059dc5856f2a406b444cdbb514f511b35bad32
7
- data.tar.gz: 40624c5a6101794364f0c952e78b74e127924b6aade3b53e028d7715d72b198ca58bf4a0fe2bc001220e1c4a5f580221ff686ec40994edff2abb6b4bd42b7620
6
+ metadata.gz: f753b40d9b216d372dba8692c69b855ac8bb382664f853be628e094557aa391fe1076c98a934f437158622b65a2cf6b5353e6672facadb408ae43441576f49ce
7
+ data.tar.gz: 7b909ee630098c28449eae7ca5dc0b018158ee3a8e240074a25bf71d602bb0529873487dd5d6f6f0ff495cd06b9b1ddfa3b1538fe42f91556fcd1a7cba666232
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # RubyEventStore
2
2
 
3
- A Ruby implementation of an EventStore. It is an integral part of `rails_event_store` gem.
3
+ Ruby implementation of an event store. Ships with in-memory event repository, generic instrumentation and dispatches events synchronously.
4
+
5
+ Find out more at [https://railseventstore.org](https://railseventstore.org/)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent'
3
+ require "concurrent"
4
4
 
5
5
  module RubyEventStore
6
6
  class Client
@@ -13,7 +13,7 @@ module RubyEventStore
13
13
 
14
14
 
15
15
  @repository = repository
16
- @mapper = Mappers::DeprecatedWrapper.new(mapper)
16
+ @mapper = mapper
17
17
  @subscriptions = subscriptions
18
18
  @broker = Broker.new(subscriptions: subscriptions, dispatcher: dispatcher)
19
19
  @clock = clock
@@ -64,7 +64,7 @@ module RubyEventStore
64
64
  # @param expected_version (see #publish)
65
65
  # @return [self]
66
66
  def link(event_ids, stream_name:, expected_version: :any)
67
- repository.link_to_stream(event_ids, Stream.new(stream_name), ExpectedVersion.new(expected_version))
67
+ repository.link_to_stream(Array(event_ids), Stream.new(stream_name), ExpectedVersion.new(expected_version))
68
68
  self
69
69
  end
70
70
 
@@ -94,6 +94,33 @@ module RubyEventStore
94
94
  repository.streams_of(event_id)
95
95
  end
96
96
 
97
+ # Gets position of the event in given stream
98
+ #
99
+ # The position is always nonnegative.
100
+ # Returns nil if the event has no specific position in stream.
101
+ # Raise error if event is not present in stream.
102
+ #
103
+ # @param event_id [String]
104
+ # @param stream_name [String]
105
+ # @return [Integer] nonnegative integer position of event in stream
106
+ # @raise [EventNotInStream]
107
+ def position_in_stream(event_id, stream_name)
108
+ repository.position_in_stream(event_id, Stream.new(stream_name))
109
+ end
110
+
111
+ # Gets position of the event in global stream
112
+ #
113
+ # The position is always nonnegative.
114
+ # Global position may have gaps, meaning, there may be event at
115
+ # position 40, but no event at position 39.
116
+ #
117
+ # @param event_id [String]
118
+ # @raise [EventNotFound]
119
+ # @return [Integer] nonnegno ative integer position of event in global stream
120
+ def global_position(event_id)
121
+ repository.global_position(event_id)
122
+ end
123
+
97
124
  # Subscribes a handler (subscriber) that will be invoked for published events of provided type.
98
125
  #
99
126
  # @overload subscribe(subscriber, to:)
@@ -131,8 +158,8 @@ module RubyEventStore
131
158
  #
132
159
  # @param to [Class, String] type of events to get list of sybscribed handlers
133
160
  # @return [Array<Object, Class>]
134
- def subscribers_for(event_type)
135
- subscriptions.all_for(event_type.to_s)
161
+ def subscribers_for(event_class)
162
+ subscriptions.all_for(event_type_resolver.call(event_class))
136
163
  end
137
164
 
138
165
  # Builder object for collecting temporary handlers (subscribers)
@@ -239,7 +266,7 @@ module RubyEventStore
239
266
  # @return [Event] deserialized event
240
267
  def deserialize(serializer:, event_type:, event_id:, data:, metadata:, timestamp: nil, valid_at: nil)
241
268
  extract_timestamp = lambda do |m|
242
- (m[:timestamp] || Time.parse(m.fetch('timestamp'))).iso8601
269
+ (m[:timestamp] || Time.parse(m.fetch("timestamp"))).iso8601
243
270
  end
244
271
 
245
272
  mapper.record_to_event(
@@ -328,6 +355,10 @@ module RubyEventStore
328
355
 
329
356
  protected
330
357
 
358
+ def event_type_resolver
359
+ subscriptions.event_type_resolver
360
+ end
361
+
331
362
  def metadata=(value)
332
363
  @metadata.value = value
333
364
  end
@@ -12,6 +12,7 @@ module RubyEventStore
12
12
  EventDuplicatedInStream = Class.new(Error)
13
13
  ReservedInternalName = Class.new(Error)
14
14
  InvalidHandler = Class.new(Error)
15
+ EventNotFoundInStream = Class.new(Error)
15
16
 
16
17
  class EventNotFound < Error
17
18
  attr_reader :event_id
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
3
+ require "securerandom"
4
4
 
5
5
  module RubyEventStore
6
6
 
@@ -31,7 +31,7 @@ module RubyEventStore
31
31
  # Type of event. Used when matching with subscribed handlers.
32
32
  # @return [String]
33
33
  def event_type
34
- self.class.name
34
+ metadata[:event_type] || self.class.name
35
35
  end
36
36
 
37
37
  # Timestamp from metadata
@@ -50,6 +50,7 @@ module RubyEventStore
50
50
 
51
51
  # Two events are equal if:
52
52
  # * they are of the same class
53
+ # * have identical event type
53
54
  # * have identical event id
54
55
  # * have identical data (verified with eql? method)
55
56
  #
@@ -59,6 +60,7 @@ module RubyEventStore
59
60
  # @return [TrueClass, FalseClass]
60
61
  def ==(other_event)
61
62
  other_event.instance_of?(self.class) &&
63
+ other_event.event_type.eql?(event_type) &&
62
64
  other_event.event_id.eql?(event_id) &&
63
65
  other_event.data.eql?(data)
64
66
  end
@@ -80,6 +82,7 @@ module RubyEventStore
80
82
  # We don't use metadata because == does not use metadata
81
83
  [
82
84
  self.class,
85
+ event_type,
83
86
  event_id,
84
87
  data
85
88
  ].hash ^ BIG_VALUE
@@ -1,40 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
3
+ require "ostruct"
4
4
  module RubyEventStore
5
5
  class InMemoryRepository
6
+ class UnsupportedVersionAnyUsage < StandardError
7
+ def initialize
8
+ super <<~EOS
9
+ Mixing expected version :any and specific position (or :auto) is unsupported.
10
+
11
+ Read more about expected versions here:
12
+ https://railseventstore.org/docs/v2/expected_version/
13
+ EOS
14
+ end
15
+ end
16
+
17
+ class EventInStream
18
+ def initialize(event_id, position)
19
+ @event_id = event_id
20
+ @position = position
21
+ end
22
+
23
+ attr_reader :event_id, :position
24
+ end
6
25
 
7
- def initialize(serializer: NULL)
26
+ def initialize(serializer: NULL, ensure_supported_any_usage: false)
8
27
  @serializer = serializer
9
28
  @streams = Hash.new { |h, k| h[k] = Array.new }
10
29
  @mutex = Mutex.new
11
30
  @storage = Hash.new
31
+ @ensure_supported_any_usage = ensure_supported_any_usage
12
32
  end
13
33
 
14
34
  def append_to_stream(records, stream, expected_version)
15
- serialized_records = Array(records).map{ |record| record.serialize(serializer) }
35
+ serialized_records = records.map { |record| record.serialize(serializer) }
16
36
 
17
37
  with_synchronize(expected_version, stream) do |resolved_version|
18
- raise WrongExpectedEventVersion unless last_stream_version(stream).equal?(resolved_version)
38
+ ensure_supported_any_usage(resolved_version, stream)
39
+ raise WrongExpectedEventVersion unless resolved_version.nil? || last_stream_version(stream).equal?(resolved_version)
19
40
 
20
- serialized_records.each do |serialized_record|
41
+ serialized_records.each_with_index do |serialized_record, index|
21
42
  raise EventDuplicatedInStream if has_event?(serialized_record.event_id)
22
43
  storage[serialized_record.event_id] = serialized_record
23
- streams[stream.name] << serialized_record.event_id
44
+ add_to_stream(stream, serialized_record, resolved_version, index)
24
45
  end
25
46
  end
26
47
  self
27
48
  end
28
49
 
29
50
  def link_to_stream(event_ids, stream, expected_version)
30
- serialized_records = Array(event_ids).map { |id| read_event(id) }
51
+ serialized_records = event_ids.map { |id| read_event(id) }
31
52
 
32
53
  with_synchronize(expected_version, stream) do |resolved_version|
33
- raise WrongExpectedEventVersion unless last_stream_version(stream).equal?(resolved_version)
54
+ ensure_supported_any_usage(resolved_version, stream)
55
+ raise WrongExpectedEventVersion unless resolved_version.nil? || last_stream_version(stream).equal?(resolved_version)
34
56
 
35
- serialized_records.each do |serialized_record|
57
+ serialized_records.each_with_index do |serialized_record, index|
36
58
  raise EventDuplicatedInStream if has_event_in_stream?(serialized_record.event_id, stream.name)
37
- streams[stream.name] << serialized_record.event_id
59
+ add_to_stream(stream, serialized_record, resolved_version, index)
38
60
  end
39
61
  end
40
62
  self
@@ -60,7 +82,7 @@ module RubyEventStore
60
82
  serialized_records
61
83
  .drop(offset)
62
84
  .take(limit)
63
- .map{|serialized_record| serialized_record.deserialize(serializer) }
85
+ .map { |serialized_record| serialized_record.deserialize(serializer) }
64
86
  end
65
87
  BatchEnumerator.new(spec.batch_size, serialized_records.size, batch_reader).each
66
88
  elsif spec.first?
@@ -102,6 +124,16 @@ module RubyEventStore
102
124
  .map { |name,| Stream.new(name) }
103
125
  end
104
126
 
127
+ def position_in_stream(event_id, stream)
128
+ event_in_stream = streams[stream.name].find {|event_in_stream| event_in_stream.event_id.eql?(event_id) }
129
+ raise EventNotFoundInStream if event_in_stream.nil?
130
+ event_in_stream.position
131
+ end
132
+
133
+ def global_position(event_id)
134
+ storage.keys.index(event_id) or raise EventNotFound.new(event_id)
135
+ end
136
+
105
137
  private
106
138
  def read_scope(spec)
107
139
  serialized_records = serialized_records_of_stream(spec.stream)
@@ -124,7 +156,7 @@ module RubyEventStore
124
156
  end
125
157
 
126
158
  def event_ids_of_stream(stream)
127
- streams.fetch(stream.name, Array.new)
159
+ streams.fetch(stream.name, Array.new).map(&:event_id)
128
160
  end
129
161
 
130
162
  def serialized_records_of_stream(stream)
@@ -143,7 +175,7 @@ module RubyEventStore
143
175
  end
144
176
 
145
177
  def last_stream_version(stream)
146
- event_ids_of_stream(stream).size - 1
178
+ streams.fetch(stream.name, Array.new).size - 1
147
179
  end
148
180
 
149
181
  def with_synchronize(expected_version, stream, &block)
@@ -158,19 +190,39 @@ module RubyEventStore
158
190
  # not for the whole read+write algorithm.
159
191
  Thread.pass
160
192
  mutex.synchronize do
161
- resolved_version = last_stream_version(stream) if expected_version.any?
162
193
  block.call(resolved_version)
163
194
  end
164
195
  end
165
196
 
166
197
  def has_event_in_stream?(event_id, stream_name)
167
- streams.fetch(stream_name, Array.new).any? { |id| id.eql?(event_id) }
198
+ streams.fetch(stream_name, Array.new).any? { |event_in_stream| event_in_stream.event_id.eql?(event_id) }
168
199
  end
169
200
 
170
201
  def index_of(source, event_id)
171
202
  source.index {|item| item.event_id.eql?(event_id)}
172
203
  end
173
204
 
205
+ def compute_position(resolved_version, index)
206
+ unless resolved_version.nil?
207
+ resolved_version + index + 1
208
+ end
209
+ end
210
+
211
+ def add_to_stream(stream, serialized_record, resolved_version, index)
212
+ streams[stream.name] << EventInStream.new(serialized_record.event_id, compute_position(resolved_version, index))
213
+ end
214
+
215
+ def ensure_supported_any_usage(resolved_version, stream)
216
+ if @ensure_supported_any_usage
217
+ stream_positions = streams.fetch(stream.name, Array.new).map(&:position)
218
+ if resolved_version.nil?
219
+ raise UnsupportedVersionAnyUsage if !stream_positions.compact.empty?
220
+ else
221
+ raise UnsupportedVersionAnyUsage if stream_positions.include?(nil)
222
+ end
223
+ end
224
+ end
225
+
174
226
  attr_reader :streams, :mutex, :storage, :serializer
175
227
  end
176
228
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ruby2_keywords"
4
+
3
5
  module RubyEventStore
4
6
  class InstrumentedDispatcher
5
7
  def initialize(dispatcher, instrumentation)
@@ -13,8 +15,16 @@ module RubyEventStore
13
15
  end
14
16
  end
15
17
 
16
- def verify(subscriber)
17
- dispatcher.verify(subscriber)
18
+ ruby2_keywords def method_missing(method_name, *arguments, &block)
19
+ if respond_to?(method_name)
20
+ dispatcher.public_send(method_name, *arguments, &block)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(method_name, _include_private)
27
+ dispatcher.respond_to?(method_name)
18
28
  end
19
29
 
20
30
  private
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ruby2_keywords"
4
+
3
5
  module RubyEventStore
4
6
  class InstrumentedRepository
5
7
  def initialize(repository, instrumentation)
@@ -7,9 +9,9 @@ module RubyEventStore
7
9
  @instrumentation = instrumentation
8
10
  end
9
11
 
10
- def append_to_stream(events, stream, expected_version)
11
- instrumentation.instrument("append_to_stream.repository.rails_event_store", events: events, stream: stream) do
12
- repository.append_to_stream(events, stream, expected_version)
12
+ def append_to_stream(records, stream, expected_version)
13
+ instrumentation.instrument("append_to_stream.repository.rails_event_store", events: records, stream: stream) do
14
+ repository.append_to_stream(records, stream, expected_version)
13
15
  end
14
16
  end
15
17
 
@@ -25,14 +27,6 @@ module RubyEventStore
25
27
  end
26
28
  end
27
29
 
28
- def has_event?(event_id)
29
- repository.has_event?(event_id)
30
- end
31
-
32
- def last_stream_event(stream)
33
- repository.last_stream_event(stream)
34
- end
35
-
36
30
  def read(specification)
37
31
  instrumentation.instrument("read.repository.rails_event_store", specification: specification) do
38
32
  repository.read(specification)
@@ -57,6 +51,18 @@ module RubyEventStore
57
51
  end
58
52
  end
59
53
 
54
+ ruby2_keywords def method_missing(method_name, *arguments, &block)
55
+ if respond_to?(method_name)
56
+ repository.public_send(method_name, *arguments, &block)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def respond_to_missing?(method_name, _include_private)
63
+ repository.respond_to?(method_name)
64
+ end
65
+
60
66
  private
61
67
  attr_reader :repository, :instrumentation
62
68
  end
@@ -1,36 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
-
5
3
  module RubyEventStore
6
4
  module Mappers
7
5
  class Default < PipelineMapper
8
- UNSET = Object.new.freeze
9
-
10
- attr_reader :serializer
11
-
12
- def initialize(serializer: UNSET, events_class_remapping: {})
13
- case serializer
14
- when UNSET
15
- @serializer = YAML
16
- else
17
- warn <<~EOW
18
- Passing serializer: to #{self.class} has been deprecated.
19
-
20
- Pass it directly to the repository and the scheduler. For example:
21
-
22
- Rails.configuration.event_store = RailsEventStore::Client.new(
23
- mapper: RubyEventStore::Mappers::Default.new,
24
- repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: #{serializer}),
25
- dispatcher: RubyEventStore::ComposedDispatcher.new(
26
- RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: #{serializer}),
27
- RubyEventStore::Dispatcher.new
28
- )
29
- )
30
- EOW
31
- @serializer = serializer
32
- end
33
-
6
+ def initialize(events_class_remapping: {})
34
7
  super(Pipeline.new(
35
8
  Transformation::EventClassRemapper.new(events_class_remapping),
36
9
  Transformation::SymbolizeMetadataKeys.new,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+
3
5
  module RubyEventStore
4
6
  module Mappers
5
7
  class EncryptionMapper < PipelineMapper
@@ -3,7 +3,7 @@
3
3
  module RubyEventStore
4
4
  module Mappers
5
5
  class ForgottenData
6
- FORGOTTEN_DATA = 'FORGOTTEN_DATA'.freeze
6
+ FORGOTTEN_DATA = "FORGOTTEN_DATA".freeze
7
7
 
8
8
  def initialize(string = FORGOTTEN_DATA)
9
9
  @string = string