ruby_event_store-rom 0.31.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75f18165eba9aa2ff1cc6cbc0fa7c150b7496a2e7b2763a94728604d6750d60d
4
- data.tar.gz: 1860e4bb2757c7882bb0ca6aed0a06e6e3ddf71573cb446c9f1823c7296ba8ef
3
+ metadata.gz: 995fff6b726e0326b38e8bfbcd9be6d919b402934768e828b57cd8db164ed7a2
4
+ data.tar.gz: 8ceb98b10e4da72899199c28f8e1626f28e946140ae58c2f937d03b0600a4c68
5
5
  SHA512:
6
- metadata.gz: d68e654d8a78dd0747f4c7b7311ed93a65a145a30e818d6ba5e2acbc13d430dfeeda70d1a27640f87ee5a011fa014eb682fba9cf546c3f3fb9b3c7ed46f1abc7
7
- data.tar.gz: bcd5d8ba1314ce02231751c14e8749669235a954519b9bc64bfdc01c96d4aa6a2e9f6bb6803f27dc83171c474bc6b7b98e41d90eb517148b9d36a78b457c5824
6
+ metadata.gz: bb9acb2c016ad4bc3d5987cbd0ad07e47de32d5c07e0f3acc34a8f813221ea40099af95c34e22d9360a533e250c66b9883749b63fbe604d2c37a8c3322b82c31
7
+ data.tar.gz: 3c3ca41d2308e2a55e8fe61904b845dab7953b6586b52848d6b09182f9896a920d94e74e0e20a0b0841a50aed61b426f60464df017f1648294a38743e38b8d35
data/Makefile CHANGED
@@ -60,14 +60,14 @@ test-fast: ## Run unit tests with --fail-fast
60
60
 
61
61
  mutate: test ## Run mutation tests
62
62
  @echo "Running mutation tests"
63
- @DATABASE_URL=$(DATABASE_URL) bundle exec mutant --include lib \
63
+ @MUTATING=true DATABASE_URL=$(DATABASE_URL) bundle exec mutant --include lib \
64
64
  $(addprefix --require ,$(REQUIRE)) \
65
65
  $(addprefix --ignore-subject ,$(IGNORE)) \
66
66
  --use rspec "$(SUBJECT)"
67
67
 
68
68
  mutate-fast: ## Run mutation tests with --fail-fast
69
69
  @echo "Running mutation tests with --fail-fast"
70
- @DATABASE_URL=$(DATABASE_URL) bundle exec mutant --include lib \
70
+ @MUTATING=true DATABASE_URL=$(DATABASE_URL) bundle exec mutant --include lib \
71
71
  $(addprefix --require ,$(REQUIRE)) \
72
72
  $(addprefix --ignore-subject ,$(IGNORE)) \
73
73
  --fail-fast \
@@ -26,7 +26,7 @@ module RubyEventStore
26
26
  def take(num)
27
27
  num.nil? ? self : super
28
28
  end
29
-
29
+
30
30
  def insert(tuple)
31
31
  verify_uniquness!(tuple)
32
32
  super
@@ -35,32 +35,36 @@ module RubyEventStore
35
35
  def delete(tuple)
36
36
  super tuple.to_h
37
37
  end
38
-
38
+
39
39
  def by_stream(stream)
40
40
  restrict(stream: normalize_stream_name(stream))
41
41
  end
42
-
42
+
43
+ def by_event_id(event_id)
44
+ restrict(event_id: event_id)
45
+ end
46
+
43
47
  def by_stream_and_event_id(stream, event_id)
44
48
  restrict(stream: normalize_stream_name(stream), event_id: event_id).one!
45
49
  end
46
-
50
+
47
51
  def max_position(stream)
48
52
  new(by_stream(stream).order(:position).dataset.reverse).project(:position).take(1).one
49
53
  end
50
-
54
+
51
55
  DIRECTION_MAP = {
52
56
  forward: [false, :>],
53
57
  backward: [true, :<]
54
58
  }.freeze
