iiif-presentation 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/README.md +22 -2
- data/VERSION +1 -1
- data/iiif-presentation.gemspec +1 -1
- data/lib/iiif/hash_behaviours.rb +1 -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 +40 -105
- 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/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 +36 -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/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 +82 -11
- data/.travis.yml +0 -11
@@ -0,0 +1,491 @@
|
|
1
|
+
require_relative '../hash_behaviours'
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module V3
|
5
|
+
class AbstractResource
|
6
|
+
include IIIF::HashBehaviours
|
7
|
+
|
8
|
+
# properties used by content resources only
|
9
|
+
CONTENT_RESOURCE_PROPERTIES = %w{ format height width duration }
|
10
|
+
|
11
|
+
# used by Collection, AnnotationCollection
|
12
|
+
PAGING_PROPERTIES = %w{ first last next prev total start_index }
|
13
|
+
|
14
|
+
# subclasses should override required_keys as appropriate, e.g. super + %w{ id }
|
15
|
+
def required_keys
|
16
|
+
%w{ type }
|
17
|
+
end
|
18
|
+
|
19
|
+
# subclasses should override prohibited_keys as appropriate, e.g. super + PAGING_PROPERTIES
|
20
|
+
def prohibited_keys
|
21
|
+
%w{ }
|
22
|
+
end
|
23
|
+
|
24
|
+
# NOTE: keys associated with a single resource type are not included below in xxx_keys methods:
|
25
|
+
# those single resource types should include additional keys by overriding xxx_keys as appropriate
|
26
|
+
|
27
|
+
def any_type_keys
|
28
|
+
# values *may* be multivalued
|
29
|
+
# NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers"
|
30
|
+
%w{ id logo viewing_hint related see_also within }
|
31
|
+
end
|
32
|
+
|
33
|
+
def string_only_keys
|
34
|
+
%w{ nav_date type format viewing_direction start_canvas }
|
35
|
+
end
|
36
|
+
|
37
|
+
def array_only_keys
|
38
|
+
%w{ metadata rights thumbnail rendering first last next prev items service }
|
39
|
+
end
|
40
|
+
|
41
|
+
def hash_only_keys
|
42
|
+
%w{ label requiredStatement summary }
|
43
|
+
end
|
44
|
+
|
45
|
+
def int_only_keys
|
46
|
+
%w{ height width total start_index }
|
47
|
+
end
|
48
|
+
|
49
|
+
def numeric_only_keys
|
50
|
+
%w{ duration }
|
51
|
+
end
|
52
|
+
|
53
|
+
def uri_only_keys
|
54
|
+
%w{ }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Not every subclass is allowed to have viewingDirect, but when it is,
|
58
|
+
# it must be one of these values
|
59
|
+
def legal_viewing_direction_values
|
60
|
+
%w{ left-to-right right-to-left top-to-bottom bottom-to-top }
|
61
|
+
end
|
62
|
+
|
63
|
+
def legal_viewing_hint_values
|
64
|
+
[]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Initialize a Presentation node
|
68
|
+
# @param [Hash] hsh - Anything in this hash will be added to the Object.
|
69
|
+
# Order is only guaranteed if an ActiveSupport::OrderedHash is passed.
|
70
|
+
# @param [boolean] include_context (default: false). Pass true if the
|
71
|
+
# context should be included.
|
72
|
+
def initialize(hsh={})
|
73
|
+
if self.class == IIIF::V3::AbstractResource
|
74
|
+
raise "#{self.class} is an abstract class. Please use one of its subclasses."
|
75
|
+
end
|
76
|
+
@data = IIIF::OrderedHash[hsh]
|
77
|
+
self.define_methods_for_any_type_keys
|
78
|
+
self.define_methods_for_string_only_keys
|
79
|
+
self.define_methods_for_array_only_keys
|
80
|
+
self.define_methods_for_hash_only_keys
|
81
|
+
self.define_methods_for_int_only_keys
|
82
|
+
self.define_methods_for_numeric_only_keys
|
83
|
+
self.define_methods_for_uri_only_keys
|
84
|
+
self.snakeize_keys
|
85
|
+
end
|
86
|
+
|
87
|
+
# Static methods / alternative constructors
|
88
|
+
class << self
|
89
|
+
# Parse from a file path, string, or existing hash
|
90
|
+
def parse(s)
|
91
|
+
ordered_hash = nil
|
92
|
+
if s.kind_of?(String) && File.exist?(s)
|
93
|
+
ordered_hash = IIIF::OrderedHash[JSON.parse(IO.read(s))]
|
94
|
+
elsif s.kind_of?(String) && !File.exist?(s)
|
95
|
+
ordered_hash = IIIF::OrderedHash[JSON.parse(s)]
|
96
|
+
elsif s.kind_of?(Hash)
|
97
|
+
ordered_hash = IIIF::OrderedHash[s]
|
98
|
+
else
|
99
|
+
m = '#parse takes a path to a file, a JSON String, or a Hash, '
|
100
|
+
m += "argument was a #{s.class}."
|
101
|
+
if s.kind_of?(String)
|
102
|
+
m+= "If you were trying to point to a file, does it exist?"
|
103
|
+
end
|
104
|
+
raise ArgumentError, m
|
105
|
+
end
|
106
|
+
return IIIF::V3::Presentation::Service.from_ordered_hash(ordered_hash)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def validate
|
111
|
+
self.required_keys.each do |k|
|
112
|
+
unless self.has_key?(k)
|
113
|
+
m = "A(n) #{k} is required for each #{self.class}"
|
114
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
self.prohibited_keys.each do |k|
|
119
|
+
if self.has_key?(k)
|
120
|
+
m = "#{k} is a prohibited key in #{self.class}"
|
121
|
+
raise IIIF::V3::Presentation::ProhibitedKeyError, m
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
self.uri_only_keys.each do |k|
|
126
|
+
if self[k]
|
127
|
+
vals = *self[k]
|
128
|
+
vals.each { |val| validate_uri(val, k) }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Note: self.define_methods_for_xxx_only_keys provides some validation at assignment time
|
133
|
+
# currently, there is NO validation when key values are assigned directly with hash syntax,
|
134
|
+
# e.g. my_image_resource['format'] = 'image/jpeg'
|
135
|
+
|
136
|
+
# Viewing Direction values
|
137
|
+
if self.has_key?('viewing_direction')
|
138
|
+
unless self.legal_viewing_direction_values.include?(self['viewing_direction'])
|
139
|
+
m = "viewingDirection must be one of #{legal_viewing_direction_values}"
|
140
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
141
|
+
end
|
142
|
+
end
|
143
|
+
# Viewing Hint can be an Array ("Any resource type may have one or more viewing hints")
|
144
|
+
if self.has_key?('viewing_hint')
|
145
|
+
viewing_hint_val = self['viewing_hint']
|
146
|
+
[*viewing_hint_val].each { |vh_val|
|
147
|
+
unless self.legal_viewing_hint_values.include?(vh_val) || (vh_val.kind_of?(String) && vh_val =~ URI::regexp)
|
148
|
+
m = "viewingHint for #{self.class} must be one or more of #{self.legal_viewing_hint_values} or a URI"
|
149
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
150
|
+
end
|
151
|
+
}
|
152
|
+
end
|
153
|
+
# Metadata is Array; each entry is a Hash containing (only) 'label' and 'value' properties
|
154
|
+
if self.has_key?('metadata') && self['metadata']
|
155
|
+
unless self['metadata'].all? { |entry| entry.kind_of?(Hash) }
|
156
|
+
m = 'metadata must be an Array with Hash members'
|
157
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
158
|
+
end
|
159
|
+
self['metadata'].each do |entry|
|
160
|
+
md_keys = entry.keys
|
161
|
+
unless md_keys.size == 2 && md_keys.include?('label') && md_keys.include?('value')
|
162
|
+
m = "metadata members must be a Hash of keys 'label' and 'value'"
|
163
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
# Thumbnail is Array; each entry is a Hash or ImageResource containing (at least) 'id' and 'type' keys
|
168
|
+
if self.has_key?('thumbnail') && self['thumbnail']
|
169
|
+
unless self['thumbnail'].all? { |entry| entry.kind_of?(IIIF::V3::Presentation::ImageResource) || entry.kind_of?(Hash) }
|
170
|
+
m = 'thumbnail must be an Array with Hash or ImageResource members'
|
171
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
172
|
+
end
|
173
|
+
self['thumbnail'].each do |entry|
|
174
|
+
thumb_keys = entry.keys
|
175
|
+
unless thumb_keys.include?('id') && thumb_keys.include?('type')
|
176
|
+
m = 'thumbnail members must include keys "id" and "type"'
|
177
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
# NavDate (navigation date)
|
182
|
+
if self.has_key?('nav_date')
|
183
|
+
begin
|
184
|
+
Date.strptime(self['nav_date'], '%Y-%m-%dT%H:%M:%SZ')
|
185
|
+
rescue ArgumentError
|
186
|
+
m = "nav_date must be of form YYYY-MM-DDThh:mm:ssZ"
|
187
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
188
|
+
end
|
189
|
+
end
|
190
|
+
# rights is Array; each entry is a Hash containing 'id' with a URI value
|
191
|
+
if self.has_key?('rights')
|
192
|
+
unless self['rights'].all? { |entry| entry.kind_of?(Hash) }
|
193
|
+
m = 'rights must be an Array with Hash members'
|
194
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
195
|
+
end
|
196
|
+
self['rights'].each do |entry|
|
197
|
+
unless entry.keys.include?('id')
|
198
|
+
m = 'rights members must be a Hash including "id"'
|
199
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
200
|
+
end
|
201
|
+
validate_uri(entry['id'], 'id') # raises IllegalValueError
|
202
|
+
end
|
203
|
+
end
|
204
|
+
# rendering is Array; each entry is a Hash containing 'label' and 'format' keys
|
205
|
+
if self.has_key?('rendering') && self['rendering']
|
206
|
+
unless self['rendering'].all? { |entry| entry.kind_of?(Hash) }
|
207
|
+
m = 'rendering must be an Array with Hash members'
|
208
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
209
|
+
end
|
210
|
+
self['rendering'].each do |entry|
|
211
|
+
rendering_keys = entry.keys
|
212
|
+
unless rendering_keys.include?('label') && rendering_keys.include?('format')
|
213
|
+
m = 'rendering members must be a Hash including keys "label" and "format"'
|
214
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
# startCanvas is a String with a URI value
|
219
|
+
if self.has_key?('start_canvas') && self['start_canvas'].kind_of?(String)
|
220
|
+
validate_uri(self['start_canvas'], 'startCanvas') # raises IllegalValueError
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Options
|
225
|
+
# * pretty: (true|false). Should the JSON be pretty-printed? (default: false)
|
226
|
+
# * All options available in #to_ordered_hash
|
227
|
+
def to_json(opts={})
|
228
|
+
hsh = self.to_ordered_hash(opts)
|
229
|
+
if opts.fetch(:pretty, false)
|
230
|
+
JSON.pretty_generate(hsh)
|
231
|
+
else
|
232
|
+
hsh.to_json
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Options:
|
237
|
+
# * force: (true|false). Skips validations.
|
238
|
+
# * include_context: (true|false). Adds the @context to the top of the
|
239
|
+
# document if it doesn't exist. Default: true.
|
240
|
+
# * sort_json_ld_keys: (true|false). Brings all properties starting with
|
241
|
+
# '@'. Default: true. to the top of the document and sorts them.
|
242
|
+
def to_ordered_hash(opts={})
|
243
|
+
include_context = opts.fetch(:include_context, true)
|
244
|
+
if include_context && !self.has_key?('@context')
|
245
|
+
self['@context'] = IIIF::V3::Presentation::CONTEXT
|
246
|
+
end
|
247
|
+
force = opts.fetch(:force, false)
|
248
|
+
sort_json_ld_keys = opts.fetch(:sort_json_ld_keys, true)
|
249
|
+
|
250
|
+
unless force
|
251
|
+
self.validate
|
252
|
+
end
|
253
|
+
|
254
|
+
export_hash = IIIF::OrderedHash.new
|
255
|
+
|
256
|
+
if sort_json_ld_keys
|
257
|
+
self.keys.select { |k| k.start_with?('@') }.sort!.each do |k|
|
258
|
+
export_hash[k] = self.data[k]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
sub_opts = {
|
263
|
+
include_context: false,
|
264
|
+
sort_json_ld_keys: sort_json_ld_keys,
|
265
|
+
force: force
|
266
|
+
}
|
267
|
+
self.keys.each do |k|
|
268
|
+
unless sort_json_ld_keys && k.start_with?('@')
|
269
|
+
if self.data[k].respond_to?(:to_ordered_hash) #.respond_to?(:to_ordered_hash)
|
270
|
+
export_hash[k] = self.data[k].to_ordered_hash(sub_opts)
|
271
|
+
|
272
|
+
elsif self.data[k].kind_of?(Hash)
|
273
|
+
export_hash[k] = IIIF::OrderedHash.new
|
274
|
+
self.data[k].each do |sub_k, v|
|
275
|
+
|
276
|
+
if v.respond_to?(:to_ordered_hash)
|
277
|
+
export_hash[k][sub_k] = v.to_ordered_hash(sub_opts)
|
278
|
+
|
279
|
+
elsif v.kind_of?(Array)
|
280
|
+
export_hash[k][sub_k] = []
|
281
|
+
v.each do |member|
|
282
|
+
if member.respond_to?(:to_ordered_hash)
|
283
|
+
export_hash[k][sub_k] << member.to_ordered_hash(sub_opts)
|
284
|
+
else
|
285
|
+
export_hash[k][sub_k] << member
|
286
|
+
end
|
287
|
+
end
|
288
|
+
else
|
289
|
+
export_hash[k][sub_k] = v
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
elsif self.data[k].kind_of?(Array)
|
294
|
+
export_hash[k] = []
|
295
|
+
|
296
|
+
self.data[k].each do |member|
|
297
|
+
if member.respond_to?(:to_ordered_hash)
|
298
|
+
export_hash[k] << member.to_ordered_hash(sub_opts)
|
299
|
+
|
300
|
+
elsif member.kind_of?(Hash)
|
301
|
+
hsh = IIIF::OrderedHash.new
|
302
|
+
export_hash[k] << hsh
|
303
|
+
member.each do |sub_k,v|
|
304
|
+
|
305
|
+
if v.respond_to?(:to_ordered_hash)
|
306
|
+
hsh[sub_k] = v.to_ordered_hash(sub_opts)
|
307
|
+
|
308
|
+
elsif v.kind_of?(Array)
|
309
|
+
hsh[sub_k] = []
|
310
|
+
|
311
|
+
v.each do |sub_member|
|
312
|
+
if sub_member.respond_to?(:to_ordered_hash)
|
313
|
+
hsh[sub_k] << sub_member.to_ordered_hash(sub_opts)
|
314
|
+
else
|
315
|
+
hsh[sub_k] << sub_member
|
316
|
+
end
|
317
|
+
end
|
318
|
+
else
|
319
|
+
hsh[sub_k] = v
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
else
|
324
|
+
export_hash[k] << member
|
325
|
+
# there are no nested arrays, right?
|
326
|
+
end
|
327
|
+
end
|
328
|
+
else
|
329
|
+
export_hash[k] = self.data[k]
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
end
|
334
|
+
export_hash.remove_empties
|
335
|
+
export_hash.camelize_keys
|
336
|
+
export_hash
|
337
|
+
end
|
338
|
+
|
339
|
+
def self.from_ordered_hash(hsh, default_klass=IIIF::OrderedHash)
|
340
|
+
# Create a new object (new_object)
|
341
|
+
type = nil
|
342
|
+
if hsh.has_key?('type')
|
343
|
+
type = IIIF::V3::AbstractResource.get_descendant_class_by_jld_type(hsh['type'])
|
344
|
+
end
|
345
|
+
new_object = type.nil? ? default_klass.new : type.new
|
346
|
+
|
347
|
+
hsh.keys.each do |key|
|
348
|
+
new_key = key.underscore == key ? key : key.underscore
|
349
|
+
if new_key == 'service'
|
350
|
+
new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Service)
|
351
|
+
elsif new_key == 'body'
|
352
|
+
new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Resource)
|
353
|
+
elsif hsh[key].kind_of?(Hash)
|
354
|
+
new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key])
|
355
|
+
elsif hsh[key].kind_of?(Array)
|
356
|
+
new_object[new_key] = []
|
357
|
+
hsh[key].each do |member|
|
358
|
+
if new_key == 'service'
|
359
|
+
new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member, IIIF::V3::Presentation::Service)
|
360
|
+
elsif member.kind_of?(Hash)
|
361
|
+
new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member)
|
362
|
+
else
|
363
|
+
new_object[new_key] << member
|
364
|
+
# Again, no nested arrays, right?
|
365
|
+
end
|
366
|
+
end
|
367
|
+
else
|
368
|
+
new_object[new_key] = hsh[key]
|
369
|
+
end
|
370
|
+
end
|
371
|
+
new_object
|
372
|
+
end
|
373
|
+
|
374
|
+
protected
|
375
|
+
|
376
|
+
def self.get_descendant_class_by_jld_type(type)
|
377
|
+
IIIF::V3::AbstractResource.all_known_subclasses.find do |klass|
|
378
|
+
klass.const_defined?(:TYPE) && klass.const_get(:TYPE) == type
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def self.all_known_subclasses
|
383
|
+
@all_known_subclasses ||= IIIF::V3::AbstractResource.descendants.reject(&:singleton_class?)
|
384
|
+
end
|
385
|
+
|
386
|
+
def data=(hsh)
|
387
|
+
@data = hsh
|
388
|
+
end
|
389
|
+
|
390
|
+
def data
|
391
|
+
@data
|
392
|
+
end
|
393
|
+
|
394
|
+
def define_methods_for_any_type_keys
|
395
|
+
define_accessor_methods(*any_type_keys)
|
396
|
+
|
397
|
+
# override the getter defined by define_accessor_methods to avoid returning
|
398
|
+
# an array for empty values.
|
399
|
+
any_type_keys.each do |key|
|
400
|
+
define_singleton_method(key) do
|
401
|
+
self.send('[]', key)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def define_methods_for_array_only_keys
|
407
|
+
define_accessor_methods(*array_only_keys) do |key, val|
|
408
|
+
unless val.kind_of?(Array)
|
409
|
+
m = "#{key} must be an Array."
|
410
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def define_methods_for_hash_only_keys
|
416
|
+
define_accessor_methods(*hash_only_keys) do |key, val|
|
417
|
+
unless val.kind_of?(Hash)
|
418
|
+
m = "#{key} must be a Hash."
|
419
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def define_methods_for_string_only_keys
|
425
|
+
define_accessor_methods(*string_only_keys) do |key, val|
|
426
|
+
unless val.kind_of?(String)
|
427
|
+
m = "#{key} must be a String."
|
428
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def define_methods_for_int_only_keys
|
434
|
+
define_accessor_methods(*int_only_keys) do |key, val|
|
435
|
+
unless val.kind_of?(Integer) && val > 0
|
436
|
+
m = "#{key} must be a positive Integer."
|
437
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def define_methods_for_numeric_only_keys
|
443
|
+
define_accessor_methods(*numeric_only_keys) do |key, val|
|
444
|
+
unless val.kind_of?(Numeric) && val > 0
|
445
|
+
m = "#{key} must be a positive Integer or Float."
|
446
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def define_methods_for_uri_only_keys
|
452
|
+
define_accessor_methods(*uri_only_keys) { |key, val| validate_uri(val, key) }
|
453
|
+
end
|
454
|
+
|
455
|
+
def define_accessor_methods(*keys, &validation)
|
456
|
+
keys.each do |key|
|
457
|
+
# Setter
|
458
|
+
define_singleton_method("#{key}=") do |val|
|
459
|
+
validation.call(key, val) if block_given?
|
460
|
+
self.send('[]=', key, val)
|
461
|
+
end
|
462
|
+
if key.camelize(:lower) != key
|
463
|
+
define_singleton_method("#{key.camelize(:lower)}=") do |val|
|
464
|
+
validation.call(key, val) if block_given?
|
465
|
+
self.send('[]=', key, val)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
# Getter
|
469
|
+
define_singleton_method(key) do
|
470
|
+
self[key] ||= []
|
471
|
+
self[key]
|
472
|
+
end
|
473
|
+
if key.camelize(:lower) != key
|
474
|
+
define_singleton_method(key.camelize(:lower)) do
|
475
|
+
self.send('[]', key)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
private
|
482
|
+
def validate_uri(val, key)
|
483
|
+
unless val.kind_of?(String) && val =~ /\A#{URI::regexp}\z/
|
484
|
+
m = "#{key} value must be a String containing a URI for #{self.class}"
|
485
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class Annotation < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'Annotation'.freeze
|
7
|
+
|
8
|
+
def required_keys
|
9
|
+
super + %w{ id motivation target }
|
10
|
+
end
|
11
|
+
|
12
|
+
def prohibited_keys
|
13
|
+
super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES +
|
14
|
+
%w{ nav_date viewing_direction start_canvas content_annotations }
|
15
|
+
end
|
16
|
+
|
17
|
+
def any_type_keys
|
18
|
+
super + %w{ body target }
|
19
|
+
end
|
20
|
+
|
21
|
+
def uri_only_keys
|
22
|
+
super + %w{ id }
|
23
|
+
end
|
24
|
+
|
25
|
+
def string_only_keys
|
26
|
+
super + %w{ motivation time_mode }
|
27
|
+
end
|
28
|
+
|
29
|
+
def legal_time_mode_values
|
30
|
+
%w{ trim scale loop }.freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
def legal_viewing_hint_values
|
34
|
+
super + %w{ none }
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(hsh={})
|
38
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
39
|
+
hsh['motivation'] = 'painting' unless hsh.has_key? 'motivation'
|
40
|
+
super(hsh)
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate
|
44
|
+
super
|
45
|
+
|
46
|
+
if self.has_key?('body') && self['body'].kind_of?(IIIF::V3::Presentation::ImageResource)
|
47
|
+
img_res_class_str = "IIIF::V3::Presentation::ImageResource"
|
48
|
+
|
49
|
+
unless self.motivation == 'painting'
|
50
|
+
m = "#{self.class} motivation must be 'painting' when body is a kind of #{img_res_class_str}"
|
51
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
52
|
+
end
|
53
|
+
|
54
|
+
body_resource = self['body']
|
55
|
+
body_id = body_resource['id']
|
56
|
+
if body_id && body_id =~ /^https?:/
|
57
|
+
validate_uri(body_id, 'anno body ImageResource id') # can raise IllegalValueError
|
58
|
+
else
|
59
|
+
m = "when #{self.class} body is a kind of #{img_res_class_str}, ImageResource id must be an http(s) URI"
|
60
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if self.has_key?('time_mode')
|
65
|
+
unless self.legal_time_mode_values.include?(self['time_mode'])
|
66
|
+
m = "timeMode for #{self.class} must be one of #{self.legal_time_mode_values}."
|
67
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class AnnotationCollection < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'AnnotationCollection'.freeze
|
7
|
+
|
8
|
+
def required_keys
|
9
|
+
super + %w{ id }
|
10
|
+
end
|
11
|
+
|
12
|
+
def int_only_keys
|
13
|
+
super + %w{ total }
|
14
|
+
end
|
15
|
+
|
16
|
+
def array_only_keys
|
17
|
+
super + %w{ content }
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: paging properties
|
21
|
+
# Collection, AnnotationCollection, (formerly layer --> AnnotationPage???) allow; forbidden o.w.
|
22
|
+
# ---
|
23
|
+
# first, last, next, prev
|
24
|
+
# id is URI, but may have other info
|
25
|
+
# total, startIndex
|
26
|
+
# The value must be a non-negative integer.
|
27
|
+
#
|
28
|
+
# don't forget to validate
|
29
|
+
|
30
|
+
def initialize(hsh={})
|
31
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
32
|
+
super(hsh)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class AnnotationPage < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'AnnotationPage'.freeze
|
7
|
+
|
8
|
+
def required_keys
|
9
|
+
super + %w{ id }
|
10
|
+
end
|
11
|
+
|
12
|
+
def prohibited_keys
|
13
|
+
super + CONTENT_RESOURCE_PROPERTIES +
|
14
|
+
%w{ first last total nav_date viewing_direction start_canvas content_annotations }
|
15
|
+
end
|
16
|
+
|
17
|
+
def uri_only_keys
|
18
|
+
super + %w{ id }
|
19
|
+
end
|
20
|
+
|
21
|
+
def array_only_keys;
|
22
|
+
super + %w{ items }
|
23
|
+
end
|
24
|
+
|
25
|
+
def legal_viewing_hint_values
|
26
|
+
super + %w{ none }
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(hsh={})
|
30
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
31
|
+
super(hsh)
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate
|
35
|
+
super
|
36
|
+
|
37
|
+
unless self['id'] =~ /^https?:/
|
38
|
+
err_msg = "id must be an http(s) URI for #{self.class}"
|
39
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
40
|
+
end
|
41
|
+
|
42
|
+
items = self['items']
|
43
|
+
if items && items.any?
|
44
|
+
unless items.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Annotation) }
|
45
|
+
err_msg = 'All entries in the items list must be a IIIF::V3::Presentation::Annotation'
|
46
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|