osullivan 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+