hashematics 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ class Person
11
+ attr_accessor :dob, :first, :id, :last, :smoker
12
+
13
+ def initialize(attrs = {})
14
+ attrs.each do |k, v|
15
+ send("#{k}=", v) if respond_to?(k)
16
+ end
17
+ end
18
+
19
+ def eql?(other)
20
+ to_hash == other.to_hash
21
+ end
22
+
23
+ def ==(other)
24
+ eql?(other)
25
+ end
26
+
27
+ def to_hash
28
+ {
29
+ id: id,
30
+ first: first,
31
+ last: last,
32
+ smoker: smoker,
33
+ dob: dob
34
+ }
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ types:
2
+ person:
3
+ properties:
4
+ :id: 'ID #'
5
+ :first: First
6
+ :last: Last
7
+ :smoker: Smoker
8
+ :dob: DOB
9
+ car:
10
+ properties:
11
+ id: 'Car ID #'
12
+ make: Make
13
+ model: Model
14
+ year: Year
15
+ house:
16
+ :properties:
17
+ id: 'House ID #'
18
+ st1: Street 1
19
+ st2: Street 2
20
+ city: City
21
+ st: St
22
+ zip: Zip
23
+ plate:
24
+ :properties:
25
+ id: 'Plate ID #'
26
+ number: Plate Number
27
+ groups:
28
+ people:
29
+ by: 'ID #'
30
+ type: person
31
+ groups:
32
+ :cars:
33
+ by: 'Car ID #'
34
+ type: car
35
+ groups:
36
+ plates:
37
+ by: 'Plate ID #'
38
+ type: plate
39
+ houses:
40
+ by: 'House ID #'
41
+ type: house
42
+ cars:
43
+ by: 'Car ID #'
44
+ type: car
@@ -0,0 +1,9 @@
1
+ ID #,First,Last,Smoker,DOB,Car ID #,Make,Model,Year,House ID #,Street 1,Street 2,City,St,Zip,Plate ID #,Plate Number
2
+ 1,Matt,Rizzo,false,1902-03-04,2,Jeep,Wrangler,1964,7,555 N. Michigan Ave,Suite 203,Chicago,IL,60075,12,JEEPY123
3
+ 1,Matt,Rizzo,false,1902-03-04,3,Mercedes,Benz,1921,7,555 N. Michigan Ave,Suite 203,Chicago,IL,60075,13,BENZY456
4
+ 1,Matt,Rizzo,false,1902-03-04,2,Jeep,Wrangler,1964,8,2813 N. Drivey Ave,Apt. 399,Roanoke,GA,99998,12,JEEPY123
5
+ 1,Matt,Rizzo,false,1902-03-04,3,Mercedes,Benz,1921,8,2813 N. Drivey Ave,Apt. 399,Roanoke,GA,99998,13,BENZY4565
6
+ 2,Katie,Bunny,true,1919-09-02,4,GMC,Terrainia,1999,9,123 Happy Ln.,Apt. 45,Mayhusee,RI,53210,14,GMC394
7
+ 2,Katie,Bunny,true,1919-09-02,5,Buick,Macartha,1982,9,123 Happy Ln.,Apt. 45,Mayhusee,RI,53210,15,BUICKY93
8
+ 3,Ralphie,Dog,true,1999-12-11,6,Audi,A1,2012,10,4500 Century Ave.,Floor 3A,Pakato,ZA,093821,,
9
+ 3,Ralphie,Dog,true,1999-12-11,6,Audi,A1,2012,11,9990 Lake St.,,Pakato,ZA,093822,,
@@ -0,0 +1,84 @@
1
+ - :id: '1'
2
+ :first: Matt
3
+ :last: Rizzo
4
+ :smoker: 'false'
5
+ :dob: '1902-03-04'
6
+ :cars:
7
+ - id: '2'
8
+ make: Jeep
9
+ model: Wrangler
10
+ year: '1964'
11
+ plates:
12
+ - id: '12'
13
+ number: JEEPY123
14
+ - id: '3'
15
+ make: Mercedes
16
+ model: Benz
17
+ year: '1921'
18
+ plates:
19
+ - id: '13'
20
+ number: BENZY4565
21
+ houses:
22
+ - id: '7'
23
+ st1: 555 N. Michigan Ave
24
+ st2: Suite 203
25
+ city: Chicago
26
+ st: IL
27
+ zip: '60075'
28
+ - id: '8'
29
+ st1: 2813 N. Drivey Ave
30
+ st2: Apt. 399
31
+ city: Roanoke
32
+ st: GA
33
+ zip: '99998'
34
+ - :id: '2'
35
+ :first: Katie
36
+ :last: Bunny
37
+ :smoker: 'true'
38
+ :dob: '1919-09-02'
39
+ :cars:
40
+ - id: '4'
41
+ make: GMC
42
+ model: Terrainia
43
+ year: '1999'
44
+ plates:
45
+ - id: '14'
46
+ number: GMC394
47
+ - id: '5'
48
+ make: Buick
49
+ model: Macartha
50
+ year: '1982'
51
+ plates:
52
+ - id: '15'
53
+ number: BUICKY93
54
+ houses:
55
+ - id: '9'
56
+ st1: 123 Happy Ln.
57
+ st2: Apt. 45
58
+ city: Mayhusee
59
+ st: RI
60
+ zip: '53210'
61
+ - :id: '3'
62
+ :first: Ralphie
63
+ :last: Dog
64
+ :smoker: 'true'
65
+ :dob: '1999-12-11'
66
+ :cars:
67
+ - id: '6'
68
+ make: Audi
69
+ model: A1
70
+ year: '2012'
71
+ plates: []
72
+ houses:
73
+ - id: '10'
74
+ st1: 4500 Century Ave.
75
+ st2: Floor 3A
76
+ city: Pakato
77
+ st: ZA
78
+ zip: '093821'
79
+ - id: '11'
80
+ st1: 9990 Lake St.
81
+ st2: null
82
+ city: Pakato
83
+ st: ZA
84
+ zip: '093822'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'spec_helper'
11
+
12
+ describe ::Hashematics::Category do
13
+ let(:csv_rows) { csv_fixture('data.csv') }
14
+
15
+ it 'should require an id_key' do
16
+ expect { described_class.new(id_key: nil) }.to raise_error ArgumentError
17
+ end
18
+
19
+ describe '#add' do
20
+ context 'with id key but no parent key' do
21
+ specify '#records should return last unique rows as records' do
22
+ category = described_class.new(id_key: 'ID #')
23
+
24
+ csv_rows.each do |csv_row|
25
+ record = ::Hashematics::Record.new(csv_row)
26
+ category.add(record)
27
+ end
28
+
29
+ expected_records = [
30
+ ::Hashematics::Record.new(csv_rows[3]),
31
+ ::Hashematics::Record.new(csv_rows[5]),
32
+ ::Hashematics::Record.new(csv_rows[7])
33
+ ]
34
+
35
+ expect(category.records).to eq(expected_records)
36
+ end
37
+ end
38
+
39
+ context 'with parent and id keys' do
40
+ specify '#records should return unique rows as records specific to a parent' do
41
+ category = described_class.new(
42
+ parent_key: 'ID #',
43
+ id_key: 'Car ID #'
44
+ )
45
+
46
+ csv_rows.each do |csv_row|
47
+ record = ::Hashematics::Record.new(csv_row)
48
+ category.add(record)
49
+ end
50
+
51
+ expected_records = [
52
+ ::Hashematics::Record.new(csv_rows[2]),
53
+ ::Hashematics::Record.new(csv_rows[3])
54
+ ]
55
+
56
+ parent = ::Hashematics::Record.new(csv_rows.first)
57
+
58
+ expect(category.records(parent)).to eq(expected_records)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,572 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'spec_helper'
11
+
12
+ describe ::Hashematics::Graph do
13
+ let(:csv_rows) { csv_fixture('data.csv') }
14
+
15
+ let(:configuration) { yaml_fixture('config.yml') }
16
+
17
+ let(:people) { yaml_fixture('people.yml') }
18
+
19
+ let(:groups) { ::Hashematics::Configuration.new(configuration).groups }
20
+
21
+ describe '#children' do
22
+ it 'returns list of child group names' do
23
+ graph = described_class.new(groups).add(csv_rows)
24
+
25
+ actual_children = graph.children
26
+
27
+ expected_children = groups.map(&:name)
28
+
29
+ expect(actual_children).to eq(expected_children)
30
+ end
31
+ end
32
+
33
+ describe '#data' do
34
+ context 'with no object_class specifications' do
35
+ it 'should parse configuration and return object graph' do
36
+ graph = described_class.new(groups).add(csv_rows)
37
+
38
+ actual_people = graph.data('people')
39
+
40
+ # binding.pry
41
+
42
+ expect(actual_people).to eq(people)
43
+ end
44
+ end
45
+
46
+ context 'with object_class open_struct specifications' do
47
+ let(:modified_configuration) do
48
+ yaml_fixture('config.yml').tap do |c|
49
+ c.dig('types', 'person')['object_class'] = 'open_struct'
50
+ end
51
+ end
52
+
53
+ let(:modified_groups) do
54
+ ::Hashematics::Configuration.new(modified_configuration).groups
55
+ end
56
+
57
+ let(:modified_people) do
58
+ yaml_fixture('people.yml').map { |h| OpenStruct.new(h) }
59
+ end
60
+
61
+ it 'should parse configuration and return object graph' do
62
+ graph = described_class.new(modified_groups).add(csv_rows)
63
+
64
+ actual_people = graph.data(:people)
65
+
66
+ expect(actual_people).to eq(modified_people)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe 'README examples' do
72
+ specify 'Getting Started should work' do
73
+ rows = [
74
+ {
75
+ id: 1,
76
+ first: 'Bruce',
77
+ last: 'Banner'
78
+ },
79
+ {
80
+ id: 2,
81
+ first: 'Tony',
82
+ last: 'Stark'
83
+ }
84
+ ]
85
+
86
+ graph = ::Hashematics.graph(rows: rows)
87
+ objects = graph.rows
88
+
89
+ expect(objects).to eq(rows)
90
+ end
91
+
92
+ specify 'Introduction to Shaping should work' do
93
+ config = {
94
+ types: {
95
+ person: {
96
+ properties: %i[id first]
97
+ }
98
+ },
99
+ groups: {
100
+ avengers: {
101
+ by: :id,
102
+ type: :person
103
+ }
104
+ }
105
+ }
106
+
107
+ rows = [
108
+ {
109
+ id: 1,
110
+ first: 'Bruce',
111
+ last: 'Banner'
112
+ },
113
+ {
114
+ id: 2,
115
+ first: 'Tony',
116
+ last: 'Stark'
117
+ }
118
+ ]
119
+
120
+ graph = ::Hashematics.graph(config: config, rows: rows)
121
+ objects = graph.data(:avengers)
122
+
123
+ expected = [
124
+ {
125
+ id: 1,
126
+ first: 'Bruce'
127
+ },
128
+ {
129
+ id: 2,
130
+ first: 'Tony'
131
+ }
132
+ ]
133
+
134
+ expect(objects).to eq(expected)
135
+ end
136
+
137
+ specify 'Cross-Mapping Shape Attribute Names should work' do
138
+ config = {
139
+ types: {
140
+ person: {
141
+ properties: {
142
+ id_number: :id,
143
+ first_name: :first
144
+ }
145
+ }
146
+ },
147
+ groups: {
148
+ avengers: {
149
+ by: :id,
150
+ type: :person
151
+ }
152
+ }
153
+ }
154
+
155
+ rows = [
156
+ {
157
+ id: 1,
158
+ first: 'Bruce',
159
+ last: 'Banner'
160
+ },
161
+ {
162
+ id: 2,
163
+ first: 'Tony',
164
+ last: 'Stark'
165
+ }
166
+ ]
167
+
168
+ graph = ::Hashematics.graph(config: config, rows: rows)
169
+ objects = graph.data(:avengers)
170
+
171
+ expected = [
172
+ {
173
+ id_number: 1,
174
+ first_name: 'Bruce'
175
+ },
176
+ {
177
+ id_number: 2,
178
+ first_name: 'Tony'
179
+ }
180
+ ]
181
+
182
+ expect(objects).to eq(expected)
183
+ end
184
+
185
+ specify 'Nested Shaping should work' do
186
+ config = {
187
+ types: {
188
+ person: {
189
+ properties: {
190
+ id: 'ID #',
191
+ first: 'First Name',
192
+ last: 'Last Name'
193
+ }
194
+ },
195
+ costume: {
196
+ properties: {
197
+ id: 'Costume ID #',
198
+ name: 'Costume Name',
199
+ color: 'Costume Color'
200
+ }
201
+ }
202
+ },
203
+ groups: {
204
+ avengers: {
205
+ by: 'ID #',
206
+ type: :person,
207
+ groups: {
208
+ costumes: {
209
+ by: 'Costume ID #',
210
+ type: :costume
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ rows = [
218
+ {
219
+ 'ID #' => 1,
220
+ 'First Name' => 'Bruce',
221
+ 'Last Name' => 'Banner',
222
+ 'Costume ID #' => 3,
223
+ 'Costume Name' => 'Basic Hulk',
224
+ 'Costume Color' => 'Green'
225
+ },
226
+ {
227
+ 'ID #' => 1,
228
+ 'First Name' => 'Bruce',
229
+ 'Last Name' => 'Banner',
230
+ 'Costume ID #' => 4,
231
+ 'Costume Name' => 'Mad Hulk',
232
+ 'Costume Color' => 'Red'
233
+ },
234
+ {
235
+ 'ID #' => 2,
236
+ 'First Name' => 'Tony',
237
+ 'Last Name' => 'Stark',
238
+ 'Costume ID #' => 5,
239
+ 'Costume Name' => 'Mark I',
240
+ 'Costume Color' => 'Gray'
241
+ },
242
+ {
243
+ 'ID #' => 2,
244
+ 'First Name' => 'Tony',
245
+ 'Last Name' => 'Stark',
246
+ 'Costume ID #' => 6,
247
+ 'Costume Name' => 'Mark IV',
248
+ 'Costume Color' => 'Red'
249
+ },
250
+ {
251
+ 'ID #' => 2,
252
+ 'First Name' => 'Tony',
253
+ 'Last Name' => 'Stark',
254
+ 'Costume ID #' => 7,
255
+ 'Costume Name' => 'Mark VI',
256
+ 'Costume Color' => 'Nano-Blue'
257
+ }
258
+ ]
259
+
260
+ graph = ::Hashematics.graph(config: config, rows: rows)
261
+ objects = graph.data(:avengers)
262
+
263
+ expected = [
264
+ {
265
+ id: 1,
266
+ first: 'Bruce',
267
+ last: 'Banner',
268
+ costumes: [
269
+ { id: 3, name: 'Basic Hulk', color: 'Green' },
270
+ { id: 4, name: 'Mad Hulk', color: 'Red' }
271
+ ]
272
+ },
273
+ {
274
+ id: 2,
275
+ first: 'Tony',
276
+ last: 'Stark',
277
+ costumes: [
278
+ { id: 5, name: 'Mark I', color: 'Gray' },
279
+ { id: 6, name: 'Mark IV', color: 'Red' },
280
+ { id: 7, name: 'Mark VI', color: 'Nano-Blue' }
281
+ ]
282
+ }
283
+ ]
284
+
285
+ expect(objects).to eq(expected)
286
+ end
287
+
288
+ specify 'Multiple Top-Level Graphs should work' do
289
+ config = {
290
+ types: {
291
+ person: {
292
+ properties: {
293
+ id: 'ID #',
294
+ first: 'First Name',
295
+ last: 'Last Name'
296
+ }
297
+ },
298
+ costume: {
299
+ properties: {
300
+ id: 'Costume ID #',
301
+ name: 'Costume Name',
302
+ color: 'Costume Color'
303
+ }
304
+ }
305
+ },
306
+ groups: {
307
+ avengers: {
308
+ by: 'ID #',
309
+ type: :person,
310
+ groups: {
311
+ costumes: {
312
+ by: 'Costume ID #',
313
+ type: :costume
314
+ }
315
+ }
316
+ },
317
+ costumes: {
318
+ by: 'Costume ID #',
319
+ type: :costume
320
+ }
321
+ }
322
+ }
323
+
324
+ rows = [
325
+ {
326
+ 'ID #' => 1,
327
+ 'First Name' => 'Bruce',
328
+ 'Last Name' => 'Banner',
329
+ 'Costume ID #' => 3,
330
+ 'Costume Name' => 'Basic Hulk',
331
+ 'Costume Color' => 'Green'
332
+ },
333
+ {
334
+ 'ID #' => 1,
335
+ 'First Name' => 'Bruce',
336
+ 'Last Name' => 'Banner',
337
+ 'Costume ID #' => 4,
338
+ 'Costume Name' => 'Mad Hulk',
339
+ 'Costume Color' => 'Red'
340
+ },
341
+ {
342
+ 'ID #' => 2,
343
+ 'First Name' => 'Tony',
344
+ 'Last Name' => 'Stark',
345
+ 'Costume ID #' => 5,
346
+ 'Costume Name' => 'Mark I',
347
+ 'Costume Color' => 'Gray'
348
+ },
349
+ {
350
+ 'ID #' => 2,
351
+ 'First Name' => 'Tony',
352
+ 'Last Name' => 'Stark',
353
+ 'Costume ID #' => 6,
354
+ 'Costume Name' => 'Mark IV',
355
+ 'Costume Color' => 'Red'
356
+ },
357
+ {
358
+ 'ID #' => 2,
359
+ 'First Name' => 'Tony',
360
+ 'Last Name' => 'Stark',
361
+ 'Costume ID #' => 7,
362
+ 'Costume Name' => 'Mark VI',
363
+ 'Costume Color' => 'Nano-Blue'
364
+ }
365
+ ]
366
+
367
+ graph = ::Hashematics.graph(config: config, rows: rows)
368
+ objects = graph.data(:costumes)
369
+
370
+ expected = [
371
+ { id: 3, name: 'Basic Hulk', color: 'Green' },
372
+ { id: 4, name: 'Mad Hulk', color: 'Red' },
373
+ { id: 5, name: 'Mark I', color: 'Gray' },
374
+ { id: 6, name: 'Mark IV', color: 'Red' },
375
+ { id: 7, name: 'Mark VI', color: 'Nano-Blue' }
376
+ ]
377
+
378
+ expect(objects).to eq(expected)
379
+ end
380
+
381
+ specify 'Handling Blanks (skip - default) should work' do
382
+ config = {
383
+ types: {
384
+ person: {
385
+ properties: {
386
+ id: 'ID #',
387
+ first: 'First Name',
388
+ last: 'Last Name'
389
+ }
390
+ },
391
+ costume: {
392
+ properties: {
393
+ id: 'Costume ID #',
394
+ name: 'Costume Name',
395
+ color: 'Costume Color'
396
+ }
397
+ }
398
+ },
399
+ groups: {
400
+ avengers: {
401
+ by: 'ID #',
402
+ type: :person,
403
+ groups: {
404
+ costumes: {
405
+ by: 'Costume ID #',
406
+ type: :costume
407
+ }
408
+ }
409
+ },
410
+ costumes: {
411
+ by: 'Costume ID #',
412
+ type: :costume
413
+ }
414
+ }
415
+ }
416
+
417
+ rows = [
418
+ {
419
+ 'ID #' => 1,
420
+ 'First Name' => 'Bruce',
421
+ 'Last Name' => 'Banner',
422
+ 'Costume ID #' => 3,
423
+ 'Costume Name' => 'Basic Hulk',
424
+ 'Costume Color' => 'Green'
425
+ },
426
+ {
427
+ 'ID #' => 2,
428
+ 'First Name' => 'Tony',
429
+ 'Last Name' => 'Stark',
430
+ 'Costume ID #' => '',
431
+ 'Costume Name' => '',
432
+ 'Costume Color' => ''
433
+ },
434
+ {
435
+ 'Costume ID #' => 4,
436
+ 'Costume Name' => 'Undercover',
437
+ 'Costume Color' => 'Purple'
438
+ }
439
+ ]
440
+
441
+ graph = ::Hashematics.graph(config: config, rows: rows)
442
+ avengers = graph.data(:avengers)
443
+ costumes = graph.data(:costumes)
444
+
445
+ expected_avengers = [
446
+ {
447
+ id: 1,
448
+ first: 'Bruce',
449
+ last: 'Banner',
450
+ costumes: [
451
+ { id: 3, name: 'Basic Hulk', color: 'Green' }
452
+ ]
453
+ },
454
+ {
455
+ id: 2,
456
+ first: 'Tony',
457
+ last: 'Stark',
458
+ costumes: []
459
+ }
460
+ ]
461
+
462
+ expected_costumes = [
463
+ { id: 3, name: 'Basic Hulk', color: 'Green' },
464
+ { id: 4, name: 'Undercover', color: 'Purple' }
465
+ ]
466
+
467
+ expect(avengers).to eq(expected_avengers)
468
+ expect(costumes).to eq(expected_costumes)
469
+ end
470
+
471
+ specify 'Handling Blanks (include) should work' do
472
+ config = {
473
+ types: {
474
+ person: {
475
+ properties: {
476
+ id: 'ID #',
477
+ first: 'First Name',
478
+ last: 'Last Name'
479
+ }
480
+ },
481
+ costume: {
482
+ properties: {
483
+ id: 'Costume ID #',
484
+ name: 'Costume Name',
485
+ color: 'Costume Color'
486
+ }
487
+ }
488
+ },
489
+ groups: {
490
+ avengers: {
491
+ by: 'ID #',
492
+ include_blank: true,
493
+ type: :person,
494
+ groups: {
495
+ costumes: {
496
+ by: 'Costume ID #',
497
+ type: :costume
498
+ }
499
+ }
500
+ },
501
+ costumes: {
502
+ by: 'Costume ID #',
503
+ include_blank: true,
504
+ type: :costume
505
+ }
506
+ }
507
+ }
508
+
509
+ rows = [
510
+ {
511
+ 'ID #' => 1,
512
+ 'First Name' => 'Bruce',
513
+ 'Last Name' => 'Banner',
514
+ 'Costume ID #' => 3,
515
+ 'Costume Name' => 'Basic Hulk',
516
+ 'Costume Color' => 'Green'
517
+ },
518
+ {
519
+ 'ID #' => 2,
520
+ 'First Name' => 'Tony',
521
+ 'Last Name' => 'Stark',
522
+ 'Costume ID #' => '',
523
+ 'Costume Name' => '',
524
+ 'Costume Color' => ''
525
+ },
526
+ {
527
+ 'Costume ID #' => 4,
528
+ 'Costume Name' => 'Undercover',
529
+ 'Costume Color' => 'Purple'
530
+ }
531
+ ]
532
+
533
+ graph = ::Hashematics.graph(config: config, rows: rows)
534
+ avengers = graph.data(:avengers)
535
+ costumes = graph.data(:costumes)
536
+
537
+ expected_avengers = [
538
+ {
539
+ id: 1,
540
+ first: 'Bruce',
541
+ last: 'Banner',
542
+ costumes: [
543
+ { id: 3, name: 'Basic Hulk', color: 'Green' }
544
+ ]
545
+ },
546
+ {
547
+ id: 2,
548
+ first: 'Tony',
549
+ last: 'Stark',
550
+ costumes: []
551
+ },
552
+ {
553
+ id: nil,
554
+ first: nil,
555
+ last: nil,
556
+ costumes: [
557
+ { id: 4, name: 'Undercover', color: 'Purple' }
558
+ ]
559
+ }
560
+ ]
561
+
562
+ expected_costumes = [
563
+ { id: 3, name: 'Basic Hulk', color: 'Green' },
564
+ { id: '', name: '', color: '' },
565
+ { id: 4, name: 'Undercover', color: 'Purple' }
566
+ ]
567
+
568
+ expect(avengers).to eq(expected_avengers)
569
+ expect(costumes).to eq(expected_costumes)
570
+ end
571
+ end
572
+ end