iiif-presentation 1.0.0 → 1.2.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/.gitignore +1 -0
  4. data/Gemfile +2 -0
  5. data/README.md +22 -2
  6. data/VERSION +1 -1
  7. data/iiif-presentation.gemspec +1 -1
  8. data/lib/iiif/hash_behaviours.rb +1 -1
  9. data/lib/iiif/presentation/canvas.rb +4 -0
  10. data/lib/iiif/presentation/service.rb +12 -0
  11. data/lib/iiif/presentation.rb +5 -4
  12. data/lib/iiif/service.rb +40 -105
  13. data/lib/iiif/v3/abstract_resource.rb +491 -0
  14. data/lib/iiif/v3/presentation/annotation.rb +74 -0
  15. data/lib/iiif/v3/presentation/annotation_collection.rb +38 -0
  16. data/lib/iiif/v3/presentation/annotation_page.rb +53 -0
  17. data/lib/iiif/v3/presentation/canvas.rb +82 -0
  18. data/lib/iiif/v3/presentation/choice.rb +51 -0
  19. data/lib/iiif/v3/presentation/collection.rb +52 -0
  20. data/lib/iiif/v3/presentation/image_resource.rb +110 -0
  21. data/lib/iiif/v3/presentation/manifest.rb +82 -0
  22. data/lib/iiif/v3/presentation/range.rb +39 -0
  23. data/lib/iiif/v3/presentation/resource.rb +30 -0
  24. data/lib/iiif/v3/presentation/sequence.rb +66 -0
  25. data/lib/iiif/v3/presentation/service.rb +51 -0
  26. data/lib/iiif/v3/presentation.rb +36 -0
  27. data/spec/fixtures/v3/manifests/complete_from_spec.json +195 -0
  28. data/spec/fixtures/v3/manifests/minimal.json +49 -0
  29. data/spec/fixtures/v3/manifests/service_only.json +14 -0
  30. data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +1 -1
  31. data/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json +1 -0
  32. data/spec/integration/iiif/presentation/image_resource_spec.rb +0 -1
  33. data/spec/integration/iiif/service_spec.rb +17 -32
  34. data/spec/integration/iiif/v3/abstract_resource_spec.rb +202 -0
  35. data/spec/integration/iiif/v3/presentation/image_resource_spec.rb +118 -0
  36. data/spec/spec_helper.rb +6 -0
  37. data/spec/unit/iiif/presentation/canvas_spec.rb +0 -1
  38. data/spec/unit/iiif/presentation/manifest_spec.rb +1 -1
  39. data/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +78 -0
  40. data/spec/unit/iiif/v3/abstract_resource_spec.rb +293 -0
  41. data/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +36 -0
  42. data/spec/unit/iiif/v3/presentation/annotation_page_spec.rb +131 -0
  43. data/spec/unit/iiif/v3/presentation/annotation_spec.rb +389 -0
  44. data/spec/unit/iiif/v3/presentation/canvas_spec.rb +337 -0
  45. data/spec/unit/iiif/v3/presentation/choice_spec.rb +120 -0
  46. data/spec/unit/iiif/v3/presentation/collection_spec.rb +55 -0
  47. data/spec/unit/iiif/v3/presentation/image_resource_spec.rb +189 -0
  48. data/spec/unit/iiif/v3/presentation/manifest_spec.rb +370 -0
  49. data/spec/unit/iiif/v3/presentation/range_spec.rb +54 -0
  50. data/spec/unit/iiif/v3/presentation/resource_spec.rb +174 -0
  51. data/spec/unit/iiif/v3/presentation/sequence_spec.rb +222 -0
  52. data/spec/unit/iiif/v3/presentation/service_spec.rb +220 -0
  53. data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +41 -0
  54. data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +31 -0
  55. data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +40 -0
  56. data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +40 -0
  57. data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +45 -0
  58. data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +45 -0
  59. data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +26 -0
  60. data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +31 -0
  61. metadata +82 -11
  62. data/.travis.yml +0 -11
@@ -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