osullivan 0.0.2

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +23 -0
  7. data/README.md +166 -0
  8. data/Rakefile +12 -0
  9. data/VERSION +1 -0
  10. data/lib/active_support/ordered_hash.rb +147 -0
  11. data/lib/iiif/hash_behaviours.rb +150 -0
  12. data/lib/iiif/presentation.rb +25 -0
  13. data/lib/iiif/presentation/abstract_resource.rb +75 -0
  14. data/lib/iiif/presentation/annotation.rb +25 -0
  15. data/lib/iiif/presentation/annotation_list.rb +28 -0
  16. data/lib/iiif/presentation/canvas.rb +45 -0
  17. data/lib/iiif/presentation/collection.rb +29 -0
  18. data/lib/iiif/presentation/image_resource.rb +115 -0
  19. data/lib/iiif/presentation/layer.rb +34 -0
  20. data/lib/iiif/presentation/manifest.rb +39 -0
  21. data/lib/iiif/presentation/range.rb +32 -0
  22. data/lib/iiif/presentation/resource.rb +21 -0
  23. data/lib/iiif/presentation/sequence.rb +35 -0
  24. data/lib/iiif/service.rb +418 -0
  25. data/osullivan.gemspec +27 -0
  26. data/spec/fixtures/manifests/complete_from_spec.json +171 -0
  27. data/spec/fixtures/manifests/minimal.json +40 -0
  28. data/spec/fixtures/manifests/service_only.json +11 -0
  29. data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +159 -0
  30. data/spec/integration/iiif/presentation/image_resource_spec.rb +123 -0
  31. data/spec/integration/iiif/service_spec.rb +211 -0
  32. data/spec/spec_helper.rb +104 -0
  33. data/spec/unit/active_support/ordered_hash_spec.rb +155 -0
  34. data/spec/unit/iiif/hash_behaviours_spec.rb +569 -0
  35. data/spec/unit/iiif/presentation/abstract_resource_spec.rb +133 -0
  36. data/spec/unit/iiif/presentation/annotation_list_spec.rb +7 -0
  37. data/spec/unit/iiif/presentation/annotation_spec.rb +7 -0
  38. data/spec/unit/iiif/presentation/canvas_spec.rb +40 -0
  39. data/spec/unit/iiif/presentation/collection_spec.rb +54 -0
  40. data/spec/unit/iiif/presentation/image_resource_spec.rb +13 -0
  41. data/spec/unit/iiif/presentation/layer_spec.rb +38 -0
  42. data/spec/unit/iiif/presentation/manifest_spec.rb +89 -0
  43. data/spec/unit/iiif/presentation/range_spec.rb +43 -0
  44. data/spec/unit/iiif/presentation/resource_spec.rb +16 -0
  45. data/spec/unit/iiif/presentation/sequence_spec.rb +110 -0
  46. data/spec/unit/iiif/presentation/shared_examples/abstract_resource_only_keys.rb +43 -0
  47. data/spec/unit/iiif/presentation/shared_examples/any_type_keys.rb +33 -0
  48. data/spec/unit/iiif/presentation/shared_examples/array_only_keys.rb +44 -0
  49. data/spec/unit/iiif/presentation/shared_examples/int_only_keys.rb +49 -0
  50. data/spec/unit/iiif/presentation/shared_examples/string_only_keys.rb +29 -0
  51. data/spec/unit/iiif/service_spec.rb +10 -0
  52. metadata +246 -0