55
-
59
+
56
60
  def ordered(direction, stream, offset_entry_id = nil)
57
61
  reverse, operator = DIRECTION_MAP[direction]
58
-
62
+
59
63
  raise ArgumentError, 'Direction must be :forward or :backward' if order.nil?
60
-
64
+
61
65
  order_columns = %i[position id]
62
66
  order_columns.delete(:position) if stream.global?
63
-
67
+
64
68
  query = by_stream(stream)
65
69
  query = query.restrict { |tuple| tuple[:id].public_send(operator, offset_entry_id) } if offset_entry_id
66
70
  query = query.order(*order_columns)
@@ -68,7 +72,7 @@ module RubyEventStore
68
72
 
69
73
  query
70
74
  end
71
-
75
+
72
76
  private
73
77
 
74
78
  # Verifies uniqueness of [stream, event_id] and [stream, position]
@@ -5,48 +5,52 @@ module RubyEventStore
5
5
  class StreamEntries < ::ROM::Relation[:sql]
6
6
  schema(:event_store_events_in_streams, as: :stream_entries, infer: true) do
7
7
  attribute :created_at, ::ROM::Types::Strict::Time.default { Time.now }
8
-
8
+
9
9
  associations do
10
10
  belongs_to :events, as: :event, foreign_key: :event_id
11
11
  end
12
12
  end
13
13
 
14
14
  alias_method :take, :limit
15
-
15
+
16
16
  SERIALIZED_GLOBAL_STREAM_NAME = 'all'.freeze
17
17
 
18
18
  def by_stream(stream)
19
19
  where(stream: normalize_stream_name(stream))
20
20
  end
21
21
 
22
+ def by_event_id(event_id)
23
+ where(event_id: event_id)
24
+ end
25
+
22
26
  def by_stream_and_event_id(stream, event_id)
23
27
  where(stream: normalize_stream_name(stream), event_id: event_id).one!
24
28
  end
25
-
29
+
26
30
  def max_position(stream)
27
31
  by_stream(stream).select(:position).order(Sequel.desc(:position)).first
28
32
  end
29
-
33
+
30
34
  DIRECTION_MAP = {
31
35
  forward: [:asc, :>],
32
36
  backward: [:desc, :<]
33
37
  }.freeze
34
-
38
+
35
39
  def ordered(direction, stream, offset_entry_id = nil)
36
40
  order, operator = DIRECTION_MAP[direction]
37
-
41
+
38
42
  raise ArgumentError, 'Direction must be :forward or :backward' if order.nil?
39
-
43
+
40
44
  order_columns = %i[position id]
41
45
  order_columns.delete(:position) if stream.global?
42
-
46
+
43
47
  query = by_stream(stream)
44
48
  query = query.where { id.public_send(operator, offset_entry_id) } if offset_entry_id
45
49
  query.order { |r| order_columns.map { |c| r[:stream_entries][c].public_send(order) } }
46
50
  end
47
-
51
+
48
52
  private
49
-
53
+
50
54
  def normalize_stream_name(stream)
51
55
  stream.global? ? SERIALIZED_GLOBAL_STREAM_NAME : stream.name
52
56
  end
@@ -11,7 +11,7 @@ module RubyEventStore
11
11
 
12
12
  def initialize(rom: ROM.env)
13
13
  raise ArgumentError, "Must specify rom" unless rom && rom.instance_of?(Env)
14
-
14
+
15
15
  @rom = rom
16
16
  @events = Repositories::Events.new(rom.container)
17
17
  @stream_entries = Repositories::StreamEntries.new(rom.container)
@@ -72,13 +72,7 @@ module RubyEventStore
72
72
  end
73
73
 
74
74
  def last_stream_event(stream)
75
- @events.read(
76
- :backward,
77
- stream,
78
- from: :head,
79
- limit: 1,
80
- batch_size: nil
81
- ).first
75
+ @events.last_stream_event(stream)
82
76
  end
