icu_tournament 1.0.13 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,7 +35,7 @@ and then:
35
35
 
36
36
  or equivalntly, just:
37
37
 
38
- puts @t.serialize('Krause')
38
+ puts t.serialize('Krause')
39
39
 
40
40
  would result in the following output:
41
41
 
@@ -72,6 +72,22 @@ Side effects of calling <em>validate!</em> or _invalid_ include:
72
72
  * the number of rounds will be set if not set already
73
73
  * the finish date will be set if not set already and if there are round dates
74
74
 
75
+ Optionally, additional validation checks can be performed given a tournament
76
+ parser/serializer. For example:
77
+
78
+ t.validate!(:type => ICU::Tournament.ForeignCSV.new)
79
+
80
+ Or equivalently:
81
+
82
+ t.validate!(:type => 'ForeignCSV')
83
+
84
+ Such additional validation is always performed before a tournament is serialized.
85
+ For example, the following are equivalent and will throw an exception if
86
+ the tournament is invalid according to either the general rules or the rules
87
+ specific for the type used:
88
+
89
+ t.serialize('ForeignCSV')
90
+ ICU::Tournament::ForeignCSV.new.serialize(t)
75
91
 
76
92
  == Ranking
77
93
 
@@ -370,6 +386,7 @@ in which case any options supplied to this method will be silently ignored.
370
386
  check_dates
371
387
  check_teams
372
388
  check_ranks(:allow_none => true)
389
+ check_type(options[:type]) if options[:type]
373
390
  true
374
391
  end
375
392
 
@@ -386,12 +403,14 @@ in which case any options supplied to this method will be silently ignored.
386
403
  end
387
404
 
388
405
  # Convenience method to serialise the tournament into a supported format.
389
- # Throws and exception unless the name of a supported format is supplied (e.g. _Krause_).
406
+ # Throws an exception unless the name of a supported format is supplied
407
+ # or if the tournament is unsuitable for serialisation in that format.
390
408
  def serialize(format)
391
409
  serializer = case format.to_s.downcase
392
410
  when 'krause' then ICU::Tournament::Krause.new
393
411
  when 'foreigncsv' then ICU::Tournament::ForeignCSV.new
394
412
  when 'swissperfect' then ICU::Tournament::SwissPerfect.new
413
+ when '' then raise "no format supplied"
395
414
  else raise "unsupported serialisation format: '#{format}'"
396
415
  end
397
416
  serializer.serialize(self)
@@ -486,6 +505,17 @@ in which case any options supplied to this method will be silently ignored.
486
505
  end
487
506
  end
488
507
 
508
+ # Validate against a specific type.
509
+ def check_type(type)
510
+ if type.respond_to?(:validate!)
511
+ type.validate!(self)
512
+ elsif type.to_s.match(/^(ForeignCSV|Krause|SwissPerfect)$/)
513
+ parser = "ICU::Tournament::#{type.to_s}".constantize.new.validate!(self)
514
+ else
515
+ raise "invalid type supplied for validation check"
516
+ end
517
+ end
518
+
489
519
  # Return an array of tie break methods and an array of tie break orders (+1 for asc, -1 for desc).
490
520
  # The first and most important method is always "score", the last and least important is always "name".
491
521
  def tie_break_data
@@ -1,6 +1,6 @@
1
1
  module ICU
2
2
  class Tournament
3
-
3
+
4
4
  =begin rdoc
5
5
 
6
6
  == Foreign CSV
@@ -34,7 +34,7 @@ This file can be parsed as follows.
34
34
 
35
35
  If the file is correctly specified, the return value from the <em>parse_file</em> method is an instance of
36
36
  ICU::Tournament (rather than <em>nil</em>, which indicates an error). In this example the file is valid, so:
37
-
37
+
38
38
  tournament.name # => "Isle of Man Masters, 2007"
39
39
  tournament.start # => "2007-09-22"
40
40
  tournament.rounds # => 9
@@ -44,7 +44,7 @@ The main player (the player whose results are being reported for rating) played
44
44
  but only 8 other players (he had a bye in round 6), so the total number of players is 9.
45
45
 
46
46
  tournament.players.size # => 9