@@ -0,0 +1,21 @@
1
+ require File.join(File.dirname(__FILE__), 'abstract_resource')
2
+
3
+ module IIIF
4
+ module Presentation
5
+ class Resource < AbstractResource
6
+
7
+ def required_keys
8
+ %w{ @id }
9
+ end
10
+
11
+ def string_only_keys
12
+ super + %w{ format }
13
+ end
14
+
15
+ def initialize(hsh={})
16
+ super(hsh)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ require File.join(File.dirname(__FILE__), 'abstract_resource')
2
+
3
+ module IIIF
4
+ module Presentation
5
+ class Sequence < AbstractResource
6
+
7
+ TYPE = 'sc:Sequence'
8
+
9
+ def array_only_keys
10
+ super + %w{ canvases }
11
+ end
12
+
13
+ def string_only_keys
14
+ super + %w{ start_canvas viewing_direction }
15
+ end
16
+
17
+ def legal_viewing_hint_values
18
+ %w{ individuals paged continuous }
19
+ end
20
+
21
+ def initialize(hsh={})
22
+ hsh['@type'] = TYPE unless hsh.has_key? '@type'
23
+ super(hsh)
24
+ end
25
+
26
+ def validate
27
+ # * Must be at least one canvas
28
+ # * All members of canvases must be a kind of Canvas
29
+ super
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,418 @@
1
+ require File.join(File.dirname(__FILE__), 'hash_behaviours')
2
+ require 'active_support/ordered_hash'
3
+ require 'active_support/inflector'
4
+ require 'json'
5
+
6
+ module IIIF
7
+ class Service
8
+ include IIIF::HashBehaviours
9
+
10
+ # Anything goes! SHOULD have @id and profile, MAY have label
11
+ # Consider subclassing this for typical services...
12
+ def required_keys; %w{ }; end
13
+ def any_type_keys; %w{ }; end
14
+ def string_only_keys; %w{ }; end
15
+ def array_only_keys; %w{ }; end
16
+ def abstract_resource_only_keys; %w{ }; end
17
+ def hash_only_keys; %w{ }; end
18
+ def int_only_keys; %w{ }; end
19
+
20
+ def initialize(hsh={})
21
+ @data = ActiveSupport::OrderedHash[hsh]
22
+ self.define_methods_for_any_type_keys
23
+ self.define_methods_for_array_only_keys
24
+ self.define_methods_for_string_only_keys
25
+ self.define_methods_for_int_only_keys
26
+ self.define_methods_for_abstract_resource_only_keys
27
+ self.snakeize_keys
28
+ end
29
+
30
+ # Static methods / alternative constructors
31
+ class << self
32
+ # Parse from a file path, string, or existing hash
33
+ def parse(s)
34
+ ordered_hash = nil
35
+ if s.kind_of?(String) && File.exists?(s)
36
+ ordered_hash = ActiveSupport::OrderedHash[JSON.parse(IO.read(s))]
37
+ elsif s.kind_of?(String) && !File.exists?(s)
38
+ ordered_hash = ActiveSupport::OrderedHash[JSON.parse(s)]
39
+ elsif s.kind_of?(Hash)
40
+ ordered_hash = ActiveSupport::OrderedHash[s]
41
+ else
42
+ m = '#parse takes a path to a file, a JSON String, or a Hash, '
43
+ m += "argument was a #{s.class}."
44
+ if s.kind_of?(String)
45
+ m+= "If you were trying to point to a file, does it exist?"
46
+ end
47
+ raise ArgumentError, m
48
+ end
49
+ return IIIF::Service.from_ordered_hash(ordered_hash)
50
+ end
51
+ end
52
+
53
+ def validate
54
+ # TODO:
55
+ # * check for required keys
56
+ # * type check Array-only values
57
+ # * type check String-only values
58
+ # * type check Integer-only values
59
+ # * type check AbstractResource-only values
60
+ self.required_keys.each do |k|
61
+ unless self.has_key?(k)
62
+ m = "A(n) #{k} is required for each #{self.class}"
63
+ raise IIIF::Presentation::MissingRequiredKeyError, m
64
+ end
65
+ end
66
+ # Viewing Direction values
67
+ if self.has_key?('viewing_direction')
68
+ unless self.legal_viewing_direction_values.include?(self['viewing_direction'])
69
+ m = "viewingDirection must be one of #{legal_viewing_direction_values}"
70
+ raise IIIF::Presentation::IllegalValueError, m
71
+ end
72
+ end
73
+ # Viewing Hint values
74
+ if self.has_key?('viewing_hint')
75
+ unless self.legal_viewing_hint_values.include?(self['viewing_hint'])
76
+ m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}."
77
+ raise IIIF::Presentation::IllegalValueError, m
78
+ end
79
+ end
80
+ # Metadata is all hashes
81
+ if self.has_key?('metadata')
82
+ unless self['metadata'].all? { |entry| entry.kind_of?(Hash) }
83
+ m = 'All entries in the metadata list must be a type of Hash'
84
+ raise IIIF::Presentation::IllegalValueError, m
85
+ end
86
+ end
87
+ end
88
+
89
+ # Options
90
+ # * pretty: (true|false). Should the JSON be pretty-printed? (default: false)
91
+ # * All options available in #to_ordered_hash
92
+ def to_json(opts={})
93
+ hsh = self.to_ordered_hash(opts)
94
+ if opts.fetch(:pretty, false)
95
+ JSON.pretty_generate(hsh)
96
+ else
97
+ hsh.to_json
98
+ end
99
+ end
100
+
101
+ # Options:
102
+ # * force: (true|false). Skips validations.
103
+ # * sort_json_ld_keys: (true|false). Brings all properties starting with
104
+ # '@'. Default: true. to the top of the document and sorts them.
105
+ def to_ordered_hash(opts={})
106
+ force = opts.fetch(:force, false)
107
+ sort_json_ld_keys = opts.fetch(:sort_json_ld_keys, true)
108
+
109
+ unless force
110
+ self.validate
111
+ end
112
+
113
+ export_hash = ActiveSupport::OrderedHash.new
114
+
115
+ if sort_json_ld_keys
116
+ self.keys.select { |k| k.start_with?('@') }.sort!.each do |k|
117
+ export_hash[k] = self.data[k]
118
+ end
119
+ end
120
+
121
+ sub_opts = {
122
+ include_context: false,
123
+ sort_json_ld_keys: sort_json_ld_keys,
124
+ force: force
125
+ }
126
+ self.keys.each do |k|
127
+ unless sort_json_ld_keys && k.start_with?('@')
128
+ if self.data[k].respond_to?(:to_ordered_hash) #.respond_to?(:to_ordered_hash)
129
+ export_hash[k] = self.data[k].to_ordered_hash(sub_opts)
130
+
131
+ elsif self.data[k].kind_of?(Hash)
132
+ export_hash[k] = ActiveSupport::OrderedHash.new
133
+ self.data[k].each do |sub_k, v|
134
+
135
+ if v.respond_to?(:to_ordered_hash)
136
+ export_hash[k][sub_k] = v.to_ordered_hash(sub_opts)
137
+
138
+ elsif v.kind_of?(Array)
139
+ export_hash[k][sub_k] = []
140
+ v.each do |member|
141
+ if member.respond_to?(:to_ordered_hash)
142
+ export_hash[k][sub_k] << member.to_ordered_hash(sub_opts)
143
+ else
144
+ export_hash[k][sub_k] << member
145
+ end
146
+ end
147
+ else
148
+ export_hash[k][sub_k] = v
149
+ end
150
+ end
151
+
152
+ elsif self.data[k].kind_of?(Array)
153
+ export_hash[k] = []
154
+
155
+ self.data[k].each do |member|
156
+ if member.respond_to?(:to_ordered_hash)
157
+ export_hash[k] << member.to_ordered_hash(sub_opts)
158
+
159
+ elsif member.kind_of?(Hash)
160
+ hsh = ActiveSupport::OrderedHash.new
161
+ export_hash[k] << hsh
162
+ member.each do |sub_k,v|
163
+
164
+ if v.respond_to?(:to_ordered_hash)
165
+ hsh[sub_k] = v.to_ordered_hash(sub_opts)
166
+
167
+ elsif v.kind_of?(Array)
168
+ hsh[sub_k] = []
169
+
170
+ v.each do |sub_member|
171
+ if sub_member.respond_to?(:to_ordered_hash)
172
+ hsh[sub_k] << sub_member.to_ordered_hash(sub_opts)
173
+ else
174
+ hsh[sub_k] << sub_member
175
+ end
176
+ end
177
+ else
178
+ hsh[sub_k] = v
179
+ end
180
+ end
181
+
182
+ else
183
+ export_hash[k] << member
184
+ # there are no nested arrays, right?
185
+ end
186
+ end
187
+ else
188
+ export_hash[k] = self.data[k]
189
+ end
190
+
191
+ end
192
+ end
193
+ export_hash.remove_empties
194
+ export_hash.camelize_keys
195
+ export_hash
196
+ end
197
+
198
+ def self.from_ordered_hash(hsh, default_klass=ActiveSupport::OrderedHash)
199
+ # Create a new object (new_object)
200
+ type = nil
201
+ if hsh.has_key?('@type')
202
+ type = IIIF::Service.get_descendant_class_by_jld_type(hsh['@type'])
203
+ end
204
+ new_object = type.nil? ? default_klass.new : type.new
205
+
206
+ hsh.keys.each do |key|
207
+ new_key = key.underscore == key ? key : key.underscore
208
+ if new_key == 'service'
209
+ new_object[new_key] = IIIF::Service.from_ordered_hash(hsh[key], IIIF::Service)
210
+ elsif new_key == 'resource'
211
+ new_object[new_key] = IIIF::Service.from_ordered_hash(hsh[key], IIIF::Presentation::Resource)
212
+ elsif hsh[key].kind_of?(Hash)
213
+ new_object[new_key] = IIIF::Service.from_ordered_hash(hsh[key])
214
+ elsif hsh[key].kind_of?(Array)
215
+ new_object[new_key] = []
216
+ hsh[key].each do |member|
217
+ if new_key == 'service'
218
+ new_object[new_key] << IIIF::Service.from_ordered_hash(member, IIIF::Service)
219
+ elsif member.kind_of?(Hash)
220
+ new_object[new_key] << IIIF::Service.from_ordered_hash(member)
221
+ else
222
+ new_object[new_key] << member
223
+ # Again, no nested arrays, right?
224
+ end
225
+ end
226
+ else
227
+ new_object[new_key] = hsh[key]
228
+ end
229
+ end
230
+ new_object
231
+ end
232
+
233
+ protected
234
+
235
+ def self.get_descendant_class_by_jld_type(type)
236
+ IIIF::Service.all_service_subclasses.select { |klass|
237
+ klass.const_defined?(:TYPE) && klass.const_get(:TYPE) == type
238
+ }.first
239
+ end
240
+
241
+ # All known subclasses of service.
242
+ def self.all_service_subclasses
243
+ klass = IIIF::Service
244
+ # !c.name.nil? filters out classes that rspec creates for some reason;
245
+ # this condition isn't necessary when using the API, afaik
246
+ descendants = ObjectSpace.each_object(Class).select { |c| c < klass && !c.name.nil? }
247
+ end
248
+
249
+ def data=(hsh)
250
+ @data = hsh
251
+ end
252
+
253
+ def data
254
+ @data
255
+ end
256
+
257
+ def define_methods_for_any_type_keys
258
+ any_type_keys.each do |key|
259
+ # Setters
260
+ define_singleton_method("#{key}=") do |arg|
261
+ self.send('[]=', key, arg)
262
+ end
263
+ if key.camelize(:lower) != key
264
+ define_singleton_method("#{key.camelize(:lower)}=") do |arg|
265
+ self.send('[]=', key, arg)
266
+ end
267
+ end
268
+ # Getters
269
+ define_singleton_method(key) do
270
+ self.send('[]', key)
271
+ end
272
+ if key.camelize(:lower) != key
273
+ define_singleton_method(key.camelize(:lower)) do
274
+ self.send('[]', key)
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ def define_methods_for_array_only_keys
281
+ array_only_keys.each do |key|
282
+ # Setters
283
+ define_singleton_method("#{key}=") do |arg|
284
+ unless arg.kind_of?(Array)
285
+ m = "#{key} must be an Array."
286
+ raise IIIF::Presentation::IllegalValueError, m
287
+ end
288
+ self.send('[]=', key, arg)
289
+ end
290
+ if key.camelize(:lower) != key
291
+ define_singleton_method("#{key.camelize(:lower)}=") do |arg|
292
+ unless arg.kind_of?(Array)
293
+ m = "#{key} must be an Array."
294
+ raise IIIF::Presentation::IllegalValueError, m
295
+ end
296
+ self.send('[]=', key, arg)
297
+ end
298
+ end
299
+ # Getters
300
+ define_singleton_method(key) do
301
+ self[key] ||= []
302
+ self[key]
303
+ end
304
+ if key.camelize(:lower) != key
305
+ define_singleton_method(key.camelize(:lower)) do
306
+ self.send('[]', key)
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ def define_methods_for_abstract_resource_only_keys
313
+ # keys in this case is an array of hashes with { key: 'k', type: Class }
314
+ abstract_resource_only_keys.each do |hsh|
315
+ key = hsh[:key]
316
+ type = hsh[:type]
317
+ # Setters
318
+ define_singleton_method("#{key}=") do |arg|
319
+ unless arg.kind_of?(type)
320
+ m = "#{key} must be an #{type}."
321
+ raise IIIF::Presentation::IllegalValueError, m
322
+ end
323
+ self.send('[]=', key, arg)
324
+ end
325
+ if key.camelize(:lower) != key
326
+ define_singleton_method("#{key.camelize(:lower)}=") do |arg|
327
+ unless arg.kind_of?(type)
328
+ m = "#{key} must be an #{type}."
329
+ raise IIIF::Presentation::IllegalValueError, m
330
+ end
331
+ self.send('[]=', key, arg)
332
+ end
333
+ end
334
+ # Getters
335
+ define_singleton_method(key) do
336
+ self[key] ||= []
337
+ self[key]
338
+ end
339
+ if key.camelize(:lower) != key
340
+ define_singleton_method(key.camelize(:lower)) do
341
+ self.send('[]', key)
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+
348
+ def define_methods_for_string_only_keys
349
+ string_only_keys.each do |key|
350
+ # Setter
351
+ define_singleton_method("#{key}=") do |arg|
352
+ unless arg.kind_of?(String)
353
+ m = "#{key} must be an String."
354
+ raise IIIF::Presentation::IllegalValueError, m
355
+ end
356
+ self.send('[]=', key, arg)
357
+ end
358
+ if key.camelize(:lower) != key
359
+ define_singleton_method("#{key.camelize(:lower)}=") do |arg|
360
+ unless arg.kind_of?(String)
361
+ m = "#{key} must be an String."
362
+ raise IIIF::Presentation::IllegalValueError, m
363
+ end
364
+ self.send('[]=', key, arg)
365
+ end
366
+ end
367
+ # Getter
368
+ define_singleton_method(key) do
369
+ self[key] ||= []
370
+ self[key]
371
+ end
372
+ if key.camelize(:lower) != key
373
+ define_singleton_method(key.camelize(:lower)) do
374
+ self.send('[]', key)
375
+ end
376
+ end
377
+ end
378
+ end
379
+
380
+ def define_methods_for_int_only_keys
381
+ int_only_keys.each do |key|
382
+ # Setter
383
+ define_singleton_method("#{key}=") do |arg|
384
+ unless arg.kind_of?(Integer) && arg > 0
385
+ m = "#{key} must be a positive Integer."
386
+ raise IIIF::Presentation::IllegalValueError, m
387
+ end
388
+ self.send('[]=', key, arg)
389
+ end
390
+ if key.camelize(:lower) != key
391
+ define_singleton_method("#{key.camelize(:lower)}=") do |arg|
392
+ unless arg.kind_of?(Integer) && arg > 0
393
+ m = "#{key} must be a positive Integer."
394
+ raise IIIF::Presentation::IllegalValueError, m
395
+ end
396
+ self.send('[]=', key, arg)
397
+ end
398
+ end
399
+ # Getter
400
+ define_singleton_method(key) do
401
+ self[key] ||= []
402
+ self[key]
403
+ end
404
+ if key.camelize(:lower) != key
405
+ define_singleton_method(key.camelize(:lower)) do
406
+ self.send('[]', key)
407
+ end
408
+ end
409
+ end
410
+ end
411
+
412
+ end
413
+ end
414
+
415
+
416
+
417
+
418
+