iiif-presentation 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +2 -0
- data/LICENSE +23 -0
- data/README.md +173 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/gemfiles/rails3.gemfile +5 -0
- data/gemfiles/rails4.gemfile +5 -0
- data/iiif-presentation.gemspec +28 -0
- data/lib/active_support/ordered_hash.rb +147 -0
- data/lib/iiif/hash_behaviours.rb +150 -0
- data/lib/iiif/presentation.rb +25 -0
- data/lib/iiif/presentation/abstract_resource.rb +75 -0
- data/lib/iiif/presentation/annotation.rb +25 -0
- data/lib/iiif/presentation/annotation_list.rb +28 -0
- data/lib/iiif/presentation/canvas.rb +45 -0
- data/lib/iiif/presentation/collection.rb +29 -0
- data/lib/iiif/presentation/image_resource.rb +115 -0
- data/lib/iiif/presentation/layer.rb +34 -0
- data/lib/iiif/presentation/manifest.rb +39 -0
- data/lib/iiif/presentation/range.rb +32 -0
- data/lib/iiif/presentation/resource.rb +21 -0
- data/lib/iiif/presentation/sequence.rb +35 -0
- data/lib/iiif/service.rb +418 -0
- data/spec/fixtures/manifests/complete_from_spec.json +171 -0
- data/spec/fixtures/manifests/minimal.json +40 -0
- data/spec/fixtures/manifests/service_only.json +11 -0
- data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +159 -0
- data/spec/integration/iiif/presentation/image_resource_spec.rb +123 -0
- data/spec/integration/iiif/service_spec.rb +211 -0
- data/spec/spec_helper.rb +104 -0
- data/spec/unit/active_support/ordered_hash_spec.rb +155 -0
- data/spec/unit/iiif/hash_behaviours_spec.rb +569 -0
- data/spec/unit/iiif/presentation/abstract_resource_spec.rb +133 -0
- data/spec/unit/iiif/presentation/annotation_list_spec.rb +7 -0
- data/spec/unit/iiif/presentation/annotation_spec.rb +7 -0
- data/spec/unit/iiif/presentation/canvas_spec.rb +40 -0
- data/spec/unit/iiif/presentation/collection_spec.rb +54 -0
- data/spec/unit/iiif/presentation/image_resource_spec.rb +13 -0
- data/spec/unit/iiif/presentation/layer_spec.rb +38 -0
- data/spec/unit/iiif/presentation/manifest_spec.rb +89 -0
- data/spec/unit/iiif/presentation/range_spec.rb +43 -0
- data/spec/unit/iiif/presentation/resource_spec.rb +16 -0
- data/spec/unit/iiif/presentation/sequence_spec.rb +110 -0
- data/spec/unit/iiif/presentation/shared_examples/abstract_resource_only_keys.rb +43 -0
- data/spec/unit/iiif/presentation/shared_examples/any_type_keys.rb +33 -0
- data/spec/unit/iiif/presentation/shared_examples/array_only_keys.rb +44 -0
- data/spec/unit/iiif/presentation/shared_examples/int_only_keys.rb +49 -0
- data/spec/unit/iiif/presentation/shared_examples/string_only_keys.rb +29 -0
- data/spec/unit/iiif/service_spec.rb +10 -0
- metadata +262 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'json'
|
3
|
+
require File.join(File.dirname(__FILE__), '../../../../lib/iiif/hash_behaviours')
|
4
|
+
|
5
|
+
|
6
|
+
describe IIIF::Presentation::AbstractResource do
|
7
|
+
|
8
|
+
let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../fixtures') }
|
9
|
+
let(:manifest_from_spec_path) { File.join(fixtures_dir, 'manifests/complete_from_spec.json') }
|
10
|
+
let(:abstract_resource_subclass) do
|
11
|
+
Class.new(IIIF::Presentation::AbstractResource) do
|
12
|
+
include IIIF::HashBehaviours
|
13
|
+
|
14
|
+
def initialize(hsh={})
|
15
|
+
hsh['@type'] = 'a:SubClass' unless hsh.has_key?('@type')
|
16
|
+
super(hsh)
|
17
|
+
end
|
18
|
+
|
19
|
+
def required_keys
|
20
|
+
super + %w{ @id }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
subject do
|
26
|
+
instance = abstract_resource_subclass.new
|
27
|
+
instance['@id'] = 'http://example.com/prefix/manifest/123'
|
28
|
+
instance
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#initialize' do
|
32
|
+
it 'raises an error if you try to instantiate AbstractResource' do
|
33
|
+
expect { IIIF::Presentation::AbstractResource.new }.to raise_error(RuntimeError)
|
34
|
+
end
|
35
|
+
it 'sets @type' do
|
36
|
+
expect(subject['@type']).to eq 'a:SubClass'
|
37
|
+
end
|
38
|
+
it 'can take any old hash' do
|
39
|
+
hsh = JSON.parse(IO.read(manifest_from_spec_path))
|
40
|
+
new_instance = abstract_resource_subclass.new(hsh)
|
41
|
+
expect(new_instance['label']).to eq 'Book 1'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'A nested object (e.g. self[\'metdata\'])' do
|
46
|
+
it 'returns [] if not set' do
|
47
|
+
expect(subject.metadata).to eq([])
|
48
|
+
end
|
49
|
+
it 'is not in #to_ordered_hash at all if we access it but do not append to it' do
|
50
|
+
subject.metadata # touch it
|
51
|
+
expect(subject.metadata).to eq([])
|
52
|
+
expect(subject.to_ordered_hash.has_key?('metadata')).to be_falsey
|
53
|
+
end
|
54
|
+
it 'gets structured as we\'d expect' do
|
55
|
+
subject.metadata << {
|
56
|
+
'label' => 'Author',
|
57
|
+
'value' => 'Anne Author'
|
58
|
+
}
|
59
|
+
subject.metadata << {
|
60
|
+
'label' => 'Published',
|
61
|
+
'value' => [
|
62
|
+
{'@value'=> 'Paris, circa 1400', '@language'=>'en'},
|
63
|
+
{'@value'=> 'Paris, environ 14eme siecle', '@language'=>'fr'}
|
64
|
+
]
|
65
|
+
}
|
66
|
+
expect(subject.metadata[0]['label']).to eq('Author')
|
67
|
+
expect(subject.metadata[1]['value'].length).to eq(2)
|
68
|
+
expect(subject.metadata[1]['value'].select { |e| e['@language'] == 'fr'}.last['@value']).to eq('Paris, environ 14eme siecle')
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'roundtrips' do
|
72
|
+
subject.metadata << {
|
73
|
+
'label' => 'Author',
|
74
|
+
'value' => 'Anne Author'
|
75
|
+
}
|
76
|
+
subject.metadata << {
|
77
|
+
'label' => 'Published',
|
78
|
+
'value' => [
|
79
|
+
{'@value'=> 'Paris, circa 1400', '@language'=>'en'},
|
80
|
+
{'@value'=> 'Paris, environ 14eme siecle', '@language'=>'fr'}
|
81
|
+
]
|
82
|
+
}
|
83
|
+
File.open('/tmp/osullivan-spec.json','w') do |f|
|
84
|
+
f.write(subject.to_json)
|
85
|
+
end
|
86
|
+
parsed = subject.class.parse('/tmp/osullivan-spec.json')
|
87
|
+
expect(parsed['metadata'][0]['label']).to eq('Author')
|
88
|
+
expect(parsed['metadata'][1]['value'].length).to eq(2)
|
89
|
+
expect(parsed['metadata'][1]['value'].select { |e| e['@language'] == 'fr'}.last['@value']).to eq('Paris, environ 14eme siecle')
|
90
|
+
File.delete('/tmp/osullivan-spec.json')
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#required_keys' do
|
95
|
+
it 'accumulates' do
|
96
|
+
expect(subject.required_keys).to eq %w{ @type @id }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#to_ordered_hash' do
|
101
|
+
describe 'adds the @context' do
|
102
|
+
before(:each) { subject.delete('@context') }
|
103
|
+
it 'by default' do
|
104
|
+
expect(subject.has_key?('@context')).to be_falsey
|
105
|
+
expect(subject.to_ordered_hash.has_key?('@context')).to be_truthy
|
106
|
+
end
|
107
|
+
it 'unless you say not to' do
|
108
|
+
expect(subject.has_key?('@context')).to be_falsey
|
109
|
+
expect(subject.to_ordered_hash(include_context: false).has_key?('@context')).to be_falsey
|
110
|
+
end
|
111
|
+
it 'or it\'s already there' do
|
112
|
+
different_ctxt = 'http://example.org/context'
|
113
|
+
subject['@context'] = different_ctxt
|
114
|
+
oh = subject.to_ordered_hash
|
115
|
+
expect(oh['@context']).to eq different_ctxt
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe 'runs the validations' do
|
120
|
+
# Test this here because there's nothing to validate on the superclass (Subject)
|
121
|
+
let(:error) { IIIF::Presentation::MissingRequiredKeyError }
|
122
|
+
before(:each) { subject.delete('@id') }
|
123
|
+
it 'raises exceptions' do
|
124
|
+
expect { subject.to_ordered_hash }.to raise_error error
|
125
|
+
end
|
126
|
+
it 'unless you tell it not to' do
|
127
|
+
expect { subject.to_ordered_hash(force: true) }.to_not raise_error
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
describe IIIF::Presentation::Canvas do
|
2
|
+
|
3
|
+
let(:fixed_values) do
|
4
|
+
{
|
5
|
+
"@context" => "http://iiif.io/api/presentation/2/context.json",
|
6
|
+
"@id" => "http://www.example.org/iiif/book1/canvas/p1",
|
7
|
+
"@type" => "sc:Canvas",
|
8
|
+
"label" => "p. 1",
|
9
|
+
"height" => 1000,
|
10
|
+
"width" => 750,
|
11
|
+
"images" => [ ],
|
12
|
+
"otherContent" => [ ]
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
describe '#initialize' do
|
18
|
+
it 'sets @type' do
|
19
|
+
expect(subject['@type']).to eq 'sc:Canvas'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#{described_class}.int_only_keys" do
|
24
|
+
it_behaves_like 'it has the appropriate methods for integer-only keys'
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#{described_class}.define_methods_for_string_only_keys" do
|
28
|
+
it_behaves_like 'it has the appropriate methods for string-only keys'
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#{described_class}.define_methods_for_array_only_keys" do
|
32
|
+
it_behaves_like 'it has the appropriate methods for array-only keys'
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#{described_class}.define_methods_for_any_type_keys" do
|
36
|
+
it_behaves_like 'it has the appropriate methods for any-type keys'
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
describe IIIF::Presentation::Collection do
|
2
|
+
|
3
|
+
let(:fixed_values) do
|
4
|
+
{
|
5
|
+
'@context' => 'http://iiif.io/api/presentation/2/context.json',
|
6
|
+
'@id' => 'http://example.org/iiif/collection/top',
|
7
|
+
'@type' => 'sc:Collection',
|
8
|
+
'label' => 'Top Level Collection for Example Organization',
|
9
|
+
'description' => 'Description of Collection',
|
10
|
+
'attribution' => 'Provided by Example Organization',
|
11
|
+
|
12
|
+
'collections' => [
|
13
|
+
{ '@id' => 'http://example.org/iiif/collection/part1',
|
14
|
+
'@type' => 'sc:Collection',
|
15
|
+
'label' => 'Sub Collection 1'
|
16
|
+
},
|
17
|
+
{ '@id' => 'http://example.org/iiif/collection/part2',
|
18
|
+
'@type' => 'sc:Collection',
|
19
|
+
'label' => 'Sub Collection 2'
|
20
|
+
}
|
21
|
+
],
|
22
|
+
'manifests' => [
|
23
|
+
{ '@id' => 'http://example.org/iiif/book1/manifest',
|
24
|
+
'@type' => 'sc:Manifest',
|
25
|
+
'label' => 'Book 1'
|
26
|
+
}
|
27
|
+
]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#initialize' do
|
32
|
+
it 'sets @type to sc:Collection by default' do
|
33
|
+
expect(subject['@type']).to eq 'sc:Collection'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#{described_class}.define_methods_for_array_only_keys" do
|
38
|
+
it_behaves_like 'it has the appropriate methods for array-only keys'
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#{described_class}.define_methods_for_string_only_keys" do
|
42
|
+
it_behaves_like 'it has the appropriate methods for string-only keys'
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#{described_class}.define_methods_for_any_type_keys" do
|
46
|
+
it_behaves_like 'it has the appropriate methods for any-type keys'
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#validate' do
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
describe IIIF::Presentation::ImageResource do
|
2
|
+
|
3
|
+
describe '#initialize' do
|
4
|
+
it 'sets @type to dcterms:Image' do
|
5
|
+
expect(subject['@type']).to eq 'dcterms:Image'
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#{described_class}.int_only_keys" do
|
10
|
+
it_behaves_like 'it has the appropriate methods for integer-only keys'
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
describe IIIF::Presentation::Layer do
|
2
|
+
|
3
|
+
let(:fixed_values) do
|
4
|
+
{
|
5
|
+
'@context' => 'http://iiif.io/api/presentation/2/context.json',
|
6
|
+
'@id' => 'http://www.example.org/iiif/book1/layer/transcription',
|
7
|
+
'@type' => 'sc:Layer',
|
8
|
+
'label' => 'Diplomatic Transcription',
|
9
|
+
'otherContent' => [
|
10
|
+
'http://www.example.org/iiif/book1/list/l1',
|
11
|
+
'http://www.example.org/iiif/book1/list/l2',
|
12
|
+
'http://www.example.org/iiif/book1/list/l3',
|
13
|
+
'http://www.example.org/iiif/book1/list/l4'
|
14
|
+
]
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
describe '#initialize' do
|
20
|
+
it 'sets @type' do
|
21
|
+
expect(subject['@type']).to eq 'sc:Layer'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#{described_class}.define_methods_for_string_only_keys" do
|
26
|
+
it_behaves_like 'it has the appropriate methods for string-only keys'
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#{described_class}.define_methods_for_array_only_keys" do
|
30
|
+
it_behaves_like 'it has the appropriate methods for array-only keys'
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#{described_class}.define_methods_for_any_type_keys" do
|
34
|
+
it_behaves_like 'it has the appropriate methods for any-type keys'
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
describe IIIF::Presentation::Manifest do
|
2
|
+
|
3
|
+
let(:subclass_subject) do
|
4
|
+
Class.new(IIIF::Presentation::Manifest) do
|
5
|
+
def initialize(hsh={})
|
6
|
+
hsh = { '@type' => 'a:SubClass' }
|
7
|
+
super(hsh)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:fixed_values) do
|
13
|
+
{
|
14
|
+
'type' => 'a:SubClass',
|
15
|
+
'id' => 'http://example.com/prefix/manifest/123',
|
16
|
+
'context' => IIIF::Presentation::CONTEXT,
|
17
|
+
'label' => 'Book 1',
|
18
|
+
'description' => 'A longer description of this example book. It should give some real information.',
|
19
|
+
'thumbnail' => {
|
20
|
+
'@id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg',
|
21
|
+
'service'=> {
|
22
|
+
'@context' => 'http://iiif.io/api/image/2/context.json',
|
23
|
+
'@id' => 'http://www.example.org/images/book1-page1',
|
24
|
+
'profile' => 'http://iiif.io/api/image/2/level1.json'
|
25
|
+
}
|
26
|
+
},
|
27
|
+
'attribution' => 'Provided by Example Organization',
|
28
|
+
'license' => 'http://www.example.org/license.html',
|
29
|
+
'logo' => 'http://www.example.org/logos/institution1.jpg',
|
30
|
+
'see_also' => 'http://www.example.org/library/catalog/book1.xml',
|
31
|
+
'service' => {
|
32
|
+
'@context' => 'http://example.org/ns/jsonld/context.json',
|
33
|
+
'@id' => 'http://example.org/service/example',
|
34
|
+
'profile' => 'http://example.org/docs/example-service.html'
|
35
|
+
},
|
36
|
+
'related' => {
|
37
|
+
'@id' => 'http://www.example.org/videos/video-book1.mpg',
|
38
|
+
'format' => 'video/mpeg'
|
39
|
+
},
|
40
|
+
'within' => 'http://www.example.org/collections/books/',
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#initialize' do
|
45
|
+
it 'sets @type to sc:Manifest by default' do
|
46
|
+
expect(subject['@type']).to eq 'sc:Manifest'
|
47
|
+
end
|
48
|
+
it 'allows subclasses to override @type' do
|
49
|
+
sub = subclass_subject.new
|
50
|
+
expect(sub['@type']).to eq 'a:SubClass'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#required_keys' do
|
55
|
+
it 'accumulates' do
|
56
|
+
expect(subject.required_keys).to eq %w{ @type @id label }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#validate' do
|
61
|
+
it 'raises an error if there is no @id' do
|
62
|
+
subject.label = 'Book 1'
|
63
|
+
expect { subject.validate }.to raise_error IIIF::Presentation::MissingRequiredKeyError
|
64
|
+
end
|
65
|
+
it 'raises an error if there is no label' do
|
66
|
+
subject['@id'] = 'http://www.example.org/iiif/book1/manifest'
|
67
|
+
expect { subject.validate }.to raise_error IIIF::Presentation::MissingRequiredKeyError
|
68
|
+
end
|
69
|
+
it 'raises an error if there is no @type' do
|
70
|
+
subject.delete('@type')
|
71
|
+
subject.label = 'Book 1'
|
72
|
+
subject['@id'] = 'http://www.example.org/iiif/book1/manifest'
|
73
|
+
expect { subject.validate }.to raise_error IIIF::Presentation::MissingRequiredKeyError
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "#{described_class}.define_methods_for_array_only_keys" do
|
78
|
+
it_behaves_like 'it has the appropriate methods for array-only keys'
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "#{described_class}.define_methods_for_string_only_keys" do
|
82
|
+
it_behaves_like 'it has the appropriate methods for string-only keys'
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#{described_class}.define_methods_for_any_type_keys" do
|
86
|
+
it_behaves_like 'it has the appropriate methods for any-type keys'
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
describe IIIF::Presentation::Range do
|
2
|
+
|
3
|
+
let(:fixed_values) do
|
4
|
+
{
|
5
|
+
'@id' => 'http://www.example.org/iiif/book1/range/r1',
|
6
|
+
'@type' => 'sc:Range',
|
7
|
+
'label' => 'Introduction',
|
8
|
+
'ranges' => [
|
9
|
+
'http://www.example.org/iiif/book1/range/r1-1',
|
10
|
+
'http://www.example.org/iiif/book1/range/r1-2'
|
11
|
+
],
|
12
|
+
'canvases' => [
|
13
|
+
'http://www.example.org/iiif/book1/canvas/p1',
|
14
|
+
'http://www.example.org/iiif/book1/canvas/p2',
|
15
|
+
'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300'
|
16
|
+
]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#initialize' do
|
21
|
+
it 'sets @type to sc:Range by default' do
|
22
|
+
expect(subject['@type']).to eq 'sc:Range'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#{described_class}.define_methods_for_array_only_keys" do
|
27
|
+
it_behaves_like 'it has the appropriate methods for array-only keys'
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#{described_class}.define_methods_for_string_only_keys" do
|
31
|
+
it_behaves_like 'it has the appropriate methods for string-only keys'
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#{described_class}.define_methods_for_any_type_keys" do
|
35
|
+
it_behaves_like 'it has the appropriate methods for any-type keys'
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#validate' do
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
describe IIIF::Presentation::Resource do
|
2
|
+
|
3
|
+
describe "#{described_class}.define_methods_for_abstract_resource_only_keys" do
|
4
|
+
it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys'
|
5
|
+
end
|
6
|
+
|
7
|
+
describe "#{described_class}.int_only_keys" do
|
8
|
+
it_behaves_like 'it has the appropriate methods for integer-only keys'
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#{described_class}.define_methods_for_string_only_keys" do
|
12
|
+
it_behaves_like 'it has the appropriate methods for string-only keys'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|