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,131 @@
1
+ describe IIIF::V3::Presentation::AnnotationPage do
2
+
3
+ describe '#required_keys' do
4
+ it 'id' do
5
+ expect(subject.required_keys).to include('id')
6
+ end
7
+ end
8
+
9
+ describe '#prohibited_keys' do
10
+ it 'contains the expected key names' do
11
+ keys = described_class::CONTENT_RESOURCE_PROPERTIES +
12
+ %w{
13
+ first
14
+ last
15
+ total
16
+ nav_date
17
+ viewing_direction
18
+ start_canvas
19
+ content_annotations
20
+ }
21
+ expect(subject.prohibited_keys).to include(*keys)
22
+ end
23
+ end
24
+
25
+ describe '#uri_only_keys' do
26
+ it 'id' do
27
+ expect(subject.uri_only_keys).to include('id')
28
+ end
29
+ end
30
+
31
+ describe '#array_only_keys' do
32
+ it 'items' do
33
+ expect(subject.array_only_keys).to include('items')
34
+ end
35
+ end
36
+
37
+ describe '#legal_viewing_hint_values' do
38
+ it 'contains none' do
39
+ expect(subject.legal_viewing_hint_values).to contain_exactly('none')
40
+ end
41
+ end
42
+
43
+ describe '#initialize' do
44
+ it 'sets type to AnnotationPage by default' do
45
+ expect(subject['type']).to eq 'AnnotationPage'
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
+ it 'allows type to be passed in' do
58
+ ap = described_class.new('type' => 'bar')
59
+ expect(ap.type).to eq 'bar'
60
+ end
61
+ end
62
+
63
+ describe '#validate' do
64
+ it 'raises IllegalValueError if id is not URI' do
65
+ exp_err_msg = "id value must be a String containing a URI for #{described_class}"
66
+ subject['id'] = 'foo'
67
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
68
+ end
69
+ it 'raises IllegalValueError if id is not http(s)' do
70
+ subject['id'] = 'ftp://www.example.org'
71
+ exp_err_msg = "id must be an http(s) URI for #{described_class}"
72
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
73
+ end
74
+ it 'raises IllegalValueError for items entry that is not an Annotation' do
75
+ subject['id'] = 'http://example.com/iiif3/annotation_page/666'
76
+ subject['items'] = [IIIF::V3::Presentation::ImageResource.new, IIIF::V3::Presentation::Annotation.new]
77
+ exp_err_msg = "All entries in the items list must be a IIIF::V3::Presentation::Annotation"
78
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
79
+ end
80
+ end
81
+
82
+ describe 'realistic examples' do
83
+ let(:ap_id) { 'http://example.com/iiif3/annotation_page/666' }
84
+ let(:anno) { IIIF::V3::Presentation::Annotation.new(
85
+ 'id' => 'http://example.com/anno/666',
86
+ 'target' => 'http://example.com/canvas/abc'
87
+ )}
88
+
89
+ describe 'stanford (purl code)' do
90
+ let(:anno_page) {
91
+ anno_page = described_class.new
92
+ anno_page['id'] = ap_id
93
+ anno_page.items << anno
94
+ anno_page
95
+ }
96
+ it 'validates' do
97
+ expect{anno_page.validate}.not_to raise_error
98
+ end
99
+ it 'has expected required values' do
100
+ expect(anno_page.id).to eq ap_id
101
+ expect(anno_page['type']).to eq 'AnnotationPage'
102
+ end
103
+ it 'has expected additional values' do
104
+ expect(anno_page.items).to eq [anno]
105
+ end
106
+ end
107
+
108
+ describe 'two items' do
109
+ let(:anno2) { IIIF::V3::Presentation::Annotation.new(
110
+ 'id' => 'http://example.com/anno/333',
111
+ 'target' => 'http://example.com/canvas/abc'
112
+ )}
113
+ let(:anno_page) {
114
+ anno_page = described_class.new
115
+ anno_page['id'] = ap_id
116
+ anno_page.items = [anno, anno2]
117
+ anno_page
118
+ }
119
+ it 'validates' do
120
+ expect{anno_page.validate}.not_to raise_error
121
+ end
122
+ it 'has expected required values' do
123
+ expect(anno_page.id).to eq ap_id
124
+ expect(anno_page['type']).to eq 'AnnotationPage'
125
+ end
126
+ it 'has expected additional values' do
127
+ expect(anno_page.items).to eq [anno, anno2]
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,389 @@
1
+ describe IIIF::V3::Presentation::Annotation do
2
+
3
+ let(:content_id) { 'http://example.org/iiif/book1/res/tei-text-p1.xml' }
4
+ let(:content_type) { 'dctypes:Text' }
5
+ let(:mimetype) { 'application/tei+xml' }
6
+ let(:image_2_api_service) { IIIF::V3::Presentation::Service.new({
7
+ 'id' => content_id,
8
+ '@id' => content_id,
9
+ 'profile' => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE
10
+ })}
11
+ let(:img_content_resource) { IIIF::V3::Presentation::ImageResource.new(
12
+ 'id' => content_id,
13
+ 'type' => content_type,
14
+ 'format' => mimetype,
15
+ 'service' => image_2_api_service
16
+ )}
17
+
18
+ describe '#required_keys' do
19
+ %w{ type id motivation target }.each do |k|
20
+ it k do
21
+ expect(subject.required_keys).to include(k)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#prohibited_keys' do
27
+ it 'contains the expected key names' do
28
+ keys = described_class::PAGING_PROPERTIES +
29
+ described_class::CONTENT_RESOURCE_PROPERTIES +
30
+ %w{
31
+ nav_date
32
+ viewing_direction
33
+ start_canvas
34
+ content_annotations
35
+ }
36
+ expect(subject.prohibited_keys).to include(*keys)
37
+ end
38
+ end
39
+
40
+ describe '#any_type_keys' do
41
+ it 'body' do
42
+ expect(subject.any_type_keys).to include('body')
43
+ end
44
+ it 'target' do
45
+ expect(subject.any_type_keys).to include('target')
46
+ end
47
+ end
48
+
49
+ describe '#uri_only_keys' do
50
+ it 'id' do
51
+ expect(subject.uri_only_keys).to include('id')
52
+ end
53
+ end
54
+
55
+ describe '#string_only_keys' do
56
+ it 'time_mode' do
57
+ expect(subject.string_only_keys).to include('time_mode')
58
+ end
59
+ end
60
+
61
+ describe '#legal_time_mode_values' do
62
+ it 'contains the expected values' do
63
+ expect(subject.legal_time_mode_values).to contain_exactly('trim', 'scale', 'loop')
64
+ end
65
+ end
66
+
67
+ describe '#legal_viewing_hint_values' do
68
+ it 'contains none' do
69
+ expect(subject.legal_viewing_hint_values).to contain_exactly('none')
70
+ end
71
+ end
72
+
73
+ describe '#initialize' do
74
+ it 'sets type to Annotation by default' do
75
+ expect(subject['type']).to eq 'Annotation'
76
+ end
77
+ it 'allows subclasses to override type' do
78
+ subclass = Class.new(described_class) do
79
+ def initialize(hsh={})
80
+ hsh = { 'type' => 'a:SubClass' }
81
+ super(hsh)
82
+ end
83
+ end
84
+ sub = subclass.new
85
+ expect(sub['type']).to eq 'a:SubClass'
86
+ end
87
+ it 'sets motivation to painting by default' do
88
+ expect(subject['motivation']).to eq 'painting'
89
+ end
90
+ it 'allows motivation to be passed in' do
91
+ my_anno = described_class.new('motivation' => 'foo')
92
+ expect(my_anno.motivation).to eq 'foo'
93
+ end
94
+ it 'allows type to be passed in' do
95
+ my_anno = described_class.new('type' => 'bar')
96
+ expect(my_anno.type).to eq 'bar'
97
+ end
98
+ end
99
+
100
+ describe '#validate' do
101
+ before(:each) do
102
+ subject['id'] = 'http://example.org/iiif/anno/1s'
103
+ subject['target'] = 'foo'
104
+ end
105
+ it 'raises IllegalValueError if id is not URI' do
106
+ exp_err_msg = "id value must be a String containing a URI for #{described_class}"
107
+ subject['id'] = 'foo'
108
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
109
+ end
110
+
111
+ it 'raises IllegalValueError if time_mode isn\'t an allowable value' do
112
+ exp_err_msg = "timeMode for #{described_class} must be one of [\"trim\", \"scale\", \"loop\"]."
113
+ subject['time_mode'] = 'foo'
114
+ expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, exp_err_msg
115
+ end
116
+
117
+ describe 'body is a kind of IIIF::V3::Presentation::ImageResource' do
118
+ let(:img_body_anno) {
119
+ subject['id'] = 'http://example.org/iiif/anno/1s'
120
+ subject['target'] = 'foo'
121
+ subject['body'] = img_content_resource
122
+ subject
123
+ }
124
+ it 'raises IllegalValueError if motivation isn\'t "painting"' do
125
+ exp_err_msg = "#{described_class} motivation must be 'painting' when body is a kind of IIIF::V3::Presentation::ImageResource"
126
+ img_body_anno['motivation'] = 'foo'
127
+ expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, exp_err_msg
128
+ end
129
+ let(:img_resource_without_id) {
130
+ IIIF::V3::Presentation::ImageResource.new(
131
+ 'type' => content_type,
132
+ 'format' => mimetype
133
+ )}
134
+ let(:http_uri_err_msg) {
135
+ "when #{described_class} body is a kind of IIIF::V3::Presentation::ImageResource, ImageResource id must be an http(s) URI"
136
+ }
137
+ it 'raises IllegalValueError if no id field in ImageResource' do
138
+ img_body_anno.body = img_resource_without_id
139
+ expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg
140
+ end
141
+ it 'raises IllegalValueError if id in ImageResource isn\'t URI' do
142
+ img_resource_without_id['id'] = 'foo'
143
+ img_body_anno.body = img_resource_without_id
144
+ expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg
145
+ end
146
+ it 'raises IllegalValueError if id in ImageResource isn\'t http(s) URI' do
147
+ img_resource_without_id['id'] = 'ftp://example.com/somewhere'
148
+ img_body_anno.body = img_resource_without_id
149
+ expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg
150
+ end
151
+ end
152
+ end
153
+
154
+ describe 'realistic examples' do
155
+ let(:anno_id) { 'http://example.org/iiif/annoation/abc666'}
156
+ let(:target_id) { 'http://example.org/iiif/canvas/abc666'}
157
+
158
+ describe 'stanford (purl code)' do
159
+ let(:anno) {
160
+ anno = described_class.new
161
+ anno['id'] = anno_id
162
+ anno['target'] = target_id
163
+ anno.body = img_content_resource
164
+ anno
165
+ }
166
+ it 'validates' do
167
+ expect{anno.validate}.not_to raise_error
168
+ end
169
+ it 'has expected required values' do
170
+ expect(anno.id).to eq anno_id
171
+ expect(anno['type']).to eq 'Annotation'
172
+ expect(anno['motivation']).to eq 'painting'
173
+ expect(anno['target']).to eq target_id
174
+ end
175
+ it 'has expected additional values' do
176
+ expect(anno['body']).to eq img_content_resource
177
+ end
178
+ end
179
+
180
+ describe 'from http://prezi3.iiif.io/api/presentation/3.0' do
181
+ describe 'body is image_resource with height and width' do
182
+ let(:img_type) { 'dctypes:Image' }
183
+ let(:img_mime) { 'image/jpeg' }
184
+ let(:img_h) { 2000 }
185
+ let(:img_w) { 1500 }
186
+ let(:img_hw_resource) { IIIF::V3::Presentation::ImageResource.new(
187
+ 'id' => content_id,
188
+ 'type' => img_type,
189
+ 'format' => img_mime,
190
+ 'height' => img_h,
191
+ 'width' => img_w,
192
+ 'service' => image_2_api_service
193
+ )}
194
+ let(:my_anno) {
195
+ anno = described_class.new
196
+ anno['id'] = anno_id
197
+ anno['target'] = target_id
198
+ anno.body = img_hw_resource
199
+ anno
200
+ }
201
+ it 'validates' do
202
+ expect{my_anno.validate}.not_to raise_error
203
+ end
204
+ it 'has expected additional values' do
205
+ expect(my_anno['body']).to eq img_hw_resource
206
+ expect(my_anno['body']['height']).to eq img_h
207
+ expect(my_anno['body']['width']).to eq img_w
208
+ end
209
+
210
+ describe 'and service with height and width and tiles' do
211
+ let(:tiles_val) { [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] }
212
+ let(:service) {
213
+ s = image_2_api_service
214
+ s['height'] = 8000
215
+ s['width'] = 6000
216
+ s['tiles'] = tiles_val
217
+ s
218
+ }
219
+ it 'validates' do
220
+ img_hw_resource['service'] = service
221
+ expect{my_anno.validate}.not_to raise_error
222
+ end
223
+ it "body['service'] has expected additional values'" do
224
+ expect(my_anno['body']['service']).to eq service
225
+ expect(my_anno['body']['service']['height']).to eq 8000
226
+ expect(my_anno['body']['service']['width']).to eq 6000
227
+ expect(my_anno['body']['service']['tiles']).to eq tiles_val
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ describe 'from digerati' do
234
+ describe 'anno body is audio' do
235
+ let(:body_id) { 'http://example.org/iiif/foo2.mp3' }
236
+ let(:body_type) { 'Audio' }
237
+ let(:audio_res) { IIIF::V3::Presentation::Resource.new(
238
+ 'id' => body_id,
239
+ 'type' => body_type
240
+ )}
241
+ let(:my_anno) {
242
+ anno = described_class.new
243
+ anno['id'] = anno_id
244
+ anno['target'] = target_id
245
+ anno.body = audio_res
246
+ anno
247
+ }
248
+ it 'validates' do
249
+ expect{my_anno.validate}.not_to raise_error
250
+ end
251
+ it 'has expected required values' do
252
+ expect(my_anno['type']).to eq 'Annotation'
253
+ expect(my_anno.id).to eq anno_id
254
+ expect(my_anno['motivation']).to eq 'painting'
255
+ expect(my_anno['target']).to eq target_id
256
+ end
257
+ it 'has expected additional values' do
258
+ expect(my_anno['body']).to eq audio_res
259
+ expect(my_anno['body']['type']).to eq body_type
260
+ expect(my_anno['body']['id']).to eq body_id
261
+ end
262
+ end
263
+
264
+ describe 'anno body is video' do
265
+ let(:body_id) { 'http://example.org/foo.webm' }
266
+ let(:body_type) { 'Video' }
267
+ let(:body_mime) { 'video/webm' }
268
+ let(:video_res) { IIIF::V3::Presentation::Resource.new(
269
+ 'id' => body_id,
270
+ 'type' => body_type,
271
+ 'format' => body_mime
272
+ )}
273
+ let(:my_anno) {
274
+ anno = described_class.new
275
+ anno['id'] = anno_id
276
+ anno['target'] = target_id
277
+ anno.body = video_res
278
+ anno
279
+ }
280
+ it 'validates' do
281
+ expect{my_anno.validate}.not_to raise_error
282
+ end
283
+ it 'has expected body values' do
284
+ expect(my_anno['body']).to eq video_res
285
+ expect(my_anno['body']['type']).to eq body_type
286
+ expect(my_anno['body']['id']).to eq body_id
287
+ expect(my_anno['body']['format']).to eq body_mime
288
+ end
289
+ end
290
+
291
+ describe 'anno body is 3d object' do
292
+ let(:body_id) { 'http://files.universalviewer.io/manifests/nelis/animal-skull/animal-skull.json' }
293
+ let(:body_type) { 'PhysicalObject' }
294
+ let(:body_mime) { 'application/vnd.threejs+json' }
295
+ let(:body_label) { 'Animal Skull' }
296
+ let(:body_res) { IIIF::V3::Presentation::Resource.new(
297
+ 'id' => body_id,
298
+ 'type' => body_type,
299
+ 'format' => body_mime,
300
+ 'label' => body_label
301
+ )}
302
+ let(:my_anno) {
303
+ anno = described_class.new
304
+ anno['id'] = anno_id
305
+ anno['target'] = target_id
306
+ anno.body = body_res
307
+ anno
308
+ }
309
+ it 'validates' do
310
+ expect{my_anno.validate}.not_to raise_error
311
+ end
312
+ it 'has expected body values' do
313
+ expect(my_anno['body']).to eq body_res
314
+ expect(my_anno['body']['type']).to eq body_type
315
+ expect(my_anno['body']['id']).to eq body_id
316
+ expect(my_anno['body']['format']).to eq body_mime
317
+ expect(my_anno['body']['label']).to eq body_label
318
+ end
319
+ end
320
+
321
+ describe 'anno body is pdf' do
322
+ let(:body_id) { 'http://example.org/iiif/some-document.pdf' }
323
+ let(:body_type) { 'Document' }
324
+ let(:body_mime) { 'application/pdf' }
325
+ let(:body_res) { IIIF::V3::Presentation::Resource.new(
326
+ 'id' => body_id,
327
+ 'type' => body_type,
328
+ 'format' => body_mime
329
+ )}
330
+ let(:my_anno) {
331
+ anno = described_class.new
332
+ anno['id'] = anno_id
333
+ anno['target'] = target_id
334
+ anno.body = body_res
335
+ anno
336
+ }
337
+ it 'validates' do
338
+ expect{my_anno.validate}.not_to raise_error
339
+ end
340
+ it 'has expected body values' do
341
+ expect(my_anno['body']).to eq body_res
342
+ expect(my_anno['body']['type']).to eq body_type
343
+ expect(my_anno['body']['id']).to eq body_id
344
+ expect(my_anno['body']['format']).to eq body_mime
345
+ end
346
+ end
347
+
348
+ describe 'anno body is choice (of 2 videos)' do
349
+ let(:body_type) { 'Video' }
350
+ let(:body1_id) { 'http://example.org/foo.mp4f' }
351
+ let(:body1_mime) { 'video/mp4; codec..xxxxx' }
352
+ let(:body1_res) { IIIF::V3::Presentation::Resource.new(
353
+ 'id' => body1_id,
354
+ 'type' => body_type,
355
+ 'format' => body1_mime
356
+ )}
357
+ let(:body2_id) { 'http://example.org/foo.webm' }
358
+ let(:body2_mime) { 'video/webm' }
359
+ let(:body2_res) { IIIF::V3::Presentation::Resource.new(
360
+ 'id' => body2_id,
361
+ 'type' => body_type,
362
+ 'format' => body2_mime
363
+ )}
364
+ let(:body_res) { IIIF::V3::Presentation::Choice.new(
365
+ 'items' => [body1_res, body2_res],
366
+ 'choiceHint' => 'client'
367
+ )}
368
+ let(:my_anno) {
369
+ anno = described_class.new
370
+ anno['id'] = anno_id
371
+ anno['target'] = target_id
372
+ anno.body = body_res
373
+ anno
374
+ }
375
+ it 'validates' do
376
+ expect{my_anno.validate}.not_to raise_error
377
+ end
378
+ it 'has expected body values' do
379
+ expect(my_anno['body']).to eq body_res
380
+ expect(my_anno['body'].keys.size).to eq 3
381
+ expect(my_anno['body']['type']).to eq 'Choice'
382
+ expect(my_anno['body'].choiceHint).to eq 'client'
383
+ expect(my_anno['body']['items']).to eq [body1_res, body2_res]
384
+ end
385
+ end
386
+ end
387
+ end
388
+
389
+ end