ruby_event_store 0.18.2 → 0.19.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.
- checksums.yaml +4 -4
- data/Makefile +1 -2
- data/lib/ruby_event_store/client.rb +23 -33
- data/lib/ruby_event_store/errors.rb +3 -2
- data/lib/ruby_event_store/event.rb +12 -1
- data/lib/ruby_event_store/in_memory_repository.rb +45 -7
- data/lib/ruby_event_store/pub_sub/broker.rb +1 -5
- data/lib/ruby_event_store/pub_sub/dispatcher.rb +14 -3
- data/lib/ruby_event_store/spec/dispatcher_lint.rb +17 -13
- data/lib/ruby_event_store/spec/event_broker_lint.rb +9 -21
- data/lib/ruby_event_store/spec/event_repository_lint.rb +374 -52
- data/lib/ruby_event_store/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf38d679db05a58d03f5cf03128e1515a44ae1e1
|
4
|
+
data.tar.gz: ffabae418e7ec0fb99fe6b546790a155d2a1c361
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7ab222cbac66327aa236df8dc5c8f5b789854559d59a2d731914e031f1874f861ce9743b51a4d6c2a5f0351458f83b6fa0c6bbce16f887de69d600d8ee70a406
|
7
|
+
data.tar.gz: 0b89629ee2da94623d4bb04998e0e686e424d94a1a05f306e168b1d0afb51d19ffd16ccb72d989249821bf0688a991237e99ce41d3d2746cba2cc664ea8a9455
|
data/Makefile
CHANGED
@@ -10,8 +10,7 @@ test: ## Run tests
|
|
10
10
|
|
11
11
|
mutate: test ## Run mutation tests
|
12
12
|
@echo "Running mutation tests - only 100% free mutation will be accepted"
|
13
|
-
@bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*"
|
14
|
-
--ignore-subject "RubyEventStore.const_missing"
|
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"
|
15
14
|
|
16
15
|
build:
|
17
16
|
@gem build -V ruby_event_store.gemspec
|
@@ -12,16 +12,22 @@ module RubyEventStore
|
|
12
12
|
@clock = clock
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
append_to_stream(
|
17
|
-
|
15
|
+
def publish_events(events, stream_name: GLOBAL_STREAM, expected_version: :any)
|
16
|
+
append_to_stream(events, stream_name: stream_name, expected_version: expected_version)
|
17
|
+
events.each do |ev|
|
18
|
+
event_broker.notify_subscribers(ev)
|
19
|
+
end
|
18
20
|
:ok
|
19
21
|
end
|
20
22
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
def publish_event(event, stream_name: GLOBAL_STREAM, expected_version: :any)
|
24
|
+
publish_events([event], stream_name: stream_name, expected_version: expected_version)
|
25
|
+
end
|
26
|
+
|
27
|
+
def append_to_stream(events, stream_name: GLOBAL_STREAM, expected_version: :any)
|
28
|
+
events = normalize_to_array(events)
|
29
|
+
events.each{|event| enrich_event_metadata(event) }
|
30
|
+
repository.append_to_stream(events, stream_name, expected_version)
|
25
31
|
:ok
|
26
32
|
end
|
27
33
|
|
@@ -64,13 +70,13 @@ module RubyEventStore
|
|
64
70
|
end
|
65
71
|
|
66
72
|
def subscribe(subscriber, event_types, &proc)
|
67
|
-
event_broker.add_subscriber(
|
73
|
+
event_broker.add_subscriber(subscriber, event_types).tap do |unsub|
|
68
74
|
handle_subscribe(unsub, &proc)
|
69
75
|
end
|
70
76
|
end
|
71
77
|
|
72
78
|
def subscribe_to_all_events(subscriber, &proc)
|
73
|
-
event_broker.add_global_subscriber(
|
79
|
+
event_broker.add_global_subscriber(subscriber).tap do |unsub|
|
74
80
|
handle_subscribe(unsub, &proc)
|
75
81
|
end
|
76
82
|
end
|
@@ -78,8 +84,8 @@ module RubyEventStore
|
|
78
84
|
private
|
79
85
|
attr_reader :repository, :page_size, :event_broker, :metadata_proc, :clock
|
80
86
|
|
81
|
-
def
|
82
|
-
|
87
|
+
def normalize_to_array(events)
|
88
|
+
return *events
|
83
89
|
end
|
84
90
|
|
85
91
|
def enrich_event_metadata(event)
|
@@ -87,14 +93,15 @@ module RubyEventStore
|
|
87
93
|
metadata[:timestamp] ||= clock.()
|
88
94
|
metadata.merge!(metadata_proc.call || {}) if metadata_proc
|
89
95
|
|
90
|
-
event.class.new(event_id: event.event_id, metadata: metadata, data: event.data)
|
96
|
+
# event.class.new(event_id: event.event_id, metadata: metadata, data: event.data)
|
91
97
|
end
|
92
98
|
|
93
|
-
def handle_subscribe(unsub)
|
94
|
-
|
95
|
-
|
99
|
+
def handle_subscribe(unsub, &proc)
|
100
|
+
begin
|
101
|
+
proc.call
|
102
|
+
ensure
|
96
103
|
unsub.()
|
97
|
-
end
|
104
|
+
end if proc
|
98
105
|
end
|
99
106
|
|
100
107
|
class Page
|
@@ -113,22 +120,5 @@ module RubyEventStore
|
|
113
120
|
attr_reader :start, :count
|
114
121
|
end
|
115
122
|
|
116
|
-
def validate_expected_version(stream_name, expected_version)
|
117
|
-
raise InvalidExpectedVersion if expected_version.nil?
|
118
|
-
case expected_version
|
119
|
-
when :any
|
120
|
-
return
|
121
|
-
when :none
|
122
|
-
return if last_stream_event_id(stream_name).nil?
|
123
|
-
else
|
124
|
-
return if last_stream_event_id(stream_name).eql?(expected_version)
|
125
|
-
end
|
126
|
-
raise WrongExpectedEventVersion
|
127
|
-
end
|
128
|
-
|
129
|
-
def last_stream_event_id(stream_name)
|
130
|
-
last = repository.last_stream_event(stream_name)
|
131
|
-
last.event_id if last
|
132
|
-
end
|
133
123
|
end
|
134
124
|
end
|
@@ -6,10 +6,11 @@ module RubyEventStore
|
|
6
6
|
SubscriberNotExist = Class.new(StandardError)
|
7
7
|
InvalidPageStart = Class.new(ArgumentError)
|
8
8
|
InvalidPageSize = Class.new(ArgumentError)
|
9
|
+
EventDuplicatedInStream = Class.new(StandardError)
|
9
10
|
|
10
11
|
class InvalidHandler < StandardError
|
11
|
-
def initialize(
|
12
|
-
super("#call method not found in #{
|
12
|
+
def initialize(object)
|
13
|
+
super("#call method not found in #{object.inspect} subscriber. Are you sure it is a valid subscriber?")
|
13
14
|
end
|
14
15
|
end
|
15
16
|
end
|
@@ -27,6 +27,17 @@ module RubyEventStore
|
|
27
27
|
other_event.data.eql?(data)
|
28
28
|
end
|
29
29
|
|
30
|
+
BIG_VALUE = 0b111111100100000010010010110011101011000101010101001100100110000
|
31
|
+
|
32
|
+
# We don't use metadata because == does not use metadata
|
33
|
+
def hash
|
34
|
+
[
|
35
|
+
self.class,
|
36
|
+
event_id,
|
37
|
+
data
|
38
|
+
].hash ^ BIG_VALUE
|
39
|
+
end
|
40
|
+
|
30
41
|
alias_method :eql?, :==
|
31
42
|
end
|
32
|
-
end
|
43
|
+
end
|
@@ -1,24 +1,34 @@
|
|
1
1
|
require 'ostruct'
|
2
|
+
require 'thread'
|
2
3
|
|
3
4
|
module RubyEventStore
|
4
5
|
class InMemoryRepository
|
5
6
|
def initialize
|
6
7
|
@all = Array.new
|
7
8
|
@streams = Hash.new
|
9
|
+
@mutex = Mutex.new
|
8
10
|
end
|
9
11
|
|
10
|
-
def
|
12
|
+
def append_to_stream(events, stream_name, expected_version)
|
13
|
+
raise InvalidExpectedVersion if !expected_version.equal?(:any) && stream_name.eql?(GLOBAL_STREAM)
|
14
|
+
events = normalize_to_array(events)
|
11
15
|
stream = read_stream_events_forward(stream_name)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
expected_version = case expected_version
|
17
|
+
when :none
|
18
|
+
-1
|
19
|
+
when :auto, :any
|
20
|
+
stream.size - 1
|
21
|
+
when Integer
|
22
|
+
expected_version
|
23
|
+
else
|
24
|
+
raise InvalidExpectedVersion
|
25
|
+
end
|
26
|
+
append_with_synchronize(events, expected_version, stream, stream_name)
|
27
|
+
self
|
16
28
|
end
|
17
29
|
|
18
30
|
def delete_stream(stream_name)
|
19
|
-
removed = read_stream_events_forward(stream_name).map(&:event_id)
|
20
31
|
streams.delete(stream_name)
|
21
|
-
all.delete_if{|ev| removed.include?(ev.event_id)}
|
22
32
|
end
|
23
33
|
|
24
34
|
def has_event?(event_id)
|
@@ -58,6 +68,34 @@ module RubyEventStore
|
|
58
68
|
private
|
59
69
|
attr_accessor :streams, :all
|
60
70
|
|
71
|
+
def normalize_to_array(events)
|
72
|
+
return *events
|
73
|
+
end
|
74
|
+
|
75
|
+
def append_with_synchronize(events, expected_version, stream, stream_name)
|
76
|
+
# expected_version :auto assumes external lock is used
|
77
|
+
# which makes reading stream before writing safe.
|
78
|
+
#
|
79
|
+
# To emulate potential concurrency issues of :auto strategy without
|
80
|
+
# such external lock we use Thread.pass to make race
|
81
|
+
# conditions more likely. And we only use mutex.synchronize for writing
|
82
|
+
# not for the whole read+write algorithm.
|
83
|
+
Thread.pass
|
84
|
+
@mutex.synchronize do
|
85
|
+
append(events, expected_version, stream, stream_name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def append(events, expected_version, stream, stream_name)
|
90
|
+
raise WrongExpectedEventVersion unless (stream.size - 1).equal?(expected_version)
|
91
|
+
events.each do |event|
|
92
|
+
all.push(event)
|
93
|
+
raise EventDuplicatedInStream if stream.any?{|ev| ev.event_id.eql?(event.event_id) }
|
94
|
+
stream.push(event)
|
95
|
+
end
|
96
|
+
streams[stream_name] = stream
|
97
|
+
end
|
98
|
+
|
61
99
|
def read_batch(source, start_event_id, count)
|
62
100
|
return source[0..count-1] if start_event_id.equal?(:head)
|
63
101
|
start_index = index_of(source, start_event_id)
|
@@ -27,16 +27,12 @@ module RubyEventStore
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def proxy_for(klass)
|
31
|
-
dispatcher.proxy_for(klass)
|
32
|
-
end
|
33
|
-
|
34
30
|
private
|
35
31
|
attr_reader :subscribers, :dispatcher
|
36
32
|
|
37
33
|
def verify_subscriber(subscriber)
|
38
34
|
raise SubscriberNotExist if subscriber.nil?
|
39
|
-
|
35
|
+
dispatcher.verify(subscriber)
|
40
36
|
end
|
41
37
|
|
42
38
|
def subscribe(subscriber, event_types)
|
@@ -1,14 +1,25 @@
|
|
1
1
|
module RubyEventStore
|
2
2
|
module PubSub
|
3
|
+
|
3
4
|
class Dispatcher
|
4
5
|
def call(subscriber, event)
|
6
|
+
subscriber = subscriber.new if Class === subscriber
|
5
7
|
subscriber.call(event)
|
6
8
|
end
|
7
9
|
|
8
|
-
def
|
9
|
-
|
10
|
-
|
10
|
+
def verify(subscriber)
|
11
|
+
subscriber = klassify(subscriber)
|
12
|
+
subscriber.respond_to?(:call) or raise InvalidHandler.new(subscriber)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def klassify(subscriber)
|
18
|
+
Class === subscriber ? subscriber.new : subscriber
|
19
|
+
rescue ArgumentError
|
20
|
+
raise InvalidHandler.new(subscriber)
|
11
21
|
end
|
12
22
|
end
|
23
|
+
|
13
24
|
end
|
14
25
|
end
|
@@ -1,30 +1,34 @@
|
|
1
1
|
RSpec.shared_examples :dispatcher do |dispatcher|
|
2
|
-
specify "calls subscribed
|
3
|
-
handler =
|
2
|
+
specify "calls subscribed instance" do
|
3
|
+
handler = HandlerClass.new
|
4
4
|
event = instance_double(::RubyEventStore::Event)
|
5
5
|
|
6
6
|
expect(handler).to receive(:call).with(event)
|
7
7
|
dispatcher.(handler, event)
|
8
8
|
end
|
9
9
|
|
10
|
-
specify "
|
10
|
+
specify "calls subscribed class" do
|
11
11
|
event = instance_double(::RubyEventStore::Event)
|
12
12
|
|
13
|
-
|
14
|
-
expect(
|
15
|
-
dispatcher.(
|
16
|
-
expect(HandlerClass.received).to eq(event)
|
13
|
+
expect(HandlerClass).to receive(:new).and_return( h = HandlerClass.new )
|
14
|
+
expect(h).to receive(:call).with(event)
|
15
|
+
dispatcher.(HandlerClass, event)
|
17
16
|
end
|
18
17
|
|
19
|
-
specify "
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
specify "allows callable classes and instances" do
|
19
|
+
expect do
|
20
|
+
dispatcher.verify(HandlerClass)
|
21
|
+
end.not_to raise_error
|
22
|
+
expect do
|
23
|
+
dispatcher.verify(HandlerClass.new)
|
24
|
+
end.not_to raise_error
|
25
|
+
expect do
|
26
|
+
dispatcher.verify(Proc.new{ "yo" })
|
27
|
+
end.not_to raise_error
|
25
28
|
end
|
26
29
|
|
27
30
|
private
|
31
|
+
|
28
32
|
class HandlerClass
|
29
33
|
@@received = nil
|
30
34
|
def self.received
|
@@ -23,6 +23,9 @@ RSpec.shared_examples :event_broker do |broker_class|
|
|
23
23
|
@dispatched = []
|
24
24
|
end
|
25
25
|
|
26
|
+
def verify(_subscriber)
|
27
|
+
end
|
28
|
+
|
26
29
|
def call(subscriber, event)
|
27
30
|
@dispatched << {subscriber: subscriber, event: event}
|
28
31
|
end
|
@@ -58,19 +61,17 @@ RSpec.shared_examples :event_broker do |broker_class|
|
|
58
61
|
end
|
59
62
|
|
60
63
|
it 'raises error when no valid method on handler' do
|
61
|
-
message = "#call method not found " +
|
62
|
-
"in InvalidTestHandler subscriber." +
|
63
|
-
" Are you sure it is a valid subscriber?"
|
64
64
|
subscriber = InvalidTestHandler.new
|
65
|
-
expect
|
65
|
+
expect do
|
66
|
+
broker.add_subscriber(subscriber, [Test1DomainEvent])
|
67
|
+
end.to raise_error(RubyEventStore::InvalidHandler)
|
66
68
|
end
|
67
69
|
|
68
70
|
it 'raises error when no valid method on global handler' do
|
69
|
-
message = "#call method not found " +
|
70
|
-
"in InvalidTestHandler subscriber." +
|
71
|
-
" Are you sure it is a valid subscriber?"
|
72
71
|
subscriber = InvalidTestHandler.new
|
73
|
-
expect
|
72
|
+
expect do
|
73
|
+
broker.add_global_subscriber(subscriber)
|
74
|
+
end.to raise_error(RubyEventStore::InvalidHandler)
|
74
75
|
end
|
75
76
|
|
76
77
|
it 'returns lambda as an output of global subscribe methods' do
|
@@ -121,19 +122,6 @@ RSpec.shared_examples :event_broker do |broker_class|
|
|
121
122
|
expect(dispatcher.dispatched).to eq([{subscriber: handler, event: event1}])
|
122
123
|
end
|
123
124
|
|
124
|
-
it "returns callable proxy" do
|
125
|
-
proxy = broker.proxy_for(TestHandler)
|
126
|
-
expect(proxy.respond_to?(:call)).to be_truthy
|
127
|
-
end
|
128
|
-
|
129
|
-
specify "fails to build proxy when no call method defined on class" do
|
130
|
-
message = "#call method not found " +
|
131
|
-
"in InvalidTestHandler subscriber." +
|
132
|
-
" Are you sure it is a valid subscriber?"
|
133
|
-
|
134
|
-
expect { broker.proxy_for(InvalidTestHandler) }.to raise_error(::RubyEventStore::InvalidHandler, message)
|
135
|
-
end
|
136
|
-
|
137
125
|
private
|
138
126
|
class HandlerClass
|
139
127
|
@@received = nil
|
@@ -6,15 +6,249 @@ RSpec.shared_examples :event_repository do |repository_class|
|
|
6
6
|
expect(repository.read_all_streams_forward(:head, 1)).to be_empty
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
specify 'publish fail if expected version is nil' do
|
10
|
+
expect do
|
11
|
+
repository.append_to_stream(event = TestDomainEvent.new, 'stream', nil)
|
12
|
+
end.to raise_error(RubyEventStore::InvalidExpectedVersion)
|
12
13
|
end
|
13
14
|
|
14
|
-
|
15
|
+
specify 'append_to_stream returns self' do
|
16
|
+
repository.
|
17
|
+
append_to_stream(event = TestDomainEvent.new, 'stream', -1).
|
18
|
+
append_to_stream(event = TestDomainEvent.new, 'stream', 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
specify 'adds initial event to a new stream' do
|
22
|
+
repository.append_to_stream(event = TestDomainEvent.new, 'stream', :none)
|
23
|
+
expect(repository.read_all_streams_forward(:head, 1).first).to eq(event)
|
24
|
+
expect(repository.read_stream_events_forward('stream').first).to eq(event)
|
25
|
+
expect(repository.read_stream_events_forward('other_stream')).to be_empty
|
26
|
+
end
|
27
|
+
|
28
|
+
specify 'adds multiple initial events to a new stream' do
|
29
|
+
repository.append_to_stream([
|
30
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
31
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
32
|
+
], 'stream', :none)
|
33
|
+
expect(repository.read_all_streams_forward(:head, 2)).to eq([event0, event1])
|
34
|
+
expect(repository.read_stream_events_forward('stream')).to eq([event0, event1])
|
35
|
+
end
|
36
|
+
|
37
|
+
specify 'correct expected version on second write' do
|
38
|
+
repository.append_to_stream([
|
39
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
40
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
41
|
+
], 'stream', :none)
|
42
|
+
repository.append_to_stream([
|
43
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
44
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
45
|
+
], 'stream', 1)
|
46
|
+
expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
|
47
|
+
expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
|
48
|
+
end
|
49
|
+
|
50
|
+
specify 'incorrect expected version on second write' do
|
51
|
+
repository.append_to_stream([
|
52
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
53
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
54
|
+
], 'stream', :none)
|
55
|
+
expect do
|
56
|
+
repository.append_to_stream([
|
57
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
58
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
59
|
+
], 'stream', 0)
|
60
|
+
end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
|
61
|
+
|
62
|
+
expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1])
|
63
|
+
expect(repository.read_stream_events_forward('stream')).to eq([event0, event1])
|
64
|
+
end
|
65
|
+
|
66
|
+
specify ':none on first and subsequent write' do
|
67
|
+
repository.append_to_stream([
|
68
|
+
eventA = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
69
|
+
], 'stream', :none)
|
70
|
+
expect do
|
71
|
+
repository.append_to_stream([
|
72
|
+
eventB = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
73
|
+
], 'stream', :none)
|
74
|
+
end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
|
75
|
+
expect(repository.read_all_streams_forward(:head, 1)).to eq([eventA])
|
76
|
+
expect(repository.read_stream_events_forward('stream')).to eq([eventA])
|
77
|
+
end
|
78
|
+
|
79
|
+
specify ':any allows stream with best-effort order and no guarantee' do
|
80
|
+
repository.append_to_stream([
|
81
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
82
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
83
|
+
], 'stream', :any)
|
84
|
+
repository.append_to_stream([
|
85
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
86
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
87
|
+
], 'stream', :any)
|
88
|
+
expect(repository.read_all_streams_forward(:head, 4).to_set).to eq(Set.new([event0, event1, event2, event3]))
|
89
|
+
expect(repository.read_stream_events_forward('stream').to_set).to eq(Set.new([event0, event1, event2, event3]))
|
90
|
+
end
|
91
|
+
|
92
|
+
specify ':auto queries for last position in given stream' do
|
93
|
+
skip unless test_expected_version_auto
|
94
|
+
repository.append_to_stream([
|
95
|
+
eventA = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
96
|
+
eventB = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
97
|
+
eventC = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
98
|
+
], 'another', :auto)
|
99
|
+
repository.append_to_stream([
|
100
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
101
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
102
|
+
], 'stream', :auto)
|
103
|
+
repository.append_to_stream([
|
104
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
105
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
106
|
+
], 'stream', 1)
|
107
|
+
end
|
108
|
+
|
109
|
+
specify ':auto starts from 0' do
|
110
|
+
skip unless test_expected_version_auto
|
111
|
+
repository.append_to_stream([
|
112
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
113
|
+
], 'stream', :auto)
|
114
|
+
expect do
|
115
|
+
repository.append_to_stream([
|
116
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
117
|
+
], 'stream', -1)
|
118
|
+
end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
|
119
|
+
end
|
120
|
+
|
121
|
+
specify ':auto queries for last position and follows in incremental way' do
|
122
|
+
skip unless test_expected_version_auto
|
123
|
+
# It is expected that there is higher level lock
|
124
|
+
# So this query is safe from race conditions
|
125
|
+
repository.append_to_stream([
|
126
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
127
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
128
|
+
], 'stream', :auto)
|
129
|
+
repository.append_to_stream([
|
130
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
131
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
132
|
+
], 'stream', :auto)
|
133
|
+
expect(repository.read_all_streams_forward(:head, 4)).to eq([
|
134
|
+
event0, event1,
|
135
|
+
event2, event3
|
136
|
+
])
|
137
|
+
expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
|
138
|
+
end
|
139
|
+
|
140
|
+
specify ':auto is compatible with manual expectation' do
|
141
|
+
skip unless test_expected_version_auto
|
142
|
+
repository.append_to_stream([
|
143
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
144
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
145
|
+
], 'stream', :auto)
|
146
|
+
repository.append_to_stream([
|
147
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
148
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
149
|
+
], 'stream', 1)
|
150
|
+
expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
|
151
|
+
expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
|
152
|
+
end
|
153
|
+
|
154
|
+
specify 'manual is compatible with auto expectation' do
|
155
|
+
skip unless test_expected_version_auto
|
156
|
+
repository.append_to_stream([
|
157
|
+
event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
158
|
+
event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
159
|
+
], 'stream', :none)
|
160
|
+
repository.append_to_stream([
|
161
|
+
event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
162
|
+
event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
|
163
|
+
], 'stream', :auto)
|
164
|
+
expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
|
165
|
+
expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
|
166
|
+
end
|
167
|
+
|
168
|
+
specify 'unlimited concurrency for :any - everything should succeed' do
|
169
|
+
skip unless test_race_conditions_any
|
170
|
+
verify_conncurency_assumptions
|
171
|
+
begin
|
172
|
+
concurrency_level = 4
|
173
|
+
|
174
|
+
fail_occurred = false
|
175
|
+
wait_for_it = true
|
176
|
+
|
177
|
+
threads = concurrency_level.times.map do |i|
|
178
|
+
Thread.new do
|
179
|
+
true while wait_for_it
|
180
|
+
begin
|
181
|
+
100.times do |j|
|
182
|
+
eid = "0000000#{i}-#{sprintf("%04d", j)}-0000-0000-000000000000"
|
183
|
+
repository.append_to_stream([
|
184
|
+
TestDomainEvent.new(event_id: eid),
|
185
|
+
], 'stream', :any)
|
186
|
+
end
|
187
|
+
rescue RubyEventStore::WrongExpectedEventVersion
|
188
|
+
fail_occurred = true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
wait_for_it = false
|
193
|
+
threads.each(&:join)
|
194
|
+
expect(fail_occurred).to eq(false)
|
195
|
+
expect(repository.read_stream_events_forward('stream').size).to eq(400)
|
196
|
+
events_in_stream = repository.read_stream_events_forward('stream')
|
197
|
+
expect(events_in_stream.size).to eq(400)
|
198
|
+
events0 = events_in_stream.select do |ev|
|
199
|
+
ev.event_id.start_with?("0-")
|
200
|
+
end
|
201
|
+
expect(events0).to eq(events0.sort_by{|ev| ev.event_id })
|
202
|
+
ensure
|
203
|
+
cleanup_concurrency_test
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
specify 'limited concurrency for :auto - some operations will fail without outside lock, stream is ordered' do
|
208
|
+
skip unless test_expected_version_auto
|
209
|
+
skip unless test_race_conditions_auto
|
210
|
+
verify_conncurency_assumptions
|
211
|
+
begin
|
212
|
+
concurrency_level = 4
|
213
|
+
|
214
|
+
fail_occurred = 0
|
215
|
+
wait_for_it = true
|
216
|
+
|
217
|
+
threads = concurrency_level.times.map do |i|
|
218
|
+
Thread.new do
|
219
|
+
true while wait_for_it
|
220
|
+
100.times do |j|
|
221
|
+
begin
|
222
|
+
eid = "0000000#{i}-#{sprintf("%04d", j)}-0000-0000-000000000000"
|
223
|
+
repository.append_to_stream([
|
224
|
+
TestDomainEvent.new(event_id: eid),
|
225
|
+
], 'stream', :auto)
|
226
|
+
sleep(rand(concurrency_level) / 1000.0)
|
227
|
+
rescue RubyEventStore::WrongExpectedEventVersion
|
228
|
+
fail_occurred +=1
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
wait_for_it = false
|
234
|
+
threads.each(&:join)
|
235
|
+
expect(fail_occurred).to be > 0
|
236
|
+
events_in_stream = repository.read_stream_events_forward('stream')
|
237
|
+
expect(events_in_stream.size).to be < 400
|
238
|
+
expect(events_in_stream.size).to be >= 100
|
239
|
+
events0 = events_in_stream.select do |ev|
|
240
|
+
ev.event_id.start_with?("0-")
|
241
|
+
end
|
242
|
+
expect(events0).to eq(events0.sort_by{|ev| ev.event_id })
|
243
|
+
additional_limited_concurrency_for_auto_check
|
244
|
+
ensure
|
245
|
+
cleanup_concurrency_test
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'appended event is stored in given stream' do
|
15
250
|
expected_event = TestDomainEvent.new(data: {})
|
16
|
-
|
17
|
-
expect(created).to eq(expected_event)
|
251
|
+
repository.append_to_stream(expected_event, 'stream', :any)
|
18
252
|
expect(repository.read_all_streams_forward(:head, 1).first).to eq(expected_event)
|
19
253
|
expect(repository.read_stream_events_forward('stream').first).to eq(expected_event)
|
20
254
|
expect(repository.read_stream_events_forward('other_stream')).to be_empty
|
@@ -22,92 +256,180 @@ RSpec.shared_examples :event_repository do |repository_class|
|
|
22
256
|
|
23
257
|
it 'data attributes are retrieved' do
|
24
258
|
event = TestDomainEvent.new(data: { order_id: 3 })
|
25
|
-
repository.
|
259
|
+
repository.append_to_stream(event, 'stream', :any)
|
26
260
|
retrieved_event = repository.read_all_streams_forward(:head, 1).first
|
27
261
|
expect(retrieved_event.data[:order_id]).to eq(3)
|
28
262
|
end
|
29
263
|
|
30
264
|
it 'metadata attributes are retrieved' do
|
31
265
|
event = TestDomainEvent.new(metadata: { request_id: 3 })
|
32
|
-
repository.
|
266
|
+
repository.append_to_stream(event, 'stream', :any)
|
33
267
|
retrieved_event = repository.read_all_streams_forward(:head, 1).first
|
34
268
|
expect(retrieved_event.metadata[:request_id]).to eq(3)
|
35
269
|
end
|
36
270
|
|
37
271
|
it 'does not have deleted streams' do
|
38
|
-
repository.
|
39
|
-
repository.
|
40
|
-
|
41
|
-
expect(repository.read_stream_events_forward('stream').count).to eq 1
|
42
|
-
expect(repository.read_stream_events_forward('other_stream').count).to eq 1
|
43
|
-
expect(repository.read_all_streams_forward(:head, 10).count).to eq 2
|
272
|
+
repository.append_to_stream(e1 = TestDomainEvent.new, 'stream', -1)
|
273
|
+
repository.append_to_stream(e2 = TestDomainEvent.new, 'other_stream', -1)
|
44
274
|
|
45
275
|
repository.delete_stream('stream')
|
46
276
|
expect(repository.read_stream_events_forward('stream')).to be_empty
|
47
|
-
expect(repository.read_stream_events_forward('other_stream')
|
48
|
-
expect(repository.read_all_streams_forward(:head, 10)
|
277
|
+
expect(repository.read_stream_events_forward('other_stream')).to eq([e2])
|
278
|
+
expect(repository.read_all_streams_forward(:head, 10)).to eq([e1,e2])
|
49
279
|
end
|
50
280
|
|
51
281
|
it 'has or has not domain event' do
|
52
|
-
|
282
|
+
just_an_id = 'd5c134c2-db65-4e87-b6ea-d196f8f1a292'
|
283
|
+
repository.append_to_stream(TestDomainEvent.new(event_id: just_an_id), 'stream', -1)
|
53
284
|
|
54
|
-
expect(repository.has_event?(
|
285
|
+
expect(repository.has_event?(just_an_id)).to be_truthy
|
286
|
+
expect(repository.has_event?(just_an_id.clone)).to be_truthy
|
55
287
|
expect(repository.has_event?('any other id')).to be_falsey
|
56
288
|
end
|
57
289
|
|
58
290
|
it 'knows last event in stream' do
|
59
|
-
repository.
|
60
|
-
repository.
|
291
|
+
repository.append_to_stream(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000001'), 'stream', -1)
|
292
|
+
repository.append_to_stream(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000002'), 'stream', 0)
|
61
293
|
|
62
|
-
expect(repository.last_stream_event('stream')).to eq(TestDomainEvent.new(event_id: '
|
294
|
+
expect(repository.last_stream_event('stream')).to eq(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000002'))
|
63
295
|
expect(repository.last_stream_event('other_stream')).to be_nil
|
64
296
|
end
|
65
297
|
|
66
298
|
it 'reads batch of events from stream forward & backward' do
|
67
|
-
event_ids =
|
68
|
-
|
69
|
-
|
70
|
-
|
299
|
+
event_ids = ["96c920b1-cdd0-40f4-907c-861b9fff7d02", "56404f79-0ba0-4aa0-8524-dc3436368ca0", "6a54dd21-f9d8-4857-a195-f5588d9e406c", "0e50a9cd-f981-4e39-93d5-697fc7285b98", "d85589bc-b993-41d4-812f-fc631d9185d5", "96bdacda-77dd-4d7d-973d-cbdaa5842855", "94688199-e6b7-4180-bf8e-825b6808e6cc", "68fab040-741e-4bc2-9cca-5b8855b0ca19", "ab60114c-011d-4d58-ab31-7ba65d99975e", "868cac42-3d19-4b39-84e8-cd32d65c2445"]
|
300
|
+
events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
|
301
|
+
repository.append_to_stream(TestDomainEvent.new, 'other_stream', -1)
|
302
|
+
events.each.with_index do |event, index|
|
303
|
+
repository.append_to_stream(event, 'stream', index - 1)
|
71
304
|
end
|
72
|
-
repository.
|
305
|
+
repository.append_to_stream(TestDomainEvent.new, 'other_stream', 0)
|
73
306
|
|
74
|
-
expect(repository.read_events_forward('stream', :head, 3)).to eq
|
75
|
-
expect(repository.read_events_forward('stream', :head, 100)).to eq
|
76
|
-
expect(repository.read_events_forward('stream',
|
77
|
-
expect(repository.read_events_forward('stream',
|
307
|
+
expect(repository.read_events_forward('stream', :head, 3)).to eq(events.first(3))
|
308
|
+
expect(repository.read_events_forward('stream', :head, 100)).to eq(events)
|
309
|
+
expect(repository.read_events_forward('stream', events[4].event_id, 4)).to eq(events[5..8])
|
310
|
+
expect(repository.read_events_forward('stream', events[4].event_id, 100)).to eq(events[5..9])
|
78
311
|
|
79
|
-
expect(repository.read_events_backward('stream', :head, 3)).to eq
|
80
|
-
expect(repository.read_events_backward('stream', :head, 100)).to eq
|
81
|
-
expect(repository.read_events_backward('stream',
|
82
|
-
expect(repository.read_events_backward('stream',
|
312
|
+
expect(repository.read_events_backward('stream', :head, 3)).to eq(events.last(3).reverse)
|
313
|
+
expect(repository.read_events_backward('stream', :head, 100)).to eq(events.reverse)
|
314
|
+
expect(repository.read_events_backward('stream', events[4].event_id, 4)).to eq(events.first(4).reverse)
|
315
|
+
expect(repository.read_events_backward('stream', events[4].event_id, 100)).to eq(events.first(4).reverse)
|
83
316
|
end
|
84
317
|
|
85
318
|
|
86
319
|
it 'reads all stream events forward & backward' do
|
87
|
-
|
88
|
-
|
89
|
-
repository.
|
90
|
-
repository.
|
91
|
-
repository.
|
320
|
+
s1 = 'stream'
|
321
|
+
s2 = 'other_stream'
|
322
|
+
repository.append_to_stream(a = TestDomainEvent.new(event_id: '7010d298-ab69-4bb1-9251-f3466b5d1282'), s1, -1)
|
323
|
+
repository.append_to_stream(b = TestDomainEvent.new(event_id: '34f88aca-aaba-4ca0-9256-8017b47528c5'), s2, -1)
|
324
|
+
repository.append_to_stream(c = TestDomainEvent.new(event_id: '8e61c864-ceae-4684-8726-97c34eb8fc4f'), s1, 0)
|
325
|
+
repository.append_to_stream(d = TestDomainEvent.new(event_id: '30963ed9-6349-450b-ac9b-8ea50115b3bd'), s2, 0)
|
326
|
+
repository.append_to_stream(e = TestDomainEvent.new(event_id: '5bdc58b7-e8a7-4621-afd6-ccb828d72457'), s2, 1)
|
92
327
|
|
93
|
-
expect(repository.read_stream_events_forward(
|
94
|
-
expect(repository.read_stream_events_backward(
|
328
|
+
expect(repository.read_stream_events_forward(s1)).to eq [a,c]
|
329
|
+
expect(repository.read_stream_events_backward(s1)).to eq [c,a]
|
95
330
|
end
|
96
331
|
|
97
332
|
it 'reads batch of events from all streams forward & backward' do
|
98
|
-
event_ids =
|
99
|
-
event_ids.
|
100
|
-
|
333
|
+
event_ids = ["96c920b1-cdd0-40f4-907c-861b9fff7d02", "56404f79-0ba0-4aa0-8524-dc3436368ca0", "6a54dd21-f9d8-4857-a195-f5588d9e406c", "0e50a9cd-f981-4e39-93d5-697fc7285b98", "d85589bc-b993-41d4-812f-fc631d9185d5", "96bdacda-77dd-4d7d-973d-cbdaa5842855", "94688199-e6b7-4180-bf8e-825b6808e6cc", "68fab040-741e-4bc2-9cca-5b8855b0ca19", "ab60114c-011d-4d58-ab31-7ba65d99975e", "868cac42-3d19-4b39-84e8-cd32d65c2445"]
|
334
|
+
events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
|
335
|
+
events.each do |ev|
|
336
|
+
repository.append_to_stream(ev, SecureRandom.uuid, -1)
|
101
337
|
end
|
102
338
|
|
103
|
-
expect(repository.read_all_streams_forward(:head, 3)).to eq
|
104
|
-
expect(repository.read_all_streams_forward(:head, 100)).to eq
|
105
|
-
expect(repository.read_all_streams_forward(
|
106
|
-
expect(repository.read_all_streams_forward(
|
339
|
+
expect(repository.read_all_streams_forward(:head, 3)).to eq(events.first(3))
|
340
|
+
expect(repository.read_all_streams_forward(:head, 100)).to eq(events)
|
341
|
+
expect(repository.read_all_streams_forward(events[4].event_id, 4)).to eq(events[5..8])
|
342
|
+
expect(repository.read_all_streams_forward(events[4].event_id, 100)).to eq(events[5..9])
|
343
|
+
|
344
|
+
expect(repository.read_all_streams_backward(:head, 3)).to eq(events.last(3).reverse)
|
345
|
+
expect(repository.read_all_streams_backward(:head, 100)).to eq(events.reverse)
|
346
|
+
expect(repository.read_all_streams_backward(events[4].event_id, 4)).to eq(events.first(4).reverse)
|
347
|
+
expect(repository.read_all_streams_backward(events[4].event_id, 100)).to eq(events.first(4).reverse)
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'reads events different uuid object but same content' do
|
351
|
+
event_ids = [
|
352
|
+
"96c920b1-cdd0-40f4-907c-861b9fff7d02",
|
353
|
+
"56404f79-0ba0-4aa0-8524-dc3436368ca0"
|
354
|
+
]
|
355
|
+
events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
|
356
|
+
repository.append_to_stream(events.first, 'stream', -1)
|
357
|
+
repository.append_to_stream(events.last, 'stream', 0)
|
358
|
+
|
359
|
+
expect(repository.read_all_streams_forward("96c920b1-cdd0-40f4-907c-861b9fff7d02", 1)).to eq([events.last])
|
360
|
+
expect(repository.read_all_streams_backward("56404f79-0ba0-4aa0-8524-dc3436368ca0", 1)).to eq([events.first])
|
361
|
+
|
362
|
+
expect(repository.read_events_forward('stream', "96c920b1-cdd0-40f4-907c-861b9fff7d02", 1)).to eq([events.last])
|
363
|
+
expect(repository.read_events_backward('stream', "56404f79-0ba0-4aa0-8524-dc3436368ca0", 1)).to eq([events.first])
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'does not allow same event twice in a stream' do
|
367
|
+
repository.append_to_stream(
|
368
|
+
TestDomainEvent.new(event_id: "a1b49edb-7636-416f-874a-88f94b859bef"),
|
369
|
+
'stream',
|
370
|
+
-1
|
371
|
+
)
|
372
|
+
expect do
|
373
|
+
repository.append_to_stream(
|
374
|
+
TestDomainEvent.new(event_id: "a1b49edb-7636-416f-874a-88f94b859bef"),
|
375
|
+
'stream',
|
376
|
+
0
|
377
|
+
)
|
378
|
+
end.to raise_error(RubyEventStore::EventDuplicatedInStream)
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'allows appending to GLOBAL_STREAM explicitly' do
|
382
|
+
event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
|
383
|
+
repository.append_to_stream(event, "all", :any)
|
384
|
+
|
385
|
+
expect(repository.read_all_streams_forward(:head, 10)).to eq([event])
|
386
|
+
end
|
387
|
+
|
388
|
+
specify 'GLOBAL_STREAM is unordered, one cannot expect specific version number to work' do
|
389
|
+
expect {
|
390
|
+
event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
|
391
|
+
repository.append_to_stream(event, "all", 42)
|
392
|
+
}.to raise_error(RubyEventStore::InvalidExpectedVersion)
|
393
|
+
end
|
394
|
+
|
395
|
+
specify 'GLOBAL_STREAM is unordered, one cannot expect :none to work' do
|
396
|
+
expect {
|
397
|
+
event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
|
398
|
+
repository.append_to_stream(event, "all", :none)
|
399
|
+
}.to raise_error(RubyEventStore::InvalidExpectedVersion)
|
400
|
+
end
|
401
|
+
|
402
|
+
specify 'GLOBAL_STREAM is unordered, one cannot expect :auto to work' do
|
403
|
+
expect {
|
404
|
+
event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
|
405
|
+
repository.append_to_stream(event, "all", :auto)
|
406
|
+
}.to raise_error(RubyEventStore::InvalidExpectedVersion)
|
407
|
+
end
|
408
|
+
|
409
|
+
specify "only :none, :any, :auto and Integer allowed as expected_version" do
|
410
|
+
[Object.new, SecureRandom.uuid, :foo].each do |invalid_expected_version|
|
411
|
+
expect {
|
412
|
+
repository.append_to_stream(
|
413
|
+
TestDomainEvent.new(event_id: SecureRandom.uuid),
|
414
|
+
'some_stream',
|
415
|
+
invalid_expected_version
|
416
|
+
)
|
417
|
+
}.to raise_error(RubyEventStore::InvalidExpectedVersion)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
specify "events not persisted if append failed" do
|
422
|
+
repository.append_to_stream([
|
423
|
+
TestDomainEvent.new(event_id: SecureRandom.uuid),
|
424
|
+
], 'stream', :none)
|
107
425
|
|
108
|
-
expect
|
109
|
-
|
110
|
-
|
111
|
-
|
426
|
+
expect do
|
427
|
+
repository.append_to_stream([
|
428
|
+
TestDomainEvent.new(
|
429
|
+
event_id: '9bedf448-e4d0-41a3-a8cd-f94aec7aa763'
|
430
|
+
),
|
431
|
+
], 'stream', :none)
|
432
|
+
end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
|
433
|
+
expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
|
112
434
|
end
|
113
435
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_event_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.19.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arkency
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|