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,54 @@
1
+ describe IIIF::V3::Presentation::Range do
2
+
3
+ let(:fixed_values) do
4
+ {
5
+ 'id' => 'http://www.example.org/iiif/book1/range/r1',
6
+ 'type' => 'Range',
7
+ 'label' => 'Introduction',
8
+ 'members' => [
9
+ {
10
+ "id" => 'http://www.example.org/iiif/book1/range/r1-1',
11
+ "type" => "Range"
12
+ },
13
+ {
14
+ "id" => 'http://www.example.org/iiif/book1/range/r1-2',
15
+ "type" => "Range"
16
+ },
17
+ {
18
+ "id" => 'http://www.example.org/iiif/book1/canvas/p1',
19
+ "type" => "Canvas"
20
+ },
21
+ {
22
+ "id" => 'http://www.example.org/iiif/book1/canvas/p2',
23
+ "type" => "Canvas"
24
+ },
25
+ {
26
+ "id" => 'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300',
27
+ "type" => "Canvas"
28
+ }
29
+ ]
30
+ }
31
+ end
32
+
33
+ describe '#initialize' do
34
+ it 'sets type to Range by default' do
35
+ expect(subject['type']).to eq 'Range'
36
+ end
37
+ end
38
+
39
+ describe "#{described_class}.define_methods_for_array_only_keys" do
40
+ it_behaves_like 'it has the appropriate methods for array-only keys v3'
41
+ end
42
+
43
+ describe "#{described_class}.define_methods_for_string_only_keys" do
44
+ it_behaves_like 'it has the appropriate methods for string-only keys v3'
45
+ end
46
+
47
+ describe "#{described_class}.define_methods_for_any_type_keys" do
48
+ it_behaves_like 'it has the appropriate methods for any-type keys v3'
49
+ end
50
+
51
+ describe '#validate' do
52
+ end
53
+
54
+ end
@@ -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