iiif-presentation 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -2
  3. data/.gitignore +1 -0
  4. data/Gemfile +2 -0
  5. data/README.md +22 -1
  6. data/VERSION +1 -1
  7. data/iiif-presentation.gemspec +1 -1
  8. data/lib/iiif/presentation/canvas.rb +4 -0
  9. data/lib/iiif/presentation/service.rb +12 -0
  10. data/lib/iiif/presentation.rb +5 -4
  11. data/lib/iiif/service.rb +38 -103
  12. data/lib/iiif/v3/abstract_resource.rb +491 -0
  13. data/lib/iiif/v3/presentation/annotation.rb +74 -0
  14. data/lib/iiif/v3/presentation/annotation_collection.rb +38 -0
  15. data/lib/iiif/v3/presentation/annotation_page.rb +53 -0
  16. data/lib/iiif/v3/presentation/canvas.rb +82 -0
  17. data/lib/iiif/v3/presentation/choice.rb +51 -0
  18. data/lib/iiif/v3/presentation/collection.rb +52 -0
  19. data/lib/iiif/v3/presentation/image_resource.rb +110 -0
  20. data/lib/iiif/v3/presentation/manifest.rb +82 -0
  21. data/lib/iiif/v3/presentation/range.rb +39 -0
  22. data/lib/iiif/v3/presentation/resource.rb +30 -0
  23. data/lib/iiif/v3/presentation/sequence.rb +66 -0
  24. data/lib/iiif/v3/presentation/service.rb +51 -0
  25. data/lib/iiif/v3/presentation.rb +36 -0
  26. data/spec/fixtures/v3/manifests/complete_from_spec.json +195 -0
  27. data/spec/fixtures/v3/manifests/minimal.json +49 -0
  28. data/spec/fixtures/v3/manifests/service_only.json +14 -0
  29. data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +1 -1
  30. data/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json +1 -0
  31. data/spec/integration/iiif/presentation/image_resource_spec.rb +0 -1
  32. data/spec/integration/iiif/service_spec.rb +17 -32
  33. data/spec/integration/iiif/v3/abstract_resource_spec.rb +202 -0
  34. data/spec/integration/iiif/v3/presentation/image_resource_spec.rb +118 -0
  35. data/spec/spec_helper.rb +6 -0
  36. data/spec/unit/iiif/presentation/canvas_spec.rb +0 -1
  37. data/spec/unit/iiif/presentation/manifest_spec.rb +1 -1
  38. data/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +78 -0
  39. data/spec/unit/iiif/v3/abstract_resource_spec.rb +293 -0
  40. data/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +36 -0
  41. data/spec/unit/iiif/v3/presentation/annotation_page_spec.rb +131 -0
  42. data/spec/unit/iiif/v3/presentation/annotation_spec.rb +389 -0
  43. data/spec/unit/iiif/v3/presentation/canvas_spec.rb +337 -0
  44. data/spec/unit/iiif/v3/presentation/choice_spec.rb +120 -0
  45. data/spec/unit/iiif/v3/presentation/collection_spec.rb +55 -0
  46. data/spec/unit/iiif/v3/presentation/image_resource_spec.rb +189 -0
  47. data/spec/unit/iiif/v3/presentation/manifest_spec.rb +370 -0
  48. data/spec/unit/iiif/v3/presentation/range_spec.rb +54 -0
  49. data/spec/unit/iiif/v3/presentation/resource_spec.rb +174 -0
  50. data/spec/unit/iiif/v3/presentation/sequence_spec.rb +222 -0
  51. data/spec/unit/iiif/v3/presentation/service_spec.rb +220 -0
  52. data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +41 -0
  53. data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +31 -0
  54. data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +40 -0
  55. data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +40 -0
  56. data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +45 -0
  57. data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +45 -0
  58. data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +26 -0
  59. data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +31 -0
  60. metadata +78 -7