47
-
47
+
48
48
  Each player has a unique number for the tournament. The main player always occurs first in this type of file, so his number is 1.
49
49
 
50
50
  player = tournament.player(1)
@@ -86,6 +86,20 @@ Or equivalently, the _serialize_ instance method of the tournament, if the appro
86
86
 
87
87
  csv = tournament.serialize('ForeignCSV')
88
88
 
89
+ Extra condtions, over and above the normal validation rules, apply before any tournament validates or can be serialized in this format.
90
+
91
+ * the tournament must have a _site_ attribute
92
+ * there must be at least one player with an _id_ (interpreted as an ICU ID number)
93
+ * all foreign players (those without an ICU ID) must have a _fed_ attribute (federation)
94
+ * all ICU players must have a result in every round (even if it is just bye or is unrateable)
95
+ * the opponents of all ICU players must have a federation (this could include other ICU players)
96
+
97
+ If any of these are not satisfied, then the following method calls will all raise an exception:
98
+
99
+ tournament.validate!(:type => 'ForeignCSV')
100
+ tournament.serialize('ForeignCSV')
101
+ ICU::Tournament::ForeignCSV.new.serialize(tournament)
102
+
89
103
  You can also build the tournament object from scratch using your own data and then serialize it.
90
104
  For example, here are the commands to reproduce the example above.
91
105
 
@@ -105,30 +119,29 @@ For example, here are the commands to reproduce the example above.
105
119
  t.add_result(ICU::Result.new(3, 1, 'D', :opponent => 4, :colour => 'B'))
106
120
  t.add_result(ICU::Result.new(4, 1, 'W', :opponent => 5, :colour => 'W'))
107
121
  t.add_result(ICU::Result.new(5, 1, 'W', :opponent => 6, :colour => 'B'))
108
- t.add_result(ICU::Result.new(6, 1, 'L'))
122
+ t.add_result(ICU::Result.new(6, 1, 'L'))
109
123
  t.add_result(ICU::Result.new(7, 1, 'D', :opponent => 7, :colour => 'W'))
110
124
  t.add_result(ICU::Result.new(8, 1, 'L', :opponent => 8, :colour => 'B'))
111
125
  t.add_result(ICU::Result.new(9, 1, 'D', :opponent => 9, :colour => 'W'))
112
- t.validate!
113
126
  puts t.serialize('ForeignCSV')
114
-
127
+
115
128
  =end
116
129
 
117
130
  class ForeignCSV
118
131
  attr_reader :error
119
-
132
+
120
133
  # Parse CSV data returning a Tournament on success or raising an exception on error.
121
134
  def parse!(csv)
122
135
  @state, @line, @round, @sum, @error = 0, 0, nil, nil, nil
123
136
  @tournament = Tournament.new('Dummy', '2000-01-01')
124
-
137
+
125
138
  Util::CSV.parse(csv, :row_sep => :auto) do |r|
126
139
  @line += 1 # increment line number
127
140
  next if r.size == 0 # skip empty lines
128
141
  r = r.map{|c| c.nil? ? '' : c.strip} # trim all spaces, turn nils to blanks
129
142
  next if r[0] == '' # skip blanks in column 1
130
143
  @r = r # remember this record for later
131
-
144
+
132
145
  begin
133
146
  case @state
134
147
  when 0 then event
@@ -145,7 +158,7 @@ For example, here are the commands to reproduce the example above.
145
158
  raise
146
159
  end
147
160
  end
148
-
161
+
149
162
  unless @state == 4
150
163
  exp = case @state
151
164
  when 0 then "the event name"
@@ -158,12 +171,12 @@ For example, here are the commands to reproduce the example above.
158
171
  raise "line #{@line}: premature termination - expected #{exp}"
159
172
  end
160
173
  raise "line #{@line}: no players found in file" if @tournament.players.size == 0
161
-
174
+
162
175
  @tournament.validate!
163
176
 
164
177
  @tournament
165
178
  end
166
-
179
+
167
180
  # Parse CSV data returning a Tournament on success or a nil on failure.
168
181
  # In the case of failure, an error message can be retrived via the <em>error</em> method.
169
182
  def parse(csv)
@@ -174,13 +187,13 @@ For example, here are the commands to reproduce the example above.
174
187
  nil
