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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -2
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/README.md +22 -1
- data/VERSION +1 -1
- data/iiif-presentation.gemspec +2 -1
- data/lib/iiif/presentation/canvas.rb +4 -0
- data/lib/iiif/presentation/service.rb +12 -0
- data/lib/iiif/presentation.rb +5 -4
- data/lib/iiif/service.rb +38 -103
- data/lib/iiif/v3/abstract_resource.rb +491 -0
- data/lib/iiif/v3/presentation/annotation.rb +74 -0
- data/lib/iiif/v3/presentation/annotation_collection.rb +38 -0
- data/lib/iiif/v3/presentation/annotation_page.rb +53 -0
- data/lib/iiif/v3/presentation/canvas.rb +82 -0
- data/lib/iiif/v3/presentation/choice.rb +51 -0
- data/lib/iiif/v3/presentation/collection.rb +52 -0
- data/lib/iiif/v3/presentation/image_resource.rb +110 -0
- data/lib/iiif/v3/presentation/manifest.rb +82 -0
- data/lib/iiif/v3/presentation/nav_place.rb +109 -0
- data/lib/iiif/v3/presentation/range.rb +39 -0
- data/lib/iiif/v3/presentation/resource.rb +30 -0
- data/lib/iiif/v3/presentation/sequence.rb +66 -0
- data/lib/iiif/v3/presentation/service.rb +51 -0
- data/lib/iiif/v3/presentation.rb +37 -0
- data/spec/fixtures/v3/manifests/complete_from_spec.json +195 -0
- data/spec/fixtures/v3/manifests/minimal.json +49 -0
- data/spec/fixtures/v3/manifests/service_only.json +14 -0
- data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +1 -1
- data/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json +1 -0
- data/spec/integration/iiif/presentation/image_resource_spec.rb +0 -1
- data/spec/integration/iiif/service_spec.rb +17 -32
- data/spec/integration/iiif/v3/abstract_resource_spec.rb +202 -0
- data/spec/integration/iiif/v3/presentation/image_resource_spec.rb +118 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/iiif/presentation/canvas_spec.rb +0 -1
- data/spec/unit/iiif/presentation/manifest_spec.rb +1 -1
- data/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +78 -0
- data/spec/unit/iiif/v3/abstract_resource_spec.rb +293 -0
- data/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +36 -0
- data/spec/unit/iiif/v3/presentation/annotation_page_spec.rb +131 -0
- data/spec/unit/iiif/v3/presentation/annotation_spec.rb +389 -0
- data/spec/unit/iiif/v3/presentation/canvas_spec.rb +337 -0
- data/spec/unit/iiif/v3/presentation/choice_spec.rb +120 -0
- data/spec/unit/iiif/v3/presentation/collection_spec.rb +55 -0
- data/spec/unit/iiif/v3/presentation/image_resource_spec.rb +189 -0
- data/spec/unit/iiif/v3/presentation/manifest_spec.rb +370 -0
- data/spec/unit/iiif/v3/presentation/nav_place_spec.rb +80 -0
- data/spec/unit/iiif/v3/presentation/range_spec.rb +54 -0
- data/spec/unit/iiif/v3/presentation/resource_spec.rb +174 -0
- data/spec/unit/iiif/v3/presentation/sequence_spec.rb +222 -0
- data/spec/unit/iiif/v3/presentation/service_spec.rb +220 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +41 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +31 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +40 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +40 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +45 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +45 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +26 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +31 -0
- 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
|