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,118 @@
1
+ describe IIIF::V3::Presentation::ImageResource do
2
+ vcr_options = {
3
+ cassette_name: 'pul_loris_cassette_v3',
4
+ record: :new_episodes,
5
+ serialize_with: :json
6
+ }
7
+
8
+ describe 'self#create_image_api_image_resource', vcr: vcr_options do
9
+
10
+ let(:image_server) { 'https://libimages.princeton.edu/loris' }
11
+
12
+ let(:valid_service_id) {
13
+ id = 'pudl0001%2F4612422%2F00000001.jp2'
14
+ "#{image_server}/#{id}"
15
+ }
16
+
17
+ let(:invalid_service_id) {
18
+ id = 'xxxx%2F4612422%2F00000001.jp2'
19
+ "#{image_server}/#{id}"
20
+ }
21
+
22
+ it 'returns an ImageResource' do
23
+ instance = described_class.create_image_api_image_resource(service_id: valid_service_id)
24
+ expect(instance.class).to be described_class
25
+ end
26
+
27
+
28
+ describe 'has expected values from our fixture' do
29
+ it 'when copy_info is false' do
30
+ opts = { service_id: valid_service_id }
31
+ resource = described_class.create_image_api_image_resource(opts)
32
+ expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg'
33
+ expect(resource['type']).to eq 'Image'
34
+ expect(resource.format).to eq "image/jpeg"
35
+ expect(resource.width).to eq 3047
36
+ expect(resource.height).to eq 7200
37
+ expect(resource.service.first['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2'
38
+ expect(resource.service.first['profile']).to eq 'http://iiif.io/api/image/2/level2.json'
39
+ end
40
+ it 'copies over all teh infos (when copy_info is true)' do
41
+ opts = { service_id: valid_service_id, copy_info: true }
42
+ resource = described_class.create_image_api_image_resource(opts)
43
+
44
+ expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg'
45
+ expect(resource['type']).to eq 'Image'
46
+ expect(resource.format).to eq "image/jpeg"
47
+ expect(resource.width).to eq 3047
48
+ expect(resource.height).to eq 7200
49
+ expect(resource.service.first['profile']).to eq [
50
+ 'http://iiif.io/api/image/2/level2.json',
51
+ {
52
+ 'supports' => [
53
+ 'canonicalLinkHeader', 'profileLinkHeader', 'mirroring',
54
+ 'rotationArbitrary', 'regionSquare', 'sizeAboveFull'
55
+ ],
56
+ 'qualities' => ['default', 'bitonal', 'gray', 'color'],
57
+ 'formats'=>['jpg', 'png', 'gif', 'webp']
58
+ }
59
+ ]
60
+ expect(resource.service.first['tiles']).to eq [ {
61
+ 'width' => 1024,
62
+ 'scaleFactors' => [ 1, 2, 4, 8, 16, 32 ]
63
+ } ]
64
+ expect(resource.service.first['sizes']).to eq [
65
+ {'width' => 96, 'height' => 225 },
66
+ {'width' => 191, 'height' => 450 },
67
+ {'width' => 381, 'height' => 900 },
68
+ {'width' => 762, 'height' => 1800 },
69
+ {'width' => 1524, 'height' => 3600 },
70
+ {'width' => 3047, 'height' => 7200 }
71
+ ]
72
+ expect(resource.service.first['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2'
73
+ expect(resource.service.first).not_to have_key('@id')
74
+ end
75
+ end
76
+
77
+ describe 'respects the params we supply' do
78
+ it ':resource_id' do
79
+ r_id = 'http://example.edu/images/some.jpg'
80
+ opts = { service_id: valid_service_id, resource_id: r_id}
81
+ resource = described_class.create_image_api_image_resource(opts)
82
+ expect(resource['id']).to eq r_id
83
+ end
84
+ it ':width' do
85
+ width = 42
86
+ opts = { service_id: valid_service_id, width: width}
87
+ resource = described_class.create_image_api_image_resource(opts)
88
+ expect(resource.width).to eq width
89
+ end
90
+ it ':height' do
91
+ height = 42
92
+ opts = { service_id: valid_service_id, height: height}
93
+ resource = described_class.create_image_api_image_resource(opts)
94
+ expect(resource.height).to eq height
95
+ end
96
+ it ':profile (service[\'profile\'])' do
97
+ profile = 'http://iiif.io/api/image/2/level1.json'
98
+ opts = { service_id: valid_service_id, profile: profile}
99
+ resource = described_class.create_image_api_image_resource(opts)
100
+ expect(resource.service.first['profile']).to eq profile
101
+ end
102
+ end
103
+
104
+ describe 'errors' do
105
+ it 'raises if :service_id is not included' do
106
+ expect {
107
+ described_class.create_image_api_image_resource
108
+ }.to raise_error
109
+ end
110
+ it 'raises if the info can\'t be pulled in' do
111
+ expect {
112
+ described_class.create_image_api_image_resource(service_id: invalid_service_id)
113
+ }.to raise_error
114
+ end
115
+ end
116
+
117
+ end
118
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  require 'iiif/presentation'
2
+ require 'iiif/v3/presentation'
2
3
  require 'simplecov'
3
4
  require 'coveralls'
5
+ require 'debug'
4
6
  Dir["#{File.dirname(__FILE__)}/unit/iiif/presentation/shared_examples/*.rb"].each do |f|
5
7
  require f
6
8
  end
9
+ Dir["#{File.dirname(__FILE__)}/unit/iiif/v3/presentation/shared_examples/*.rb"].each do |f|
10
+ require f
11
+ end
7
12
  require 'vcr'
13
+ require 'webmock/rspec'
8
14
 
9
15
  VCR.configure do |c|
10
16
  c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
@@ -43,4 +43,3 @@ describe IIIF::Presentation::Canvas do
43
43
  end
44
44
 
45
45
  end
46
-
@@ -14,7 +14,7 @@ describe IIIF::Presentation::Manifest do
14
14
  'type' => 'a:SubClass',
15
15
  'id' => 'http://example.com/prefix/manifest/123',
16
16
  'context' => IIIF::Presentation::CONTEXT,
17
- 'label' => 'Book 1',
17
+ 'label' => {'en' => ['Book 1']},
18
18
  'description' => 'A longer description of this example book. It should give some real information.',
19
19
  'thumbnail' => {
20
20
  '@id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg',
@@ -0,0 +1,78 @@
1
+ class AbstractResourceSubClass < IIIF::V3::AbstractResource
2
+ TYPE = 'Ignore'
3
+ # need a property here for type of key not already initialized in AbstractResource
4
+ def array_only_keys
5
+ super + %w{ array my_array }
6
+ end
7
+ def hash_only_keys
8
+ super + %w{ hash my_hash }
9
+ end
10
+ def int_only_keys
11
+ super + %w{ int my_int }
12
+ end
13
+ def numeric_only_keys
14
+ super + %w{ num my_num }
15
+ end
16
+ def uri_only_keys
17
+ super + %w{ uri my_uri }
18
+ end
19
+
20
+ def initialize(hsh={})
21
+ hsh = { 'type' => 'a:SubClass' }
22
+ super(hsh)
23
+ end
24
+ end
25
+
26
+ describe AbstractResourceSubClass do
27
+
28
+ describe "*define_methods_for_any_type_keys" do
29
+ # shared_example expects fixed_values; these are roughly based on Stanford purl code
30
+ # (see https://github.com/sul-dlss/purl/blob/master/app/models/iiif3_presentation_manifest.rb)
31
+ let(:fixed_values) do
32
+ {
33
+ 'label' => 'foo',
34
+ 'description' => 'bar',
35
+ 'thumbnail' => IIIF::V3::Presentation::ImageResource.new(
36
+ 'type' => 'Image',
37
+ 'id' => "http://example.org/full/!400,400/0/default.jpg",
38
+ 'format' => 'image/jpeg'
39
+ ),
40
+ 'attribution' => ['foo'],
41
+ 'logo' => {
42
+ 'id' => 'https://example.org/default.jpg',
43
+ 'service' => IIIF::V3::Presentation::Service.new(
44
+ '@context' => 'http://iiif.io/api/image/2/context.json',
45
+ '@id' => 'http://example.org/1',
46
+ 'id' => 'http://example.org/1',
47
+ 'profile' => 'http://example.org/whatever'
48
+ )
49
+ },
50
+ 'see_also' => {
51
+ 'id' => 'http://example.org/whatever',
52
+ 'format' => 'application/mods+xml'
53
+ },
54
+ 'related' => ['no', 'idea'],
55
+ 'within' => {'foo' => 'bar'}
56
+ }
57
+ end
58
+ it_behaves_like 'it has the appropriate methods for any-type keys v3'
59
+ end
60
+ describe "*define_methods_for_array_only_keys" do
61
+ it_behaves_like 'it has the appropriate methods for array-only keys v3'
62
+ end
63
+ describe "*define_methods_for_hash_only_keys" do
64
+ it_behaves_like 'it has the appropriate methods for hash-only keys v3'
65
+ end
66
+ describe "*define_methods_for_int_only_keys" do
67
+ it_behaves_like 'it has the appropriate methods for integer-only keys v3'
68
+ end
69
+ describe "*define_methods_for_numeric_only_keys" do
70
+ it_behaves_like 'it has the appropriate methods for numeric-only keys v3'
71
+ end
72
+ describe "*define_methods_for_string_only_keys" do
73
+ it_behaves_like 'it has the appropriate methods for string-only keys v3'
74
+ end
75
+ describe "*define_methods_for_uri_only_keys" do
76
+ it_behaves_like 'it has the appropriate methods for uri-only keys v3'
77
+ end
78
+ end
@@ -0,0 +1,293 @@
1
+ require 'active_support/inflector'
2
+ require 'json'
3
+ require_relative '../../../../lib/iiif/hash_behaviours'
4
+
5
+ describe IIIF::V3::AbstractResource do
6
+
7
+ let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../fixtures') }
8
+ let(:manifest_from_spec_path) { File.join(fixtures_dir, 'v3/manifests/complete_from_spec.json') }
9
+ let(:abstract_resource_subclass) do
10
+ Class.new(IIIF::V3::AbstractResource) do
11
+ include IIIF::HashBehaviours
12
+
13
+ def initialize(hsh={})
14
+ hsh['type'] = 'a:SubClass' unless hsh.has_key?('type')
15
+ super(hsh)
16
+ end
17
+
18
+ def required_keys
19
+ super + %w{ id }
20
+ end
21
+
22
+ def prohibited_keys
23
+ super + %w{ verboten }
24
+ end
25
+
26
+ def legal_viewing_hint_values
27
+ %w{ viewing_hint1 viewing_hint2 }
28
+ end
29
+ end
30
+ end
31
+ subject do
32
+ instance = abstract_resource_subclass.new
33
+ instance['id'] = 'http://example.com/prefix/manifest/123'
34
+ instance
35
+ end
36
+
37
+ describe '#required_keys' do
38
+ it 'accumulate' do
39
+ expect(subject.required_keys).to eq %w{ type id }
40
+ end
41
+ end
42
+
43
+ describe '#initialize' do
44
+ it 'raises an error if you try to instantiate AbstractResource' do
45
+ expect { IIIF::V3::AbstractResource.new }.to raise_error(RuntimeError)
46
+ end
47
+ it 'sets type' do
48
+ expect(subject['type']).to eq 'a:SubClass'
49
+ end
50
+ it 'can take any old hash' do
51
+ hsh = JSON.parse(IO.read(manifest_from_spec_path))
52
+ new_instance = abstract_resource_subclass.new(hsh)
53
+ expect(new_instance['label']['en']).to include 'Book 1'
54
+ end
55
+ end
56
+
57
+ describe '#validate' do
58
+ it 'raises MissingRequiredKeyError if required key is missing' do
59
+ subject.required_keys.each { |k| subject.delete(k) }
60
+ expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError
61
+ end
62
+ it 'raises ProhibitedKeyError if prohibited key is present' do
63
+ subject['verboten'] = 666
64
+ exp_err_msg = "verboten is a prohibited key in #{subject.class}"
65
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::ProhibitedKeyError, exp_err_msg)
66
+ end
67
+ it 'raises IllegalValueError for bad viewing_direction' do
68
+ subject['viewing_direction'] = 'foo'
69
+ exp_err_msg = "viewingDirection must be one of #{subject.legal_viewing_direction_values}"
70
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
71
+ end
72
+ describe 'viewing_hint' do
73
+ it 'can be a uri' do
74
+ subject['viewing_hint'] = 'https://example.org/viewiing_hint'
75
+ expect { subject.validate }.not_to raise_error
76
+ end
77
+ it 'can be a member of legal_viewing_hint_values' do
78
+ subject['viewing_hint'] = subject.legal_viewing_hint_values.first
79
+ expect { subject.validate }.not_to raise_error
80
+ end
81
+ it 'raises IllegalValueError for bad viewing_hint' do
82
+ subject['viewing_hint'] = 'foo'
83
+ exp_err_msg = "viewingHint for #{subject.class} must be one or more of #{subject.legal_viewing_hint_values} or a URI"
84
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
85
+ end
86
+ it 'can have multiple values' do
87
+ subject['viewing_hint'] = [subject.legal_viewing_hint_values.first, subject.legal_viewing_hint_values.last]
88
+ expect { subject.validate }.not_to raise_error
89
+ end
90
+ end
91
+ describe 'metadata' do
92
+ it 'raises IllegalValueError for entry that is not a Hash' do
93
+ subject['metadata'] = ['error']
94
+ exp_err_msg = "metadata must be an Array with Hash members"
95
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
96
+ end
97
+ it 'does not raise error for entry that contains exactly "label" and "value"' do
98
+ subject['metadata'] = [{ 'label' => 'bar', 'value' => 'foo' }]
99
+ expect { subject.validate }.not_to raise_error
100
+ end
101
+ it 'raises IllegalValueError for entry that does not contain exactly "label" and "value"' do
102
+ subject['metadata'] = [{ 'label' => 'bar', 'bar' => 'foo' }]
103
+ exp_err_msg = "metadata members must be a Hash of keys 'label' and 'value'"
104
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
105
+ end
106
+ end
107
+ describe 'thumbnail' do
108
+ let (:exp_basic_err_msg) { "thumbnail must be an Array with Hash or ImageResource members" }
109
+ let (:exp_keys_msg) { 'thumbnail members must include keys "id" and "type"' }
110
+ it 'raises IllegalValueError for entry that is not a Hash or ImageResource object' do
111
+ subject['thumbnail'] = ['error']
112
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_basic_err_msg)
113
+ end
114
+ it 'does not raise error for entry Hash with "id" and "type"' do
115
+ subject['thumbnail'] = [{ 'id' => 'bar', 'type' => 'foo', 'random' => 'xxx' }]
116
+ expect { subject.validate }.not_to raise_error
117
+ end
118
+ it 'raises IllegalValueError for entry Hash that does not contain "id" and "type"' do
119
+ subject['thumbnail'] = [{ 'id' => 'bar' }]
120
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_keys_msg)
121
+ end
122
+ it 'does not raise error for entry ImageResource with "id"' do
123
+ subject['thumbnail'] = [IIIF::V3::Presentation::ImageResource.new({ 'id' => 'bar', 'label' => 'xxx' })]
124
+ expect { subject.validate }.not_to raise_error
125
+ end
126
+ it 'raises IllegalValueError for entry ImageResource without id' do
127
+ subject['thumbnail'] = [IIIF::V3::Presentation::ImageResource.new]
128
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_keys_msg)
129
+ end
130
+ end
131
+ describe 'nav_date' do
132
+ it 'does not raise error for value of form YYYY-MM-DDThh:mm:ssZ' do
133
+ subject['nav_date'] = '1991-01-02T13:04:27Z'
134
+ expect { subject.validate }.not_to raise_error
135
+ end
136
+ it 'raises IllegalValueError for value not of form YYYY-MM-DDThh:mm:ssZ' do
137
+ subject['nav_date'] = '1991-01-02T13:04:27+0500'
138
+ exp_err_msg = 'nav_date must be of form YYYY-MM-DDThh:mm:ssZ'
139
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
140
+ end
141
+ end
142
+ describe 'rights' do
143
+ it 'raises IllegalValueError for entry that is not a Hash' do
144
+ subject['rights'] = ['error']
145
+ exp_err_msg = "rights must be an Array with Hash members"
146
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
147
+ end
148
+ it 'does not raise error for entry with "id" that is URI' do
149
+ subject['rights'] = [{ 'id' => 'http://example.org/rights', 'format' => 'text/html' }]
150
+ expect { subject.validate }.not_to raise_error
151
+ end
152
+ it 'raises IllegalValueError for entry with "id" that is not URI' do
153
+ subject['rights'] = [{ 'id' => 'bar', 'format' => 'text/html' }]
154
+ exp_err_msg = "id value must be a String containing a URI for #{subject.class}"
155
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
156
+ end
157
+ it 'raises IllegalValueError for entry that does not contain "id"' do
158
+ subject['rights'] = [{ 'whoops' => 'http://example.org/rights' }]
159
+ exp_err_msg = 'rights members must be a Hash including "id"'
160
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
161
+ end
162
+ end
163
+ describe 'rendering' do
164
+ it 'raises IllegalValueError for entry that is not a Hash' do
165
+ subject['rendering'] = ['error']
166
+ exp_err_msg = "rendering must be an Array with Hash members"
167
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
168
+ end
169
+ it 'does not raise error for entry with "label" and "format"' do
170
+ subject['rendering'] = [{ 'label' => 'bar', 'format' => 'foo', 'random' => 'xxx' }]
171
+ expect { subject.validate }.not_to raise_error
172
+ end
173
+ it 'raises IllegalValueError for entry that does not contain "label" and "format"' do
174
+ subject['rendering'] = [{ 'label' => 'bar' }]
175
+ exp_err_msg = 'rendering members must be a Hash including keys "label" and "format"'
176
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
177
+ end
178
+ end
179
+ describe 'startCanvas' do
180
+ it 'raises IllegalValueError for entry that is not URI' do
181
+ subject.startCanvas = 'foo'
182
+ exp_err_msg = "startCanvas value must be a String containing a URI for #{subject.class}"
183
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg)
184
+ end
185
+ end
186
+ end
187
+
188
+ describe 'A nested object (e.g. self[\'metadata\'])' do
189
+ it 'returns [] if not set' do
190
+ expect(subject.metadata).to eq([])
191
+ end
192
+ it 'is not in #to_ordered_hash at all if we access it but do not append to it' do
193
+ subject.metadata # touch it
194
+ expect(subject.metadata).to eq([])
195
+ expect(subject.to_ordered_hash.has_key?('metadata')).to be_falsey
196
+ end
197
+ it 'gets structured as we\'d expect' do
198
+ subject.metadata << {
199
+ 'label' => 'Author',
200
+ 'value' => 'Anne Author'
201
+ }
202
+ subject.metadata << {
203
+ 'label' => 'Published',
204
+ 'value' => [
205
+ {'@value'=> 'Paris, circa 1400', '@language'=>'en'},
206
+ {'@value'=> 'Paris, environ 14eme siecle', '@language'=>'fr'}
207
+ ]
208
+ }
209
+ expect(subject.metadata[0]['label']).to eq('Author')
210
+ expect(subject.metadata[1]['value'].length).to eq(2)
211
+ expect(subject.metadata[1]['value'].select { |e| e['@language'] == 'fr'}.last['@value']).to eq('Paris, environ 14eme siecle')
212
+ end
213
+
214
+ it 'roundtrips' do
215
+ subject.metadata << {
216
+ 'label' => 'Author',
217
+ 'value' => 'Anne Author'
218
+ }
219
+ subject.metadata << {
220
+ 'label' => 'Published',
221
+ 'value' => [
222
+ {'@value'=> 'Paris, circa 1400', '@language'=>'en'},
223
+ {'@value'=> 'Paris, environ 14eme siecle', '@language'=>'fr'}
224
+ ]
225
+ }
226
+ File.open('/tmp/osullivan-spec.json','w') do |f|
227
+ f.write(subject.to_json)
228
+ end
229
+ parsed = subject.class.parse('/tmp/osullivan-spec.json')
230
+ expect(parsed['metadata'][0]['label']).to eq('Author')
231
+ expect(parsed['metadata'][1]['value'].length).to eq(2)
232
+ expect(parsed['metadata'][1]['value'].select { |e| e['@language'] == 'fr'}.last['@value']).to eq('Paris, environ 14eme siecle')
233
+ File.delete('/tmp/osullivan-spec.json')
234
+ end
235
+ end
236
+
237
+ describe '#to_ordered_hash' do
238
+ describe 'adds the @context' do
239
+ before(:each) { subject.delete('@context') }
240
+ it 'by default' do
241
+ expect(subject.has_key?('@context')).to be_falsey
242
+ expect(subject.to_ordered_hash.has_key?('@context')).to be_truthy
243
+ end
244
+ it 'unless you say not to' do
245
+ expect(subject.has_key?('@context')).to be_falsey
246
+ expect(subject.to_ordered_hash(include_context: false).has_key?('@context')).to be_falsey
247
+ end
248
+ it 'or it\'s already there' do
249
+ different_ctxt = 'http://example.org/context'
250
+ subject['@context'] = different_ctxt
251
+ oh = subject.to_ordered_hash
252
+ expect(oh['@context']).to eq different_ctxt
253
+ end
254
+ end
255
+
256
+ describe 'runs the validations' do
257
+ let(:error) { IIIF::V3::Presentation::MissingRequiredKeyError }
258
+ before(:each) { subject.delete('id') }
259
+ it 'raises exceptions' do
260
+ expect { subject.to_ordered_hash }.to raise_error error
261
+ end
262
+ it 'unless you tell it not to' do
263
+ expect { subject.to_ordered_hash(force: true) }.to_not raise_error
264
+ end
265
+ end
266
+ end
267
+
268
+ describe '*get_descendant_class_by_jld_type' do
269
+ before do
270
+ class DummyClass < IIIF::V3::AbstractResource
271
+ TYPE = "Collection"
272
+ def self.singleton_class?
273
+ true
274
+ end
275
+ end
276
+ end
277
+ after do
278
+ Object.send(:remove_const, :DummyClass)
279
+ end
280
+ it 'gets the right class' do
281
+ klass = described_class.get_descendant_class_by_jld_type('Canvas')
282
+ expect(klass).to eq IIIF::V3::Presentation::Canvas
283
+ end
284
+ context "when there are singleton classes which are returned" do
285
+ it "gets the right class" do
286
+ allow(IIIF::V3::AbstractResource).to receive(:descendants).and_return([DummyClass, IIIF::V3::Presentation::Collection])
287
+ klass = described_class.get_descendant_class_by_jld_type('Collection')
288
+ expect(klass).to eq IIIF::V3::Presentation::Collection
289
+ end
290
+ end
291
+ end
292
+
293
+ end
@@ -0,0 +1,36 @@
1
+ describe IIIF::V3::Presentation::AnnotationCollection do
2
+
3
+ let(:fixed_values) do
4
+ {
5
+ '@context' => [
6
+ 'http://iiif.io/api/presentation/3/context.json',
7
+ 'http://www.w3.org/ns/anno.jsonld'
8
+ ],
9
+ 'id' => 'http://www.example.org/iiif/book1/annoColl/transcription',
10
+ 'type' => 'AnnotationCollection',
11
+ 'label' => 'Diplomatic Transcription',
12
+ 'first' => 'http://www.example.org/iiif/book1/list/l1',
13
+ 'last' => 'http://www.example.org/iiif/book1/list/l4',
14
+ }
15
+ end
16
+
17
+
18
+ describe '#initialize' do
19
+ it 'sets type' do
20
+ expect(subject['type']).to eq 'AnnotationCollection'
21
+ end
22
+ end
23
+
24
+ describe "#{described_class}.define_methods_for_string_only_keys" do
25
+ it_behaves_like 'it has the appropriate methods for string-only keys v3'
26
+ end
27
+
28
+ describe "#{described_class}.define_methods_for_array_only_keys" do
29
+ it_behaves_like 'it has the appropriate methods for array-only keys v3'
30
+ end
31
+
32
+ describe "#{described_class}.define_methods_for_any_type_keys" do
33
+ it_behaves_like 'it has the appropriate methods for any-type keys v3'
34
+ end
35
+
36
+ end