175
188
  end
176
189
  end
177
-
190
+
178
191
  # Same as <em>parse!</em> except the input is a file name rather than file contents.
179
192
  def parse_file!(file)
180
193
  csv = open(file) { |f| f.read }
181
194
  parse!(csv)
182
195
  end
183
-
196
+
184
197
  # Same as <em>parse</em> except the input is a file name rather than file contents.
185
198
  def parse_file(file)
186
199
  begin
@@ -190,10 +203,10 @@ For example, here are the commands to reproduce the example above.
190
203
  nil
191
204
  end
192
205
  end
193
-
206
+
194
207
  # Serialise a tournament back into CSV format.
195
208
  def serialize(t)
196
- return nil unless t.class == ICU::Tournament;
209
+ t.validate!(:type => self)
197
210
  Util::CSV.generate do |csv|
198
211
  csv << ["Event", t.name]
199
212
  csv << ["Start", t.start]
@@ -226,36 +239,52 @@ For example, here are the commands to reproduce the example above.
226
239
  end
227
240
  end
228
241
 
242
+ # Additional tournament validation rules for this specific type.
243
+ def validate!(t)
244
+ raise "missing site" unless t.site.to_s.length > 0
245
+ icu = t.players.find_all { |p| p.id }
246
+ raise "there must be at least one ICU player (with an ID number)" if icu.size == 0
247
+ foreign = t.players.find_all { |p| !p.id }
248
+ raise "all foreign players must have a federation" if foreign.detect { |f| !f.fed }
249
+ icu.each do |p|
250
+ (1..t.rounds).each do |r|
251
+ result = p.find_result(r)
252
+ raise "ICU players must have a result in every round" unless result
253
+ raise "all opponents of ICU players must have a federation" if result.opponent && !t.player(result.opponent).fed
254
+ end
255
+ end
256
+ end
257
+
229
258
  private
230
-
259
+
231
260
  def event
232
261
  abort "the 'Event' keyword", 0 unless @r[0].match(/^(Event|Tournament)$/i)
233
262
  abort "the event name", 1 unless @r.size > 1 && @r[1] != ''
234
263
  @tournament.name = @r[1]
235
264
  @state = 1
236
265
  end
237
-
266
+
238
267
  def start
239
268
  abort "the 'Start' keyword", 0 unless @r[0].match(/^(Start(\s+Date)?|Date)$/i)
240
269
  abort "the start date", 1 unless @r.size > 1 && @r[1] != ''
241
270
  @tournament.start = @r[1]
242
271
  @state = 2
243
272
  end
244
-
273
+
245
274
  def rounds
246
275
  abort "the 'Rounds' keyword", 0 unless @r[0].match(/(Number of )?Rounds$/)
247
276
  abort "the number of rounds", 1 unless @r.size > 1 && @r[1].match(/^[1-9]\d*/)
248
277
  @tournament.rounds = @r[1]
249
278
  @state = 3
250
279
  end
251
-
280
+
252
281
  def website
253
282
  abort "the 'Website' keyword", 0 unless @r[0].match(/^(Web(\s?site)?|Site)$/i)
254
283
  abort "the event website", 1 unless @r.size > 1 && @r[1] != ''
255
284
  @tournament.site = @r[1]
256
285
  @state = 4
257
286
  end
258
-
287
+
259
288
  def player
260
289
  abort "the 'Player' keyword", 0 unless @r[0].match(/^Player$/i)
261
290
  abort "a player's ICU number", 1 unless @r.size > 1 && @r[1].match(/^[1-9]/i)
@@ -274,7 +303,7 @@ For example, here are the commands to reproduce the example above.
274
303
  @round = 0
275
304
  @state = 5
276
305
  end
277
-
306
+
278
307
  def result
279
308
  @round+= 1
280
309
  abort "round number #{round}", 0 unless @r[0].to_i == @round
@@ -309,14 +338,14 @@ For example, here are the commands to reproduce the example above.
309
338
  end
310
339
  @state = 6 if @round == @tournament.rounds
311
340
  end
312
-
341
+
313
342
  def total
314
343
  points = @player.points
315
344
  abort "the 'Total' keyword", 0 unless @r[0].match(/^Total$/i)
