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,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