83
77
 
84
78
  def read_event(event_id)
@@ -88,15 +82,14 @@ module RubyEventStore
88
82
  end
89
83
 
90
84
  def read(specification)
91
- raise ReservedInternalName if specification.stream_name.eql?(@stream_entries.stream_entries.class::SERIALIZED_GLOBAL_STREAM_NAME)
92
-
93
- @events.read(
94
- specification.direction,
95
- specification.stream,
96
- from: specification.start,
97
- limit: (specification.count if specification.limit?),
98
- batch_size: (specification.batch_size if specification.batched?)
99
- )
85
+ raise ReservedInternalName if specification.stream.name.eql?(@stream_entries.stream_entries.class::SERIALIZED_GLOBAL_STREAM_NAME)
86
+
87
+ @events.read(specification)
88
+ end
89
+
90
+ def streams_of(event_id)
91
+ @stream_entries.streams_of(event_id)
92
+ .map{|name| Stream.new(name)}
100
93
  end
101
94
 
102
95
  private
@@ -35,31 +35,54 @@ module RubyEventStore
35
35
  events.map_with(:event_to_serialized_record).by_pk(event_id).one!
36
36
  end
37
37
 
38
- def read(direction, stream, from:, limit:, batch_size:)
39
- unless from.equal?(:head)
40
- offset_entry_id = stream_entries.by_stream_and_event_id(stream, from).fetch(:id)
38
+ def last_stream_event(stream)
39
+ query = stream_entries.ordered(:backward, stream)
40
+ query = query_builder(query, limit: 1)
41
+ query.first
42
+ end
43
+
44
+ def read(specification)
45
+ unless specification.head?
46
+ offset_entry_id = stream_entries.by_stream_and_event_id(specification.stream, specification.start).fetch(:id)
41
47
  end
42
48
 
43
- if batch_size
49
+ direction = specification.forward? ? :forward : :backward
50
+ limit = specification.limit if specification.limit?
51
+ if specification.last? && specification.head?
52
+ direction = specification.forward? ? :backward : :forward
53
+ end
54
+
55
+ query = stream_entries.ordered(direction, specification.stream, offset_entry_id)
56
+
57
+ if specification.batched?
44
58
  reader = ->(offset, limit) do
45
- stream_entries
46
- .ordered(direction, stream, offset_entry_id)
47
- .offset(offset)
48
- .take(limit)
49
- .combine(:event)
50
- .map_with(:stream_entry_to_serialized_record, auto_struct: false)
51
- .to_ary
59
+ query_builder(query, offset: offset, limit: limit).to_ary
52
60
  end
53
- BatchEnumerator.new(batch_size, limit || Float::INFINITY, reader).each
61
+ BatchEnumerator.new(specification.batch_size, limit || Float::INFINITY, reader).each
54
62
  else
55
- stream_entries
56
- .ordered(direction, stream, offset_entry_id)
57
- .take(limit)
58
- .combine(:event)
59
- .map_with(:stream_entry_to_serialized_record, auto_struct: false)
60
- .each
63
+ query = query_builder(query, limit: limit)
64
+ if specification.head?
65
+ specification.first? || specification.last? ? query.first : query.each
66
+ else
67
+ if specification.last?
68
+ query.to_ary.last
69
+ else
70
+ specification.first? ? query.first : query.each
71
+ end
72
+ end
61
73
  end
62
74
  end
75
+
76
+ protected
77
+
78
+ def query_builder(query, offset: nil, limit: nil)
79
+ query = query.offset(offset) if offset
80
+ query = query.take(limit) if limit
81
+
82
+ query
83
+ .combine(:event)
84
+ .map_with(:stream_entry_to_serialized_record, auto_struct: false)
85
+ end
63
86
  end
64
87
  end
65
88
  end