316
345
  abort "the player's (#{@player.object_id}, #{@player.results.size}) total points to be #{points}", 1 unless @r[1].to_f == points
317
346
  @state = 4
318
347
  end
319
-
348
+
320
349
  def abort(expected, cell)
321
350
  got = @r[cell]
322
351
  error = "line #{@line}"
@@ -199,7 +199,7 @@ attributes in an ICU::Tournament instance.
199
199
 
200
200
  # Serialise a tournament back into Krause format.
201
201
  def serialize(t)
202
- return nil unless t.class == ICU::Tournament;
202
+ t.validate!(:type => self)
203
203
  krause = ''
204
204
  krause << "012 #{t.name}\n"
205
205
  krause << "022 #{t.city}\n" if t.city
@@ -225,6 +225,11 @@ attributes in an ICU::Tournament instance.
225
225
  krause
226
226
  end
227
227
 
228
+ # Additional tournament validation rules for this specific type.
229
+ def validate!(t)
230
+ # None.
231
+ end
232
+
228
233
  private
229
234
 
230
235
  def set_name
@@ -146,7 +146,7 @@ See ICU::Tournament for more about tie-breaks.
146
146
 
147
147
  # Serialise a tournament to SwissPerfect text export format.
148
148
  def serialize(t)
149
- return nil unless t.class == ICU::Tournament && t.players.size > 2;
149
+ t.validate!(:type => self)
150
150
 
151
151
  # Ensure a nice set of numbers.
152
152
  t.renumber(:order)
@@ -173,6 +173,11 @@ See ICU::Tournament for more about tie-breaks.
173
173
  sp
174
174
  end
175
175
 
176
+ # Additional tournament validation rules for this specific type.
177
+ def validate!(t)
178
+ # None.
179
+ end
180
+
176
181
  private
177
182
 
178
183
  def get_files(file, arg)
@@ -1,5 +1,5 @@
1
1
  module ICU
2
2
  class Tournament
3
- VERSION = "1.0.13"
3
+ VERSION = "1.1.0"
4
4
  end
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,8 @@
1
1
  require 'rubygems'
2
2
  require 'rspec'
3
3
  require File.expand_path(File.dirname(__FILE__) + '/../lib/icu_tournament')
4
+
5
+ RSpec.configure do |c|
6
+ c.filter_run :focus => true
7
+ c.run_all_when_everything_filtered = true
8
+ end
@@ -412,10 +412,6 @@ CSV
412
412
  it "should serialize back to the original" do
413
413
  @f.serialize(@t).should == @csv
414
414
  end
415
-
416
- it "should return nil on invalid input" do
417
- @f.serialize('Rubbish').should be_nil
418
- end
419
415
  end
420
416
 
421
417
  context "serialisation of ForeignCSV documentation example" do
@@ -515,9 +511,8 @@ Player,456,Fox,Anthony
515
511
  9,=,W,Phillips,Roy,2271,,MAU
516
512
  Total,4.0
517
513
  CSV
518
- @t = ICU::Tournament.new("Isle of Man Masters, 2007", '2007-09-22', :round => 9)
514
+ @t = ICU::Tournament.new("Isle of Man Masters, 2007", '2007-09-22')
519
515
  @t.site = 'http://www.bcmchess.co.uk/monarch2007/'
520
- @t.rounds = 9
521
516
  @t.add_player(ICU::Player.new('Anthony', 'Fox', 1, :id => 456, :rating => 2100, :fed => 'IRL'))
522
517
  @t.add_player(ICU::Player.new('Peter P.', 'Taylor', 2, :rating => 2209, :fed => 'ENG'))
523
518
  @t.add_player(ICU::Player.new('Egozi', 'Nadav', 3, :rating => 2205, :fed => 'ISR'))
@@ -574,6 +569,72 @@ CSV
574
569
  t.players.size.should == 16
575
570
  end
576
571
  end
