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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/.gitignore +1 -0
  4. data/Gemfile +2 -0
  5. data/README.md +22 -2
  6. data/VERSION +1 -1
  7. data/iiif-presentation.gemspec +1 -1
  8. data/lib/iiif/hash_behaviours.rb +1 -1
  9. data/lib/iiif/presentation/canvas.rb +4 -0
  10. data/lib/iiif/presentation/service.rb +12 -0
  11. data/lib/iiif/presentation.rb +5 -4
  12. data/lib/iiif/service.rb +40 -105
  13. data/lib/iiif/v3/abstract_resource.rb +491 -0
  14. data/lib/iiif/v3/presentation/annotation.rb +74 -0
  15. data/lib/iiif/v3/presentation/annotation_collection.rb +38 -0
  16. data/lib/iiif/v3/presentation/annotation_page.rb +53 -0
  17. data/lib/iiif/v3/presentation/canvas.rb +82 -0
  18. data/lib/iiif/v3/presentation/choice.rb +51 -0
  19. data/lib/iiif/v3/presentation/collection.rb +52 -0
  20. data/lib/iiif/v3/presentation/image_resource.rb +110 -0
  21. data/lib/iiif/v3/presentation/manifest.rb +82 -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 +36 -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/range_spec.rb +54 -0
  50. data/spec/unit/iiif/v3/presentation/resource_spec.rb +174 -0
  51. data/spec/unit/iiif/v3/presentation/sequence_spec.rb +222 -0
  52. data/spec/unit/iiif/v3/presentation/service_spec.rb +220 -0
  53. data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +41 -0
  54. data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +31 -0
  55. data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +40 -0
  56. data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +40 -0
  57. data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +45 -0
  58. data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +45 -0
  59. data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +26 -0
  60. data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +31 -0
  61. metadata +82 -11
  62. 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