@@ -14,7 +14,7 @@ module RubyEventStore
14
14
 
15
15
  def create_changeset(event_ids, stream, resolved_version, global_stream: nil)
16
16
  tuples = []
17
-
17
+
18
18
  event_ids.each_with_index do |event_id, index|
19
19
  tuples << {
20
20
  stream: stream.name,
@@ -41,6 +41,11 @@ module RubyEventStore
41
41
  (stream_entries.max_position(stream) || {})[:position]
42
42
  })
43
43
  end
44
+
45
+ def streams_of(event_id)
46
+ stream_entries.by_event_id(event_id).map{|e| e[:stream]}
47
+ .reject{|s| s == stream_entries.class::SERIALIZED_GLOBAL_STREAM_NAME}
48
+ end
44
49
  end
45
50
  end
46
51
  end
@@ -1,5 +1,5 @@
1
1
  module RubyEventStore
2
2
  module ROM
3
- VERSION = "0.31.1"
3
+ VERSION = "0.32.0"
4
4
  end
5
5
  end
@@ -18,11 +18,15 @@ module RubyEventStore::ROM
18
18
  let(:test_expected_version_auto) { true }
19
19
  let(:test_link_events_to_stream) { true }
20
20
  let(:test_binary) { false }
21
+ let(:test_change) { false }
21
22
 
22
23
  let(:default_stream) { RubyEventStore::Stream.new('stream') }
23
24
  let(:global_stream) { RubyEventStore::Stream.new('all') }
24
25
  let(:mapper) { RubyEventStore::Mappers::NullMapper.new }
25
-
26
+
27
+ let(:reader) { RubyEventStore::SpecificationReader.new(repository, mapper) }
28
+ let(:specification) { RubyEventStore::Specification.new(reader) }
29
+
26
30
  it_behaves_like :event_repository, repository_class
27
31
 
28
32
  specify "#initialize requires ROM::Env" do
@@ -45,80 +49,80 @@ module RubyEventStore::ROM
45
49
 
46
50
  specify "all considered internal detail" do
47
51
  repository.append_to_stream(
48
- [SRecord.new],
52
+ [RubyEventStore::SRecord.new],
49
53
  RubyEventStore::Stream.new(RubyEventStore::GLOBAL_STREAM),
50
54
  RubyEventStore::ExpectedVersion.any
51
55
  )
52
56
  reserved_stream = RubyEventStore::Stream.new("all")
53
57
 
54
- expect{ repository.read(RubyEventStore::Specification.new(repository, mapper).stream("all").result) }.to raise_error(RubyEventStore::ReservedInternalName)
55
- expect{ repository.read(RubyEventStore::Specification.new(repository, mapper).stream("all").backward.result) }.to raise_error(RubyEventStore::ReservedInternalName)
56
- expect{ repository.read(RubyEventStore::Specification.new(repository, mapper).stream("all").from(:head).limit(5).result) }.to raise_error(RubyEventStore::ReservedInternalName)
57
- expect{ repository.read(RubyEventStore::Specification.new(repository, mapper).stream("all").from(:head).limit(5).backward.result) }.to raise_error(RubyEventStore::ReservedInternalName)
58
+ expect{ repository.read(specification.stream("all").result) }.to raise_error(RubyEventStore::ReservedInternalName)
59
+ expect{ repository.read(specification.stream("all").backward.result) }.to raise_error(RubyEventStore::ReservedInternalName)
60
+ expect{ repository.read(specification.stream("all").from(:head).limit(5).result) }.to raise_error(RubyEventStore::ReservedInternalName)
61
+ expect{ repository.read(specification.stream("all").from(:head).limit(5).backward.result) }.to raise_error(RubyEventStore::ReservedInternalName)
58
62
  end
59
63
 
60
64
  specify "explicit sorting by position rather than accidental" do
61
65
  events = [
62
- SRecord.new(
66
+ RubyEventStore::SRecord.new(
63
67
  event_id: u1 = SecureRandom.uuid,
64
68
  data: YAML.dump({}),
65
69
  metadata: YAML.dump({}),
66
70
  event_type: "TestDomainEvent"
67
71
  ),
68
- SRecord.new(
72
+ RubyEventStore::SRecord.new(
69
73
  event_id: u2 = SecureRandom.uuid,
70
74
  data: YAML.dump({}),
71
75
  metadata: YAML.dump({}),
72
76
  event_type: "TestDomainEvent"
73
77
  ),
74
- SRecord.new(
78
+ RubyEventStore::SRecord.new(
75
79
  event_id: u3 = SecureRandom.uuid,
76
80
  data: YAML.dump({}),
77
81
  metadata: YAML.dump({}),
78
82
  event_type: "TestDomainEvent"
79
83
  )
80
84
  ]
81
-
85
+
82
86
  repo = Repositories::Events.new(container)
83
87
  repo.create_changeset(events).commit
84
88
 
85
89
  expect(repo.events.to_a.size).to eq(3)
86
-
90
+
87
91
  repo.stream_entries.changeset(Repositories::StreamEntries::Create, [
88
92
  {stream: default_stream.name, event_id: events[1].event_id, position: 1},
89
93
  {stream: default_stream.name, event_id: events[0].event_id, position: 0},
90
94
  {stream: default_stream.name, event_id: events[2].event_id, position: 2}
91
95
  ]).commit
92
-
96
+
93
97
  expect(repo.stream_entries.to_a.size).to eq(3)
94
-
98
+
95
99
  # ActiveRecord::Schema.define do
96
100
  # self.verbose = false
97
101
  # remove_index :event_store_events_in_streams, [:stream, :position]
98
102
  # end
99
103
 
100
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).stream("stream").from(:head).limit(3).result).map(&:event_id)).to eq([u1,u2,u3])
101
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).stream("stream").result).map(&:event_id)).to eq([u1,u2,u3])
104
+ expect(repository.read(specification.stream("stream").from(:head).limit(3).result).map(&:event_id)).to eq([u1,u2,u3])
105
+ expect(repository.read(specification.stream("stream").result).map(&:event_id)).to eq([u1,u2,u3])
102
106
 
103
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).stream("stream").backward.from(:head).limit(3).result).map(&:event_id)).to eq([u3,u2,u1])
104
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).stream("stream").backward.result).map(&:event_id)).to eq([u3,u2,u1])
107
+ expect(repository.read(specification.stream("stream").backward.from(:head).limit(3).result).map(&:event_id)).to eq([u3,u2,u1])
108
+ expect(repository.read(specification.stream("stream").backward.result).map(&:event_id)).to eq([u3,u2,u1])
105
109
  end
106
110
 
107
111
  specify "explicit sorting by id rather than accidental for all events" do
108
112
  events = [
109
- SRecord.new(
113
+ RubyEventStore::SRecord.new(
110
114
  event_id: u1 = SecureRandom.uuid,
111
115
  data: YAML.dump({}),
112
116
  metadata: YAML.dump({}),
113
117
  event_type: "TestDomainEvent"
114
118
  ),
115
- SRecord.new(
119
+ RubyEventStore::SRecord.new(
116
120
  event_id: u2 = SecureRandom.uuid,
117
121
  data: YAML.dump({}),
118
122
  metadata: YAML.dump({}),
119
123
  event_type: "TestDomainEvent"
120
124
  ),
121
- SRecord.new(
125
+ RubyEventStore::SRecord.new(
122
126
  event_id: u3 = SecureRandom.uuid,
123
127
  data: YAML.dump({}),
124
128
  metadata: YAML.dump({}),
@@ -130,37 +134,37 @@ module RubyEventStore::ROM
130
134
  repo.create_changeset(events).commit
131
135
 
132
136
  expect(repo.events.to_a.size).to eq(3)
133
-
137
+
134
138
  repo.stream_entries.changeset(Repositories::StreamEntries::Create, [
135
139
  {stream: global_stream.name, event_id: events[0].event_id, position: 1},
136
140
  {stream: global_stream.name, event_id: events[1].event_id, position: 0},
137
141
  {stream: global_stream.name, event_id: events[2].event_id, position: 2}
138
142
  ]).commit
139
-
143
+
140
144
  expect(repo.stream_entries.to_a.size).to eq(3)
141
-
142
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).from(:head).limit(3).result).map(&:event_id)).to eq([u1,u2,u3])
143
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).from(:head).limit(3).backward.result).map(&:event_id)).to eq([u3,u2,u1])
145
+
146
+ expect(repository.read(specification.from(:head).limit(3).result).map(&:event_id)).to eq([u1,u2,u3])
147
+ expect(repository.read(specification.from(:head).limit(3).backward.result).map(&:event_id)).to eq([u3,u2,u1])
144
148
  end