572
+
573
+ context "type validation" do
574
+ before(:each) do
575
+ @p = ICU::Tournament::ForeignCSV.new
576
+ @t = ICU::Tournament.new("Isle of Man Masters, 2007", '2007-09-22')
577
+ @t.site = 'http://www.bcmchess.co.uk/monarch2007/'
578
+ @t.add_player(ICU::Player.new('Anthony', 'Fox', 1, :id => 456))
579
+ @t.add_player(ICU::Player.new('Peter', 'Cafolla', 2, :id => 159))
580
+ @t.add_player(ICU::Player.new('Peter P.', 'Taylor', 10, :rating => 2209, :fed => 'ENG'))
581
+ @t.add_player(ICU::Player.new('Egozi', 'Nadav', 20, :rating => 2205, :fed => 'ISR'))
582
+ @t.add_player(ICU::Player.new('Tim R.', 'Spanton', 30, :rating => 1982, :fed => 'ENG'))
583
+ @t.add_player(ICU::Player.new('Alan', 'Grant', 40, :rating => 2223, :fed => 'SCO'))
584
+ @t.add_result(ICU::Result.new(1, 1, 'W', :opponent => 10, :colour => 'W'))
585
+ @t.add_result(ICU::Result.new(1, 2, 'L', :opponent => 20, :colour => 'B'))
586
+ @t.add_result(ICU::Result.new(2, 1, 'D', :opponent => 30, :colour => 'B'))
587
+ @t.add_result(ICU::Result.new(2, 2, 'L', :opponent => 40, :colour => 'W'))
588
+ end
589
+
590
+ it "should pass" do
591
+ @t.invalid.should be_false
592
+ @t.invalid(:type => @p).should be_false
593
+ end
594
+
595
+ it "should fail if there's no site" do
596
+ @t.site = nil;
597
+ @t.invalid(:type => @p).to_s.should match(/site/)
598
+ end
599
+
600
+ it "should fail if there are no ICU players" do
601
+ [1, 2].each { |n| @t.player(n).id = nil }
602
+ @t.player(2).id = nil;
603
+ @t.invalid(:type => @p).to_s.should match(/ID/)
604
+ end
605
+
606
+ it "should fail unless all foreign players have a federation" do
607
+ @t.player(10).fed = nil;
608
+ @t.invalid(:type => @p).to_s.should match(/federation/)
609
+ end
610
+
611
+ it "should fail unless every ICU player has a result in every round" do
612
+ @t.add_player(ICU::Player.new('Mark', 'Orr', 3, :id => 1350))
613
+ @t.add_result(ICU::Result.new(1, 3, 'W', :opponent => 30, :colour => 'B'))
614
+ @t.invalid(:type => @p).to_s.should match(/result/)
615
+ @t.add_result(ICU::Result.new(2, 3, 'W', :opponent => 10, :colour => 'W'))
616
+ @t.invalid(:type => @p).should be_false
617
+ end
618
+
619
+ it "should fail unless every ICU player's opponents have a federation" do
620
+ @t.add_player(ICU::Player.new('Mark', 'Orr', 3, :id => 1350))
621
+ @t.add_result(ICU::Result.new(1, 3, 'W', :opponent => 30, :colour => 'B'))
622
+ @t.add_result(ICU::Result.new(2, 3, 'W', :opponent => 10, :colour => 'W'))
623
+ @t.add_result(ICU::Result.new(3, 1, 'D', :opponent => 40, :colour => 'W'))
624
+ @t.add_result(ICU::Result.new(3, 2, 'L', :opponent => 3, :colour => 'B'))
625
+ @t.invalid(:type => @p).to_s.should match(/opponents.*federation/)
626
+ @t.player(2).fed = 'IRL'
627
+ @t.invalid(:type => @p).to_s.should match(/opponents.*federation/)
628
+ @t.player(3).fed = 'IRL'
629
+ @t.invalid(:type => @p).should be_false
630
+ end
631
+
632
+ it "should be serializable unless invalid" do
633
+ lambda { @p.serialize(@t) }.should_not raise_error
634
+ @t.site = nil;
635
+ lambda { @p.serialize(@t) }.should raise_error
636
+ end
637
+ end
577
638
  end
578
639
  end
579
640
  end
@@ -158,7 +158,7 @@ KRAUSE
158
158
  @t.add_result(ICU::Result.new(1, 1, 'L', :opponent => 2, :colour => 'B'))