@@ -0,0 +1,337 @@
1
+ describe IIIF::V3::Presentation::Canvas do
2
+
3
+ describe '#required_keys' do
4
+ %w{ type id label }.each do |k|
5
+ it k do
6
+ expect(subject.required_keys).to include(k)
7
+ end
8
+ end
9
+ end
10
+
11
+ describe '#prohibited_keys' do
12
+ it 'contains the expected key names' do
13
+ keys = described_class::PAGING_PROPERTIES +
14
+ %w{
15
+ viewing_direction
16
+ format
17
+ nav_date
18
+ start_canvas
19
+ content_annotations
20
+ }
21
+ expect(subject.prohibited_keys).to include(*keys)
22
+ end
23
+ end
24
+
25
+ describe '#int_only_keys' do
26
+ it 'depth (for 3d objects)' do
27
+ expect(subject.int_only_keys).to include('depth')
28
+ end
29
+ end
30
+
31
+ describe '#array_only_keys' do
32
+ it 'content' do
33
+ expect(subject.array_only_keys).to include('content')
34
+ end
35
+ end
36
+
37
+ describe '#legal_viewing_hint_values' do
38
+ it 'contains the expected values' do
39
+ expect(subject.legal_viewing_hint_values).to contain_exactly('paged', 'continuous', 'non-paged', 'facing-pages', 'auto-advance')
40
+ end
41
+ end
42
+
43
+ describe '#initialize' do
44
+ it 'sets type to Canvas by default' do
45
+ expect(subject['type']).to eq 'Canvas'
46
+ end
47
+ it 'allows subclasses to override type' do
48
+ subclass = Class.new(described_class) do
49
+ def initialize(hsh={})
50
+ hsh = { 'type' => 'a:SubClass' }
51
+ super(hsh)
52
+ end
53
+ end
54
+ sub = subclass.new
55
+ expect(sub['type']).to eq 'a:SubClass'
56
+ end
57
+ end
58
+
59
+ describe '#validate' do
60
+ let(:canvas_id) { 'http://example.org/iiif/book1/canvas/c1' }
61
+ let(:exp_id_err_msg) { "id must be an http(s) URI without a fragment for #{described_class}" }
62
+ before(:each) do
63
+ subject['id'] = canvas_id
64
+ subject['label'] = 'foo'
65
+ end
66
+ it 'raises IllegalValueError if id is not URI' do
67
+ subject['id'] = 'foo'
68
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg)
69
+ end
70
+ it 'raises IllegalValueError if id is not http(s)' do
71
+ subject['id'] = 'ftp://www.example.org'
72
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg)
73
+ end
74
+ it 'raises IllegalValueError if id has a fragment' do
75
+ subject['id'] = 'http://www.example.org#foo'
76
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg)
77
+ end
78
+
79
+ # let(:exp_extent_err_msg) { "#{described_class} must have (a height and a width) and/or a duration" }
80
+ # (see sul-dlss/purl/issues/169)
81
+ let(:exp_extent_err_msg) { "#{described_class} requires both height and width or neither" }
82
+ it 'raises IllegalValueError if height is a string' do
83
+ subject['height'] = 'foo'
84
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg)
85
+ end
86
+ it 'raises IllegalValueError if height but no width' do
87
+ subject['height'] = 666
88
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg)
89
+ end
90
+ it 'raises IllegalValueError if width but no height' do
91
+ subject['width'] = 666
92
+ subject['duration'] = 66.6
93
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg)
94
+ end
95
+ it 'raises IllegalValueError if no width, height or duration' do
96
+ # (see sul-dlss/purl/issues/169)
97
+ skip('while this is in the current v3 spec, it does not make sense for some content (e.g. txt files)')
98
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg)
99
+ end
100
+ it 'allows width, height and duration' do
101
+ subject['width'] = 666
102
+ subject['height'] = 666
103
+ subject['duration'] = 66.6
104
+ expect { subject.validate }.not_to raise_error
105
+ end
106
+
107
+ it 'raises IllegalValueError for content entry that is not an AnnotationPage' do
108
+ subject['content'] = [IIIF::V3::Presentation::AnnotationPage.new, IIIF::V3::Presentation::Annotation.new]
109
+ exp_err_msg = "All entries in the content list must be a IIIF::V3::Presentation::AnnotationPage"
110
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
111
+ end
112
+
113
+ it 'IllegalValueError for content with annotation target not the canvas id' do
114
+ anno = IIIF::V3::Presentation::Annotation.new(
115
+ 'id' => 'http://example.com/anno/666',
116
+ 'target' => canvas_id)
117
+ anno_page = IIIF::V3::Presentation::AnnotationPage.new(
118
+ 'id' => "http://example.org/iiif/book1/page/p1/1",
119
+ 'items' => [anno])
120
+ subject['content'] = [anno_page]
121
+
122
+ expect { subject.validate }.not_to raise_error
123
+
124
+ anno['target'] = 'http://example.com/canvas/abc'
125
+ exp_err_msg = 'URI of the canvas must be repeated in the target field of included Annotations'
126
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
127
+ end
128
+ end
129
+
130
+ describe 'realistic examples' do
131
+ let(:canvas_id) { 'http://example.org/iiif/book1/canvas/c1' }
132
+ let(:minimal_canvas_object) { described_class.new({
133
+ "id" => canvas_id,
134
+ 'label' => {"en" => ["so minimal it's not here"]},
135
+ 'height' => 1000,
136
+ 'width' => 1000
137
+ })}
138
+ let(:anno_page) { IIIF::V3::Presentation::AnnotationPage.new(
139
+ "id" => "http://example.org/iiif/book1/page/p1/1",
140
+ 'items' => []
141
+ )}
142
+ describe 'minimal canvas' do
143
+ it 'validates' do
144
+ expect{minimal_canvas_object.validate}.not_to raise_error
145
+ end
146
+ it 'has expected required values' do
147
+ expect(minimal_canvas_object.type).to eq described_class::TYPE
148
+ expect(minimal_canvas_object.id).to eq canvas_id
149
+ expect(minimal_canvas_object.label['en']).to include "so minimal it's not here"
150
+ expect(minimal_canvas_object.height).to eq 1000
151
+ expect(minimal_canvas_object.width).to eq 1000
152
+ end
153
+ end
154
+ describe 'minimal with empty content' do
155
+ let(:canvas_object) {
156
+ minimal_canvas_object['content'] = []
157
+ minimal_canvas_object
158
+ }
159
+ it 'validates' do
160
+ expect{canvas_object.validate}.not_to raise_error
161
+ end
162
+ it 'has empty array for content' do
163
+ expect(canvas_object.content).to eq []
164
+ end
165
+ end
166
+ describe 'minimal with content' do
167
+ let(:canvas_object) {
168
+ minimal_canvas_object['content'] = [anno_page, anno_page]
169
+ minimal_canvas_object
170
+ }
171
+ it 'validates' do
172
+ expect{canvas_object.validate}.not_to raise_error
173
+ end
174
+ it 'has content value' do
175
+ expect(canvas_object.content.size).to eq 2
176
+ expect(canvas_object.content).to eq [anno_page, anno_page]
177
+ end
178
+
179
+ describe 'stanford (purl code)' do
180
+ let(:canvas_object) {
181
+ c = described_class.new
182
+ c['id'] = canvas_id
183
+ c.label = {'en' => ['label']}
184
+ c.content << anno_page
185
+ c
186
+ }
187
+ describe 'non-image' do
188
+ it 'validates' do
189
+ expect{canvas_object.validate}.not_to raise_error
190
+ end
191
+ it 'has expected required values' do
192
+ expect(canvas_object.type).to eq described_class::TYPE
193
+ expect(canvas_object.id).to eq canvas_id
194
+ expect(canvas_object.label['en']).to include "label"
195
+ end
196
+ it 'has expected additional values' do
197
+ expect(canvas_object.content).to eq [anno_page]
198
+ end
199
+ end
200
+ describe 'image' do
201
+ let(:img_canvas) {
202
+ canvas_object.height = 666
203
+ canvas_object.width = 888
204
+ canvas_object
205
+ }
206
+ it 'validates' do
207
+ expect{img_canvas.validate}.not_to raise_error
208
+ end
209
+ it 'has expected required values' do
210
+ expect(img_canvas.type).to eq described_class::TYPE
211
+ expect(img_canvas.id).to eq canvas_id
212
+ expect(img_canvas.label['en']).to include "label"
213
+ expect(img_canvas.height).to eq 666
214
+ expect(img_canvas.width).to eq 888
215
+ end
216
+ it 'has expected additional values' do
217
+ expect(img_canvas.content).to eq [anno_page]
218
+ end
219
+ end
220
+ end
221
+
222
+ describe 'file object' do
223
+ describe 'without extent info' do
224
+ let(:file_object) { described_class.new({
225
+ "id" => "https://example.org/bd742gh0511/iiif3/canvas/bd742gh0511_1",
226
+ "label" => {"en" => ["File 1"]},
227
+ "content" => [anno_page]
228
+ })}
229
+ it 'validates' do
230
+ expect{file_object.validate}.not_to raise_error
231
+ end
232
+ end
233
+ end
234
+
235
+ describe 'image object' do
236
+ describe 'without extent info' do
237
+ let(:image_object) { described_class.new({
238
+ "id" => "https://example.org/yv090xk3108/iiif3/canvas/yv090xk3108_1",
239
+ "label" => {"en" => ["image"]},
240
+ "content" => [anno_page]
241
+ })}
242
+ it 'validates' do
243
+ expect{image_object.validate}.not_to raise_error
244
+ end
245
+ end
246
+ describe 'with extent given' do
247
+ let(:image_object) { described_class.new({
248
+ "id" => "https://example.org/yy816tv6021/iiif3/canvas/yy816tv6021_3",
249
+ "label" => {"en" => ["Image of media (1 of 2)"]},
250
+ "height" => 3456,
251
+ "width" => 5184,
252
+ "content" => [anno_page]
253
+ })}
254
+ it 'validates' do
255
+ expect{image_object.validate}.not_to raise_error
256
+ end
257
+ end
258
+ end
259
+
260
+ describe 'audio object' do
261
+ describe 'without duration' do
262
+ let(:canvas_for_audio) { described_class.new({
263
+ "id" => "https://example.org/xk681bt2506/iiif3/canvas/xk681bt2506_1",
264
+ "label" => {"en" => ["Audio file 1"]},
265
+ "content" => [anno_page]
266
+ })}
267
+ it 'validates' do
268
+ expect{canvas_for_audio.validate}.not_to raise_error
269
+ end
270
+ end
271
+ describe 'digerati example' do
272
+ let(:canvas_for_audio) { described_class.new({
273
+ "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2",
274
+ "label" => "Track 2",
275
+ 'summary' => {
276
+ 'en' => ['foo']
277
+ },
278
+ "duration" => 45,
279
+ "content" => [anno_page]
280
+ })}
281
+ it 'validates' do
282
+ expect{canvas_for_audio.validate}.not_to raise_error
283
+ end
284
+ it 'duration' do
285
+ expect(canvas_for_audio.duration).to eq 45
286
+ end
287
+ it 'summary' do
288
+ expect(canvas_for_audio.summary['en'].first).to eq 'foo'
289
+ end
290
+ end
291
+ end
292
+
293
+ describe '3d object' do
294
+ let(:canvas_3d_object) { described_class.new({
295
+ "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/3d",
296
+ "thumbnail" => [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg",
297
+ 'type' => 'Image'}],
298
+ "width" => 10000,
299
+ "height" => 10000,
300
+ "depth" => 10000,
301
+ "label" => "A stage for an object",
302
+ "content" => [anno_page]
303
+ })}
304
+ it 'validates' do
305
+ expect{canvas_3d_object.validate}.not_to raise_error
306
+ end
307
+ it 'thumbnail' do
308
+ expect(canvas_3d_object.thumbnail).to eq [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", 'type' => 'Image'}]
309
+ end
310
+ it 'depth' do
311
+ expect(canvas_3d_object.depth).to eq 10000
312
+ end
313
+ end
314
+
315
+ describe 'video object' do
316
+ describe 'with extent info' do
317
+ let(:canvas_for_video) { described_class.new({
318
+ "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/1",
319
+ "label" => "Associate multiple Video representations as Choice",
320
+ "height" => 1000,
321
+ "width" => 1000,
322
+ "duration" => 100,
323
+ "content" => [anno_page]
324
+ }) }
325
+ it 'validates' do
326
+ expect{canvas_for_video.validate}.not_to raise_error
327
+ end
328
+ it 'height, width, duration' do
329
+ expect(canvas_for_video.height).to eq 1000
330
+ expect(canvas_for_video.width).to eq 1000
331
+ expect(canvas_for_video.duration).to eq 100
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,120 @@
1
+ describe IIIF::V3::Presentation::Choice do
2
+
3
+ describe '#prohibited_keys' do
4
+ it 'contains the expected key names' do
5
+ keys = described_class::PAGING_PROPERTIES +
6
+ described_class::CONTENT_RESOURCE_PROPERTIES +
7
+ %w{
8
+ nav_date
9
+ viewing_direction
10
+ start_canvas
11
+ content_annotations
12
+ }
13
+ expect(subject.prohibited_keys).to include(*keys)
14
+ end
15
+ end
16
+
17
+ describe '#any_type_keys' do
18
+ it 'default' do
19
+ expect(subject.any_type_keys).to include('default')
20
+ end
21
+ end
22
+
23
+ describe '#string_only_keys' do
24
+ it 'choice_hint' do
25
+ expect(subject.string_only_keys).to include('choice_hint')
26
+ end
27
+ end
28
+
29
+ describe '#array_only_keys' do
30
+ it 'items' do
31
+ expect(subject.array_only_keys).to include('items')
32
+ end
33
+ end
34
+
35
+ describe '#legal_choice_hint_values' do
36
+ it 'contains the expected values' do
37
+ expect(subject.legal_choice_hint_values).to contain_exactly('client', 'user')
38
+ end
39
+ end
40
+
41
+ describe '#legal_viewing_hint_values' do
42
+ it 'contains none' do
43
+ expect(subject.legal_viewing_hint_values).to contain_exactly('none')
44
+ end
45
+ end
46
+
47
+ describe '#initialize' do
48
+ it 'sets type to Choice by default' do
49
+ expect(subject['type']).to eq 'Choice'
50
+ end
51
+ it 'allows subclasses to override type' do
52
+ subclass = Class.new(described_class) do
53
+ def initialize(hsh={})
54
+ hsh = { 'type' => 'a:SubClass' }
55
+ super(hsh)
56
+ end
57
+ end
58
+ sub = subclass.new
59
+ expect(sub['type']).to eq 'a:SubClass'
60
+ end
61
+ it 'allows type to be passed in' do
62
+ my_choice = described_class.new('type' => 'bar')
63
+ expect(my_choice.type).to eq 'bar'
64
+ end
65
+ end
66
+
67
+ describe '#validate' do
68
+ it 'raises an IllegalValueError if choice_hint isn\'t an allowable value' do
69
+ exp_err_msg = "choiceHint for #{described_class} must be one of [\"client\", \"user\"]."
70
+ subject['choice_hint'] = 'foo'
71
+ expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, exp_err_msg
72
+ end
73
+ end
74
+
75
+ describe 'realistic examples' do
76
+ describe 'from digerati' do
77
+ let(:item_type) { 'Video' }
78
+ let(:item1_id) { 'http://example.org/foo.mp4f' }
79
+ let(:item1_mime) { 'video/mp4' }
80
+ let(:item1_res) { IIIF::V3::Presentation::Resource.new(
81
+ 'id' => item1_id,
82
+ 'type' => item_type,
83
+ 'format' => item1_mime
84
+ )}
85
+ let(:item2_id) { 'http://example.org/foo.webm' }
86
+ let(:item2_mime) { 'video/webm' }
87
+ let(:item2_res) { IIIF::V3::Presentation::Resource.new(
88
+ 'id' => item2_id,
89
+ 'type' => item_type,
90
+ 'format' => item2_mime
91
+ )}
92
+ let(:choice) { IIIF::V3::Presentation::Choice.new(
93
+ 'choiceHint' => 'client',
94
+ 'items' => [item1_res, item2_res]
95
+ )}
96
+ it 'validates' do
97
+ expect{choice.validate}.not_to raise_error
98
+ end
99
+ it 'has expected required values' do
100
+ expect(choice['type']).to eq 'Choice'
101
+ end
102
+ it 'has expected additional values' do
103
+ expect(choice.id).to be_nil
104
+ expect(choice['choice_hint']).to eq 'client'
105
+ expect(choice.choiceHint).to eq 'client'
106
+ expect(choice['items']).to eq [item1_res, item2_res]
107
+ first = choice['items'].first
108
+ expect(first.keys.size).to eq 3
109
+ expect(first['id']).to eq item1_id
110
+ expect(first['type']).to eq item_type
111
+ expect(first['format']).to eq item1_mime
112
+ second = choice['items'].last
113
+ expect(second.keys.size).to eq 3
114
+ expect(second['id']).to eq item2_id
115
+ expect(second['type']).to eq item_type
116
+ expect(second['format']).to eq item2_mime
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,55 @@
1
+ describe IIIF::V3::Presentation::Collection do
2
+
3
+ let(:fixed_values) do
4
+ {
5
+ "@context" => [
6
+ "http://iiif.io/api/presentation/3/context.json",
7
+ "http://www.w3.org/ns/anno.jsonld"
8
+ ],
9
+ 'id' => 'http://example.org/iiif/collection/top',
10
+ 'type' => 'Collection',
11
+ 'label' => 'Top Level Collection for Example Organization',
12
+ 'description' => 'Description of Collection',
13
+ 'attribution' => 'Provided by Example Organization',
14
+
15
+ 'collections' => [
16
+ { 'id' => 'http://example.org/iiif/collection/part1',
17
+ 'type' => 'Collection',
18
+ 'label' => 'Sub Collection 1'
19
+ },
20
+ { 'id' => 'http://example.org/iiif/collection/part2',
21
+ 'type' => 'Collection',
22
+ 'label' => 'Sub Collection 2'
23
+ }
24
+ ],
25
+ 'manifests' => [
26
+ { 'id' => 'http://example.org/iiif/book1/manifest',
27
+ 'type' => 'Manifest',
28
+ 'label' => 'Book 1'
29
+ }
30
+ ]
31
+ }
32
+ end
33
+
34
+ describe '#initialize' do
35
+ it 'sets type to Collection by default' do
36
+ expect(subject['type']).to eq 'Collection'
37
+ end
38
+ end
39
+
40
+ describe "#{described_class}.define_methods_for_array_only_keys" do
41
+ it_behaves_like 'it has the appropriate methods for array-only keys v3'
42
+ end
43
+
44
+ describe "#{described_class}.define_methods_for_string_only_keys" do
45
+ it_behaves_like 'it has the appropriate methods for string-only keys v3'
46
+ end
47
+
48
+ describe "#{described_class}.define_methods_for_any_type_keys" do
49
+ it_behaves_like 'it has the appropriate methods for any-type keys v3'
50
+ end
51
+
52
+ describe '#validate' do
53
+ end
54
+
55
+ end