ruby_event_store 2.0.0 → 2.3.0

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