159
159
  @t.add_result(ICU::Result.new(2, 1, 'L', :opponent => 2, :colour => 'W', :rateable => false))
160
160
  @t.add_result(ICU::Result.new(3, 1, 'W', :opponent => 2, :colour => 'B'))
161
- @t.add_result(ICU::Result.new(4, 1, 'W', :opponent => 2, :colour => 'B'))
161
+ @t.add_result(ICU::Result.new(4, 1, 'D', :opponent => 2, :colour => 'W'))
162
162
  serializer = ICU::Tournament::Krause.new
163
163
  @k = serializer.serialize(@t)
164
164
  end
@@ -173,7 +173,7 @@ KRAUSE
173
173
  end
174
174
 
175
175
  context "serialisation" do
176
- before(:all) do
176
+ before(:each) do
177
177
  @krause = <<KRAUSE
178
178
  012 Las Vegas National Open
179
179
  022 Las Vegas
@@ -204,10 +204,6 @@ KRAUSE
204
204
  it "should serialize using the convenience method of the tournament object" do
205
205
  @t.serialize('Krause').should == @krause
206
206
  end
207
-
208
- it "should return nil on invalid input" do
209
- @q.serialize('Rubbish').should be_nil
210
- end
211
207
  end
212
208
 
213
209
  context "auto-ranking" do
@@ -374,13 +370,13 @@ KRAUSE
374
370
  lambda { t = @p.parse!(@k) }.should raise_error(/opponent/)
375
371
  end
376
372
  end
377
-
373
+
378
374
  context "parsing files" do
379
375
  before(:each) do
380
376
  @p = ICU::Tournament::Krause.new
381
377
  @s = File.dirname(__FILE__) + '/samples/krause'
382
378
  end
383
-
379
+
384
380
  it "should error on a non-existant valid file" do
385
381
  file = "#{@s}/not_there.tab"
386
382
  lambda { @p.parse_file!(file) }.should raise_error
@@ -388,7 +384,7 @@ KRAUSE
388
384
  t.should be_nil
389
385
  @p.error.should match(/no such file/i)
390
386
  end
391
-
387
+
392
388
  it "should error on an invalid file" do
393
389
  file = "#{@s}/invalid.tab"
394
390
  lambda { @p.parse_file!(file) }.should raise_error
@@ -396,7 +392,7 @@ KRAUSE
396
392
  t.should be_nil
397
393
  @p.error.should match(/tournament name missing/i)
398
394
  end
399
-
395
+
400
396
  it "should parse a valid file" do
401
397
  file = "#{@s}/valid.tab"
402
398
  lambda { @p.parse_file!(file) }.should_not raise_error
@@ -853,5 +853,26 @@ EOS
853
853
  lambda { @c.parse_file!("#{@s}/krause/valid.tab", 'NoSuchType') }.should raise_error(/invalid format/i)
854
854
  end
855
855
  end
856
+
857
+ context "type specific validation" do
858
+ before(:all) do
859
+ @t = Tournament.new('Bangor Bash', '2009-11-09')
860
+ @t.add_player(Player.new('Bobby', 'Fischer', 1))
861
+ @t.add_player(Player.new('Garry', 'Kasparov', 2))
862
+ @t.add_player(Player.new('Mark', 'Orr', 3))
863
+ @t.add_result(Result.new(1, 1, '=', :opponent => 2, :colour => 'W'))
864
+ @t.add_result(Result.new(2, 2, 'L', :opponent => 3, :colour => 'W'))
865
+ @t.add_result(Result.new(3, 3, 'W', :opponent => 1, :colour => 'W'))
866
+ end
867
+
868
+ it "should pass generic validation" do
869
+ @t.invalid.should be_false
870
+ end
871
+
872
+ it "should fail type-specific validation when the type supplied is inappropriate" do
873
+ @t.invalid(:type => String).should match(/invalid type/)
874
+ @t.invalid(:type => "AbCd").should match(/invalid type/)
875
+ end
876
+ end
856
877
  end
857
878
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 1
7
+ - 1
7
8
  - 0
8
- - 13
9
- version: 1.0.13
9
+ version: 1.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Mark Orr
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-10-16 00:00:00 +01:00
17
+ date: 2010-10-17 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency