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,174 @@
1
+ describe IIIF::V3::Presentation::Resource do
2
+ describe '#required_keys' do
3
+ it 'id' do
4
+ expect(subject.required_keys).to include('id')
5
+ end
6
+ it 'type' do
7
+ expect(subject.required_keys).to include('type')
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
+ nav_date
16
+ viewing_direction
17
+ start_canvas
18
+ content_annotations
19
+ }
20
+ expect(subject.prohibited_keys).to include(*keys)
21
+ end
22
+ end
23
+
24
+ describe '#uri_only_keys' do
25
+ it 'id' do
26
+ expect(subject.uri_only_keys).to include('id')
27
+ end
28
+ end
29
+
30
+ describe '#initialize' do
31
+ it 'allows subclasses to override type' do
32
+ subclass = Class.new(described_class) do
33
+ def initialize(hsh={})
34
+ hsh = { 'type' => 'a:SubClass' }
35
+ super(hsh)
36
+ end
37
+ end
38
+ sub = subclass.new
39
+ expect(sub['type']).to eq 'a:SubClass'
40
+ end
41
+ end
42
+
43
+ describe '#validate' do
44
+ it 'raises an IllegalValueError if id is not URI' do
45
+ subject['id'] = 'foo'
46
+ subject['type'] = 'image/jpeg'
47
+ exp_err_msg = "id value must be a String containing a URI for #{described_class}"
48
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
49
+ end
50
+ it 'raises an IllegalValueError if id is not http' do
51
+ subject['id'] = 'ftp://www.example.org'
52
+ subject['type'] = 'image/jpeg'
53
+ exp_err_msg = "id must be an http(s) URI for #{described_class}"
54
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
55
+ end
56
+ end
57
+
58
+ describe 'realistic examples' do
59
+ describe 'non-image examples from http://prezi3.iiif.io/api/presentation/3.0' do
60
+ describe 'audio' do
61
+ let(:file_id) { 'http://example.org/iiif/book1/res/music.mp3' }
62
+ let(:file_type) { 'dctypes:Sound' }
63
+ let(:file_mimetype) { 'audio/mpeg' }
64
+ let(:resource_object) { IIIF::V3::Presentation::Resource.new({
65
+ 'id' => file_id,
66
+ 'type' => file_type,
67
+ 'format' => file_mimetype
68
+ })}
69
+ it 'validates' do
70
+ expect{resource_object.validate}.not_to raise_error
71
+ end
72
+ it 'has expected required values' do
73
+ expect(resource_object.id).to eq file_id
74
+ end
75
+ it 'has expected other values' do
76
+ expect(resource_object.type).to eq file_type
77
+ expect(resource_object.format).to eq file_mimetype
78
+ expect(resource_object.service).to eq []
79
+ end
80
+ end
81
+ describe 'text' do
82
+ let(:file_id) { 'http://example.org/iiif/book1/res/tei-text-p1.xml' }
83
+ let(:file_type) { 'dctypes:Text' }
84
+ let(:file_mimetype) { 'application/tei+xml' }
85
+ let(:resource_object) { IIIF::V3::Presentation::Resource.new({
86
+ 'id' => file_id,
87
+ 'type' => file_type,
88
+ 'format' => file_mimetype
89
+ })}
90
+ it 'validates' do
91
+ expect{resource_object.validate}.not_to raise_error
92
+ end
93
+ it 'has expected required values' do
94
+ expect(resource_object.id).to eq file_id
95
+ end
96
+ it 'has expected other values' do
97
+ expect(resource_object.type).to eq file_type
98
+ expect(resource_object.format).to eq file_mimetype
99
+ expect(resource_object.service).to eq []
100
+ end
101
+ end
102
+
103
+ end
104
+ describe 'stanford' do
105
+ describe 'non-image resource per purl code' do
106
+ let(:file_id) { 'https://example.org/file/abc666/ocr.txt' }
107
+ let(:file_type) { 'Document' }
108
+ let(:file_mimetype) { 'text/plain' }
109
+ let(:resource_object) {
110
+ resource = IIIF::V3::Presentation::Resource.new
111
+ resource['id'] = file_id
112
+ resource['type'] = file_type
113
+ resource.format = file_mimetype
114
+ resource
115
+ }
116
+ describe 'world visible' do
117
+ it 'validates' do
118
+ expect{resource_object.validate}.not_to raise_error
119
+ end
120
+ it 'has expected required values' do
121
+ expect(resource_object.id).to eq file_id
122
+ end
123
+ it 'has expected other values' do
124
+ expect(resource_object.type).to eq file_type
125
+ expect(resource_object.format).to eq file_mimetype
126
+ expect(resource_object.service).to eq []
127
+ end
128
+ end
129
+ describe 'requires login' do
130
+ # let(:auth_token_service) {
131
+ # IIIF::V3::Presentation::Service.new({
132
+ # 'id' => 'https://example.org/image/iiif/token',
133
+ # 'profile' => IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE
134
+ # })}
135
+ let(:service_label) { 'login message' }
136
+ let(:token_service_id) { 'https://example.org/iiif/token' }
137
+ let(:login_service) {
138
+ IIIF::V3::Presentation::Service.new(
139
+ 'id' => 'https://example.org/auth/iiif',
140
+ 'profile' => 'http://iiif.io/api/auth/1/login',
141
+ 'label' => service_label,
142
+ 'service' => [{
143
+ 'id' => token_service_id,
144
+ 'profile' => 'http://iiif.io/api/auth/1/token'
145
+ }]
146
+ )
147
+ }
148
+ let(:resource_object_w_login) {
149
+ resource = resource_object
150
+ resource.service = [login_service]
151
+ resource
152
+ }
153
+ it 'validates' do
154
+ expect{resource_object_w_login.validate}.not_to raise_error
155
+ end
156
+ it 'has expected service value' do
157
+ service_obj = resource_object_w_login.service.first
158
+ expect(service_obj.class).to eq IIIF::V3::Presentation::Service
159
+ expect(service_obj.keys.size).to eq 4
160
+ expect(service_obj.id).to eq 'https://example.org/auth/iiif'
161
+ expect(service_obj.profile).to eq IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE
162
+ expect(service_obj.label).to eq service_label
163
+ expect(service_obj.service.class).to eq Array
164
+ expect(service_obj.service.size).to eq 1
165
+ expect(service_obj.service.first.keys.size).to eq 2
166
+ expect(service_obj.service.first['id']).to eq token_service_id
167
+ expect(service_obj.service.first['profile']).to eq IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ end
@@ -0,0 +1,222 @@
1
+ describe IIIF::V3::Presentation::Sequence do
2
+
3
+ describe '#required_keys' do
4
+ # NOTE: relaxing requirement for items as Universal Viewer currently only accepts canvases
5
+ # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
6
+ # %w{ type items }.each do |k|
7
+ %w{ type }.each do |k|
8
+ it k do
9
+ expect(subject.required_keys).to include(k)
10
+ end
11
+ end
12
+ end
13
+
14
+ describe '#prohibited_keys' do
15
+ it 'contains the expected key names' do
16
+ keys = described_class::CONTENT_RESOURCE_PROPERTIES +
17
+ described_class::PAGING_PROPERTIES +
18
+ %w{
19
+ nav_date
20
+ content_annotations
21
+ }
22
+ expect(subject.prohibited_keys).to include(*keys)
23
+ end
24
+ end
25
+
26
+ describe '#array_only_keys' do
27
+ it 'items' do
28
+ expect(subject.array_only_keys).to include('items')
29
+ end
30
+ # NOTE: also allowing sequences as Universal Viewer currently only accepts canvases
31
+ # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
32
+ it 'canvases' do
33
+ expect(subject.array_only_keys).to include('canvases')
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('individuals', 'paged', 'continuous', 'auto-advance')
40
+ end
41
+ end
42
+
43
+ describe '#initialize' do
44
+ it 'sets type to Sequence by default' do
45
+ expect(subject['type']).to eq 'Sequence'
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(:req_key_msg) { "The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)" }
61
+ let(:bad_val_msg) { "All entries in the (items or canvases) list must be a IIIF::V3::Presentation::Canvas" }
62
+ it 'raises MissingRequiredKeyError if no items or canvases key' do
63
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg)
64
+ end
65
+ describe 'items' do
66
+ it 'raises MissingRequiredKeyError for items as empty Array' do
67
+ subject['items'] = []
68
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg)
69
+ end
70
+ it 'raises IllegalValueError for items entry that is not a Canvas' do
71
+ subject['items'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new]
72
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg)
73
+ end
74
+ end
75
+ describe 'canvases' do
76
+ it 'raises MissingRequiredKeyError for canvases as empty Array' do
77
+ subject['items'] = []
78
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg)
79
+ end
80
+ it 'raises IllegalValueError for canvases entry that is not a Canvas' do
81
+ subject['canvases'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new]
82
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg)
83
+ end
84
+ end
85
+ end
86
+
87
+ describe 'realistic examples' do
88
+ let!(:canvas_object) { IIIF::V3::Presentation::Canvas.new({
89
+ "id" => "https://example.org/abc666/iiif3/canvas/0001",
90
+ "label" => "p. 1",
91
+ "height" => 7579,
92
+ "width" => 10108,
93
+ "content" => []
94
+ })}
95
+ describe 'minimal sequence' do
96
+ let!(:sequence_object) { described_class.new({
97
+ "items" => [canvas_object]
98
+ })}
99
+ it 'validates' do
100
+ expect{sequence_object.validate}.not_to raise_error
101
+ end
102
+ it 'has expected required values' do
103
+ expect(sequence_object.type).to eq 'Sequence'
104
+ expect(sequence_object.items.size).to be 1
105
+ expect(sequence_object.items.first).to eq canvas_object
106
+ end
107
+ end
108
+
109
+ describe 'example from Stanford purl' do
110
+ let(:seq_id) { 'https://example.org/abc666#sequence-1' }
111
+ let(:sequence_object) {
112
+ s = described_class.new({
113
+ "id" => seq_id,
114
+ "label" => "Current order"
115
+ })
116
+ s.canvases << canvas_object
117
+ s
118
+ }
119
+ it 'validates' do
120
+ expect{sequence_object.validate}.not_to raise_error
121
+ end
122
+ it 'has expected required values' do
123
+ expect(sequence_object.type).to eq 'Sequence'
124
+ expect(sequence_object.canvases.size).to be 1
125
+ expect(sequence_object.canvases.first).to eq canvas_object
126
+ end
127
+ it 'has expected string values' do
128
+ expect(sequence_object.id).to eq seq_id
129
+ expect(sequence_object.label).to eq "Current order"
130
+ end
131
+ describe 'with viewingDirection' do
132
+ let(:sequence_vd) {
133
+ sequence_object.viewingDirection = 'left-to-right'
134
+ sequence_object
135
+ }
136
+ it 'validates' do
137
+ expect{sequence_vd.validate}.not_to raise_error
138
+ end
139
+ it 'has expected required values' do
140
+ expect(sequence_vd.type).to eq 'Sequence'
141
+ expect(sequence_vd.canvases.size).to be 1
142
+ expect(sequence_vd.canvases.first).to eq canvas_object
143
+ end
144
+ it 'has expected string values' do
145
+ expect(sequence_vd.id).to eq seq_id
146
+ expect(sequence_vd.label).to eq "Current order"
147
+ expect(sequence_vd.viewing_direction).to eq 'left-to-right'
148
+ expect(sequence_vd.viewingDirection).to eq 'left-to-right'
149
+ end
150
+ end
151
+ end
152
+
153
+ describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do
154
+ let!(:sequence_object) { described_class.new({
155
+ "id" => "http://example.org/iiif/book1/sequence/normal",
156
+ "label" => {"en" => "Current Page Order"},
157
+ "viewingDirection" => "left-to-right",
158
+ "viewingHint" => ["paged"],
159
+ "startCanvas" => canvas_object.id,
160
+ "items" => [canvas_object]
161
+ })}
162
+ it 'validates' do
163
+ expect{sequence_object.validate}.not_to raise_error
164
+ end
165
+ it 'has expected required values' do
166
+ expect(sequence_object.type).to eq 'Sequence'
167
+ expect(sequence_object.items.size).to be 1
168
+ expect(sequence_object.items.first).to eq canvas_object
169
+ end
170
+ it 'has expected string values' do
171
+ expect(sequence_object.id).to eq "http://example.org/iiif/book1/sequence/normal"
172
+ expect(sequence_object.viewingDirection).to eq "left-to-right"
173
+ expect(sequence_object.startCanvas).to eq "https://example.org/abc666/iiif3/canvas/0001"
174
+ end
175
+ it 'has expected additional content' do
176
+ expect(sequence_object.viewingHint).to eq ["paged"]
177
+ expect(sequence_object.label).to eq ({"en" => "Current Page Order"})
178
+ end
179
+ end
180
+
181
+ describe 'another example' do
182
+ let!(:sequence_object) { described_class.new({
183
+ "id" => "http://example.com/prefix/sequence/456",
184
+ 'label' => 'Book 1',
185
+ 'description' => 'A longer description of this example book. It should give some real information.',
186
+ 'thumbnail' => [{
187
+ 'id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg',
188
+ 'type' => 'Image',
189
+ 'service'=> {
190
+ '@context' => 'http://iiif.io/api/image/2/context.json',
191
+ 'id' => 'http://www.example.org/images/book1-page1',
192
+ 'profile' => 'http://iiif.io/api/image/2/level1.json'
193
+ }
194
+ }],
195
+ 'attribution' => 'Provided by Example Organization',
196
+ 'rights' => [{'id' => 'http://www.example.org/license.html'}],
197
+ 'logo' => 'http://www.example.org/logos/institution1.jpg',
198
+ 'see_also' => 'http://www.example.org/library/catalog/book1.xml',
199
+ 'service' => {
200
+ '@context' => 'http://example.org/ns/jsonld/context.json',
201
+ 'id' => 'http://example.org/service/example',
202
+ 'profile' => 'http://example.org/docs/example-service.html'
203
+ },
204
+ 'related' => {
205
+ 'id' => 'http://www.example.org/videos/video-book1.mpg',
206
+ 'format' => 'video/mpeg'
207
+ },
208
+ 'within' => 'http://www.example.org/collections/books/',
209
+ # Sequence
210
+ 'metadata' => [{'label'=>'Author', 'value'=>'Anne Author'}],
211
+ "items" => [canvas_object],
212
+ 'start_canvas' => 'http://www.example.org/iiif/book1/canvas/p2',
213
+ "viewingDirection" => "left-to-right",
214
+ "viewingHint" => ["paged"],
215
+ "startCanvas" => canvas_object.id,
216
+ })}
217
+ it 'validates' do
218
+ expect{sequence_object.validate}.not_to raise_error
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,220 @@
1
+ describe IIIF::V3::Presentation::Service do
2
+
3
+ describe '#required_keys' do
4
+ it '"type" is not required' do
5
+ expect(subject.required_keys).not_to include('type')
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
+ described_class::PAGING_PROPERTIES +
13
+ %w{
14
+ nav_date
15
+ viewing_direction
16
+ start_canvas
17
+ content_annotations
18
+ }
19
+ expect(subject.prohibited_keys).to include(*keys)
20
+ end
21
+ end
22
+
23
+ describe '#uri_only_keys' do
24
+ it '@context' do
25
+ expect(subject.uri_only_keys).to include('@context')
26
+ end
27
+ it '@id' do
28
+ expect(subject.uri_only_keys).to include('@id')
29
+ end
30
+ it 'id' do
31
+ expect(subject.uri_only_keys).to include('id')
32
+ end
33
+ end
34
+
35
+ describe '#any_type_keys' do
36
+ it 'profile' do
37
+ expect(subject.any_type_keys).to include('profile')
38
+ end
39
+ end
40
+
41
+ let(:id_uri) { "https://example.org/image1" }
42
+
43
+ describe '#initialize' do
44
+ it 'assigns hash values passed in' do
45
+ label_val = 'foo'
46
+ inner_service_id = 'http://example.org/whatever'
47
+ inner_service_profile = 'http://iiif.io/api/auth/1/token'
48
+ inner_service_val = described_class.new({
49
+ 'id' => inner_service_id,
50
+ 'profile' => inner_service_profile
51
+ })
52
+ service_obj = described_class.new({
53
+ 'type' => described_class::IIIF_IMAGE_V2_TYPE,
54
+ 'id' => id_uri,
55
+ 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE,
56
+ 'label' => label_val,
57
+ 'service' => [inner_service_val]
58
+ })
59
+ expect(service_obj.keys.size).to eq 5
60
+ expect(service_obj['id']).to eq id_uri
61
+ expect(service_obj['type']).to eq described_class::IIIF_IMAGE_V2_TYPE
62
+ expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE
63
+ expect(service_obj['label']).to eq label_val
64
+ expect(service_obj['service'][0]['id']).to eq inner_service_id
65
+ expect(service_obj['service'][0]['profile']).to eq inner_service_profile
66
+ end
67
+ it 'allows both "id" and "@id" as keys' do
68
+ id_uri = "https://stacks.stanford.edu/image/iiif/wy534zh7137%2FSULAIR_rosette"
69
+ service_obj = described_class.new({
70
+ 'id' => id_uri,
71
+ '@id' => id_uri
72
+ })
73
+ expect(service_obj.keys.size).to eq 2
74
+ expect(service_obj['id']).to eq id_uri
75
+ expect(service_obj['@id']).to eq id_uri
76
+ end
77
+ it 'allows non-URI profile value' do
78
+ expect{
79
+ described_class.new({
80
+ "profile" => [
81
+ "http://iiif.io/api/image/2/level2.json",
82
+ {
83
+ "formats" => [ "gif", "pdf" ],
84
+ "qualities" => [ "color", "gray" ],
85
+ "supports" => [ "canonicalLinkHeader", "rotationArbitrary", "http://example.com/feature" ]
86
+ }
87
+ ]
88
+ })
89
+ }.not_to raise_error
90
+ end
91
+ end
92
+
93
+ describe '#validate' do
94
+ describe 'type = IIIF_IMAGE_API_V2_TYPE' do
95
+ it 'must have a "@id" or "id"' do
96
+ service_obj = described_class.new({
97
+ 'type' => described_class::IIIF_IMAGE_V2_TYPE,
98
+ 'nope' => id_uri,
99
+ 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE
100
+ })
101
+ exp_err_msg = 'id or @id values are required for IIIF::V3::Presentation::Service with type or @type ImageService2'
102
+ expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg)
103
+ end
104
+ it 'should have a profile' do
105
+ service_obj = described_class.new({
106
+ '@type' => described_class::IIIF_IMAGE_V2_TYPE,
107
+ '@id' => id_uri
108
+ })
109
+ exp_err_msg = 'profile should be present for IIIF::V3::Presentation::Service with type or @type ImageService2'
110
+ expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg)
111
+ end
112
+ it 'should have matching values for "@id" and "id" if both are specified' do
113
+ service_obj = described_class.new({
114
+ '@type' => described_class::IIIF_IMAGE_V2_TYPE,
115
+ '@id' => id_uri,
116
+ 'id' => "#{id_uri}/foo",
117
+ 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE
118
+ })
119
+ exp_err_msg = 'id and @id values must match for IIIF::V3::Presentation::Service with type or @type ImageService2'
120
+ expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
121
+ end
122
+ end
123
+ end
124
+
125
+ describe 'realistic examples' do
126
+ describe 'from Stanford purl manifests' do
127
+ it 'iiif image v2 service' do
128
+ service_obj = described_class.new({
129
+ 'type' => described_class::IIIF_IMAGE_V2_TYPE,
130
+ 'id' => id_uri,
131
+ '@id' => id_uri,
132
+ 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE
133
+ })
134
+ expect{service_obj.validate}.not_to raise_error
135
+ expect(service_obj.keys.size).to eq 4
136
+ expect(service_obj.keys).to include('type', '@id', 'id', 'profile')
137
+ expect(service_obj['type']).to eq described_class::IIIF_IMAGE_V2_TYPE
138
+ expect(service_obj['id']).to eq id_uri
139
+ expect(service_obj['@id']).to eq id_uri
140
+ expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE
141
+ end
142
+ it 'login service' do
143
+ token_service = described_class.new({
144
+ 'id' => 'https://example.org/image/iiif/token',
145
+ 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE
146
+ })
147
+ expect{token_service.validate}.not_to raise_error
148
+ expect(token_service.class).to eq described_class
149
+ expect(token_service.keys.size).to eq 2
150
+ expect(token_service.keys).to include('id', 'profile')
151
+ expect(token_service['id']).to eq 'https://example.org/image/iiif/token'
152
+ expect(token_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE
153
+ service_obj = described_class.new(
154
+ 'id' => 'https://example.org/auth/iiif',
155
+ 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE,
156
+ 'label' => 'label value',
157
+ 'service' => [token_service]
158
+ )
159
+ expect{service_obj.validate}.not_to raise_error
160
+ expect(service_obj.keys.size).to eq 4
161
+ expect(service_obj.keys).to include('id', 'profile', 'label', 'service')
162
+ expect(service_obj['id']).to eq 'https://example.org/auth/iiif'
163
+ expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE
164
+ expect(service_obj['label']).to eq 'label value'
165
+ inner_service = service_obj['service'][0]
166
+ expect(inner_service.class).to eq described_class
167
+ expect{inner_service.validate}.not_to raise_error
168
+ expect(inner_service.keys.size).to eq 2
169
+ expect(inner_service.keys).to include('id', 'profile')
170
+ expect(inner_service['id']).to eq 'https://example.org/image/iiif/token'
171
+ expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE
172
+ end
173
+ it 'triply nested service' do
174
+ inner = described_class.new({
175
+ 'id' => 'https://example.org/image/iiif/token',
176
+ 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE
177
+ })
178
+ expect{inner.validate}.not_to raise_error
179
+ middle = described_class.new({
180
+ "id" => "https://example.org/auth/iiif",
181
+ "profile" => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE,
182
+ "label" => "Stanford-affiliated? Login to view",
183
+ "service" => [inner]
184
+ })
185
+ expect{middle.validate}.not_to raise_error
186
+ outer = described_class.new({
187
+ "@type" => described_class::IIIF_IMAGE_V2_TYPE,
188
+ "@id" => "https://example.org/iiif/yy816tv6021_img_1",
189
+ "id" => "https://example.org/iiif/yy816tv6021_img_1",
190
+ "profile" => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE,
191
+ "service" => [middle]
192
+ })
193
+ expect{outer.validate}.not_to raise_error
194
+ end
195
+ end
196
+ describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do
197
+ it 'iiif image v2' do
198
+ service_obj = described_class.new({
199
+ "type" => described_class::IIIF_IMAGE_V2_TYPE,
200
+ "id" => "http://example.org/images/book1-page2",
201
+ "@id" => "http://example.org/images/book1-page2",
202
+ "profile" => "http://iiif.io/api/image/2/level1.json",
203
+ "height" => 8000,
204
+ "width" => 6000,
205
+ "tiles" => [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}]
206
+ })
207
+ expect(service_obj.keys.size).to eq 7
208
+ expect(service_obj['type']).to eq described_class::IIIF_IMAGE_V2_TYPE
209
+ expect(service_obj['id']).to eq 'http://example.org/images/book1-page2'
210
+ expect(service_obj['@id']).to eq 'http://example.org/images/book1-page2'
211
+ expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE
212
+ expect(service_obj['height']).to eq 8000
213
+ expect(service_obj['width']).to eq 6000
214
+ expect(service_obj['tiles']).to eq [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}]
215
+ # TODO: note that this won't validate because "height is a prohibited key in IIIF::V3::Presentation::Service"
216
+ # expect{service_obj.validate}.not_to raise_error
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,41 @@
1
+ shared_examples 'it has the appropriate methods for abstract_resource_only_keys v3' do
2
+
3
+ described_class.new.abstract_resource_only_keys.each do |entry|
4
+
5
+ describe "#{entry[:key]}=" do
6
+ it "sets #{entry[:key]}" do
7
+ @ex = entry[:type].new
8
+ subject.send("#{entry[:key]}=", @ex)
9
+ expect(subject[entry[:key]]).to eq @ex
10
+ end
11
+ if entry[:key].camelize(:lower) != entry[:key]
12
+ it "is aliased as ##{entry[:key].camelize(:lower)}=" do
13
+ @ex = entry[:type].new
14
+ subject.send("#{entry[:key].camelize(:lower)}=", @ex)
15
+ expect(subject[entry[:key]]).to eq @ex
16
+ end
17
+ end
18
+ it "raises an exception when attempting to set it to something other than an #{entry[:type]}" do
19
+ e = IIIF::V3::Presentation::IllegalValueError
20
+ expect { subject.send("#{entry[:key]}=", 'Foo') }.to raise_error e
21
+ end
22
+ end
23
+
24
+ describe "#{entry[:key]}" do
25
+ it "gets #{entry[:key]}" do
26
+ @ex = entry[:type].new
27
+ subject[entry[:key]] = @ex
28
+ expect(subject.send(entry[:key])).to eq @ex
29
+ end
30
+ if entry[:key].camelize(:lower) != entry[:key]
31
+ it "is aliased as ##{entry[:key].camelize(:lower)}" do
32
+ @ex = entry[:type].new
33
+ subject[entry[:key]] = @ex
34
+ expect(subject.send("#{entry[:key].camelize(:lower)}")).to eq @ex
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end