iiif-presentation 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) 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 +2 -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/nav_place.rb +109 -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 +37 -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/nav_place_spec.rb +80 -0
  50. data/spec/unit/iiif/v3/presentation/range_spec.rb +54 -0
  51. data/spec/unit/iiif/v3/presentation/resource_spec.rb +174 -0
  52. data/spec/unit/iiif/v3/presentation/sequence_spec.rb +222 -0
  53. data/spec/unit/iiif/v3/presentation/service_spec.rb +220 -0
  54. data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +41 -0
  55. data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +31 -0
  56. data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +40 -0
  57. data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +40 -0
  58. data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +45 -0
  59. data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +45 -0
  60. data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +26 -0
  61. data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +31 -0
  62. metadata +93 -5
@@ -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