145
149
 
146
150
  specify "nested transaction - events still not persisted if append failed" do
147
151
  repository.append_to_stream([
148
- event = SRecord.new(event_id: SecureRandom.uuid),
152
+ event = RubyEventStore::SRecord.new(event_id: SecureRandom.uuid),
149
153
  ], default_stream, RubyEventStore::ExpectedVersion.none)
150
154
 
151
155
  env.unit_of_work do
152
156
  expect do
153
157
  repository.append_to_stream([
154
- SRecord.new(
158
+ RubyEventStore::SRecord.new(
155
159
  event_id: '9bedf448-e4d0-41a3-a8cd-f94aec7aa763'
156
160
  ),
157
161
  ], default_stream, RubyEventStore::ExpectedVersion.none)
158
162
  end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
159
163
  expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
160
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).from(:head).limit(2).result).to_a).to eq([event])
164
+ expect(repository.read(specification.from(:head).limit(2).result).to_a).to eq([event])
161
165
  end
162
166
  expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
163
- expect(repository.read(RubyEventStore::Specification.new(repository, mapper).from(:head).limit(2).result).to_a).to eq([event])
167
+ expect(repository.read(specification.from(:head).limit(2).result).to_a).to eq([event])
164
168
  end
165
169
 
166
170
  def cleanup_concurrency_test
@@ -36,9 +36,10 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency 'childprocess'
37
37
  spec.add_development_dependency 'google-protobuf', '~> 3.5.1.2'
38
38
 
39
- spec.add_dependency 'ruby_event_store', '= 0.31.1'
39
+ spec.add_dependency 'ruby_event_store', '= 0.32.0'
40
40
  spec.add_dependency 'sequel', '>= 4.49'
41
41
  spec.add_dependency 'dry-types', '~> 0.12.2'
42
+ spec.add_dependency 'dry-initializer', '= 2.5.0'
42
43
  spec.add_dependency 'rom-sql', '>= 2.4'
43
44
  spec.add_dependency 'rom-repository', '>= 2.0'
44
45
  spec.add_dependency 'rom-changeset', '>= 1.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_event_store-rom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.1
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Van Horn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-17 00:00:00.000000000 Z
11
+ date: 2018-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -156,14 +156,14 @@ dependencies:
156
156
  requirements:
157
157
  - - '='
158
158
  - !ruby/object:Gem::Version
159
- version: 0.31.1
159
+ version: 0.32.0
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - '='
165
165
  - !ruby/object:Gem::Version
166
- version: 0.31.1
166
+ version: 0.32.0
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: sequel
169
169
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
194
  version: 0.12.2
195
+ - !ruby/object:Gem::Dependency
196
+ name: dry-initializer
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - '='
200
+ - !ruby/object:Gem::Version
201
+ version: 2.5.0
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - '='
207
+ - !ruby/object:Gem::Version
208
+ version: 2.5.0
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: rom-sql
197
211
  requirement: !ruby/object:Gem::Requirement