From 377aa2083c9bd12e4894d3c517cc02a6fe608b4b Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 26 Jun 2017 17:13:02 -0700 Subject: [PATCH 01/91] adding v3 spec/fixtures --- .../v3/manifests/complete_from_spec.json | 181 ++++++++++++++++++ spec/fixtures/v3/manifests/minimal.json | 44 +++++ spec/fixtures/v3/manifests/service_only.json | 10 + 3 files changed, 235 insertions(+) create mode 100644 spec/fixtures/v3/manifests/complete_from_spec.json create mode 100644 spec/fixtures/v3/manifests/minimal.json create mode 100644 spec/fixtures/v3/manifests/service_only.json diff --git a/spec/fixtures/v3/manifests/complete_from_spec.json b/spec/fixtures/v3/manifests/complete_from_spec.json new file mode 100644 index 0000000..74a5fff --- /dev/null +++ b/spec/fixtures/v3/manifests/complete_from_spec.json @@ -0,0 +1,181 @@ +{ + "id": "http://www.example.org/iiif/book1/manifest", + "type": "Manifest", + "label": "Book 1", + "metadata": [ + { + "label": "Author", + "value": "Anne Author" + }, + { + "label": "Published", + "value": [ + { + "@value": "Paris, circa 1400", + "@language": "en" + }, + { + "@value": "Paris, environ 14eme siecle", + "@language": "fr" + } + ] + } + ], + "description": "A longer description of this example book. It should give some real information.", + "rights": "http://www.example.org/license.html", + "attribution": "Provided by Example Organization", + "service": { + "@context": "http://example.org/ns/jsonld/context.json", + "id": "http://example.org/service/example", + "profile": "http://example.org/docs/example-service.html" + }, + "seeAlso": { + "id": "http://www.example.org/library/catalog/book1.marc", + "format": "application/marc" + }, + "within": "http://www.example.org/collections/books/", + "sequences": [ + { + "id": "http://www.example.org/iiif/book1/sequence/normal", + "type": "Sequence", + "label": "Current Page Order", + "viewingDirection": "left-to-right", + "viewingHint": "paged", + "canvases": [ + { + "id": "http://www.example.org/iiif/book1/canvas/p1", + "type": "Canvas", + "label": "p. 1", + "height": 1000, + "width": 750, + "content": [ + { + "id": "http://www.example.org/iiif/book1/page/p1", + "type": "AnnotationPage", + "items": [ + { + "type": "Annotation", + "motivation": "painting", + "target": "http://www.example.org/iiif/book1/canvas/p1", + "resource": { + "id": "http://www.example.org/iiif/book1/res/page1.jpg", + "type": "dctypes:Image", + "format": "image/jpeg", + "height": 2000, + "width": 1500, + "service": { + "id": "http://www.example.org/images/book1-page1", + "profile": "http://iiif.io/api/image/2/level1.json" + } + } + } + ] + }, + { + "id": "http://www.example.org/iiif/book1/page/p666", + "type": "AnnotationPage" + } + ] + }, + { + "id": "http://www.example.org/iiif/book1/canvas/p2", + "type": "Canvas", + "label": "p. 2", + "height": 1000, + "width": 750, + "content": [ + { + "id": "http://www.example.org/iiif/book1/page/p1", + "type": "AnnotationPage", + "items": [ + { + "type": "Annotation", + "motivation": "painting", + "resource": { + "id": "http://www.example.org/images/book1-page2/full/1500,2000/0/default.jpg", + "type": "dctypes:Image", + "format": "image/jpeg", + "height": 2000, + "width": 1500, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "id": "http://www.example.org/images/book1-page2", + "profile": "http://iiif.io/api/image/2/level1.json", + "height": 8000, + "width": 6000, + "tiles": [ + { + "width": 512, + "scaleFactors": [ + 1, + 2, + 4, + 8, + 16 + ] + } + ] + } + }, + "target": "http://www.example.org/iiif/book1/canvas/p2" + } + ] + }, + { + "id": "http://www.example.org/iiif/book1/list/p2", + "type": "AnnotationPage" + } + ] + }, + { + "id": "http://www.example.org/iiif/book1/canvas/p3", + "type": "Canvas", + "label": "p. 3", + "height": 1000, + "width": 750, + "content": [ + { + "id": "http://www.example.org/iiif/book1/page/p1", + "type": "AnnotationPage", + "items": [ + { + "type": "Annotation", + "motivation": "painting", + "target": "http://www.example.org/iiif/book1/canvas/p3", + "resource": { + "id": "http://www.example.org/iiif/book1/res/page3.jpg", + "type": "dctypes:Image", + "format": "image/jpeg", + "height": 2000, + "width": 1500, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "id": "http://www.example.org/images/book1-page3", + "profile": "http://iiif.io/api/image/2/level1.json" + } + } + } + ] + }, + { + "id": "http://www.example.org/iiif/book1/page/p333", + "type": "AnnotationPage" + } + ] + } + ] + } + ], + "structures": [ + { + "id": "http://www.example.org/iiif/book1/range/r1", + "type": "Range", + "label": "Introduction", + "canvases": [ + "http://www.example.org/iiif/book1/canvas/p1", + "http://www.example.org/iiif/book1/canvas/p2", + "http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300" + ] + } + ] +} diff --git a/spec/fixtures/v3/manifests/minimal.json b/spec/fixtures/v3/manifests/minimal.json new file mode 100644 index 0000000..48833d1 --- /dev/null +++ b/spec/fixtures/v3/manifests/minimal.json @@ -0,0 +1,44 @@ +{ + "type": "Manifest", + "id": "http://www.example.org/iiif/book1/manifest", + "label": "Book 1", + "sequences": [ + { + "id": "http://www.example.org/iiif/book1/sequence/normal", + "label": "Current Page Order", + "canvases": [ + { + "id": "http://www.example.org/iiif/book1/canvas/p1", + "type": "Canvas", + "label": "p. 1", + "height": 1000, + "width": 750, + "content": [ + { + "id": "http://www.example.org/iiif/book1/page/p1", + "type": "AnnotationPage", + "items": [ + { + "type": "Annotation", + "motivation": "painting", + "target": "http://www.example.org/iiif/book1/canvas/p1", + "resource": { + "id": "http://www.example.org/iiif/book1/res/page1.jpg", + "type": "dctypes:Image", + "format": "image/jpeg", + "height": 2000, + "width": 1500, + "service": { + "id": "http://www.example.org/images/book1-page1", + "profile": "http://iiif.io/api/image/2/level1.json" + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/spec/fixtures/v3/manifests/service_only.json b/spec/fixtures/v3/manifests/service_only.json new file mode 100644 index 0000000..68342a9 --- /dev/null +++ b/spec/fixtures/v3/manifests/service_only.json @@ -0,0 +1,10 @@ +{ + "type": "Manifest", + "id": "http://www.example.org/iiif/book1/manifest", + "label": "Book 1", + "service": { + "context": "http://example.org/ns/jsonld/context.json", + "id": "http://example.org/service/example", + "profile": "http://example.org/docs/example-service.html" + } +} From dc6bb8ace36e8e604a038c439753675faf12000e Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 26 Jun 2017 18:05:04 -0700 Subject: [PATCH 02/91] first pass at iiif3: id, type, prefixes of types, rights, leave off context; all tests pass except integration/v3/image_resource --- lib/iiif/v3/hash_behaviours.rb | 150 +++++ lib/iiif/v3/ordered_hash.rb | 148 +++++ lib/iiif/v3/presentation.rb | 30 + lib/iiif/v3/presentation/abstract_resource.rb | 80 +++ lib/iiif/v3/presentation/annotation.rb | 26 + lib/iiif/v3/presentation/annotation_list.rb | 30 + lib/iiif/v3/presentation/canvas.rb | 46 ++ lib/iiif/v3/presentation/collection.rb | 30 + lib/iiif/v3/presentation/image_resource.rb | 116 ++++ lib/iiif/v3/presentation/layer.rb | 35 ++ lib/iiif/v3/presentation/manifest.rb | 39 ++ lib/iiif/v3/presentation/range.rb | 33 + lib/iiif/v3/presentation/resource.rb | 22 + lib/iiif/v3/presentation/sequence.rb | 35 ++ lib/iiif/v3/service.rb | 412 +++++++++++++ .../v3/presentation/image_resource_spec.rb | 120 ++++ spec/integration/iiif/v3/service_spec.rb | 210 +++++++ spec/spec_helper.rb | 4 + spec/unit/iiif/v3/hash_behaviours_spec.rb | 568 ++++++++++++++++++ spec/unit/iiif/v3/ordered_hash_spec.rb | 154 +++++ .../v3/presentation/abstract_resource_spec.rb | 132 ++++ .../v3/presentation/annotation_list_spec.rb | 7 + .../iiif/v3/presentation/annotation_spec.rb | 7 + spec/unit/iiif/v3/presentation/canvas_spec.rb | 44 ++ .../iiif/v3/presentation/collection_spec.rb | 51 ++ .../v3/presentation/image_resource_spec.rb | 13 + spec/unit/iiif/v3/presentation/layer_spec.rb | 36 ++ .../iiif/v3/presentation/manifest_spec.rb | 87 +++ spec/unit/iiif/v3/presentation/range_spec.rb | 41 ++ .../iiif/v3/presentation/resource_spec.rb | 15 + .../iiif/v3/presentation/sequence_spec.rb | 108 ++++ .../abstract_resource_only_keys.rb | 43 ++ .../shared_examples/any_type_keys.rb | 33 + .../shared_examples/array_only_keys.rb | 42 ++ .../shared_examples/int_only_keys.rb | 47 ++ .../shared_examples/string_only_keys.rb | 28 + spec/unit/iiif/v3/service_spec.rb | 28 + 37 files changed, 3050 insertions(+) create mode 100644 lib/iiif/v3/hash_behaviours.rb create mode 100644 lib/iiif/v3/ordered_hash.rb create mode 100644 lib/iiif/v3/presentation.rb create mode 100644 lib/iiif/v3/presentation/abstract_resource.rb create mode 100644 lib/iiif/v3/presentation/annotation.rb create mode 100644 lib/iiif/v3/presentation/annotation_list.rb create mode 100644 lib/iiif/v3/presentation/canvas.rb create mode 100644 lib/iiif/v3/presentation/collection.rb create mode 100644 lib/iiif/v3/presentation/image_resource.rb create mode 100644 lib/iiif/v3/presentation/layer.rb create mode 100644 lib/iiif/v3/presentation/manifest.rb create mode 100644 lib/iiif/v3/presentation/range.rb create mode 100644 lib/iiif/v3/presentation/resource.rb create mode 100644 lib/iiif/v3/presentation/sequence.rb create mode 100644 lib/iiif/v3/service.rb create mode 100644 spec/integration/iiif/v3/presentation/image_resource_spec.rb create mode 100644 spec/integration/iiif/v3/service_spec.rb create mode 100644 spec/unit/iiif/v3/hash_behaviours_spec.rb create mode 100644 spec/unit/iiif/v3/ordered_hash_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/abstract_resource_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/annotation_list_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/annotation_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/canvas_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/collection_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/image_resource_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/layer_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/manifest_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/range_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/resource_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/sequence_spec.rb create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb create mode 100644 spec/unit/iiif/v3/service_spec.rb diff --git a/lib/iiif/v3/hash_behaviours.rb b/lib/iiif/v3/hash_behaviours.rb new file mode 100644 index 0000000..f783842 --- /dev/null +++ b/lib/iiif/v3/hash_behaviours.rb @@ -0,0 +1,150 @@ +require 'forwardable' + +module IIIF + module V3 + module HashBehaviours + extend Forwardable + + # TODO: + # * reject + # * replace + + def_delegators :@data, :[], :[]=, :camelize_keys, :delete, :empty?, + :fetch, :has_key?, :has_value?, :include?, :insert, :insert_after, + :insert_before, :key, :key?, :keys, :length, :member?, :shift, :size, + :snakeize_keys, :store, :unshift, :value?, :values + + + ### + # Methods that take a block and should return an instance (self or a new' + # instance) have been overridden to do so, rather than an' + # IIIF::V3::OrderedHash based on the internal hash + + SIMPLE_SELF_RETURNERS = %w[delete_if each each_key each_value keep_if] + + SIMPLE_SELF_RETURNERS.each do |method_name| + define_method(method_name) do |*arg, &block| + unless block.nil? # block_given? doesn't seem to work in this context + @data.send(method_name, *arg, &block) + return self + else + @data.send(method_name) + end + end + end + + # Clear is the only method that returns self but doesn't accept a block + def clear + @data.clear + return self + end + + # Returns a new instance of this class containing the contents of' + # another_obj. The argument can be any object that implements two + # methods: + # + # obj.each { |k,v| block } + # obj.has_key? + # + # If no block is specified, the value for entries with duplicate keys' + # will be those of the argument, but at the index of the original; all' + # other entries will be appended to the end. + # + # If a block is specified the value for each duplicate key is determined' + # by calling the block with the key, its value in hsh and its value in' + # another_obj. + def merge another_obj + new_instance = self.class.new + # self.clone # Would this be better? What happens to other attributes of the class? + if block_given? + self.each do |k,v| + if another_obj.has_key? k + new_instance[k] = yield(k, self[k], another_obj[k]) + else + new_instance[k] = v + end + end + else + self.each { |k,v| new_instance[k] = v } + another_obj.each { |k,v| new_instance[k] = v } + end + new_instance + end + + # Adds the entries from another obj to this one. The argument can be any + # object that implements two methods: + # + # obj.each { |k,v| block } + # obj.has_key? + # + # If no block is specified, the value for entries with duplicate keys' + # will be those of the argument, but at the index of the original; all' + # other entries will be appended to the end. + # + # If a block is specified the value for each duplicate key is determined' + # by calling the block with the key, its value in hsh and its value in' + # another_obj. + def merge! another_obj + if block_given? + self.each do |k,v| + if another_obj.has_key? k + self[k] = yield(k, self[k], another_obj[k]) + else + self[k] = v + end + end + else + self.each { |k,v| self[k] = v } + another_obj.each { |k,v| self[k] = v } + end + self + end + alias update merge! + + # Deletes entries for which the supplied block evaluates to true. + # Equivalent to #delete_if, but returns nil if there were no changes + def reject! + if block_given? + return_nil = true + @data.each do |k, v| + if yield(k, v) + @data.delete(k) + return_nil = false + end + end + return return_nil ? nil : self + else + return self.data.reject! + end + end + + # Returns a new instance consisting of entries for which the block returns + # true. Not that an enumerator is not available for the OrderedHash' + # implementation + def select + new_instance = self.class.new + if block_given? + @data.each { |k,v| new_instance.data[k] = v if yield(k,v) } + end + return new_instance + end + + # Deletes entries for which the supplied block evaluates to false. + # Equivalent to Hash#keep_if, but returns nil if no changes were made. + def select! + if block_given? + return_nil = true + @data.each do |k,v| + unless yield(k,v) + @data.delete(k) + return_nil = false + end + end + return nil if return_nil + end + self + end + + end + end +end diff --git a/lib/iiif/v3/ordered_hash.rb b/lib/iiif/v3/ordered_hash.rb new file mode 100644 index 0000000..e0f25e2 --- /dev/null +++ b/lib/iiif/v3/ordered_hash.rb @@ -0,0 +1,148 @@ +require 'active_support/inflector' + +module IIIF + module V3 + class OrderedHash < ::Hash + + # Insert a new key and value at the suppplied index. + # + # Note that this is slightly different from Array#insert in that new + # entries must be added one at a time, i.e. insert(n, k, v, k, v...) is + # not supported. + # + # @param [Integer] index + # @param [Object] key + # @param [Object] value + def insert(index, key, value) + tmp = IIIF::V3::OrderedHash.new + index = self.length + 1 + index if index < 0 + if index < 0 + m = "Index #{index} is too small for current length (#{length})" + raise IndexError, m + end + if index > 0 + i=0 + self.each do |k,v| + tmp[k] = v + self.delete(k) + i+=1 + break if i == index + end + end + tmp[key] = value + tmp.merge!(self) # copy the remaining to tmp + self.clear # start over... + self.merge!(tmp) # now put them all back + self + end + + # Insert a key and value before an existing key or the first entry for' + # which the supplied block evaluates to true. The block takes precendence + # over the supplied key. + # Options:' + # * :existing_key (default: nil). If nil or not supplied then a block is required. + # * :new_key (required) + # * :value (required) + # @raise KeyError if the supplied existing key is not found, the new + # key exists, or the block never evaluates to true. + def insert_before(hsh, &block) + existing_key = hsh.fetch(:existing_key, nil) + new_key = hsh[:new_key] + value = hsh[:value] + if block_given? + self.insert_here(0, new_key, value, &block) + else + self.insert_here(0, new_key, value, existing_key) + end + end + + # Insert a key and value after an existing key or the first entry for' + # which the supplied block evaluates to true. The block takes precendence + # over the supplied key. + # Options:' + # * :existing_key (default: nil). If nil or not supplied then a block is required. + # * :new_key (required) + # * :value (required) + # @raise KeyError if the supplied existing key is not found, the new + # key exists, or the block never evaluates to true. + def insert_after(hsh, &block) + existing_key = hsh.fetch(:existing_key, nil) + new_key = hsh[:new_key] + value = hsh[:value] + if block_given? + self.insert_here(1, new_key, value, &block) + else + self.insert_here(1, new_key, value, existing_key) + end + end + + # Delete any keys that are empty arrays + def remove_empties + self.keys.each do |key| + if (self[key].kind_of?(Array) && self[key].empty?) || self[key].nil? + self.delete(key) + end + end + end + + # Covert snake_case keys to camelCase + def camelize_keys + self.keys.each_with_index do |key, i| + if key != key.camelize(:lower) + self.insert(i, key.camelize(:lower), self[key]) + self.delete(key) + end + end + self + end + + # Covert camelCase keys to snake_case + def snakeize_keys + self.keys.each_with_index do |key, i| + if key != key.underscore + self.insert(i, key.underscore, self[key]) + self.delete(key) + end + end + self + end + + + # Prepends an entry to the front of the object. + # Note that this is slightly different from Array#unshift in that new + # entries must be added one at a time, i.e. unshift([k,v],[k,v],...) is + # not currently supported. + def unshift k,v + self.insert(0, k, v) + self + end + + protected + def insert_here(where, new_key, value, existing_key=nil, &block) + idx = nil + if block_given? + self.each_with_index do |(k,v), i| + if yield(k, v) + idx = i + break + end + end + if idx.nil? + raise KeyError, "Supplied block never evaluates to true" + end + else + unless self.has_key?(existing_key) + raise KeyError, "Existing key '#{existing_key}' does not exist" + end + if self.has_key?(new_key) + raise KeyError, "Supplied new key '#{new_key}' already exists" + end + idx = self.keys.index(existing_key) + where + end + self.insert(idx, new_key, value) + self + end + + end + end +end diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb new file mode 100644 index 0000000..911bb86 --- /dev/null +++ b/lib/iiif/v3/presentation.rb @@ -0,0 +1,30 @@ +require File.join(File.dirname(__FILE__), 'service') +%w{ +abstract_resource + annotation + annotation_list + canvas + collection + layer + manifest + resource + image_resource + sequence + range +}.each do |f| + require File.join(File.dirname(__FILE__), 'presentation', f) +end + +require_relative 'ordered_hash' + +module IIIF + module V3 + module Presentation + # TODO: when v3 is baked, there will be a context + # CONTEXT ||= 'http://iiif.io/api/presentation/2/context.json' + + class MissingRequiredKeyError < StandardError; end + class IllegalValueError < StandardError; end + end + end +end diff --git a/lib/iiif/v3/presentation/abstract_resource.rb b/lib/iiif/v3/presentation/abstract_resource.rb new file mode 100644 index 0000000..334a380 --- /dev/null +++ b/lib/iiif/v3/presentation/abstract_resource.rb @@ -0,0 +1,80 @@ +require File.join(File.dirname(__FILE__), '../service') + +module IIIF + module V3 + module Presentation + class AbstractResource < Service + + # Every subclass should override the following five methods where + # appropriate, see Subclasses for how. + def required_keys + %w{ type } + end + + def any_type_keys # these are allowed on all classes + %w{ label description thumbnail attribution rights logo see_also + related within } + end + + def string_only_keys + %w{ viewing_hint } # should any of the any_type_keys be here? + end + + def array_only_keys + %w{ metadata } + end + + def abstract_resource_only_keys + super + [ { key: 'service', type: IIIF::V3::Service } ] + end + + def hash_only_keys + %w{ } + end + + def int_only_keys + %w{ } + end + + # Not every subclass is allowed to have viewingDirect, but when it is, + # it must be one of these values + def legal_viewing_direction_values + %w{ left-to-right right-to-left top-to-bottom bottom-to-top } + end + + def legal_viewing_hint_values + [] + end + + # Initialize a Presentation node + # @param [Hash] hsh - Anything in this hash will be added to the Object.' + # Order is only guaranteed if an ActiveSupport::OrderedHash is passed. + # @param [boolean] include_context (default: false). Pass true if the' + # context should be included. + def initialize(hsh={}) + if self.class == IIIF::V3::Presentation::AbstractResource + raise "#{self.class} is an abstract class. Please use one of its subclasses." + end + super(hsh) + end + + + # Options: + # * force: (true|false). Skips validations. + # * include_context: (true|false). Adds the @context to the top of the + # document if it doesn't exist. Default: true. + # * sort_json_ld_keys: (true|false). Brings all properties starting with + # '@'. Default: true. to the top of the document and sorts them. + def to_ordered_hash(opts={}) + # TODO: when v3 is baked, there will be a context + # include_context = opts.fetch(:include_context, true) + # if include_context && !self.has_key?('@context') + # self['@context'] = IIIF::V3::Presentation::CONTEXT + # end + super(opts) + end + + end + end + end +end diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb new file mode 100644 index 0000000..aabe76d --- /dev/null +++ b/lib/iiif/v3/presentation/annotation.rb @@ -0,0 +1,26 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Annotation < AbstractResource + + TYPE = 'Annotation' + + def required_keys + super + %w{ motivation } + end + + def abstract_resource_only_keys + super + [ { key: 'resource', type: IIIF::V3::Presentation::Resource } ] + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + hsh['motivation'] = 'painting' unless hsh.has_key? 'motivation' + super(hsh) + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/annotation_list.rb b/lib/iiif/v3/presentation/annotation_list.rb new file mode 100644 index 0000000..c81e304 --- /dev/null +++ b/lib/iiif/v3/presentation/annotation_list.rb @@ -0,0 +1,30 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class AnnotationList < AbstractResource + + TYPE = 'AnnotationList' + + def required_keys + super + %w{ id } + end + + def array_only_keys; + super + %w{ resources }; + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # Each member or resources must be a kind of Annotation + end + + end + end + end +end diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb new file mode 100644 index 0000000..509f356 --- /dev/null +++ b/lib/iiif/v3/presentation/canvas.rb @@ -0,0 +1,46 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Canvas < AbstractResource + + # TODO (?) a simple 'Image Canvas' constructor. + + TYPE = 'Canvas' + + def required_keys + super + %w{ id width height label } + end + + def any_type_keys + super + %w{ } + end + + def array_only_keys + super + %w{ images other_content } + end + + # TODO: test and validate + def int_only_keys + super + %w{ width height } + end + + def legal_viewing_hint_values + super + %w{ non-paged } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # all members of images must be an annotation + # all members of otherContent must be an annotation list + super + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/collection.rb b/lib/iiif/v3/presentation/collection.rb new file mode 100644 index 0000000..9bcafa7 --- /dev/null +++ b/lib/iiif/v3/presentation/collection.rb @@ -0,0 +1,30 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Collection < AbstractResource + + TYPE = 'Collection' + + def required_keys + super + %w{ id label } + end + + def array_only_keys + super + %w{ collections manifests } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # each member of collections and manifests must be a Hash + # each member of collections and manifests MUST have id, type, and label + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb new file mode 100644 index 0000000..90a190d --- /dev/null +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -0,0 +1,116 @@ +require File.join(File.dirname(__FILE__), 'resource') +require 'faraday' +require 'json' + +module IIIF + module V3 + module Presentation + class ImageResource < Resource + + TYPE = 'dctypes:Image' + + def int_only_keys + super + %w{ width height } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + class << self + IMAGE_API_DEFAULT_PARAMS = '/full/!200,200/0/default.jpg' + IMAGE_API_CONTEXT = 'http://iiif.io/api/image/2/context.json' + DEFAULT_FORMAT = 'image/jpeg' + # Create a new ImageResource that includes a IIIF Image API Service + # See http://iiif.io/api/presentation/2.0/#image-resources + # + # Params + # * :service_id (required) - The base URI for the image on the image + # server. + # * :resource_id - The id for the resource; if supplied this should + # resolve to an actual image. Default: + # "#{:service_id}/full/!200,200/0/default.jpg" + # * :format - The format of the image that is returned when + # `:resource_id` is resolved. Default: 'image/jpeg' + # * :height (Integer) + # * :profile (String) + # * :width (Integer) - If width, height, and profile are not supplied, + # this method will try to get the info from the server (based on + # :resource_id) and raise an Exception if this is not possible for + # some reason. + # * :copy_info (bool)- Even if width and height are supplied, try to + # get the info.json from the server and copy it in. Default: false + # + # Raises: + # * KeyError if `:service_id` is not supplied + # * Expections related to HTTP problems if a call to an image server fails + # + # The result is something like this: + # + # { + # "id":"http://www.example.org/iiif/book1/res/page1.jpg", + # "type":"dctypes:Image", + # "format":"image/jpeg", + # "service": { + # "@context": "http://iiif.io/api/image/2/context.json", + # "id":"http://www.example.org/images/book1-page1", + # "profile":"http://iiif.io/api/image/2/profiles/level2.json", + # }, + # "height":2000, + # "width":1500 + # } + # + def create_image_api_image_resource(params={}) + + service_id = params.fetch(:service_id) + resource_id_default = "#{service_id}#{IMAGE_API_DEFAULT_PARAMS}" + resource_id = params.fetch(:resource_id, resource_id_default) + format = params.fetch(:format, DEFAULT_FORMAT) + height = params.fetch(:height, nil) + profile = params.fetch(:profile, nil) + width = params.fetch(:width, nil) + copy_info = params.fetch(:copy_info, false) + + have_whp = [width, height, profile].all? { |prop| !prop.nil? } + + remote_info = get_info(service_id) if !have_whp || copy_info + + resource = self.new + resource['id'] = resource_id + resource.format = format + resource.width = width.nil? ? remote_info['width'] : width + resource.height = height.nil? ? remote_info['height'] : height + resource.service = Service.new + if copy_info + resource.service.merge!(remote_info) + else + resource.service['@context'] = IMAGE_API_CONTEXT + resource.service['id'] = service_id + if profile.nil? + if remote_info['profile'].kind_of?(Array) + resource.service['profile'] = remote_info['profile'][0] + else + resource.service['profile'] = remote_info['profile'] + end + else + resource.service['profile'] = profile + end + end + return resource + end + + protected + def get_info(svc_id) + conn = Faraday.new("#{svc_id}/info.json") do |c| + c.use Faraday::Response::RaiseError + c.use Faraday::Adapter::NetHttp + end + resp = conn.get # raises exceptions that indicate HTTP problems + JSON.parse(resp.body) + end + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/layer.rb b/lib/iiif/v3/presentation/layer.rb new file mode 100644 index 0000000..4cb69f5 --- /dev/null +++ b/lib/iiif/v3/presentation/layer.rb @@ -0,0 +1,35 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Layer < AbstractResource + + TYPE = 'Layer' + + def required_keys + super + %w{ id label } + end + + def array_only_keys + super + %w{ other_content } + end + + def string_only_keys + super + %w{ viewing_direction } # should any of the any_type_keys be here? + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # Must all members of otherContent and images must be a URI (string), or + # can they be inline? + super + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb new file mode 100644 index 0000000..d040e14 --- /dev/null +++ b/lib/iiif/v3/presentation/manifest.rb @@ -0,0 +1,39 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Manifest < AbstractResource + + TYPE = 'Manifest' + + def required_keys + super + %w{ id label } + end + + def string_only_keys + super + %w{ viewing_direction } + end + + def array_only_keys + super + %w{ sequences structures } + end + + def legal_viewing_hint_values + %w{ individuals paged continuous } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # TODO: check types of sequences and structure members + + super + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/range.rb b/lib/iiif/v3/presentation/range.rb new file mode 100644 index 0000000..8d54e90 --- /dev/null +++ b/lib/iiif/v3/presentation/range.rb @@ -0,0 +1,33 @@ +require File.join(File.dirname(__FILE__), 'sequence') + +module IIIF + module V3 + module Presentation + class Range < Sequence + + TYPE = 'Range' + + def required_keys + super + %w{ id label } + end + + def array_only_keys + super + %w{ ranges } + end + + def legal_viewing_hint_values + super + %w{ top } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # Values of the ranges array must be strings + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/resource.rb b/lib/iiif/v3/presentation/resource.rb new file mode 100644 index 0000000..eb36101 --- /dev/null +++ b/lib/iiif/v3/presentation/resource.rb @@ -0,0 +1,22 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Resource < AbstractResource + + def required_keys + %w{ id } + end + + def string_only_keys + super + %w{ format } + end + + def initialize(hsh={}) + super(hsh) + end + end + end + end +end diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb new file mode 100644 index 0000000..bb43c86 --- /dev/null +++ b/lib/iiif/v3/presentation/sequence.rb @@ -0,0 +1,35 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Sequence < AbstractResource + + TYPE = 'Sequence' + + def array_only_keys + super + %w{ canvases } + end + + def string_only_keys + super + %w{ start_canvas viewing_direction } + end + + def legal_viewing_hint_values + %w{ individuals paged continuous } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + # * Must be at least one canvas + # * All members of canvases must be a kind of Canvas + super + end + end + end + end +end diff --git a/lib/iiif/v3/service.rb b/lib/iiif/v3/service.rb new file mode 100644 index 0000000..82c9426 --- /dev/null +++ b/lib/iiif/v3/service.rb @@ -0,0 +1,412 @@ +require File.join(File.dirname(__FILE__), 'hash_behaviours') +require 'active_support/core_ext/class/subclasses' +require 'active_support/ordered_hash' +require 'active_support/inflector' +require 'json' + +module IIIF + module V3 + class Service + include IIIF::V3::HashBehaviours + + # Anything goes! SHOULD have id and profile, MAY have label + # Consider subclassing this for typical services... + def required_keys; %w{ }; end + def any_type_keys; %w{ }; end + def string_only_keys; %w{ }; end + def array_only_keys; %w{ }; end + def abstract_resource_only_keys; %w{ }; end + def hash_only_keys; %w{ }; end + def int_only_keys; %w{ }; end + + def initialize(hsh={}) + @data = IIIF::V3::OrderedHash[hsh] + self.define_methods_for_any_type_keys + self.define_methods_for_array_only_keys + self.define_methods_for_string_only_keys + self.define_methods_for_int_only_keys + self.define_methods_for_abstract_resource_only_keys + self.snakeize_keys + end + + # Static methods / alternative constructors + class << self + # Parse from a file path, string, or existing hash + def parse(s) + ordered_hash = nil + if s.kind_of?(String) && File.exists?(s) + ordered_hash = IIIF::V3::OrderedHash[JSON.parse(IO.read(s))] + elsif s.kind_of?(String) && !File.exists?(s) + ordered_hash = IIIF::V3::OrderedHash[JSON.parse(s)] + elsif s.kind_of?(Hash) + ordered_hash = IIIF::V3::OrderedHash[s] + else + m = '#parse takes a path to a file, a JSON String, or a Hash, ' + m += "argument was a #{s.class}." + if s.kind_of?(String) + m+= "If you were trying to point to a file, does it exist?" + end + raise ArgumentError, m + end + return IIIF::V3::Service.from_ordered_hash(ordered_hash) + end + end + + def validate + # TODO: + # * check for required keys + # * type check Array-only values + # * type check String-only values + # * type check Integer-only values + # * type check AbstractResource-only values + self.required_keys.each do |k| + unless self.has_key?(k) + m = "A(n) #{k} is required for each #{self.class}" + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + end + # Viewing Direction values + if self.has_key?('viewing_direction') + unless self.legal_viewing_direction_values.include?(self['viewing_direction']) + m = "viewingDirection must be one of #{legal_viewing_direction_values}" + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + # Viewing Hint values + if self.has_key?('viewing_hint') + unless self.legal_viewing_hint_values.include?(self['viewing_hint']) + m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + # Metadata is all hashes + if self.has_key?('metadata') + unless self['metadata'].all? { |entry| entry.kind_of?(Hash) } + m = 'All entries in the metadata list must be a type of Hash' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + + # Options + # * pretty: (true|false). Should the JSON be pretty-printed? (default: false) + # * All options available in #to_ordered_hash + def to_json(opts={}) + hsh = self.to_ordered_hash(opts) + if opts.fetch(:pretty, false) + JSON.pretty_generate(hsh) + else + hsh.to_json + end + end + + # Options: + # * force: (true|false). Skips validations. + # * sort_json_ld_keys: (true|false). Brings all properties starting with + # '@'. Default: true. to the top of the document and sorts them. + def to_ordered_hash(opts={}) + force = opts.fetch(:force, false) + sort_json_ld_keys = opts.fetch(:sort_json_ld_keys, true) + + unless force + self.validate + end + + export_hash = IIIF::V3::OrderedHash.new + + if sort_json_ld_keys + self.keys.select { |k| k.start_with?('@') }.sort!.each do |k| + export_hash[k] = self.data[k] + end + end + + sub_opts = { + include_context: false, + sort_json_ld_keys: sort_json_ld_keys, + force: force + } + self.keys.each do |k| + unless sort_json_ld_keys && k.start_with?('@') + if self.data[k].respond_to?(:to_ordered_hash) #.respond_to?(:to_ordered_hash) + export_hash[k] = self.data[k].to_ordered_hash(sub_opts) + + elsif self.data[k].kind_of?(Hash) + export_hash[k] = IIIF::V3::OrderedHash.new + self.data[k].each do |sub_k, v| + + if v.respond_to?(:to_ordered_hash) + export_hash[k][sub_k] = v.to_ordered_hash(sub_opts) + + elsif v.kind_of?(Array) + export_hash[k][sub_k] = [] + v.each do |member| + if member.respond_to?(:to_ordered_hash) + export_hash[k][sub_k] << member.to_ordered_hash(sub_opts) + else + export_hash[k][sub_k] << member + end + end + else + export_hash[k][sub_k] = v + end + end + + elsif self.data[k].kind_of?(Array) + export_hash[k] = [] + + self.data[k].each do |member| + if member.respond_to?(:to_ordered_hash) + export_hash[k] << member.to_ordered_hash(sub_opts) + + elsif member.kind_of?(Hash) + hsh = IIIF::V3::OrderedHash.new + export_hash[k] << hsh + member.each do |sub_k,v| + + if v.respond_to?(:to_ordered_hash) + hsh[sub_k] = v.to_ordered_hash(sub_opts) + + elsif v.kind_of?(Array) + hsh[sub_k] = [] + + v.each do |sub_member| + if sub_member.respond_to?(:to_ordered_hash) + hsh[sub_k] << sub_member.to_ordered_hash(sub_opts) + else + hsh[sub_k] << sub_member + end + end + else + hsh[sub_k] = v + end + end + + else + export_hash[k] << member + # there are no nested arrays, right? + end + end + else + export_hash[k] = self.data[k] + end + + end + end + export_hash.remove_empties + export_hash.camelize_keys + export_hash + end + + def self.from_ordered_hash(hsh, default_klass=IIIF::V3::OrderedHash) + # Create a new object (new_object) + type = nil + if hsh.has_key?('type') + type = IIIF::V3::Service.get_descendant_class_by_jld_type(hsh['type']) + end + new_object = type.nil? ? default_klass.new : type.new + + hsh.keys.each do |key| + new_key = key.underscore == key ? key : key.underscore + if new_key == 'service' + new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key], IIIF::V3::Service) + elsif new_key == 'resource' + new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Resource) + elsif hsh[key].kind_of?(Hash) + new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key]) + elsif hsh[key].kind_of?(Array) + new_object[new_key] = [] + hsh[key].each do |member| + if new_key == 'service' + new_object[new_key] << IIIF::V3::Service.from_ordered_hash(member, IIIF::V3::Service) + elsif member.kind_of?(Hash) + new_object[new_key] << IIIF::V3::Service.from_ordered_hash(member) + else + new_object[new_key] << member + # Again, no nested arrays, right? + end + end + else + new_object[new_key] = hsh[key] + end + end + new_object + end + + protected + + def self.get_descendant_class_by_jld_type(type) + IIIF::V3::Service.all_service_subclasses.find do |klass| + klass.const_defined?(:TYPE) && klass.const_get(:TYPE) == type + end + end + + # All known subclasses of service. + def self.all_service_subclasses + @all_service_subclasses ||= IIIF::V3::Service.descendants.reject(&:singleton_class?) + end + + def data=(hsh) + @data = hsh + end + + def data + @data + end + + def define_methods_for_any_type_keys + any_type_keys.each do |key| + # Setters + define_singleton_method("#{key}=") do |arg| + self.send('[]=', key, arg) + end + if key.camelize(:lower) != key + define_singleton_method("#{key.camelize(:lower)}=") do |arg| + self.send('[]=', key, arg) + end + end + # Getters + define_singleton_method(key) do + self.send('[]', key) + end + if key.camelize(:lower) != key + define_singleton_method(key.camelize(:lower)) do + self.send('[]', key) + end + end + end + end + + def define_methods_for_array_only_keys + array_only_keys.each do |key| + # Setters + define_singleton_method("#{key}=") do |arg| + unless arg.kind_of?(Array) + m = "#{key} must be an Array." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + if key.camelize(:lower) != key + define_singleton_method("#{key.camelize(:lower)}=") do |arg| + unless arg.kind_of?(Array) + m = "#{key} must be an Array." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + end + # Getters + define_singleton_method(key) do + self[key] ||= [] + self[key] + end + if key.camelize(:lower) != key + define_singleton_method(key.camelize(:lower)) do + self.send('[]', key) + end + end + end + end + + def define_methods_for_abstract_resource_only_keys + # keys in this case is an array of hashes with { key: 'k', type: Class } + abstract_resource_only_keys.each do |hsh| + key = hsh[:key] + type = hsh[:type] + # Setters + define_singleton_method("#{key}=") do |arg| + unless arg.kind_of?(type) + m = "#{key} must be an #{type}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + if key.camelize(:lower) != key + define_singleton_method("#{key.camelize(:lower)}=") do |arg| + unless arg.kind_of?(type) + m = "#{key} must be an #{type}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + end + # Getters + define_singleton_method(key) do + self[key] ||= [] + self[key] + end + if key.camelize(:lower) != key + define_singleton_method(key.camelize(:lower)) do + self.send('[]', key) + end + end + end + end + + + def define_methods_for_string_only_keys + string_only_keys.each do |key| + # Setter + define_singleton_method("#{key}=") do |arg| + unless arg.kind_of?(String) + m = "#{key} must be an String." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + if key.camelize(:lower) != key + define_singleton_method("#{key.camelize(:lower)}=") do |arg| + unless arg.kind_of?(String) + m = "#{key} must be an String." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + end + # Getter + define_singleton_method(key) do + self[key] ||= [] + self[key] + end + if key.camelize(:lower) != key + define_singleton_method(key.camelize(:lower)) do + self.send('[]', key) + end + end + end + end + + def define_methods_for_int_only_keys + int_only_keys.each do |key| + # Setter + define_singleton_method("#{key}=") do |arg| + unless arg.kind_of?(Integer) && arg > 0 + m = "#{key} must be a positive Integer." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + if key.camelize(:lower) != key + define_singleton_method("#{key.camelize(:lower)}=") do |arg| + unless arg.kind_of?(Integer) && arg > 0 + m = "#{key} must be a positive Integer." + raise IIIF::V3::Presentation::IllegalValueError, m + end + self.send('[]=', key, arg) + end + end + # Getter + define_singleton_method(key) do + self[key] ||= [] + self[key] + end + if key.camelize(:lower) != key + define_singleton_method(key.camelize(:lower)) do + self.send('[]', key) + end + end + end + end + end + end +end diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb new file mode 100644 index 0000000..0e23b66 --- /dev/null +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -0,0 +1,120 @@ +describe IIIF::V3::Presentation::ImageResource do + vcr_options = { + cassette_name: 'pul_loris_cassette', + record: :new_episodes, + serialize_with: :json + } + + describe 'self#create_image_api_image_resource', vcr: vcr_options do + + let(:image_server) { 'http://libimages.princeton.edu/loris2' } + + let(:valid_service_id) { + id = 'pudl0001%2F4612422%2F00000001.jp2' + "#{image_server}/#{id}" + } + + let(:invalid_service_id) { + id = 'xxxx%2F4612422%2F00000001.jp2' + "#{image_server}/#{id}" + } + + it 'returns an ImageResource' do + instance = described_class.create_image_api_image_resource(service_id: valid_service_id) + expect(instance.class).to be described_class + end + + + describe 'has expected values from our fixture' do + it 'when copy_info is false' do + opts = { service_id: valid_service_id } + resource = described_class.create_image_api_image_resource(opts) + # expect(resource['@context']).to eq 'http://iiif.io/api/presentation/2/context.json' + # @context is only added when we call to_json... + expect(resource['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' + expect(resource['type']).to eq 'dctypes:Image' + expect(resource.format).to eq "image/jpeg" + expect(resource.width).to eq 3047 + expect(resource.height).to eq 7200 + expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json' + expect(resource.service['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2' + expect(resource.service['profile']).to eq 'http://iiif.io/api/image/2/level2.json' + end + it 'copies over all teh infos (when copy_info is true)' do + opts = { service_id: valid_service_id, copy_info: true } + resource = described_class.create_image_api_image_resource(opts) + expect(resource['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' + expect(resource['type']).to eq 'dctypes:Image' + expect(resource.format).to eq "image/jpeg" + expect(resource.width).to eq 3047 + expect(resource.height).to eq 7200 + expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json' + expect(resource.service['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2' + expect(resource.service['profile']).to eq [ + 'http://iiif.io/api/image/2/level2.json', + { + 'supports' => [ + 'canonicalLinkHeader', 'profileLinkHeader', 'mirroring', + 'rotationArbitrary', 'sizeAboveFull' + ], + 'qualities' => ['default', 'bitonal', 'gray', 'color'], + 'formats'=>['jpg', 'png', 'gif', 'webp'] + } + ] + expect(resource.service['tiles']).to eq [ { + 'width' => 1024, + 'scaleFactors' => [ 1, 2, 4, 8, 16, 32 ] + } ] + expect(resource.service['sizes']).to eq [ + {'width' => 96, 'height' => 225 }, + {'width' => 191, 'height' => 450 }, + {'width' => 381, 'height' => 900 }, + {'width' => 762, 'height' => 1800 }, + {'width' => 1524, 'height' => 3600 }, + {'width' => 3047, 'height' => 7200 } + ] + end + end + + describe 'respects the params we supply' do + it ':resource_id' do + r_id = 'http://example.edu/images/some.jpg' + opts = { service_id: valid_service_id, resource_id: r_id} + resource = described_class.create_image_api_image_resource(opts) + expect(resource['id']).to eq r_id + end + it ':width' do + width = 42 + opts = { service_id: valid_service_id, width: width} + resource = described_class.create_image_api_image_resource(opts) + expect(resource.width).to eq width + end + it ':height' do + height = 42 + opts = { service_id: valid_service_id, height: height} + resource = described_class.create_image_api_image_resource(opts) + expect(resource.height).to eq height + end + it ':profile (service[\'profile\'])' do + profile = 'http://iiif.io/api/image/2/level1.json' + opts = { service_id: valid_service_id, profile: profile} + resource = described_class.create_image_api_image_resource(opts) + expect(resource.service['profile']).to eq profile + end + end + + describe 'errors' do + it 'raises if :service_id is not included' do + expect { + described_class.create_image_api_image_resource + }.to raise_error + end + it 'raises if the info can\'t be pulled in' do + expect { + described_class.create_image_api_image_resource(service_id: invalid_service_id) + }.to raise_error + end + end + + end +end diff --git a/spec/integration/iiif/v3/service_spec.rb b/spec/integration/iiif/v3/service_spec.rb new file mode 100644 index 0000000..ab26ea3 --- /dev/null +++ b/spec/integration/iiif/v3/service_spec.rb @@ -0,0 +1,210 @@ +require 'active_support/inflector' +require 'json' + +describe IIIF::V3::Service do + + let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../fixtures') } + let(:manifest_from_spec_path) { File.join(fixtures_dir, 'v3/manifests/complete_from_spec.json') } + + describe 'self.parse' do + it 'works from a file' do + s = described_class.parse(manifest_from_spec_path) + expect(s['label']).to eq 'Book 1' + end + it 'works from a string of JSON' do + file = File.open(manifest_from_spec_path, 'rb') + json_string = file.read + file.close + s = described_class.parse(json_string) + expect(s['label']).to eq 'Book 1' + end + describe 'works from a hash' do + it 'plain old' do + h = JSON.parse(IO.read(manifest_from_spec_path)) + s = described_class.parse(h) + expect(s['label']).to eq 'Book 1' + end + it 'IIIF::V3::OrderedHash' do + h = JSON.parse(IO.read(manifest_from_spec_path)) + oh = IIIF::V3::OrderedHash[h] + s = described_class.parse(oh) + expect(s['label']).to eq 'Book 1' + end + end + it 'turns camels to snakes' do + s = described_class.parse(manifest_from_spec_path) + expect(s.keys.include?('see_also')).to be_truthy + expect(s.keys.include?('seeAlso')).to be_falsey + end + end + + describe 'self#from_ordered_hash' do + let(:fixture) { JSON.parse('{ + "id": "http://example.com/manifest", + "type": "Manifest", + "label": "My Manifest", + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "id":"http://www.example.org/images/book1-page1", + "profile":"http://iiif.io/api/image/2/profiles/level2.json" + }, + "some_other_thing": { + "foo" : "bar" + }, + "seeAlso": { + "id": "http://www.example.org/library/catalog/book1.marc", + "format": "application/marc" + }, + "sequences": [ + { + "id":"http://www.example.org/iiif/book1/sequence/normal", + "type": "Sequence", + "label": "Current Page Order", + + "viewingDirection":"left-to-right", + "viewingHint":"paged", + "startCanvas": "http://www.example.org/iiif/book1/canvas/p2", + + "canvases": [ + { + "id": "http://example.com/canvas", + "type": "Canvas", + "width": 10, + "height": 20, + "label": "My Canvas", + "otherContent": [ + { + "id": "http://example.com/content", + "type": "AnnotationList", + "motivation": "painting" + } + ] + } + ] + } + ] + }') + } + it 'doesn\'t raise a NoMethodError when we check the keys' do + expect { described_class.from_ordered_hash(fixture) }.to_not raise_error + end + it 'turns the fixture into a Manifest instance' do + expected_klass = IIIF::V3::Presentation::Manifest + parsed = described_class.from_ordered_hash(fixture) + expect(parsed.class).to be expected_klass + end + it 'turns keys without "type" into an OrderedHash' do + expected_klass = IIIF::V3::OrderedHash + parsed = described_class.from_ordered_hash(fixture) + expect(parsed['some_other_thing'].class).to be expected_klass + end + + it 'turns services into Services' do + expected_klass = IIIF::V3::Service + parsed = described_class.from_ordered_hash(fixture) + expect(parsed['service'].class).to be expected_klass + end + + it 'round-trips' do + fp = '/tmp/osullivan-spec.json' + parsed = described_class.from_ordered_hash(fixture) + File.open(fp,'w') do |f| + f.write(parsed.to_json) + end + from_file = IIIF::V3::Service.parse('/tmp/osullivan-spec.json') + File.delete(fp) + # is this sufficient? + expect(parsed.to_ordered_hash.to_a - from_file.to_ordered_hash.to_a).to eq [] + expect(from_file.to_ordered_hash.to_a - parsed.to_ordered_hash.to_a).to eq [] + end + it 'turns each member of "sequences" into an instance of Sequence' do + expected_klass = IIIF::V3::Presentation::Sequence + parsed = described_class.from_ordered_hash(fixture) + parsed['sequences'].each do |s| + expect(s.class).to be expected_klass + end + end + it 'turns each member of sequences/canvaes in an instance of Canvas' do + expected_klass = IIIF::V3::Presentation::Canvas + parsed = described_class.from_ordered_hash(fixture) + parsed['sequences'].each do |s| + s.canvases.each do |c| + expect(c.class).to be expected_klass + end + end + end + it 'turns the keys into snakes' do + expect(described_class.from_ordered_hash(fixture).has_key?('seeAlso')).to be_falsey + expect(described_class.from_ordered_hash(fixture).has_key?('see_also')).to be_truthy + end + it 'copies over plain-old key-values' do + parsed = described_class.from_ordered_hash(fixture) + expect(parsed['label']).to eq 'My Manifest' + end + + end + + describe '#to_ordered_hash' do + let(:logo_uri) { 'http://www.example.org/logos/institution1.jpg' } + let(:within_uri) { 'http://www.example.org/collections/books/' } + let(:see_also) { 'http://www.example.org/library/catalog/book1.xml' } + + describe 'it puts the json-ld keys at the top' do + let(:extra_props) { [ + ['label','foo'], + ['logo','http://example.com/logo.jpg'], + ['within','http://example.com/something'] + ] } + let(:sorted_ld_keys) { + subject.keys.select { |k| k.start_with?('@') }.sort! + } + before(:each) { + extra_props.reverse.each do |k,v| + subject.unshift(k,v) + end + } + + it 'by default' do + (0..extra_props.length-1).each do |i| + expect(subject.keys[i]).to eq(extra_props[i][0]) + end + oh = subject.to_ordered_hash + (0..sorted_ld_keys.length-1).each do |i| + expect(oh.keys[i]).to eq(sorted_ld_keys[i]) + end + end + it 'unless you say not to' do + (0..extra_props.length-1).each do |i| + expect(subject.keys[i]).to eq(extra_props[i][0]) + end + oh = subject.to_ordered_hash(sort_json_ld_keys: false) + (0..extra_props.length-1).each do |i| + expect(oh.keys[i]).to eq(extra_props[i][0]) + end + end + end + + describe 'removes empty keys' do + it 'if they\'re arrays' do + subject['logo'] = logo_uri + subject['within'] = [] + ordered_hash = subject.to_ordered_hash + expect(ordered_hash.has_key?('within')).to be_falsey + end + it 'if they\'re nil' do + subject['logo'] = logo_uri + subject['within'] = nil + ordered_hash = subject.to_ordered_hash + expect(ordered_hash.has_key?('within')).to be_falsey + end + end + + it 'converts snake_case keys to camelCase' do + subject['see_also'] = logo_uri + subject['within'] = within_uri + ordered_hash = subject.to_ordered_hash + expect(ordered_hash.keys.include?('seeAlso')).to be_truthy + end + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 900f616..2eab7c8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,14 @@ require 'iiif/presentation' +require 'iiif/v3/presentation' require 'simplecov' require 'coveralls' require 'debug' Dir["#{File.dirname(__FILE__)}/unit/iiif/presentation/shared_examples/*.rb"].each do |f| require f end +Dir["#{File.dirname(__FILE__)}/unit/iiif/v3/presentation/shared_examples/*.rb"].each do |f| + require f +end require 'vcr' VCR.configure do |c| diff --git a/spec/unit/iiif/v3/hash_behaviours_spec.rb b/spec/unit/iiif/v3/hash_behaviours_spec.rb new file mode 100644 index 0000000..878e0a9 --- /dev/null +++ b/spec/unit/iiif/v3/hash_behaviours_spec.rb @@ -0,0 +1,568 @@ +require File.join(File.dirname(__FILE__), '../../../spec_helper') +require 'active_support/ordered_hash' +describe IIIF::V3::HashBehaviours do + + let(:hash_like_class) do + Class.new do + include IIIF::V3::HashBehaviours + attr_accessor :data # Accessible for easier expects...not sure you'd do this in a real class + def initialize() + @data = IIIF::V3::OrderedHash.new + end + end + end + + # TODO: let(:init_data)...rather than repeating so much below + subject { hash_like_class.new } + + describe '#[]=' do + it 'assigns a new k and value to the node' do + subject['foo'] = 'bar' + expect(subject.data).to eq({'foo' => 'bar'}) + end + it 'always puts new entries at the end' do + subject['baz'] = 'qux' + subject['quux'] = 'corge' + subject['grault'] = 'garply' + expect(subject.data[subject.data.keys.last]).to eq 'garply' + end + it 'replaces keys that already exist in the same place' do + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + expect(subject.data.select {|k,v| k == 'plugh'}).to eq({'plugh'=>'wobble'}) + end + end + + describe '#[]' do + it 'retrieves the expected value' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + expect(subject['wibble']).to eq 'wobble' + end + it 'returns nil if the key is not found' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + expect(subject['flob']).to be_nil + end + end + + describe '#clear' do + it 'clears all properties' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + subject.clear + expect(subject.keys).to eq [] + end + it 'returns the instance on which it was called' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + expect(subject.clear).to eq subject + end + end + + describe '#delete' do + it 'removes an entry from the object' do + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + subject.delete('waldo') + expect(subject.data).to eq({'plugh' => 'xyzzy'}) + end + it 'returns the value of the entry that was removed' do + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.delete('waldo')).to eq 'fred' + end + it 'can take a block as well' do + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect { |b| subject.delete('wubble', &b) }.to yield_with_args + expect(subject.delete('foo') {|e| e.reverse }).to eq 'oof' + end + end + + describe '#delete_if' do + it 'can take a block' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect { |b| subject.delete_if(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) + end + it 'returns the instance' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect( subject.delete_if { |k,v| k.start_with?('w') } ).to eq subject + end + it 'works' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + subject.delete_if { |k,v| k.start_with?('w') } + expect(subject.data).to eq({'plugh' => 'xyzzy'}) + end + it 'returns an enumerator if no block is supplied' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.delete_if).to be_a Enumerator + end + end + + describe '#each' do + it 'yields' do + subject['plugh'] = 'xyzzy' + expect { |b| subject.each(&b) }.to yield_with_args + end + it 'returns the instance' do + subject.data['waldo'] = 'fred' + subject.data['plugh'] = 'xyzzy' + expect(subject.each { |k,v| nil }).to eq subject + end + it 'loops as expected' do + subject.data['wibble'] = 'foo' + subject.data['waldo'] = 'fred' + subject.data['plugh'] = 'xyzzy' + capped_keys = [] + subject.each { |k,v| capped_keys << k.capitalize } + expect(capped_keys).to eq ['Wibble', 'Waldo', 'Plugh'] + end + it 'returns an enumerator if no block is supplied' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.delete_if).to be_a Enumerator + end + end + + describe '#each_key' do + it 'yields' do + subject['plugh'] = 'xyzzy' + expect { |b| subject.each_key(&b) }.to yield_with_args + end + it 'returns the instance' do + subject.data['waldo'] = 'fred' + subject.data['plugh'] = 'xyzzy' + expect(subject.each_key { |k| nil }).to eq subject + end + it 'loops as expected' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + key_accumulator = [] + subject.each_key { |k| key_accumulator << k } + expect(key_accumulator).to eq ['wibble', 'waldo', 'plugh'] + end + it 'returns an enumerator if no block is supplied' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.each_key).to be_a Enumerator + end + end + + describe '#each_value' do + it 'yields' do + subject['plugh'] = 'xyzzy' + expect { |b| subject.each_value(&b) }.to yield_with_args + end + it 'returns the instance' do + subject.data['waldo'] = 'fred' + subject.data['plugh'] = 'xyzzy' + expect(subject.each_value { |v| nil }).to eq subject + end + it 'loops as expected' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + value_accumulator = [] + subject.each_value { |v| value_accumulator << v } + expect(value_accumulator).to eq ['foo', 'fred', 'xyzzy'] + end + it 'returns an enumerator if no block is supplied' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.each_value).to be_a Enumerator + end + + end + + describe '#empty' do + it 'returns true when there are no entries' do + expect(subject.empty?).to be_truthy + end + it 'returns false when we have data' do + subject['waldo'] = 'fred' + expect(subject.empty?).to be_falsey + end + end + + describe '#fetch' do + it 'retrieves the expected value' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + expect(subject.fetch('wibble')).to eq 'wobble' + end + it 'returns the default if the key is not found and one is supplied' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + expect(subject.fetch('flob', 'waldo')).to eq 'waldo' + end + it 'raises a KeyError if the key is not found and no default is supplied' do + expect { subject.fetch('flob') }.to raise_error KeyError + end + it 'can take a block as well' do + subject['wibble'] = 'wobble' + subject['wubble'] = 'fred' + expect(subject.fetch('wubble') {|e| e.capitalize }).to eq 'fred' # value takes precence + expect { |b| subject.fetch('foo', &b) }.to yield_with_args + expect(subject.fetch('foo') {|e| e.reverse }).to eq 'oof' + end + end + + describe '#has_key? (and aliases)' do + it 'is true when the key exists' do + subject['wibble'] = 'wobble' + expect(subject.has_key? 'wibble').to be_truthy + end + it 'is false when the key does not exist' do + expect(subject.has_key? 'wibble').to be_falsey + end + end + + describe '#has_value? (and aliases)' do + it 'is true when the value exists' do + subject['wibble'] = 'wobble' + expect(subject.has_value? 'wobble').to be_truthy + end + it 'is false when the value does not exist' do + expect(subject.has_value? 'wobble').to be_falsey + end + end + + describe '#keep_if' do + it 'can take a block' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect { |b| subject.keep_if(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) + end + it 'returns the instance' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect( subject.keep_if { |k,v| k.start_with?('w') } ).to eq subject + end + it 'works' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + subject.keep_if { |k,v| k.start_with?('w') } + expect(subject.data).to eq({'wibble'=>'foo', 'waldo'=>'fred'}) + end + end + + describe '#key' do + it 'is the key associated with a value' do + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + expect(subject.key 'wibble').to eq 'thud' + expect(subject.key 'wobble').to eq 'plugh' + end + it 'is nil if the value is not found' do + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + expect(subject.key 'foo').to be_nil + end + end + + describe '#keys' do + it 'is an array of all of the keys in the object' do + subject['foo'] = 'bar' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.keys).to eq ['foo', 'waldo', 'plugh'] + end + end + + describe '#length' do + it 'works' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + expect(subject.length).to eq 2 + end + end + + describe '#merge' do + it 'returns a new instance of the calling class' do + subject['wibble'] = 'foo' + another = hash_like_class.new + another['waldo'] = 'fred' + merged = subject.merge(another) + # clear them all to confirm we're not testing equality of anything other + # than that we have different instances + subject.data.clear + another.data.clear + merged.data.clear + expect(subject.merge(another).class).to eq subject.class # same class but' + expect(merged).to_not be subject # different instance + expect(merged).to_not be another # different instance + end + # it 'adds new entries to the end' do + # subject['wibble'] = 'foo' + # subject['plugh'] = 'xyzzy' + # another = hash_like_class.new + # another['waldo'] = 'fred' + # new_instance = subject.merge(another) + # expect(new_instance.data.last).to eq ['waldo', 'fred'] + # end + it 'retains the index position for existing entries, replacing the value' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = hash_like_class.new + another['plugh'] = 'fred' + new_instance = subject.merge(another) + expect(new_instance['plugh']).to eq 'fred' + expect(new_instance[new_instance.keys[1]]).to eq 'fred' + end + it 'takes a block' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = hash_like_class.new + another['plugh'] = 'fred' + # e.g. give a block that turns common keys into an Array + new_instance = subject.merge(another) { |k, old_val,new_val| [old_val, new_val] } + expect(new_instance['wibble']).to eq 'foo' + expect(new_instance['plugh']).to eq ['xyzzy', 'fred'] + expect(new_instance['foo']).to eq 'bar' + expect(new_instance.data).to eq({'wibble'=>'foo', 'plugh'=>['xyzzy', 'fred'], 'foo'=>'bar'}) + end + describe 'takes anything that implements `#each { |k,v| block }` and #has_key?' do + it 'returns a new instance of the calling class' do + subject['wibble'] = 'foo' + another = {'waldo' => 'fred'} + merged = subject.merge(another) + # clear them all to confirm we're not testing equality of anything other + # than that we have different instances + subject.data.clear + another.clear + merged.data.clear + expect(subject.merge(another).class).to eq subject.class # same class but' + expect(merged).to_not be subject # different instance + expect(merged).to_not be another # different instance + end + # it 'adds new entries to the end' do + # subject['wibble'] = 'foo' + # subject['plugh'] = 'xyzzy' + # another = {'waldo' => 'fred'} + # new_instance = subject.merge(another) + # expect(new_instance.data.last).to eq ['waldo', 'fred'] + # end + it 'retains the index position for existing entries, replacing the value' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = {'plugh' => 'fred' } + another['plugh'] = 'fred' + new_instance = subject.merge(another) + expect(new_instance['plugh']).to eq 'fred' + expect(new_instance[new_instance.keys[1]]).to eq 'fred' + end + it 'takes a block' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = {'plugh' => 'fred'} + # e.g. give a block that turns common keys into an Array + new_instance = subject.merge(another) { |k, old_val,new_val| [old_val, new_val] } + expect(new_instance['wibble']).to eq 'foo' + expect(new_instance['plugh']).to eq ['xyzzy', 'fred'] + expect(new_instance['foo']).to eq 'bar' + expect(new_instance.data).to eq({'wibble'=>'foo', 'plugh'=>['xyzzy', 'fred'], 'foo'=>'bar'}) + end + end + end + + describe '#merge!' do + it 'returns the instance on which is was called' do + subject['wibble'] = 'foo' + another = hash_like_class.new + another['waldo'] = 'fred' + expect(subject.merge!(another)).to eq subject # same instance + end + it 'adds new entries to the end' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + another = hash_like_class.new + another['waldo'] = 'fred' + subject.merge!(another) + expect(subject.data[subject.data.keys.last]).to eq 'fred' + end + it 'retains the index position for existing entries, replacing the value' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = hash_like_class.new + another['plugh'] = 'fred' + subject.merge!(another) + expect(subject['plugh']).to eq 'fred' + expect(subject.data[subject.keys[1]]).to eq 'fred' + end + it 'takes a block' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = hash_like_class.new + another['plugh'] = 'fred' + # e.g. give a block that turns common keys into an Array + subject.merge!(another) { |k, old_val,new_val| [old_val, new_val] } + expect(subject['wibble']).to eq 'foo' + expect(subject['plugh']).to eq ['xyzzy', 'fred'] + expect(subject['foo']).to eq 'bar' + expect(subject.data).to eq({'wibble'=>'foo', 'plugh'=>['xyzzy', 'fred'], 'foo'=>'bar'}) + end + describe 'takes anything that implements `#each { |k,v| block }` and #has_key?' do + it 'returns a new instance of the calling class' do + subject['wibble'] = 'foo' + another = {'waldo' => 'fred'} + expect(subject.merge!(another)).to eq subject # same instance + end + it 'adds new entries to the end' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + another = {'waldo' => 'fred'} + subject.merge!(another) + expect(subject.data[subject.data.keys.last]).to eq 'fred' + end + it 'retains the index position for existing entries, replacing the value' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = {'plugh' => 'fred' } + subject.merge!(another) + expect(subject['plugh']).to eq 'fred' + expect(subject.data[subject.keys[1]]).to eq 'fred' + end + it 'takes a block' do + subject['wibble'] = 'foo' + subject['plugh'] = 'xyzzy' + subject['foo'] = 'bar' + another = {'plugh' => 'fred'} + subject.merge!(another) { |k, old_val,new_val| "#{k}, #{old_val}, #{new_val}" } + expect(subject['wibble']).to eq 'foo' + expect(subject['plugh']).to eq 'plugh, xyzzy, fred' + expect(subject['foo']).to eq 'bar' + expect(subject.data).to eq({'wibble'=>'foo', 'plugh'=>'plugh, xyzzy, fred', 'foo'=>'bar'}) + end + end + end + + describe '#reject!' do + it 'can take a block' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect { |b| subject.reject!(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) + end + it 'returns the instance if there were changes' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect( subject.reject! { |k| k.start_with?('w') } ).to be subject + end + it 'returns nil if there were no changes' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect( subject.reject! { |k| k.start_with?('X') } ).to be_nil + end + it 'works' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + subject.reject! { |k| k.start_with?('w') } + expect(subject.data).to eq({'plugh' => 'xyzzy'}) + end + end + + describe '#select' do + it 'yields' do + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + subject['waldo'] = 'fred' + expect { |b| subject.select(&b) }.to yield_successive_args(['thud', 'wibble'], ['plugh', 'wobble'], ['waldo', 'fred']) + end + it 'returns a new instance of the class' do + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + subject['waldo'] = 'fred' + expect( subject.select{ |k,v| true }.class ).to eq subject.class + expect( subject.select{ |k,v| true } ).to_not eq subject + end + it 'selects but doesn\'t delete from the original instance' do + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + subject['waldo'] = 'fred' + expect( subject.select{ |k,v| k.include?('u') }.data ).to eq({'thud'=>'wibble', 'plugh'=>'wobble'}) + expect( subject.data ).to eq subject.data + end + end + + describe '#select!' do + it 'can take a block' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect { |b| subject.select!(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) + end + it 'returns nil if there were no changes' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['wobble'] = 'xyzzy' + expect( subject.select! { |k,v| k.start_with?('w') } ).to be_nil + end + it 'returns the instance if there were changes' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect( subject.select! { |k,v| k.start_with?('w') } ).to eq subject + end + it 'works' do + subject['wibble'] = 'foo' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + subject.select! { |k,v| k.start_with?('p') } + expect(subject.data).to eq({'plugh' => 'xyzzy'}) + end + end + + describe '#shift' do + it 'returns the first element in the hash without a param' do + subject['thud'] = 'wibble' + subject['plugh'] = 'wobble' + expect(subject.shift).to eq ['thud','wibble'] + expect(subject.data).to eq({'plugh' => 'wobble'}) + end + end + + describe 'store' do + it 'works as an alias for []=' do + subject.store('foo', 'bar') + expect(subject.data).to eq({'foo' => 'bar'}) + end + end + + describe '#values' do + it 'is an array of all of the keys in the object' do + subject['foo'] = 'bar' + subject['waldo'] = 'fred' + subject['plugh'] = 'xyzzy' + expect(subject.values).to eq ['bar', 'fred', 'xyzzy'] + end + end + +end diff --git a/spec/unit/iiif/v3/ordered_hash_spec.rb b/spec/unit/iiif/v3/ordered_hash_spec.rb new file mode 100644 index 0000000..3b1b438 --- /dev/null +++ b/spec/unit/iiif/v3/ordered_hash_spec.rb @@ -0,0 +1,154 @@ +describe IIIF::V3::OrderedHash do + + describe '#camelize_keys' do + before(:each) do + @uri = 'http://www.example.org/descriptions/book1.xml' + @within_uri = 'http://www.example.org/collections/books/' + subject['see_also'] = @uri + subject['within'] = @within_uri + end + it 'changes snake_case keys to camelCase' do + subject.camelize_keys # #send gets past protection + expect(subject.keys.include?('seeAlso')).to be_truthy + expect(subject.keys.include?('see_also')).to be_falsey + end + it 'keeps the right values' do + subject.camelize_keys + expect(subject['seeAlso']).to eq @uri + expect(subject['within']).to eq @within_uri + end + it 'keeps things in the same position' do + see_also_position = subject.keys.index('see_also') + within_position = subject.keys.index('within') + subject.camelize_keys + expect(subject.keys[see_also_position]).to eq 'seeAlso' + expect(subject.keys[within_position]).to eq 'within' + end + + end + + describe '#snakeize_keys' do + before(:each) do + @uri = 'http://www.example.org/descriptions/book1.xml' + @within_uri = 'http://www.example.org/collections/books/' + subject['seeAlso'] = @uri + subject['within'] = @within_uri + end + it 'changes camelCase keys to snake_case' do + subject.snakeize_keys + expect(subject.keys.include?('see_also')).to be_truthy + expect(subject.keys.include?('seeAlso')).to be_falsey + end + it 'keeps the right values' do + subject.snakeize_keys + expect(subject['see_also']).to eq @uri + expect(subject['within']).to eq @within_uri + end + it 'keeps things in the same position' do + see_also_position = subject.keys.index('seeAlso') + within_position = subject.keys.index('within') + subject.snakeize_keys + expect(subject.keys[see_also_position]).to eq 'see_also' + expect(subject.keys[within_position]).to eq 'within' + end + end + + describe 'insertion patches' do + + let (:init_data) { [ ['wubble', 'fred'], ['baz', 'qux'], ['grault','garply'] ] } + + subject do + hsh = described_class.new + init_data.each { |e| hsh[e[0]] = e[1] } + hsh + end + + describe '#insert' do + it 'inserts as expected' do + subject.insert(2, 'quux', 'corge') + expect(subject[subject.keys[0]]).to eq 'fred' + expect(subject[subject.keys[1]]).to eq 'qux' + expect(subject[subject.keys[2]]).to eq 'corge' + expect(subject[subject.keys[3]]).to eq 'garply' + end + it 'returns the instance' do + expect(subject.insert(1, 'quux','corge')).to eq subject + end + it 'raises IndexError if a negative index is too small' do + expect { subject.insert(-5, 'quux','corge') }.to raise_error IndexError + end + it 'puts index -1 on the end' do + subject.insert(-1, 'thud','wibble') + expect(subject[subject.keys.last]).to eq 'wibble' + end + end + + describe '#insert_before' do + it 'inserts in the expected place with a supplied key' do + subject.insert_before(existing_key: 'grault', new_key: 'quux', value: 'corge') + expect(subject.keys).to eq ['wubble','baz','quux','grault'] + end + it 'inserts in the expected place with a supplied block' do + subject.insert_before(new_key: 'quux', value: 'corge') { |k,v| k.start_with?('g') } + expect(subject.keys).to eq ['wubble','baz','quux','grault'] + end + it 'returns the instance' do + expect(subject.insert_before(existing_key: 'grault', new_key: 'quux', value: 'corge')).to be subject + end + describe 'raises KeyError' do + it 'when the supplied existing key is not found' do + expect { subject.insert_before(existing_key: 'foo', new_key: 'quux', value: 'corge') }.to raise_error KeyError + end + it 'when the supplied new key already exists' do + expect { subject.insert_before(existing_key: 'grault', new_key: 'wubble', value: 'corge') }.to raise_error KeyError + end + end + end + + describe '#insert_after' do + it 'inserts in the expected place with a supplied key' do + subject.insert_after(existing_key: 'baz', new_key: 'quux', value: 'corge') + expect(subject.keys).to eq ['wubble','baz','quux','grault'] + end + it 'inserts in the expected place with a supplied block' do + subject.insert_after(new_key: 'quux', value: 'corge') { |k,v| k.start_with?('g') } + expect(subject.keys).to eq ['wubble','baz','quux','grault'] + end + it 'returns the instance' do + expect(subject.insert_after(existing_key: 'baz', new_key: 'quux', value: 'corge')).to be subject + end + describe 'raises KeyError' do + it 'when the supplied existing key is not found' do + expect { subject.insert_after(existing_key: 'foo', new_key: 'quux', value: 'corge') }.to raise_error KeyError + end + it 'when the supplied new key already exists' do + expect { subject.insert_after(existing_key: 'grault', new_key: 'wubble', value: 'corge') }.to raise_error KeyError + end + end + end + + describe '#unshift' do + it 'adds an entry to the front of the object' do + subject.unshift('thud','wibble') + expect(subject[subject.keys[0]]).to eq 'wibble' + end + it 'returns the instance' do + expect(subject.unshift('thud','wibble')).to be subject + end + end + + describe '#remove_empties' do + it 'if they\'re arrays' do + subject[:wubble] = [] + subject.remove_empties + expect(subject.has_key?(:wubble)).to be_falsey + end + it 'if they\'re nil' do + subject[:wubble] = nil + subject.remove_empties + expect(subject.has_key?(:wubble)).to be_falsey + end + end + + end +end diff --git a/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb b/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb new file mode 100644 index 0000000..d2bf99d --- /dev/null +++ b/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb @@ -0,0 +1,132 @@ +require 'active_support/inflector' +require 'json' +require File.join(File.dirname(__FILE__), '../../../../../lib/iiif/v3/hash_behaviours') + + +describe IIIF::V3::Presentation::AbstractResource do + + let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../../fixtures') } + let(:manifest_from_spec_path) { File.join(fixtures_dir, 'v3/manifests/complete_from_spec.json') } + let(:abstract_resource_subclass) do + Class.new(IIIF::V3::Presentation::AbstractResource) do + include IIIF::V3::HashBehaviours + + def initialize(hsh={}) + hsh['type'] = 'a:SubClass' unless hsh.has_key?('type') + super(hsh) + end + + def required_keys + super + %w{ id } + end + end + end + + subject do + instance = abstract_resource_subclass.new + instance['id'] = 'http://example.com/prefix/manifest/123' + instance + end + + describe '#initialize' do + it 'raises an error if you try to instantiate AbstractResource' do + expect { IIIF::V3::Presentation::AbstractResource.new }.to raise_error(RuntimeError) + end + it 'sets type' do + expect(subject['type']).to eq 'a:SubClass' + end + it 'can take any old hash' do + hsh = JSON.parse(IO.read(manifest_from_spec_path)) + new_instance = abstract_resource_subclass.new(hsh) + expect(new_instance['label']).to eq 'Book 1' + end + end + + describe 'A nested object (e.g. self[\'metdata\'])' do + it 'returns [] if not set' do + expect(subject.metadata).to eq([]) + end + it 'is not in #to_ordered_hash at all if we access it but do not append to it' do + subject.metadata # touch it + expect(subject.metadata).to eq([]) + expect(subject.to_ordered_hash.has_key?('metadata')).to be_falsey + end + it 'gets structured as we\'d expect' do + subject.metadata << { + 'label' => 'Author', + 'value' => 'Anne Author' + } + subject.metadata << { + 'label' => 'Published', + 'value' => [ + {'@value'=> 'Paris, circa 1400', '@language'=>'en'}, + {'@value'=> 'Paris, environ 14eme siecle', '@language'=>'fr'} + ] + } + expect(subject.metadata[0]['label']).to eq('Author') + expect(subject.metadata[1]['value'].length).to eq(2) + expect(subject.metadata[1]['value'].select { |e| e['@language'] == 'fr'}.last['@value']).to eq('Paris, environ 14eme siecle') + end + + it 'roundtrips' do + subject.metadata << { + 'label' => 'Author', + 'value' => 'Anne Author' + } + subject.metadata << { + 'label' => 'Published', + 'value' => [ + {'@value'=> 'Paris, circa 1400', '@language'=>'en'}, + {'@value'=> 'Paris, environ 14eme siecle', '@language'=>'fr'} + ] + } + File.open('/tmp/osullivan-spec.json','w') do |f| + f.write(subject.to_json) + end + parsed = subject.class.parse('/tmp/osullivan-spec.json') + expect(parsed['metadata'][0]['label']).to eq('Author') + expect(parsed['metadata'][1]['value'].length).to eq(2) + expect(parsed['metadata'][1]['value'].select { |e| e['@language'] == 'fr'}.last['@value']).to eq('Paris, environ 14eme siecle') + File.delete('/tmp/osullivan-spec.json') + end + end + + describe '#required_keys' do + it 'accumulates' do + expect(subject.required_keys).to eq %w{ type id } + end + end + + describe '#to_ordered_hash' do + describe 'does not add the @context' do + before(:each) { subject.delete('@context') } + it 'by default' do + expect(subject.has_key?('@context')).to be_falsey + expect(subject.to_ordered_hash.has_key?('@context')).to be_falsey + end + it 'unless you say not to' do + expect(subject.has_key?('@context')).to be_falsey + expect(subject.to_ordered_hash(include_context: false).has_key?('@context')).to be_falsey + end + it 'or it\'s already there' do + different_ctxt = 'http://example.org/context' + subject['@context'] = different_ctxt + oh = subject.to_ordered_hash + expect(oh['@context']).to eq different_ctxt + end + end + + describe 'runs the validations' do + # Test this here because there's nothing to validate on the superclass (Subject) + let(:error) { IIIF::V3::Presentation::MissingRequiredKeyError } + before(:each) { subject.delete('id') } + it 'raises exceptions' do + expect { subject.to_ordered_hash }.to raise_error error + end + it 'unless you tell it not to' do + expect { subject.to_ordered_hash(force: true) }.to_not raise_error + end + end + end + +end diff --git a/spec/unit/iiif/v3/presentation/annotation_list_spec.rb b/spec/unit/iiif/v3/presentation/annotation_list_spec.rb new file mode 100644 index 0000000..f26fe3c --- /dev/null +++ b/spec/unit/iiif/v3/presentation/annotation_list_spec.rb @@ -0,0 +1,7 @@ +describe IIIF::V3::Presentation::AnnotationList do + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + +end diff --git a/spec/unit/iiif/v3/presentation/annotation_spec.rb b/spec/unit/iiif/v3/presentation/annotation_spec.rb new file mode 100644 index 0000000..1e161b2 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/annotation_spec.rb @@ -0,0 +1,7 @@ +describe IIIF::V3::Presentation::Annotation do + + describe "#{described_class}.define_methods_for_abstract_resource_only_keys" do + it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' + end + +end diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb new file mode 100644 index 0000000..f53a097 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -0,0 +1,44 @@ +describe IIIF::V3::Presentation::Canvas do + + let(:fixed_values) do + { + "id" => "http://www.example.org/iiif/book1/canvas/p1", + "type" => "Canvas", + "label" => "p. 1", + "height" => 1000, + "width" => 750, + "images" => [ ], + "otherContent" => [ ] + } + end + + + describe '#initialize' do + it 'sets type' do + expect(subject['type']).to eq 'Canvas' + end + end + + describe "#{described_class}.int_only_keys" do + it_behaves_like 'it has the appropriate methods for integer-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_any_type_keys" do + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end + + describe "#legal_viewing_hint_values" do + it "should not error" do + expect{subject.legal_viewing_hint_values}.not_to raise_error + end + end + +end diff --git a/spec/unit/iiif/v3/presentation/collection_spec.rb b/spec/unit/iiif/v3/presentation/collection_spec.rb new file mode 100644 index 0000000..3cc1547 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/collection_spec.rb @@ -0,0 +1,51 @@ +describe IIIF::V3::Presentation::Collection do + + let(:fixed_values) do + { + 'id' => 'http://example.org/iiif/collection/top', + 'type' => 'Collection', + 'label' => 'Top Level Collection for Example Organization', + 'description' => 'Description of Collection', + 'attribution' => 'Provided by Example Organization', + + 'collections' => [ + { 'id' => 'http://example.org/iiif/collection/part1', + 'type' => 'Collection', + 'label' => 'Sub Collection 1' + }, + { 'id' => 'http://example.org/iiif/collection/part2', + 'type' => 'Collection', + 'label' => 'Sub Collection 2' + } + ], + 'manifests' => [ + { 'id' => 'http://example.org/iiif/book1/manifest', + 'type' => 'Manifest', + 'label' => 'Book 1' + } + ] + } + end + + describe '#initialize' do + it 'sets type to Collection by default' do + expect(subject['type']).to eq 'Collection' + end + end + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe "#{described_class}.define_methods_for_any_type_keys" do + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end + + describe '#validate' do + end + +end diff --git a/spec/unit/iiif/v3/presentation/image_resource_spec.rb b/spec/unit/iiif/v3/presentation/image_resource_spec.rb new file mode 100644 index 0000000..e0951c1 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/image_resource_spec.rb @@ -0,0 +1,13 @@ +describe IIIF::V3::Presentation::ImageResource do + + describe '#initialize' do + it 'sets type to dctypes:Image' do + expect(subject['type']).to eq 'dctypes:Image' + end + end + + describe "#{described_class}.int_only_keys" do + it_behaves_like 'it has the appropriate methods for integer-only keys v3' + end + +end diff --git a/spec/unit/iiif/v3/presentation/layer_spec.rb b/spec/unit/iiif/v3/presentation/layer_spec.rb new file mode 100644 index 0000000..2ba91a7 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/layer_spec.rb @@ -0,0 +1,36 @@ +describe IIIF::V3::Presentation::Layer do + + let(:fixed_values) do + { + 'id' => 'http://www.example.org/iiif/book1/layer/transcription', + 'type' => 'Layer', + 'label' => 'Diplomatic Transcription', + 'otherContent' => [ + 'http://www.example.org/iiif/book1/list/l1', + 'http://www.example.org/iiif/book1/list/l2', + 'http://www.example.org/iiif/book1/list/l3', + 'http://www.example.org/iiif/book1/list/l4' + ] + } + end + + + describe '#initialize' do + it 'sets type' do + expect(subject['type']).to eq 'Layer' + end + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_any_type_keys" do + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end + +end diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb new file mode 100644 index 0000000..fa1ff9a --- /dev/null +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -0,0 +1,87 @@ +describe IIIF::V3::Presentation::Manifest do + + let(:subclass_subject) do + Class.new(IIIF::V3::Presentation::Manifest) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + end + + let(:fixed_values) do + { + 'type' => 'a:SubClass', + 'id' => 'http://example.com/prefix/manifest/123', + 'label' => 'Book 1', + 'description' => 'A longer description of this example book. It should give some real information.', + 'thumbnail' => { + 'id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg', + 'service'=> { + '@context' => 'http://iiif.io/api/image/2/context.json', + 'id' => 'http://www.example.org/images/book1-page1', + 'profile' => 'http://iiif.io/api/image/2/level1.json' + } + }, + 'attribution' => 'Provided by Example Organization', + 'rights' => 'http://www.example.org/license.html', + 'logo' => 'http://www.example.org/logos/institution1.jpg', + 'see_also' => 'http://www.example.org/library/catalog/book1.xml', + 'service' => { + '@context' => 'http://example.org/ns/jsonld/context.json', + 'id' => 'http://example.org/service/example', + 'profile' => 'http://example.org/docs/example-service.html' + }, + 'related' => { + 'id' => 'http://www.example.org/videos/video-book1.mpg', + 'format' => 'video/mpeg' + }, + 'within' => 'http://www.example.org/collections/books/', + } + end + + describe '#initialize' do + it 'sets type to Manifest by default' do + expect(subject['type']).to eq 'Manifest' + end + it 'allows subclasses to override type' do + sub = subclass_subject.new + expect(sub['type']).to eq 'a:SubClass' + end + end + + describe '#required_keys' do + it 'accumulates' do + expect(subject.required_keys).to eq %w{ type id label } + end + end + + describe '#validate' do + it 'raises an error if there is no id' do + subject.label = 'Book 1' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + end + it 'raises an error if there is no label' do + subject['id'] = 'http://www.example.org/iiif/book1/manifest' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + end + it 'raises an error if there is no type' do + subject.delete('type') + subject.label = 'Book 1' + subject['id'] = 'http://www.example.org/iiif/book1/manifest' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + end + end + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe "#{described_class}.define_methods_for_any_type_keys" do + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end +end diff --git a/spec/unit/iiif/v3/presentation/range_spec.rb b/spec/unit/iiif/v3/presentation/range_spec.rb new file mode 100644 index 0000000..bde4721 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/range_spec.rb @@ -0,0 +1,41 @@ +describe IIIF::V3::Presentation::Range do + + let(:fixed_values) do + { + 'id' => 'http://www.example.org/iiif/book1/range/r1', + 'type' => 'Range', + 'label' => 'Introduction', + 'ranges' => [ + 'http://www.example.org/iiif/book1/range/r1-1', + 'http://www.example.org/iiif/book1/range/r1-2' + ], + 'canvases' => [ + 'http://www.example.org/iiif/book1/canvas/p1', + 'http://www.example.org/iiif/book1/canvas/p2', + 'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300' + ] + } + end + + describe '#initialize' do + it 'sets type to Range by default' do + expect(subject['type']).to eq 'Range' + end + end + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe "#{described_class}.define_methods_for_any_type_keys" do + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end + + describe '#validate' do + end + +end diff --git a/spec/unit/iiif/v3/presentation/resource_spec.rb b/spec/unit/iiif/v3/presentation/resource_spec.rb new file mode 100644 index 0000000..eec8f2d --- /dev/null +++ b/spec/unit/iiif/v3/presentation/resource_spec.rb @@ -0,0 +1,15 @@ +describe IIIF::V3::Presentation::Resource do + + describe "#{described_class}.define_methods_for_abstract_resource_only_keys" do + it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' + end + + describe "#{described_class}.int_only_keys" do + it_behaves_like 'it has the appropriate methods for integer-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + +end diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb new file mode 100644 index 0000000..a222fa2 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -0,0 +1,108 @@ +describe IIIF::V3::Presentation::Sequence do + + let(:subclass_subject) do + Class.new(IIIF::V3::Presentation::Sequence) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + end + + let(:fixed_values) do + { + 'type' => 'Sequence', + 'id' => 'http://example.com/prefix/sequence/456', + 'label' => 'Book 1', + 'description' => 'A longer description of this example book. It should give some real information.', + 'thumbnail' => { + 'id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg', + 'service'=> { + '@context' => 'http://iiif.io/api/image/2/context.json', + 'id' => 'http://www.example.org/images/book1-page1', + 'profile' => 'http://iiif.io/api/image/2/level1.json' + } + }, + 'attribution' => 'Provided by Example Organization', + 'rights' => 'http://www.example.org/license.html', + 'logo' => 'http://www.example.org/logos/institution1.jpg', + 'see_also' => 'http://www.example.org/library/catalog/book1.xml', + 'service' => { + '@context' => 'http://example.org/ns/jsonld/context.json', + 'id' => 'http://example.org/service/example', + 'profile' => 'http://example.org/docs/example-service.html' + }, + 'related' => { + 'id' => 'http://www.example.org/videos/video-book1.mpg', + 'format' => 'video/mpeg' + }, + 'within' => 'http://www.example.org/collections/books/', + # Sequence + 'metadata' => [{'label'=>'Author', 'value'=>'Anne Author'}], + 'canvases' => [{ + 'id' => 'http://www.example.org/iiif/book1/canvas/p1', + 'type' => 'Canvas', + 'label' => 'p. 1', + 'height' => 1000, + 'width' => 750, + 'images'=> [] + }], + 'viewing_hint' => 'paged', + 'start_canvas' => 'http://www.example.org/iiif/book1/canvas/p2', + 'viewing_direction' => 'right-to-left', + } + end + + describe '#initialize' do + it 'sets type to Sequence by default' do + expect(subject['type']).to eq 'Sequence' + end + it 'allows subclasses to override type' do + sub = subclass_subject.new + expect(sub['type']).to eq 'a:SubClass' + end + end + + describe '#required_keys' do + it 'accumulates from the superclass' do + expect(subject.required_keys).to eq %w{ type } + end + end + + describe '#string_only_keys' do + it 'accumulates from the superclass' do + expect(subject.string_only_keys).to eq %w{ viewing_hint start_canvas viewing_direction } + end + end + + describe '#array_only_keys' do + it 'accumulates from the superclass' do + expect(subject.array_only_keys).to eq %w{ metadata canvases } + end + end + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe "#{described_class}.define_methods_for_any_type_keys" do + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end + + describe '#validate' do + it 'raises an error if viewing_hint isn\'t an allowable value' do + subject['viewing_hint'] = 'foo' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + it 'raises an error if viewing_directon isn\'t an allowable value' do + subject['viewing_direction'] = 'foo-to-bar' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end + + +end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb new file mode 100644 index 0000000..037a7d5 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb @@ -0,0 +1,43 @@ +require 'set' + +shared_examples 'it has the appropriate methods for abstract_resource_only_keys v3' do + + described_class.new.abstract_resource_only_keys.each do |entry| + + describe "#{entry[:key]}=" do + it "sets #{entry[:key]}" do + @ex = entry[:type].new + subject.send("#{entry[:key]}=", @ex) + expect(subject[entry[:key]]).to eq @ex + end + if entry[:key].camelize(:lower) != entry[:key] + it "is aliased as ##{entry[:key].camelize(:lower)}=" do + @ex = entry[:type].new + subject.send("#{entry[:key].camelize(:lower)}=", @ex) + expect(subject[entry[:key]]).to eq @ex + end + end + it "raises an exception when attempting to set it to something other than an #{entry[:type]}" do + e = IIIF::V3::Presentation::IllegalValueError + expect { subject.send("#{entry[:key]}=", 'Foo') }.to raise_error e + end + end + + describe "#{entry[:key]}" do + it "gets #{entry[:key]}" do + @ex = entry[:type].new + subject[entry[:key]] = @ex + expect(subject.send(entry[:key])).to eq @ex + end + if entry[:key].camelize(:lower) != entry[:key] + it "is aliased as ##{entry[:key].camelize(:lower)}" do + @ex = entry[:type].new + subject[entry[:key]] = @ex + expect(subject.send("#{entry[:key].camelize(:lower)}")).to eq @ex + end + end + end + + end + +end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb new file mode 100644 index 0000000..930855a --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb @@ -0,0 +1,33 @@ +require 'set' + +shared_examples 'it has the appropriate methods for any-type keys v3' do + + described_class.new.any_type_keys.each do |prop| + describe "##{prop}=" do + it "sets self['#{prop}']" do + subject.send("#{prop}=", fixed_values[prop]) + expect(subject[prop]).to eq fixed_values[prop] + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}=" do + subject.send("#{prop.camelize(:lower)}=", fixed_values[prop]) + expect(subject[prop]).to eq fixed_values[prop] + end + end + end + + describe "##{prop}" do + it "gets self[#{prop}]" do + subject.send("[]=", prop, fixed_values[prop]) + expect(subject.send("#{prop}")).to eq fixed_values[prop] + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}" do + subject.send("[]=", prop, fixed_values[prop]) + expect(subject.send("#{prop.camelize(:lower)}")).to eq fixed_values[prop] + end + end + end + end + +end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb new file mode 100644 index 0000000..199801c --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb @@ -0,0 +1,42 @@ +require 'set' + +shared_examples 'it has the appropriate methods for array-only keys v3' do + + described_class.new.array_only_keys.each do |prop| + + describe "#{prop}=" do + it "sets #{prop}" do + ex = [{'label' => 'XYZ'}] + subject.send("#{prop}=", ex) + expect(subject[prop]).to eq ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}=" do + ex = [{'label' => 'XYZ'}] + subject.send("#{prop.camelize(:lower)}=", ex) + expect(subject[prop]).to eq ex + end + end + it 'raises an exception when attempting to set it to something other than an Array' do + expect { subject.send("#{prop}=", 'Foo') }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end + + describe "#{prop}" do + it "gets #{prop}" do + ex = [{'label' => 'XYZ'}] + subject[prop] = ex + expect(subject.send(prop)).to eq ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}" do + ex = [{'label' => 'XYZ'}] + subject[prop] = ex + expect(subject.send("#{prop.camelize(:lower)}")).to eq ex + end + end + end + + end + +end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb new file mode 100644 index 0000000..069d008 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb @@ -0,0 +1,47 @@ +require 'set' + +shared_examples 'it has the appropriate methods for integer-only keys v3' do + + described_class.new.int_only_keys.each do |prop| + + describe "#{prop}=" do + before(:all) do + @ex = 7200 + end + it "sets #{prop}" do + subject.send("#{prop}=", @ex) + expect(subject[prop]).to eq @ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}=" do + subject.send("#{prop.camelize(:lower)}=", @ex) + expect(subject[prop]).to eq @ex + end + end + it 'raises an exception when attempting to set it to something other than an Integer' do + expect { subject.send("#{prop}=", 'Foo') }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + it 'raises an exception when attempting to set it to a negative number' do + expect { subject.send("#{prop}=", -1) }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end + + describe "#{prop}" do + before(:all) do + @ex = 7200 + end + it "gets #{prop}" do + subject[prop] = @ex + expect(subject.send(prop)).to eq @ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}" do + subject[prop] = @ex + expect(subject.send("#{prop.camelize(:lower)}")).to eq @ex + end + end + end + + end + +end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb new file mode 100644 index 0000000..065a94e --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb @@ -0,0 +1,28 @@ +require 'set' + +shared_examples 'it has the appropriate methods for string-only keys v3' do + + described_class.new.string_only_keys.each do |prop| + + describe "#{prop}=" do + it "sets #{prop}" do + ex = 'foo' + subject.send("#{prop}=", ex) + expect(subject[prop]).to eq ex + end + it 'raises an exception when attempting to set it to something other than a String' do + expect { subject.send("#{prop}=", ['Foo']) }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end + + describe "#{prop}" do + it "gets #{prop}" do + ex = 'bar' + subject[prop] = ex + expect(subject.send(prop)).to eq ex + end + end + + end + +end diff --git a/spec/unit/iiif/v3/service_spec.rb b/spec/unit/iiif/v3/service_spec.rb new file mode 100644 index 0000000..ddfd329 --- /dev/null +++ b/spec/unit/iiif/v3/service_spec.rb @@ -0,0 +1,28 @@ +describe IIIF::V3::Service do + + describe 'self#get_descendant_class_by_jld_type' do + before do + class DummyClass < IIIF::V3::Service + TYPE = "Collection" + def self.singleton_class? + true + end + end + end + after do + Object.send(:remove_const, :DummyClass) + end + it 'gets the right class' do + klass = described_class.get_descendant_class_by_jld_type('Canvas') + expect(klass).to eq IIIF::V3::Presentation::Canvas + end + context "when there are singleton classes which are returned" do + it "gets the right class" do + allow(IIIF::V3::Service).to receive(:descendants).and_return([DummyClass, IIIF::V3::Presentation::Collection]) + klass = described_class.get_descendant_class_by_jld_type('Collection') + expect(klass).to eq IIIF::V3::Presentation::Collection + end + end + end + +end From 899e02a1f2f59f90a8856f0e71732c9c1e99b695 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 10:45:10 -0700 Subject: [PATCH 03/91] spec/integration/iiif/v3/presentation/image_resource_spec.rb works without VCR --- .../iiif/v3/presentation/image_resource_spec.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb index 0e23b66..b3dba32 100644 --- a/spec/integration/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -7,7 +7,7 @@ describe 'self#create_image_api_image_resource', vcr: vcr_options do - let(:image_server) { 'http://libimages.princeton.edu/loris2' } + let(:image_server) { 'https://libimages.princeton.edu/loris' } let(:valid_service_id) { id = 'pudl0001%2F4612422%2F00000001.jp2' @@ -31,31 +31,30 @@ resource = described_class.create_image_api_image_resource(opts) # expect(resource['@context']).to eq 'http://iiif.io/api/presentation/2/context.json' # @context is only added when we call to_json... - expect(resource['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' + expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' expect(resource['type']).to eq 'dctypes:Image' expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json' - expect(resource.service['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2' + expect(resource.service['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' expect(resource.service['profile']).to eq 'http://iiif.io/api/image/2/level2.json' end it 'copies over all teh infos (when copy_info is true)' do opts = { service_id: valid_service_id, copy_info: true } resource = described_class.create_image_api_image_resource(opts) - expect(resource['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' + expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' expect(resource['type']).to eq 'dctypes:Image' expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json' - expect(resource.service['id']).to eq 'http://libimages.princeton.edu/loris2/pudl0001%2F4612422%2F00000001.jp2' expect(resource.service['profile']).to eq [ 'http://iiif.io/api/image/2/level2.json', { 'supports' => [ 'canonicalLinkHeader', 'profileLinkHeader', 'mirroring', - 'rotationArbitrary', 'sizeAboveFull' + 'rotationArbitrary', 'regionSquare', 'sizeAboveFull' ], 'qualities' => ['default', 'bitonal', 'gray', 'color'], 'formats'=>['jpg', 'png', 'gif', 'webp'] @@ -73,6 +72,8 @@ {'width' => 1524, 'height' => 3600 }, {'width' => 3047, 'height' => 7200 } ] + skip('loris is still v2 and returns @id, not id') + expect(resource.service['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' end end From 8ab4fb85b73176cb7ff58a3ef90dcd2ef6e08389 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 15:35:34 -0700 Subject: [PATCH 04/91] vcr works for specs Conflicts: spec/fixtures/vcr_cassettes/pul_loris_cassette.json --- spec/fixtures/vcr_cassettes/pul_loris_cassette.json | 2 +- spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json | 1 + spec/integration/iiif/presentation/image_resource_spec.rb | 1 - spec/integration/iiif/v3/presentation/image_resource_spec.rb | 2 +- spec/spec_helper.rb | 1 + 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json diff --git a/spec/fixtures/vcr_cassettes/pul_loris_cassette.json b/spec/fixtures/vcr_cassettes/pul_loris_cassette.json index b25b518..bc04454 100644 --- a/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +++ b/spec/fixtures/vcr_cassettes/pul_loris_cassette.json @@ -1 +1 @@ -{"http_interactions":[{"request":{"method":"get","uri":"https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/info.json","body":{"encoding":"US-ASCII","string":""},"headers":{"User-Agent":["Faraday v1.0.1"]}},"response":{"status":{"code":200,"message":"OK"},"headers":{"date":["Wed, 17 Jun 2020 19:38:19 GMT"],"server":["Apache/2.4.18 (Ubuntu)"],"link":[";rel=\"profile\",;rel=\"http://www.w3.org/ns/json-ld#context\";type=\"application/ld+json\""],"access-control-allow-origin":["*"],"access-control-allow-methods":["GET"],"access-control-allow-headers":["X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"],"last-modified":["Sat, 25 Jan 2020 18:37:32 GMT"],"content-length":["753"],"content-type":["application/json"]},"body":{"encoding":"UTF-8","string":"{\"profile\": [\"http://iiif.io/api/image/2/level2.json\", {\"supports\": [\"canonicalLinkHeader\", \"profileLinkHeader\", \"mirroring\", \"rotationArbitrary\", \"regionSquare\", \"sizeAboveFull\"], \"qualities\": [\"default\", \"bitonal\", \"gray\", \"color\"], \"formats\": [\"jpg\", \"png\", \"gif\", \"webp\"]}], \"tiles\": [{\"width\": 1024, \"scaleFactors\": [1, 2, 4, 8, 16, 32]}], \"protocol\": \"http://iiif.io/api/image\", \"sizes\": [{\"width\": 96, \"height\": 225}, {\"width\": 191, \"height\": 450}, {\"width\": 381, \"height\": 900}, {\"width\": 762, \"height\": 1800}, {\"width\": 1524, \"height\": 3600}, {\"width\": 3047, \"height\": 7200}], \"height\": 7200, \"width\": 3047, \"@context\": \"http://iiif.io/api/image/2/context.json\", \"@id\": \"https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2\"}"},"http_version":null},"recorded_at":"Wed, 17 Jun 2020 19:38:19 GMT"},{"request":{"method":"get","uri":"https://libimages.princeton.edu/loris/xxxx%2F4612422%2F00000001.jp2/info.json","body":{"encoding":"US-ASCII","string":""},"headers":{"User-Agent":["Faraday v1.0.1"]}},"response":{"status":{"code":404,"message":"NOT FOUND"},"headers":{"date":["Wed, 17 Jun 2020 19:38:20 GMT"],"server":["Apache/2.4.18 (Ubuntu)"],"link":[";rel=\"profile\""],"access-control-allow-origin":["*"],"access-control-allow-methods":["GET"],"access-control-allow-headers":["X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"],"content-length":["86"],"content-type":["text/plain"]},"body":{"encoding":"UTF-8","string":"Not Found: Source image not found for identifier: xxxx%2F4612422%2F00000001.jp2. (404)"},"http_version":null},"recorded_at":"Wed, 17 Jun 2020 19:38:20 GMT"}],"recorded_with":"VCR 5.1.0"} \ No newline at end of file +{"http_interactions":[{"request":{"method":"get","uri":"https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/info.json","body":{"encoding":"US-ASCII","string":""},"headers":{"User-Agent":["Faraday v1.0.1"]}},"response":{"status":{"code":200,"message":"OK"},"headers":{"date":["Wed, 17 Jun 2020 19:38:19 GMT"],"server":["Apache/2.4.18 (Ubuntu)"],"link":[";rel=\"profile\",;rel=\"http://www.w3.org/ns/json-ld#context\";type=\"application/ld+json\""],"access-control-allow-origin":["*"],"access-control-allow-methods":["GET"],"access-control-allow-headers":["X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"],"last-modified":["Sat, 25 Jan 2020 18:37:32 GMT"],"content-length":["753"],"content-type":["application/json"]},"body":{"encoding":"UTF-8","string":"{\"profile\": [\"http://iiif.io/api/image/2/level2.json\", {\"supports\": [\"canonicalLinkHeader\", \"profileLinkHeader\", \"mirroring\", \"rotationArbitrary\", \"regionSquare\", \"sizeAboveFull\"], \"qualities\": [\"default\", \"bitonal\", \"gray\", \"color\"], \"formats\": [\"jpg\", \"png\", \"gif\", \"webp\"]}], \"tiles\": [{\"width\": 1024, \"scaleFactors\": [1, 2, 4, 8, 16, 32]}], \"protocol\": \"http://iiif.io/api/image\", \"sizes\": [{\"width\": 96, \"height\": 225}, {\"width\": 191, \"height\": 450}, {\"width\": 381, \"height\": 900}, {\"width\": 762, \"height\": 1800}, {\"width\": 1524, \"height\": 3600}, {\"width\": 3047, \"height\": 7200}], \"height\": 7200, \"width\": 3047, \"@context\": \"http://iiif.io/api/image/2/context.json\", \"@id\": \"https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2\"}"},"http_version":null},"recorded_at":"Wed, 17 Jun 2020 19:38:19 GMT"},{"request":{"method":"get","uri":"https://libimages.princeton.edu/loris/xxxx%2F4612422%2F00000001.jp2/info.json","body":{"encoding":"US-ASCII","string":""},"headers":{"User-Agent":["Faraday v1.0.1"]}},"response":{"status":{"code":404,"message":"NOT FOUND"},"headers":{"date":["Wed, 17 Jun 2020 19:38:20 GMT"],"server":["Apache/2.4.18 (Ubuntu)"],"link":[";rel=\"profile\""],"access-control-allow-origin":["*"],"access-control-allow-methods":["GET"],"access-control-allow-headers":["X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"],"content-length":["86"],"content-type":["text/plain"]},"body":{"encoding":"UTF-8","string":"Not Found: Source image not found for identifier: xxxx%2F4612422%2F00000001.jp2. (404)"},"http_version":null},"recorded_at":"Wed, 17 Jun 2020 19:38:20 GMT"}],"recorded_with":"VCR 5.1.0"} diff --git a/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json b/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json new file mode 100644 index 0000000..2768251 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json @@ -0,0 +1 @@ +{"http_interactions":[{"request":{"method":"get","uri":"https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/info.json","body":{"encoding":"US-ASCII","string":""},"headers":{"User-Agent":["Faraday v0.12.1"],"Accept-Encoding":["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],"Accept":["*/*"]}},"response":{"status":{"code":200,"message":"OK"},"headers":{"Date":["Tue, 27 Jun 2017 22:33:33 GMT"],"Server":["Apache/2.4.18 (Ubuntu)"],"Link":[";rel=\"profile\",;rel=\"http://www.w3.org/ns/json-ld#context\";type=\"application/ld+json\""],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET"],"Access-Control-Allow-Headers":["X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"],"Last-Modified":["Tue, 11 Apr 2017 07:03:53 GMT"],"Content-Length":["753"],"Content-Type":["application/json"]},"body":{"encoding":"UTF-8","string":"{\"profile\": [\"http://iiif.io/api/image/2/level2.json\", {\"supports\": [\"canonicalLinkHeader\", \"profileLinkHeader\", \"mirroring\", \"rotationArbitrary\", \"regionSquare\", \"sizeAboveFull\"], \"qualities\": [\"default\", \"bitonal\", \"gray\", \"color\"], \"formats\": [\"jpg\", \"png\", \"gif\", \"webp\"]}], \"tiles\": [{\"width\": 1024, \"scaleFactors\": [1, 2, 4, 8, 16, 32]}], \"protocol\": \"http://iiif.io/api/image\", \"sizes\": [{\"width\": 96, \"height\": 225}, {\"width\": 191, \"height\": 450}, {\"width\": 381, \"height\": 900}, {\"width\": 762, \"height\": 1800}, {\"width\": 1524, \"height\": 3600}, {\"width\": 3047, \"height\": 7200}], \"height\": 7200, \"width\": 3047, \"@context\": \"http://iiif.io/api/image/2/context.json\", \"@id\": \"https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2\"}"},"http_version":null},"recorded_at":"Tue, 27 Jun 2017 22:33:33 GMT"},{"request":{"method":"get","uri":"https://libimages.princeton.edu/loris/xxxx%2F4612422%2F00000001.jp2/info.json","body":{"encoding":"US-ASCII","string":""},"headers":{"User-Agent":["Faraday v0.12.1"],"Accept-Encoding":["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],"Accept":["*/*"]}},"response":{"status":{"code":404,"message":"NOT FOUND"},"headers":{"Date":["Tue, 27 Jun 2017 22:33:33 GMT"],"Server":["Apache/2.4.18 (Ubuntu)"],"Link":[";rel=\"profile\""],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET"],"Access-Control-Allow-Headers":["X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"],"Content-Length":["86"],"Content-Type":["text/plain"]},"body":{"encoding":"UTF-8","string":"Not Found: Source image not found for identifier: xxxx%2F4612422%2F00000001.jp2. (404)"},"http_version":null},"recorded_at":"Tue, 27 Jun 2017 22:33:33 GMT"}],"recorded_with":"VCR 2.9.3"} \ No newline at end of file diff --git a/spec/integration/iiif/presentation/image_resource_spec.rb b/spec/integration/iiif/presentation/image_resource_spec.rb index e19a8af..51ed9e9 100644 --- a/spec/integration/iiif/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/presentation/image_resource_spec.rb @@ -7,7 +7,6 @@ describe 'self#create_image_api_image_resource', vcr: vcr_options do - # 301 moved to https.../loris let(:image_server) { 'https://libimages.princeton.edu/loris' } let(:valid_service_id) { diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb index b3dba32..9c63e63 100644 --- a/spec/integration/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -1,6 +1,6 @@ describe IIIF::V3::Presentation::ImageResource do vcr_options = { - cassette_name: 'pul_loris_cassette', + cassette_name: 'pul_loris_cassette_v3', record: :new_episodes, serialize_with: :json } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2eab7c8..00ef2e5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,6 +10,7 @@ require f end require 'vcr' +require 'webmock/rspec' VCR.configure do |c| c.cassette_library_dir = 'spec/fixtures/vcr_cassettes' From 6d0ae9eb2bc96733bed94f04e47576fb70e8fbfc Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 16:00:41 -0700 Subject: [PATCH 05/91] add none as a legal value for viewingHint --- lib/iiif/v3/presentation/manifest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index d040e14..81ca44e 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -20,7 +20,7 @@ def array_only_keys end def legal_viewing_hint_values - %w{ individuals paged continuous } + %w{ individuals paged continuous none } end def initialize(hsh={}) From 299d7dac69c94a05133b7d170d0bac65e07395f4 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 16:05:29 -0700 Subject: [PATCH 06/91] v3 image_resource converts referenced v2 images into v3-ish --- lib/iiif/v3/presentation/image_resource.rb | 1 + spec/integration/iiif/v3/presentation/image_resource_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 90a190d..b62e764 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -84,6 +84,7 @@ def create_image_api_image_resource(params={}) resource.service = Service.new if copy_info resource.service.merge!(remote_info) + resource.service['id'] ||= resource.service.delete('@id') else resource.service['@context'] = IMAGE_API_CONTEXT resource.service['id'] = service_id diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb index 9c63e63..655df6c 100644 --- a/spec/integration/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -43,6 +43,7 @@ it 'copies over all teh infos (when copy_info is true)' do opts = { service_id: valid_service_id, copy_info: true } resource = described_class.create_image_api_image_resource(opts) + expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' expect(resource['type']).to eq 'dctypes:Image' expect(resource.format).to eq "image/jpeg" @@ -72,8 +73,8 @@ {'width' => 1524, 'height' => 3600 }, {'width' => 3047, 'height' => 7200 } ] - skip('loris is still v2 and returns @id, not id') expect(resource.service['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' + expect(resource.service).not_to have_key('@id') end end From 7bbe7ddfd5b40df75af04981819db8d418840675 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 16:17:09 -0700 Subject: [PATCH 07/91] v3 has AnnotationPage but no AnnotationList --- lib/iiif/v3/presentation.rb | 2 +- .../presentation/{annotation_list.rb => annotation_page.rb} | 6 +++--- .../{annotation_list_spec.rb => annotation_page_spec.rb} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename lib/iiif/v3/presentation/{annotation_list.rb => annotation_page.rb} (80%) rename spec/unit/iiif/v3/presentation/{annotation_list_spec.rb => annotation_page_spec.rb} (75%) diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 911bb86..4a5d894 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -2,7 +2,7 @@ %w{ abstract_resource annotation - annotation_list + annotation_page canvas collection layer diff --git a/lib/iiif/v3/presentation/annotation_list.rb b/lib/iiif/v3/presentation/annotation_page.rb similarity index 80% rename from lib/iiif/v3/presentation/annotation_list.rb rename to lib/iiif/v3/presentation/annotation_page.rb index c81e304..cd4cad2 100644 --- a/lib/iiif/v3/presentation/annotation_list.rb +++ b/lib/iiif/v3/presentation/annotation_page.rb @@ -3,16 +3,16 @@ module IIIF module V3 module Presentation - class AnnotationList < AbstractResource + class AnnotationPage < AbstractResource - TYPE = 'AnnotationList' + TYPE = 'AnnotationPage' def required_keys super + %w{ id } end def array_only_keys; - super + %w{ resources }; + super + %w{ items }; end def initialize(hsh={}) diff --git a/spec/unit/iiif/v3/presentation/annotation_list_spec.rb b/spec/unit/iiif/v3/presentation/annotation_page_spec.rb similarity index 75% rename from spec/unit/iiif/v3/presentation/annotation_list_spec.rb rename to spec/unit/iiif/v3/presentation/annotation_page_spec.rb index f26fe3c..6ae7f00 100644 --- a/spec/unit/iiif/v3/presentation/annotation_list_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_page_spec.rb @@ -1,4 +1,4 @@ -describe IIIF::V3::Presentation::AnnotationList do +describe IIIF::V3::Presentation::AnnotationPage do describe "#{described_class}.define_methods_for_array_only_keys" do it_behaves_like 'it has the appropriate methods for array-only keys v3' From cafdd40ca037b0a24defcfd588779dd0c9ecc7ff Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 16:30:38 -0700 Subject: [PATCH 08/91] v3 canvas and service: replace 'images' and 'otherContent' with 'content' --- lib/iiif/v3/presentation/canvas.rb | 5 ++--- spec/integration/iiif/v3/service_spec.rb | 4 ++-- spec/unit/iiif/v3/presentation/canvas_spec.rb | 3 +-- spec/unit/iiif/v3/presentation/sequence_spec.rb | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index 509f356..f3f3118 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -18,7 +18,7 @@ def any_type_keys end def array_only_keys - super + %w{ images other_content } + super + %w{ content } end # TODO: test and validate @@ -36,8 +36,7 @@ def initialize(hsh={}) end def validate - # all members of images must be an annotation - # all members of otherContent must be an annotation list + # all members of content are of type AnnotationPage super end end diff --git a/spec/integration/iiif/v3/service_spec.rb b/spec/integration/iiif/v3/service_spec.rb index ab26ea3..1e9ef20 100644 --- a/spec/integration/iiif/v3/service_spec.rb +++ b/spec/integration/iiif/v3/service_spec.rb @@ -72,10 +72,10 @@ "width": 10, "height": 20, "label": "My Canvas", - "otherContent": [ + "content": [ { "id": "http://example.com/content", - "type": "AnnotationList", + "type": "AnnotationPage", "motivation": "painting" } ] diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index f53a097..c228a88 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -7,8 +7,7 @@ "label" => "p. 1", "height" => 1000, "width" => 750, - "images" => [ ], - "otherContent" => [ ] + "content" => [ ] } end diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index a222fa2..af954a2 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -45,7 +45,7 @@ def initialize(hsh={}) 'label' => 'p. 1', 'height' => 1000, 'width' => 750, - 'images'=> [] + 'content'=> [] }], 'viewing_hint' => 'paged', 'start_canvas' => 'http://www.example.org/iiif/book1/canvas/p2', From 652285b458986dc65649ddc5ce3720ed4a2cadab Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 27 Jun 2017 17:16:41 -0700 Subject: [PATCH 09/91] add v3 AnnotationCollection, remove Layer --- lib/iiif/v3/presentation.rb | 2 +- .../{layer.rb => annotation_collection.rb} | 17 ++++++++++------- ...er_spec.rb => annotation_collection_spec.rb} | 16 ++++++---------- 3 files changed, 17 insertions(+), 18 deletions(-) rename lib/iiif/v3/presentation/{layer.rb => annotation_collection.rb} (56%) rename spec/unit/iiif/v3/presentation/{layer_spec.rb => annotation_collection_spec.rb} (59%) diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 4a5d894..6d4e938 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -2,10 +2,10 @@ %w{ abstract_resource annotation + annotation_collection annotation_page canvas collection - layer manifest resource image_resource diff --git a/lib/iiif/v3/presentation/layer.rb b/lib/iiif/v3/presentation/annotation_collection.rb similarity index 56% rename from lib/iiif/v3/presentation/layer.rb rename to lib/iiif/v3/presentation/annotation_collection.rb index 4cb69f5..5bbc947 100644 --- a/lib/iiif/v3/presentation/layer.rb +++ b/lib/iiif/v3/presentation/annotation_collection.rb @@ -3,20 +3,25 @@ module IIIF module V3 module Presentation - class Layer < AbstractResource + class AnnotationCollection < AbstractResource - TYPE = 'Layer' + TYPE = 'AnnotationCollection' def required_keys - super + %w{ id label } + super + %w{ id } + end + + def int_only_keys + super + %w{ total } end def array_only_keys - super + %w{ other_content } + super + %w{ content } end def string_only_keys - super + %w{ viewing_direction } # should any of the any_type_keys be here? + # first and last are actually uris + super + %w{ viewing_direction, first, last } end def initialize(hsh={}) @@ -25,8 +30,6 @@ def initialize(hsh={}) end def validate - # Must all members of otherContent and images must be a URI (string), or - # can they be inline? super end end diff --git a/spec/unit/iiif/v3/presentation/layer_spec.rb b/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb similarity index 59% rename from spec/unit/iiif/v3/presentation/layer_spec.rb rename to spec/unit/iiif/v3/presentation/annotation_collection_spec.rb index 2ba91a7..7bd22af 100644 --- a/spec/unit/iiif/v3/presentation/layer_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb @@ -1,23 +1,19 @@ -describe IIIF::V3::Presentation::Layer do +describe IIIF::V3::Presentation::AnnotationCollection do let(:fixed_values) do { - 'id' => 'http://www.example.org/iiif/book1/layer/transcription', - 'type' => 'Layer', + 'id' => 'http://www.example.org/iiif/book1/annoColl/transcription', + 'type' => 'AnnotationCollection', 'label' => 'Diplomatic Transcription', - 'otherContent' => [ - 'http://www.example.org/iiif/book1/list/l1', - 'http://www.example.org/iiif/book1/list/l2', - 'http://www.example.org/iiif/book1/list/l3', - 'http://www.example.org/iiif/book1/list/l4' - ] + 'first' => 'http://www.example.org/iiif/book1/list/l1', + 'last' => 'http://www.example.org/iiif/book1/list/l4', } end describe '#initialize' do it 'sets type' do - expect(subject['type']).to eq 'Layer' + expect(subject['type']).to eq 'AnnotationCollection' end end From da23df5c41e3dac77cca06484da63cbb75bb1274 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Wed, 28 Jun 2017 15:22:40 -0700 Subject: [PATCH 10/91] Make canvas width and height optional --- lib/iiif/v3/presentation/canvas.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index f3f3118..81401a1 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -10,7 +10,7 @@ class Canvas < AbstractResource TYPE = 'Canvas' def required_keys - super + %w{ id width height label } + super + %w{ id label } end def any_type_keys From d39f8c271337041b20929f657773cf6f188d7d4f Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 28 Jun 2017 16:00:39 -0700 Subject: [PATCH 11/91] v3 add @context back in --- lib/iiif/v3/presentation.rb | 6 ++++-- lib/iiif/v3/presentation/abstract_resource.rb | 13 ++++++------- spec/fixtures/v3/manifests/complete_from_spec.json | 5 +++++ spec/fixtures/v3/manifests/minimal.json | 5 +++++ spec/fixtures/v3/manifests/service_only.json | 6 +++++- spec/integration/iiif/v3/service_spec.rb | 4 ++++ .../iiif/v3/presentation/abstract_resource_spec.rb | 4 ++-- .../v3/presentation/annotation_collection_spec.rb | 4 ++++ spec/unit/iiif/v3/presentation/canvas_spec.rb | 4 ++++ spec/unit/iiif/v3/presentation/collection_spec.rb | 4 ++++ 10 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 6d4e938..16c393e 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -20,8 +20,10 @@ module IIIF module V3 module Presentation - # TODO: when v3 is baked, there will be a context - # CONTEXT ||= 'http://iiif.io/api/presentation/2/context.json' + CONTEXT ||= [ + 'http://www.w3.org/ns/anno.jsonld', + 'http://iiif.io/api/presentation/3/context.json' + ] class MissingRequiredKeyError < StandardError; end class IllegalValueError < StandardError; end diff --git a/lib/iiif/v3/presentation/abstract_resource.rb b/lib/iiif/v3/presentation/abstract_resource.rb index 334a380..ed37f3a 100644 --- a/lib/iiif/v3/presentation/abstract_resource.rb +++ b/lib/iiif/v3/presentation/abstract_resource.rb @@ -47,9 +47,9 @@ def legal_viewing_hint_values end # Initialize a Presentation node - # @param [Hash] hsh - Anything in this hash will be added to the Object.' + # @param [Hash] hsh - Anything in this hash will be added to the Object. # Order is only guaranteed if an ActiveSupport::OrderedHash is passed. - # @param [boolean] include_context (default: false). Pass true if the' + # @param [boolean] include_context (default: false). Pass true if the # context should be included. def initialize(hsh={}) if self.class == IIIF::V3::Presentation::AbstractResource @@ -66,11 +66,10 @@ def initialize(hsh={}) # * sort_json_ld_keys: (true|false). Brings all properties starting with # '@'. Default: true. to the top of the document and sorts them. def to_ordered_hash(opts={}) - # TODO: when v3 is baked, there will be a context - # include_context = opts.fetch(:include_context, true) - # if include_context && !self.has_key?('@context') - # self['@context'] = IIIF::V3::Presentation::CONTEXT - # end + include_context = opts.fetch(:include_context, true) + if include_context && !self.has_key?('@context') + self['@context'] = IIIF::V3::Presentation::CONTEXT + end super(opts) end diff --git a/spec/fixtures/v3/manifests/complete_from_spec.json b/spec/fixtures/v3/manifests/complete_from_spec.json index 74a5fff..f92bbb2 100644 --- a/spec/fixtures/v3/manifests/complete_from_spec.json +++ b/spec/fixtures/v3/manifests/complete_from_spec.json @@ -1,4 +1,8 @@ { + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], "id": "http://www.example.org/iiif/book1/manifest", "type": "Manifest", "label": "Book 1", @@ -64,6 +68,7 @@ "height": 2000, "width": 1500, "service": { + "@context": "http://iiif.io/api/image/2/context.json", "id": "http://www.example.org/images/book1-page1", "profile": "http://iiif.io/api/image/2/level1.json" } diff --git a/spec/fixtures/v3/manifests/minimal.json b/spec/fixtures/v3/manifests/minimal.json index 48833d1..09027b8 100644 --- a/spec/fixtures/v3/manifests/minimal.json +++ b/spec/fixtures/v3/manifests/minimal.json @@ -1,4 +1,8 @@ { + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], "type": "Manifest", "id": "http://www.example.org/iiif/book1/manifest", "label": "Book 1", @@ -29,6 +33,7 @@ "height": 2000, "width": 1500, "service": { + "@context": "http://iiif.io/api/image/2/context.json", "id": "http://www.example.org/images/book1-page1", "profile": "http://iiif.io/api/image/2/level1.json" } diff --git a/spec/fixtures/v3/manifests/service_only.json b/spec/fixtures/v3/manifests/service_only.json index 68342a9..f31d7c2 100644 --- a/spec/fixtures/v3/manifests/service_only.json +++ b/spec/fixtures/v3/manifests/service_only.json @@ -1,9 +1,13 @@ { + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], "type": "Manifest", "id": "http://www.example.org/iiif/book1/manifest", "label": "Book 1", "service": { - "context": "http://example.org/ns/jsonld/context.json", + "@context": "http://example.org/ns/jsonld/context.json", "id": "http://example.org/service/example", "profile": "http://example.org/docs/example-service.html" } diff --git a/spec/integration/iiif/v3/service_spec.rb b/spec/integration/iiif/v3/service_spec.rb index 1e9ef20..d80931e 100644 --- a/spec/integration/iiif/v3/service_spec.rb +++ b/spec/integration/iiif/v3/service_spec.rb @@ -40,6 +40,10 @@ describe 'self#from_ordered_hash' do let(:fixture) { JSON.parse('{ + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], "id": "http://example.com/manifest", "type": "Manifest", "label": "My Manifest", diff --git a/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb b/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb index d2bf99d..e6b5e42 100644 --- a/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb @@ -98,11 +98,11 @@ def required_keys end describe '#to_ordered_hash' do - describe 'does not add the @context' do + describe 'adds the @context' do before(:each) { subject.delete('@context') } it 'by default' do expect(subject.has_key?('@context')).to be_falsey - expect(subject.to_ordered_hash.has_key?('@context')).to be_falsey + expect(subject.to_ordered_hash.has_key?('@context')).to be_truthy end it 'unless you say not to' do expect(subject.has_key?('@context')).to be_falsey diff --git a/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb b/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb index 7bd22af..90b2a4a 100644 --- a/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb @@ -2,6 +2,10 @@ let(:fixed_values) do { + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], 'id' => 'http://www.example.org/iiif/book1/annoColl/transcription', 'type' => 'AnnotationCollection', 'label' => 'Diplomatic Transcription', diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index c228a88..fa61c91 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -2,6 +2,10 @@ let(:fixed_values) do { + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], "id" => "http://www.example.org/iiif/book1/canvas/p1", "type" => "Canvas", "label" => "p. 1", diff --git a/spec/unit/iiif/v3/presentation/collection_spec.rb b/spec/unit/iiif/v3/presentation/collection_spec.rb index 3cc1547..ad4c812 100644 --- a/spec/unit/iiif/v3/presentation/collection_spec.rb +++ b/spec/unit/iiif/v3/presentation/collection_spec.rb @@ -2,6 +2,10 @@ let(:fixed_values) do { + "@context": [ + "http://iiif.io/api/presentation/3/context.json", + "http://www.w3.org/ns/anno.jsonld" + ], 'id' => 'http://example.org/iiif/collection/top', 'type' => 'Collection', 'label' => 'Top Level Collection for Example Organization', From 4c94642217bff7d21915b05b6c4f8b7db962cc9c Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 28 Jun 2017 16:05:45 -0700 Subject: [PATCH 12/91] v3 remove dctypes prefix from image_resource type --- lib/iiif/v3/presentation/image_resource.rb | 6 +++--- spec/fixtures/v3/manifests/complete_from_spec.json | 6 +++--- spec/fixtures/v3/manifests/minimal.json | 2 +- .../integration/iiif/v3/presentation/image_resource_spec.rb | 4 ++-- spec/unit/iiif/v3/presentation/image_resource_spec.rb | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index b62e764..199f6a2 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -7,7 +7,7 @@ module V3 module Presentation class ImageResource < Resource - TYPE = 'dctypes:Image' + TYPE = 'Image' def int_only_keys super + %w{ width height } @@ -50,8 +50,8 @@ class << self # # { # "id":"http://www.example.org/iiif/book1/res/page1.jpg", - # "type":"dctypes:Image", - # "format":"image/jpeg", + # "type": "Image", + # "format": "image/jpeg", # "service": { # "@context": "http://iiif.io/api/image/2/context.json", # "id":"http://www.example.org/images/book1-page1", diff --git a/spec/fixtures/v3/manifests/complete_from_spec.json b/spec/fixtures/v3/manifests/complete_from_spec.json index f92bbb2..fbf7784 100644 --- a/spec/fixtures/v3/manifests/complete_from_spec.json +++ b/spec/fixtures/v3/manifests/complete_from_spec.json @@ -63,7 +63,7 @@ "target": "http://www.example.org/iiif/book1/canvas/p1", "resource": { "id": "http://www.example.org/iiif/book1/res/page1.jpg", - "type": "dctypes:Image", + "type": "Image", "format": "image/jpeg", "height": 2000, "width": 1500, @@ -98,7 +98,7 @@ "motivation": "painting", "resource": { "id": "http://www.example.org/images/book1-page2/full/1500,2000/0/default.jpg", - "type": "dctypes:Image", + "type": "Image", "format": "image/jpeg", "height": 2000, "width": 1500, @@ -149,7 +149,7 @@ "target": "http://www.example.org/iiif/book1/canvas/p3", "resource": { "id": "http://www.example.org/iiif/book1/res/page3.jpg", - "type": "dctypes:Image", + "type": "Image", "format": "image/jpeg", "height": 2000, "width": 1500, diff --git a/spec/fixtures/v3/manifests/minimal.json b/spec/fixtures/v3/manifests/minimal.json index 09027b8..f8f7827 100644 --- a/spec/fixtures/v3/manifests/minimal.json +++ b/spec/fixtures/v3/manifests/minimal.json @@ -28,7 +28,7 @@ "target": "http://www.example.org/iiif/book1/canvas/p1", "resource": { "id": "http://www.example.org/iiif/book1/res/page1.jpg", - "type": "dctypes:Image", + "type": "Image", "format": "image/jpeg", "height": 2000, "width": 1500, diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb index 655df6c..f257cf4 100644 --- a/spec/integration/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -32,7 +32,7 @@ # expect(resource['@context']).to eq 'http://iiif.io/api/presentation/2/context.json' # @context is only added when we call to_json... expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' - expect(resource['type']).to eq 'dctypes:Image' + expect(resource['type']).to eq 'Image' expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 @@ -45,7 +45,7 @@ resource = described_class.create_image_api_image_resource(opts) expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' - expect(resource['type']).to eq 'dctypes:Image' + expect(resource['type']).to eq 'Image' expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 diff --git a/spec/unit/iiif/v3/presentation/image_resource_spec.rb b/spec/unit/iiif/v3/presentation/image_resource_spec.rb index e0951c1..43d241f 100644 --- a/spec/unit/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/image_resource_spec.rb @@ -1,8 +1,8 @@ describe IIIF::V3::Presentation::ImageResource do describe '#initialize' do - it 'sets type to dctypes:Image' do - expect(subject['type']).to eq 'dctypes:Image' + it 'sets type to Image' do + expect(subject['type']).to eq 'Image' end end From e050d254d2666db1b0aacc0f65182a807d4e4d3f Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 28 Jun 2017 16:15:03 -0700 Subject: [PATCH 13/91] v3 Annotations have 'body' not 'resource' --- lib/iiif/v3/presentation/annotation.rb | 2 +- lib/iiif/v3/service.rb | 2 +- spec/fixtures/v3/manifests/complete_from_spec.json | 6 +++--- spec/fixtures/v3/manifests/minimal.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb index aabe76d..4ab3c03 100644 --- a/lib/iiif/v3/presentation/annotation.rb +++ b/lib/iiif/v3/presentation/annotation.rb @@ -12,7 +12,7 @@ def required_keys end def abstract_resource_only_keys - super + [ { key: 'resource', type: IIIF::V3::Presentation::Resource } ] + super + [ { key: 'body', type: IIIF::V3::Presentation::Resource } ] end def initialize(hsh={}) diff --git a/lib/iiif/v3/service.rb b/lib/iiif/v3/service.rb index 82c9426..ac545ae 100644 --- a/lib/iiif/v3/service.rb +++ b/lib/iiif/v3/service.rb @@ -209,7 +209,7 @@ def self.from_ordered_hash(hsh, default_klass=IIIF::V3::OrderedHash) new_key = key.underscore == key ? key : key.underscore if new_key == 'service' new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key], IIIF::V3::Service) - elsif new_key == 'resource' + elsif new_key == 'body' new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Resource) elsif hsh[key].kind_of?(Hash) new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key]) diff --git a/spec/fixtures/v3/manifests/complete_from_spec.json b/spec/fixtures/v3/manifests/complete_from_spec.json index fbf7784..f30649a 100644 --- a/spec/fixtures/v3/manifests/complete_from_spec.json +++ b/spec/fixtures/v3/manifests/complete_from_spec.json @@ -61,7 +61,7 @@ "type": "Annotation", "motivation": "painting", "target": "http://www.example.org/iiif/book1/canvas/p1", - "resource": { + "body": { "id": "http://www.example.org/iiif/book1/res/page1.jpg", "type": "Image", "format": "image/jpeg", @@ -96,7 +96,7 @@ { "type": "Annotation", "motivation": "painting", - "resource": { + "body": { "id": "http://www.example.org/images/book1-page2/full/1500,2000/0/default.jpg", "type": "Image", "format": "image/jpeg", @@ -147,7 +147,7 @@ "type": "Annotation", "motivation": "painting", "target": "http://www.example.org/iiif/book1/canvas/p3", - "resource": { + "body": { "id": "http://www.example.org/iiif/book1/res/page3.jpg", "type": "Image", "format": "image/jpeg", diff --git a/spec/fixtures/v3/manifests/minimal.json b/spec/fixtures/v3/manifests/minimal.json index f8f7827..e4db4d4 100644 --- a/spec/fixtures/v3/manifests/minimal.json +++ b/spec/fixtures/v3/manifests/minimal.json @@ -26,7 +26,7 @@ "type": "Annotation", "motivation": "painting", "target": "http://www.example.org/iiif/book1/canvas/p1", - "resource": { + "body": { "id": "http://www.example.org/iiif/book1/res/page1.jpg", "type": "Image", "format": "image/jpeg", From 813478701fc5931a9f5772e1f02c81fb47194033 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 28 Jun 2017 16:36:51 -0700 Subject: [PATCH 14/91] v3 update Range --- lib/iiif/v3/presentation/range.rb | 4 +-- .../v3/manifests/complete_from_spec.json | 17 ++++++++--- spec/unit/iiif/v3/presentation/range_spec.rb | 29 ++++++++++++++----- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/iiif/v3/presentation/range.rb b/lib/iiif/v3/presentation/range.rb index 8d54e90..b703db5 100644 --- a/lib/iiif/v3/presentation/range.rb +++ b/lib/iiif/v3/presentation/range.rb @@ -12,7 +12,7 @@ def required_keys end def array_only_keys - super + %w{ ranges } + super + %w{ members } end def legal_viewing_hint_values @@ -25,7 +25,7 @@ def initialize(hsh={}) end def validate - # Values of the ranges array must be strings + # Values of the members array must be canvas or range end end end diff --git a/spec/fixtures/v3/manifests/complete_from_spec.json b/spec/fixtures/v3/manifests/complete_from_spec.json index f30649a..5e47642 100644 --- a/spec/fixtures/v3/manifests/complete_from_spec.json +++ b/spec/fixtures/v3/manifests/complete_from_spec.json @@ -176,10 +176,19 @@ "id": "http://www.example.org/iiif/book1/range/r1", "type": "Range", "label": "Introduction", - "canvases": [ - "http://www.example.org/iiif/book1/canvas/p1", - "http://www.example.org/iiif/book1/canvas/p2", - "http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300" + "members": [ + { + "id": "http://www.example.org/iiif/book1/canvas/p1", + "type": "Canvas" + }, + { + "id": "http://www.example.org/iiif/book1/canvas/p2", + "type": "Canvas" + }, + { + "id": "http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300", + "type": "Canvas" + } ] } ] diff --git a/spec/unit/iiif/v3/presentation/range_spec.rb b/spec/unit/iiif/v3/presentation/range_spec.rb index bde4721..0fe78d6 100644 --- a/spec/unit/iiif/v3/presentation/range_spec.rb +++ b/spec/unit/iiif/v3/presentation/range_spec.rb @@ -5,14 +5,27 @@ 'id' => 'http://www.example.org/iiif/book1/range/r1', 'type' => 'Range', 'label' => 'Introduction', - 'ranges' => [ - 'http://www.example.org/iiif/book1/range/r1-1', - 'http://www.example.org/iiif/book1/range/r1-2' - ], - 'canvases' => [ - 'http://www.example.org/iiif/book1/canvas/p1', - 'http://www.example.org/iiif/book1/canvas/p2', - 'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300' + 'members' => [ + { + "id": 'http://www.example.org/iiif/book1/range/r1-1', + "type": "Range" + }, + { + "id": 'http://www.example.org/iiif/book1/range/r1-2', + "type": "Range" + }, + { + "id": 'http://www.example.org/iiif/book1/canvas/p1', + "type": "Canvas" + }, + { + "id": 'http://www.example.org/iiif/book1/canvas/p2', + "type": "Canvas" + }, + { + "id": 'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300', + "type": "Canvas" + } ] } end From 212548eb79419ad8d44a9898aa3a1cda39deb49a Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 12:06:01 -0700 Subject: [PATCH 15/91] Fix Ruby 2.1 compatibility --- .../annotation_collection_spec.rb | 6 +++--- spec/unit/iiif/v3/presentation/canvas_spec.rb | 2 +- .../iiif/v3/presentation/collection_spec.rb | 2 +- spec/unit/iiif/v3/presentation/range_spec.rb | 20 +++++++++---------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb b/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb index 90b2a4a..0d796cd 100644 --- a/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb @@ -2,9 +2,9 @@ let(:fixed_values) do { - "@context": [ - "http://iiif.io/api/presentation/3/context.json", - "http://www.w3.org/ns/anno.jsonld" + '@context' => [ + 'http://iiif.io/api/presentation/3/context.json', + 'http://www.w3.org/ns/anno.jsonld' ], 'id' => 'http://www.example.org/iiif/book1/annoColl/transcription', 'type' => 'AnnotationCollection', diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index fa61c91..8043f17 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -2,7 +2,7 @@ let(:fixed_values) do { - "@context": [ + "@context" => [ "http://iiif.io/api/presentation/3/context.json", "http://www.w3.org/ns/anno.jsonld" ], diff --git a/spec/unit/iiif/v3/presentation/collection_spec.rb b/spec/unit/iiif/v3/presentation/collection_spec.rb index ad4c812..229418f 100644 --- a/spec/unit/iiif/v3/presentation/collection_spec.rb +++ b/spec/unit/iiif/v3/presentation/collection_spec.rb @@ -2,7 +2,7 @@ let(:fixed_values) do { - "@context": [ + "@context" => [ "http://iiif.io/api/presentation/3/context.json", "http://www.w3.org/ns/anno.jsonld" ], diff --git a/spec/unit/iiif/v3/presentation/range_spec.rb b/spec/unit/iiif/v3/presentation/range_spec.rb index 0fe78d6..2db2565 100644 --- a/spec/unit/iiif/v3/presentation/range_spec.rb +++ b/spec/unit/iiif/v3/presentation/range_spec.rb @@ -7,24 +7,24 @@ 'label' => 'Introduction', 'members' => [ { - "id": 'http://www.example.org/iiif/book1/range/r1-1', - "type": "Range" + "id" => 'http://www.example.org/iiif/book1/range/r1-1', + "type" => "Range" }, { - "id": 'http://www.example.org/iiif/book1/range/r1-2', - "type": "Range" + "id" => 'http://www.example.org/iiif/book1/range/r1-2', + "type" => "Range" }, { - "id": 'http://www.example.org/iiif/book1/canvas/p1', - "type": "Canvas" + "id" => 'http://www.example.org/iiif/book1/canvas/p1', + "type" => "Canvas" }, { - "id": 'http://www.example.org/iiif/book1/canvas/p2', - "type": "Canvas" + "id" => 'http://www.example.org/iiif/book1/canvas/p2', + "type" => "Canvas" }, { - "id": 'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300', - "type": "Canvas" + "id" => 'http://www.example.org/iiif/book1/canvas/p3#xywh=0,0,750,300', + "type" => "Canvas" } ] } From f62918bc551f1cafb158f3de1b0ae835989baa60 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:34:56 -0700 Subject: [PATCH 16/91] Refactor service accessor initialization to remove duplicate code --- lib/iiif/service.rb | 129 ++++++++++---------------------------------- 1 file changed, 27 insertions(+), 102 deletions(-) diff --git a/lib/iiif/service.rb b/lib/iiif/service.rb index 91d229f..351af6d 100644 --- a/lib/iiif/service.rb +++ b/lib/iiif/service.rb @@ -24,6 +24,7 @@ def initialize(hsh={}) self.define_methods_for_array_only_keys self.define_methods_for_string_only_keys self.define_methods_for_int_only_keys + self.define_methods_for_numeric_only_keys self.define_methods_for_abstract_resource_only_keys self.snakeize_keys end @@ -255,56 +256,22 @@ def data end def define_methods_for_any_type_keys + define_accessor_methods(*any_type_keys) + + # override the getter defined by define_accessor_methods to avoid returning + # an array for empty values. any_type_keys.each do |key| - # Setters - define_singleton_method("#{key}=") do |arg| - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - self.send('[]=', key, arg) - end - end - # Getters define_singleton_method(key) do self.send('[]', key) end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end - end end end def define_methods_for_array_only_keys - array_only_keys.each do |key| - # Setters - define_singleton_method("#{key}=") do |arg| - unless arg.kind_of?(Array) - m = "#{key} must be an Array." - raise IIIF::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(Array) - m = "#{key} must be an Array." - raise IIIF::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - end - # Getters - define_singleton_method(key) do - self[key] ||= [] - self[key] - end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end + define_accessor_methods(*array_only_keys) do |key, arg| + unless arg.kind_of?(Array) + m = "#{key} must be an Array." + raise IIIF::Presentation::IllegalValueError, m end end end @@ -314,85 +281,44 @@ def define_methods_for_abstract_resource_only_keys abstract_resource_only_keys.each do |hsh| key = hsh[:key] type = hsh[:type] - # Setters - define_singleton_method("#{key}=") do |arg| + + define_accessor_methods(key) do |key, arg| unless arg.kind_of?(type) m = "#{key} must be an #{type}." raise IIIF::Presentation::IllegalValueError, m end - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(type) - m = "#{key} must be an #{type}." - raise IIIF::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - end - # Getters - define_singleton_method(key) do - self[key] ||= [] - self[key] - end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end end end end - def define_methods_for_string_only_keys - string_only_keys.each do |key| - # Setter - define_singleton_method("#{key}=") do |arg| - unless arg.kind_of?(String) - m = "#{key} must be an String." - raise IIIF::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(String) - m = "#{key} must be an String." - raise IIIF::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - end - # Getter - define_singleton_method(key) do - self[key] ||= [] - self[key] - end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end + define_accessor_methods(*string_only_keys) do |key, arg| + unless arg.kind_of?(String) + m = "#{key} must be an String." + raise IIIF::Presentation::IllegalValueError, m end end end def define_methods_for_int_only_keys - int_only_keys.each do |key| + define_accessor_methods(*int_only_keys) do |key, arg| + unless arg.kind_of?(Integer) && arg > 0 + m = "#{key} must be a positive Integer." + raise IIIF::Presentation::IllegalValueError, m + end + end + end + + def define_accessor_methods(*keys, &validation) + keys.each do |key| # Setter define_singleton_method("#{key}=") do |arg| - unless arg.kind_of?(Integer) && arg > 0 - m = "#{key} must be a positive Integer." - raise IIIF::Presentation::IllegalValueError, m - end + validation.call(key, arg) if block_given? self.send('[]=', key, arg) end if key.camelize(:lower) != key define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(Integer) && arg > 0 - m = "#{key} must be a positive Integer." - raise IIIF::Presentation::IllegalValueError, m - end + validation.call(key, arg) if block_given? self.send('[]=', key, arg) end end @@ -408,6 +334,5 @@ def define_methods_for_int_only_keys end end end - end end From 962d34afb1e9e9180f307559de280808f2aa40f1 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:38:08 -0700 Subject: [PATCH 17/91] Add support for canvas duration, a non-negative floating point attribute --- lib/iiif/presentation/canvas.rb | 4 ++ lib/iiif/service.rb | 10 ++++ spec/unit/iiif/presentation/canvas_spec.rb | 5 +- .../shared_examples/numeric_only_keys_spec.rb | 47 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb diff --git a/lib/iiif/presentation/canvas.rb b/lib/iiif/presentation/canvas.rb index a3748d8..5a3edbd 100644 --- a/lib/iiif/presentation/canvas.rb +++ b/lib/iiif/presentation/canvas.rb @@ -25,6 +25,10 @@ def int_only_keys super + %w{ width height } end + def numeric_only_keys + super + %w{ duration } + end + def legal_viewing_hint_values super + %w{ non-paged } end diff --git a/lib/iiif/service.rb b/lib/iiif/service.rb index 351af6d..f023708 100644 --- a/lib/iiif/service.rb +++ b/lib/iiif/service.rb @@ -17,6 +17,7 @@ def array_only_keys; %w{ }; end def abstract_resource_only_keys; %w{ }; end def hash_only_keys; %w{ }; end def int_only_keys; %w{ }; end + def numeric_only_keys; %w{ }; end def initialize(hsh={}) @data = IIIF::OrderedHash[hsh] @@ -309,6 +310,15 @@ def define_methods_for_int_only_keys end end + def define_methods_for_numeric_only_keys + define_accessor_methods(*numeric_only_keys) do |key, arg| + unless arg.kind_of?(Numeric) && arg > 0 + m = "#{key} must be a positive Integer or Float." + raise IIIF::Presentation::IllegalValueError, m + end + end + end + def define_accessor_methods(*keys, &validation) keys.each do |key| # Setter diff --git a/spec/unit/iiif/presentation/canvas_spec.rb b/spec/unit/iiif/presentation/canvas_spec.rb index fb65955..543fcfa 100644 --- a/spec/unit/iiif/presentation/canvas_spec.rb +++ b/spec/unit/iiif/presentation/canvas_spec.rb @@ -24,6 +24,10 @@ it_behaves_like 'it has the appropriate methods for integer-only keys' end + describe "#{described_class}.numeric_only_keys" do + it_behaves_like 'it has the appropriate methods for numeric-only keys' + end + describe "#{described_class}.define_methods_for_string_only_keys" do it_behaves_like 'it has the appropriate methods for string-only keys' end @@ -43,4 +47,3 @@ end end - diff --git a/spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb b/spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb new file mode 100644 index 0000000..0e2a706 --- /dev/null +++ b/spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb @@ -0,0 +1,47 @@ +require 'set' + +shared_examples 'it has the appropriate methods for numeric-only keys' do + + described_class.new.numeric_only_keys.each do |prop| + + describe "#{prop}=" do + before(:all) do + @ex = 7200.0 + end + it "sets #{prop}" do + subject.send("#{prop}=", @ex) + expect(subject[prop]).to eq @ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}=" do + subject.send("#{prop.camelize(:lower)}=", @ex) + expect(subject[prop]).to eq @ex + end + end + it 'raises an exception when attempting to set it to something other than an Integer' do + expect { subject.send("#{prop}=", 'Foo') }.to raise_error IIIF::Presentation::IllegalValueError + end + it 'raises an exception when attempting to set it to a negative number' do + expect { subject.send("#{prop}=", -1.0) }.to raise_error IIIF::Presentation::IllegalValueError + end + end + + describe "#{prop}" do + before(:all) do + @ex = 7200.0 + end + it "gets #{prop}" do + subject[prop] = @ex + expect(subject.send(prop)).to eq @ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}" do + subject[prop] = @ex + expect(subject.send("#{prop.camelize(:lower)}")).to eq @ex + end + end + end + + end + +end From 5f42e5ea88ae9e64be650cdb2ad5adbdcf275e13 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:42:28 -0700 Subject: [PATCH 18/91] Port numeric support over to v3 manifests --- lib/iiif/v3/presentation/resource.rb | 4 + lib/iiif/v3/service.rb | 136 +++++------------- spec/unit/iiif/presentation/canvas_spec.rb | 4 - spec/unit/iiif/v3/presentation/canvas_spec.rb | 4 + .../iiif/v3/presentation/resource_spec.rb | 4 + .../shared_examples/numeric_only_keys_spec.rb | 2 +- 6 files changed, 49 insertions(+), 105 deletions(-) rename spec/unit/iiif/{ => v3}/presentation/shared_examples/numeric_only_keys_spec.rb (99%) diff --git a/lib/iiif/v3/presentation/resource.rb b/lib/iiif/v3/presentation/resource.rb index eb36101..6514ba5 100644 --- a/lib/iiif/v3/presentation/resource.rb +++ b/lib/iiif/v3/presentation/resource.rb @@ -13,6 +13,10 @@ def string_only_keys super + %w{ format } end + def numeric_only_keys + super + %w{ duration } + end + def initialize(hsh={}) super(hsh) end diff --git a/lib/iiif/v3/service.rb b/lib/iiif/v3/service.rb index ac545ae..2819400 100644 --- a/lib/iiif/v3/service.rb +++ b/lib/iiif/v3/service.rb @@ -18,6 +18,7 @@ def array_only_keys; %w{ }; end def abstract_resource_only_keys; %w{ }; end def hash_only_keys; %w{ }; end def int_only_keys; %w{ }; end + def numeric_only_keys; %w{ }; end def initialize(hsh={}) @data = IIIF::V3::OrderedHash[hsh] @@ -25,6 +26,7 @@ def initialize(hsh={}) self.define_methods_for_array_only_keys self.define_methods_for_string_only_keys self.define_methods_for_int_only_keys + self.define_methods_for_numeric_only_keys self.define_methods_for_abstract_resource_only_keys self.snakeize_keys end @@ -254,56 +256,22 @@ def data end def define_methods_for_any_type_keys + define_accessor_methods(*any_type_keys) + + # override the getter defined by define_accessor_methods to avoid returning + # an array for empty values. any_type_keys.each do |key| - # Setters - define_singleton_method("#{key}=") do |arg| - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - self.send('[]=', key, arg) - end - end - # Getters define_singleton_method(key) do self.send('[]', key) end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end - end end end def define_methods_for_array_only_keys - array_only_keys.each do |key| - # Setters - define_singleton_method("#{key}=") do |arg| - unless arg.kind_of?(Array) - m = "#{key} must be an Array." - raise IIIF::V3::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(Array) - m = "#{key} must be an Array." - raise IIIF::V3::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - end - # Getters - define_singleton_method(key) do - self[key] ||= [] - self[key] - end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end + define_accessor_methods(*array_only_keys) do |key, arg| + unless arg.kind_of?(Array) + m = "#{key} must be an Array." + raise IIIF::V3::Presentation::IllegalValueError, m end end end @@ -313,85 +281,53 @@ def define_methods_for_abstract_resource_only_keys abstract_resource_only_keys.each do |hsh| key = hsh[:key] type = hsh[:type] - # Setters - define_singleton_method("#{key}=") do |arg| + + define_accessor_methods(key) do |key, arg| unless arg.kind_of?(type) m = "#{key} must be an #{type}." raise IIIF::V3::Presentation::IllegalValueError, m end - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(type) - m = "#{key} must be an #{type}." - raise IIIF::V3::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - end - # Getters - define_singleton_method(key) do - self[key] ||= [] - self[key] - end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end end end end - def define_methods_for_string_only_keys - string_only_keys.each do |key| - # Setter - define_singleton_method("#{key}=") do |arg| - unless arg.kind_of?(String) - m = "#{key} must be an String." - raise IIIF::V3::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(String) - m = "#{key} must be an String." - raise IIIF::V3::Presentation::IllegalValueError, m - end - self.send('[]=', key, arg) - end + define_accessor_methods(*string_only_keys) do |key, arg| + unless arg.kind_of?(String) + m = "#{key} must be an String." + raise IIIF::V3::Presentation::IllegalValueError, m end - # Getter - define_singleton_method(key) do - self[key] ||= [] - self[key] + end + end + + def define_methods_for_int_only_keys + define_accessor_methods(*int_only_keys) do |key, arg| + unless arg.kind_of?(Integer) && arg > 0 + m = "#{key} must be a positive Integer." + raise IIIF::V3::Presentation::IllegalValueError, m end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end + end + end + + def define_methods_for_numeric_only_keys + define_accessor_methods(*numeric_only_keys) do |key, arg| + unless arg.kind_of?(Numeric) && arg > 0 + m = "#{key} must be a positive Integer or Float." + raise IIIF::Presentation::IllegalValueError, m end end end - def define_methods_for_int_only_keys - int_only_keys.each do |key| + def define_accessor_methods(*keys, &validation) + keys.each do |key| # Setter define_singleton_method("#{key}=") do |arg| - unless arg.kind_of?(Integer) && arg > 0 - m = "#{key} must be a positive Integer." - raise IIIF::V3::Presentation::IllegalValueError, m - end + validation.call(key, arg) if block_given? self.send('[]=', key, arg) end if key.camelize(:lower) != key define_singleton_method("#{key.camelize(:lower)}=") do |arg| - unless arg.kind_of?(Integer) && arg > 0 - m = "#{key} must be a positive Integer." - raise IIIF::V3::Presentation::IllegalValueError, m - end + validation.call(key, arg) if block_given? self.send('[]=', key, arg) end end diff --git a/spec/unit/iiif/presentation/canvas_spec.rb b/spec/unit/iiif/presentation/canvas_spec.rb index 543fcfa..8f14585 100644 --- a/spec/unit/iiif/presentation/canvas_spec.rb +++ b/spec/unit/iiif/presentation/canvas_spec.rb @@ -24,10 +24,6 @@ it_behaves_like 'it has the appropriate methods for integer-only keys' end - describe "#{described_class}.numeric_only_keys" do - it_behaves_like 'it has the appropriate methods for numeric-only keys' - end - describe "#{described_class}.define_methods_for_string_only_keys" do it_behaves_like 'it has the appropriate methods for string-only keys' end diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index 8043f17..056f05a 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -26,6 +26,10 @@ it_behaves_like 'it has the appropriate methods for integer-only keys v3' end + describe "#{described_class}.numeric_only_keys" do + it_behaves_like 'it has the appropriate methods for numeric-only keys v3' + end + describe "#{described_class}.define_methods_for_string_only_keys" do it_behaves_like 'it has the appropriate methods for string-only keys v3' end diff --git a/spec/unit/iiif/v3/presentation/resource_spec.rb b/spec/unit/iiif/v3/presentation/resource_spec.rb index eec8f2d..a004f40 100644 --- a/spec/unit/iiif/v3/presentation/resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/resource_spec.rb @@ -8,6 +8,10 @@ it_behaves_like 'it has the appropriate methods for integer-only keys v3' end + describe "#{described_class}.numeric_only_keys" do + it_behaves_like 'it has the appropriate methods for numeric-only keys v3' + end + describe "#{described_class}.define_methods_for_string_only_keys" do it_behaves_like 'it has the appropriate methods for string-only keys v3' end diff --git a/spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys_spec.rb similarity index 99% rename from spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb rename to spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys_spec.rb index 0e2a706..14dbb97 100644 --- a/spec/unit/iiif/presentation/shared_examples/numeric_only_keys_spec.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys_spec.rb @@ -1,6 +1,6 @@ require 'set' -shared_examples 'it has the appropriate methods for numeric-only keys' do +shared_examples 'it has the appropriate methods for numeric-only keys v3' do described_class.new.numeric_only_keys.each do |prop| From 0ea3015b2eeaa22f5ab0bedd77ed0427f1c72db9 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:45:11 -0700 Subject: [PATCH 19/91] Add auto-advanced as a legal viewing hint value --- lib/iiif/v3/presentation/collection.rb | 4 ++++ lib/iiif/v3/presentation/manifest.rb | 2 +- lib/iiif/v3/presentation/sequence.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/iiif/v3/presentation/collection.rb b/lib/iiif/v3/presentation/collection.rb index 9bcafa7..dc0ec84 100644 --- a/lib/iiif/v3/presentation/collection.rb +++ b/lib/iiif/v3/presentation/collection.rb @@ -15,6 +15,10 @@ def array_only_keys super + %w{ collections manifests } end + def legal_viewing_hint_values + %w{ auto-advance } + end + def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' super(hsh) diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index 81ca44e..6f65196 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -20,7 +20,7 @@ def array_only_keys end def legal_viewing_hint_values - %w{ individuals paged continuous none } + %w{ individuals paged continuous auto-advance none } end def initialize(hsh={}) diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index bb43c86..657b74b 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -16,7 +16,7 @@ def string_only_keys end def legal_viewing_hint_values - %w{ individuals paged continuous } + %w{ individuals paged continuous auto-advance } end def initialize(hsh={}) From 5ed2b8fc7b83b1e9d546ca48663bc25280786c20 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:50:03 -0700 Subject: [PATCH 20/91] Add timeMode property to Annotation --- lib/iiif/v3/presentation/annotation.rb | 20 +++++++++++++++++++ .../iiif/v3/presentation/annotation_spec.rb | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb index 4ab3c03..9208c31 100644 --- a/lib/iiif/v3/presentation/annotation.rb +++ b/lib/iiif/v3/presentation/annotation.rb @@ -15,11 +15,31 @@ def abstract_resource_only_keys super + [ { key: 'body', type: IIIF::V3::Presentation::Resource } ] end + def string_only_keys + super + %w{ time_mode } + end + + def legal_time_mode_values + %w{ trim scale loop } + end + def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' hsh['motivation'] = 'painting' unless hsh.has_key? 'motivation' super(hsh) end + + def validate + super + + # time mode values + if self.has_key?('time_mode') + unless self.legal_time_mode_values.include?(self['time_mode']) + m = "timeMode for #{self.class} must be one of #{self.legal_time_mode_values}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end end end end diff --git a/spec/unit/iiif/v3/presentation/annotation_spec.rb b/spec/unit/iiif/v3/presentation/annotation_spec.rb index 1e161b2..11e541a 100644 --- a/spec/unit/iiif/v3/presentation/annotation_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_spec.rb @@ -4,4 +4,10 @@ it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' end + describe '#validate' do + it 'raises an error if time_mode isn\'t an allowable value' do + subject['time_mode'] = 'foo' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end end From 9a972b56389a280b863b9b40a37b959348432f0f Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:51:03 -0700 Subject: [PATCH 21/91] Add viewing hint 'together' for collections --- lib/iiif/v3/presentation/collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation/collection.rb b/lib/iiif/v3/presentation/collection.rb index dc0ec84..74849a0 100644 --- a/lib/iiif/v3/presentation/collection.rb +++ b/lib/iiif/v3/presentation/collection.rb @@ -16,7 +16,7 @@ def array_only_keys end def legal_viewing_hint_values - %w{ auto-advance } + %w{ auto-advance together } end def initialize(hsh={}) From 3c43759b6802767cdc7f81f87f1fe379ba9c007f Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Thu, 29 Jun 2017 07:58:55 -0700 Subject: [PATCH 22/91] Add Choices --- lib/iiif/v3/presentation.rb | 1 + lib/iiif/v3/presentation/choice.rb | 45 +++++++++++++++++++ spec/unit/iiif/v3/presentation/choice_spec.rb | 17 +++++++ 3 files changed, 63 insertions(+) create mode 100644 lib/iiif/v3/presentation/choice.rb create mode 100644 spec/unit/iiif/v3/presentation/choice_spec.rb diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 16c393e..7d38fe8 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -5,6 +5,7 @@ annotation_collection annotation_page canvas + choice collection manifest resource diff --git a/lib/iiif/v3/presentation/choice.rb b/lib/iiif/v3/presentation/choice.rb new file mode 100644 index 0000000..8c76aca --- /dev/null +++ b/lib/iiif/v3/presentation/choice.rb @@ -0,0 +1,45 @@ +require File.join(File.dirname(__FILE__), 'abstract_resource') + +module IIIF + module V3 + module Presentation + class Choice < AbstractResource + + TYPE = 'Choice' + + def string_only_keys + super + %w{ choice_hint } + end + + def array_only_keys; + super + %w{ items }; + end + + def legal_viewing_hint_values + %w{ none } + end + + def legal_choice_hint_values + %w{ client user } + end + + def initialize(hsh={}) + hsh['type'] = TYPE unless hsh.has_key? 'type' + super(hsh) + end + + def validate + super + + # time mode values + if self.has_key?('choice_hint') + unless self.legal_choice_hint_values.include?(self['choice_hint']) + m = "choiceHint for #{self.class} must be one of #{self.legal_choice_hint_values}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + end + end + end +end diff --git a/spec/unit/iiif/v3/presentation/choice_spec.rb b/spec/unit/iiif/v3/presentation/choice_spec.rb new file mode 100644 index 0000000..0ffc7f3 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/choice_spec.rb @@ -0,0 +1,17 @@ +describe IIIF::V3::Presentation::Choice do + + describe "#{described_class}.define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + + describe "#{described_class}.define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + + describe '#validate' do + it 'raises an error if choice_hint isn\'t an allowable value' do + subject['choice_hint'] = 'foo' + expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end +end From 70e885c6fb6c03f053ae562af9e69d82539655e1 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 11:13:24 -0700 Subject: [PATCH 23/91] v3 refactored AbstractResource up one level and made rights an array-only key --- lib/iiif/v3/abstract_resource.rb | 75 ++++++++++++++++++ lib/iiif/v3/presentation.rb | 10 +-- lib/iiif/v3/presentation/abstract_resource.rb | 79 ------------------- lib/iiif/v3/presentation/annotation.rb | 4 +- .../v3/presentation/annotation_collection.rb | 4 +- lib/iiif/v3/presentation/annotation_page.rb | 4 +- lib/iiif/v3/presentation/canvas.rb | 4 +- lib/iiif/v3/presentation/choice.rb | 4 +- lib/iiif/v3/presentation/collection.rb | 4 +- lib/iiif/v3/presentation/image_resource.rb | 1 - lib/iiif/v3/presentation/manifest.rb | 4 +- lib/iiif/v3/presentation/range.rb | 2 - lib/iiif/v3/presentation/resource.rb | 4 +- lib/iiif/v3/presentation/sequence.rb | 4 +- .../abstract_resource_spec.rb | 10 +-- .../iiif/v3/presentation/sequence_spec.rb | 4 +- 16 files changed, 96 insertions(+), 121 deletions(-) create mode 100644 lib/iiif/v3/abstract_resource.rb delete mode 100644 lib/iiif/v3/presentation/abstract_resource.rb rename spec/unit/iiif/v3/{presentation => }/abstract_resource_spec.rb (93%) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb new file mode 100644 index 0000000..1e95fb7 --- /dev/null +++ b/lib/iiif/v3/abstract_resource.rb @@ -0,0 +1,75 @@ +require File.join(File.dirname(__FILE__), 'service') + +module IIIF + module V3 + class AbstractResource < Service + + # Every subclass should override the following five methods where + # appropriate, see Subclasses for how. + def required_keys + %w{ type } + end + + def any_type_keys # these are allowed on all classes + %w{ label description thumbnail attribution logo see_also + related within } + end + + def string_only_keys + %w{ viewing_hint } # should any of the any_type_keys be here? + end + + def array_only_keys + %w{ metadata rights } + end + + def abstract_resource_only_keys + super + [ { key: 'service', type: IIIF::V3::Service } ] + end + + def hash_only_keys + %w{ } + end + + def int_only_keys + %w{ } + end + + # Not every subclass is allowed to have viewingDirect, but when it is, + # it must be one of these values + def legal_viewing_direction_values + %w{ left-to-right right-to-left top-to-bottom bottom-to-top } + end + + def legal_viewing_hint_values + [] + end + + # Initialize a Presentation node + # @param [Hash] hsh - Anything in this hash will be added to the Object. + # Order is only guaranteed if an ActiveSupport::OrderedHash is passed. + # @param [boolean] include_context (default: false). Pass true if the + # context should be included. + def initialize(hsh={}) + if self.class == IIIF::V3::AbstractResource + raise "#{self.class} is an abstract class. Please use one of its subclasses." + end + super(hsh) + end + + # Options: + # * force: (true|false). Skips validations. + # * include_context: (true|false). Adds the @context to the top of the + # document if it doesn't exist. Default: true. + # * sort_json_ld_keys: (true|false). Brings all properties starting with + # '@'. Default: true. to the top of the document and sorts them. + def to_ordered_hash(opts={}) + include_context = opts.fetch(:include_context, true) + if include_context && !self.has_key?('@context') + self['@context'] = IIIF::V3::Presentation::CONTEXT + end + super(opts) + end + end + end +end diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 7d38fe8..a4b8e10 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -1,6 +1,8 @@ +require_relative 'abstract_resource' +require_relative 'ordered_hash' + require File.join(File.dirname(__FILE__), 'service') %w{ -abstract_resource annotation annotation_collection annotation_page @@ -9,15 +11,13 @@ collection manifest resource - image_resource + image_resource sequence - range + range }.each do |f| require File.join(File.dirname(__FILE__), 'presentation', f) end -require_relative 'ordered_hash' - module IIIF module V3 module Presentation diff --git a/lib/iiif/v3/presentation/abstract_resource.rb b/lib/iiif/v3/presentation/abstract_resource.rb deleted file mode 100644 index ed37f3a..0000000 --- a/lib/iiif/v3/presentation/abstract_resource.rb +++ /dev/null @@ -1,79 +0,0 @@ -require File.join(File.dirname(__FILE__), '../service') - -module IIIF - module V3 - module Presentation - class AbstractResource < Service - - # Every subclass should override the following five methods where - # appropriate, see Subclasses for how. - def required_keys - %w{ type } - end - - def any_type_keys # these are allowed on all classes - %w{ label description thumbnail attribution rights logo see_also - related within } - end - - def string_only_keys - %w{ viewing_hint } # should any of the any_type_keys be here? - end - - def array_only_keys - %w{ metadata } - end - - def abstract_resource_only_keys - super + [ { key: 'service', type: IIIF::V3::Service } ] - end - - def hash_only_keys - %w{ } - end - - def int_only_keys - %w{ } - end - - # Not every subclass is allowed to have viewingDirect, but when it is, - # it must be one of these values - def legal_viewing_direction_values - %w{ left-to-right right-to-left top-to-bottom bottom-to-top } - end - - def legal_viewing_hint_values - [] - end - - # Initialize a Presentation node - # @param [Hash] hsh - Anything in this hash will be added to the Object. - # Order is only guaranteed if an ActiveSupport::OrderedHash is passed. - # @param [boolean] include_context (default: false). Pass true if the - # context should be included. - def initialize(hsh={}) - if self.class == IIIF::V3::Presentation::AbstractResource - raise "#{self.class} is an abstract class. Please use one of its subclasses." - end - super(hsh) - end - - - # Options: - # * force: (true|false). Skips validations. - # * include_context: (true|false). Adds the @context to the top of the - # document if it doesn't exist. Default: true. - # * sort_json_ld_keys: (true|false). Brings all properties starting with - # '@'. Default: true. to the top of the document and sorts them. - def to_ordered_hash(opts={}) - include_context = opts.fetch(:include_context, true) - if include_context && !self.has_key?('@context') - self['@context'] = IIIF::V3::Presentation::CONTEXT - end - super(opts) - end - - end - end - end -end diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb index 9208c31..d19fe71 100644 --- a/lib/iiif/v3/presentation/annotation.rb +++ b/lib/iiif/v3/presentation/annotation.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Annotation < AbstractResource + class Annotation < IIIF::V3::AbstractResource TYPE = 'Annotation' diff --git a/lib/iiif/v3/presentation/annotation_collection.rb b/lib/iiif/v3/presentation/annotation_collection.rb index 5bbc947..1950284 100644 --- a/lib/iiif/v3/presentation/annotation_collection.rb +++ b/lib/iiif/v3/presentation/annotation_collection.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class AnnotationCollection < AbstractResource + class AnnotationCollection < IIIF::V3::AbstractResource TYPE = 'AnnotationCollection' diff --git a/lib/iiif/v3/presentation/annotation_page.rb b/lib/iiif/v3/presentation/annotation_page.rb index cd4cad2..882afd6 100644 --- a/lib/iiif/v3/presentation/annotation_page.rb +++ b/lib/iiif/v3/presentation/annotation_page.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class AnnotationPage < AbstractResource + class AnnotationPage < IIIF::V3::AbstractResource TYPE = 'AnnotationPage' diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index 81401a1..ab53d61 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Canvas < AbstractResource + class Canvas < IIIF::V3::AbstractResource # TODO (?) a simple 'Image Canvas' constructor. diff --git a/lib/iiif/v3/presentation/choice.rb b/lib/iiif/v3/presentation/choice.rb index 8c76aca..693923f 100644 --- a/lib/iiif/v3/presentation/choice.rb +++ b/lib/iiif/v3/presentation/choice.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Choice < AbstractResource + class Choice < IIIF::V3::AbstractResource TYPE = 'Choice' diff --git a/lib/iiif/v3/presentation/collection.rb b/lib/iiif/v3/presentation/collection.rb index 74849a0..35971a6 100644 --- a/lib/iiif/v3/presentation/collection.rb +++ b/lib/iiif/v3/presentation/collection.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Collection < AbstractResource + class Collection < IIIF::V3::AbstractResource TYPE = 'Collection' diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 199f6a2..74d9f2e 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -1,4 +1,3 @@ -require File.join(File.dirname(__FILE__), 'resource') require 'faraday' require 'json' diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index 6f65196..f4c7115 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Manifest < AbstractResource + class Manifest < IIIF::V3::AbstractResource TYPE = 'Manifest' diff --git a/lib/iiif/v3/presentation/range.rb b/lib/iiif/v3/presentation/range.rb index b703db5..14d712a 100644 --- a/lib/iiif/v3/presentation/range.rb +++ b/lib/iiif/v3/presentation/range.rb @@ -1,5 +1,3 @@ -require File.join(File.dirname(__FILE__), 'sequence') - module IIIF module V3 module Presentation diff --git a/lib/iiif/v3/presentation/resource.rb b/lib/iiif/v3/presentation/resource.rb index 6514ba5..767e273 100644 --- a/lib/iiif/v3/presentation/resource.rb +++ b/lib/iiif/v3/presentation/resource.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Resource < AbstractResource + class Resource < IIIF::V3::AbstractResource def required_keys %w{ id } diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index 657b74b..ae0bc52 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -1,9 +1,7 @@ -require File.join(File.dirname(__FILE__), 'abstract_resource') - module IIIF module V3 module Presentation - class Sequence < AbstractResource + class Sequence < IIIF::V3::AbstractResource TYPE = 'Sequence' diff --git a/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb similarity index 93% rename from spec/unit/iiif/v3/presentation/abstract_resource_spec.rb rename to spec/unit/iiif/v3/abstract_resource_spec.rb index e6b5e42..cbc3a16 100644 --- a/spec/unit/iiif/v3/presentation/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -1,14 +1,14 @@ require 'active_support/inflector' require 'json' -require File.join(File.dirname(__FILE__), '../../../../../lib/iiif/v3/hash_behaviours') +require File.join(File.dirname(__FILE__), '../../../../lib/iiif/v3/hash_behaviours') -describe IIIF::V3::Presentation::AbstractResource do +describe IIIF::V3::AbstractResource do - let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../../fixtures') } + let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../fixtures') } let(:manifest_from_spec_path) { File.join(fixtures_dir, 'v3/manifests/complete_from_spec.json') } let(:abstract_resource_subclass) do - Class.new(IIIF::V3::Presentation::AbstractResource) do + Class.new(IIIF::V3::AbstractResource) do include IIIF::V3::HashBehaviours def initialize(hsh={}) @@ -30,7 +30,7 @@ def required_keys describe '#initialize' do it 'raises an error if you try to instantiate AbstractResource' do - expect { IIIF::V3::Presentation::AbstractResource.new }.to raise_error(RuntimeError) + expect { IIIF::V3::AbstractResource.new }.to raise_error(RuntimeError) end it 'sets type' do expect(subject['type']).to eq 'a:SubClass' diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index af954a2..2103c48 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -24,7 +24,7 @@ def initialize(hsh={}) } }, 'attribution' => 'Provided by Example Organization', - 'rights' => 'http://www.example.org/license.html', + 'rights' => [{'id' => 'http://www.example.org/license.html'}], 'logo' => 'http://www.example.org/logos/institution1.jpg', 'see_also' => 'http://www.example.org/library/catalog/book1.xml', 'service' => { @@ -77,7 +77,7 @@ def initialize(hsh={}) describe '#array_only_keys' do it 'accumulates from the superclass' do - expect(subject.array_only_keys).to eq %w{ metadata canvases } + expect(subject.array_only_keys).to eq %w{ metadata rights canvases } end end From edb06293e417f0d1ccb563eb85d5c4d54dbd334d Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Sun, 2 Jul 2017 20:44:46 -0700 Subject: [PATCH 24/91] remove duplicated v3 classes of hash_behaviors and ordered_hash --- lib/iiif/v3/hash_behaviours.rb | 150 ------ lib/iiif/v3/ordered_hash.rb | 148 ----- lib/iiif/v3/presentation.rb | 2 +- lib/iiif/v3/service.rb | 20 +- spec/integration/iiif/v3/service_spec.rb | 6 +- spec/unit/iiif/v3/abstract_resource_spec.rb | 4 +- spec/unit/iiif/v3/hash_behaviours_spec.rb | 568 -------------------- spec/unit/iiif/v3/ordered_hash_spec.rb | 154 ------ 8 files changed, 16 insertions(+), 1036 deletions(-) delete mode 100644 lib/iiif/v3/hash_behaviours.rb delete mode 100644 lib/iiif/v3/ordered_hash.rb delete mode 100644 spec/unit/iiif/v3/hash_behaviours_spec.rb delete mode 100644 spec/unit/iiif/v3/ordered_hash_spec.rb diff --git a/lib/iiif/v3/hash_behaviours.rb b/lib/iiif/v3/hash_behaviours.rb deleted file mode 100644 index f783842..0000000 --- a/lib/iiif/v3/hash_behaviours.rb +++ /dev/null @@ -1,150 +0,0 @@ -require 'forwardable' - -module IIIF - module V3 - module HashBehaviours - extend Forwardable - - # TODO: - # * reject - # * replace - - def_delegators :@data, :[], :[]=, :camelize_keys, :delete, :empty?, - :fetch, :has_key?, :has_value?, :include?, :insert, :insert_after, - :insert_before, :key, :key?, :keys, :length, :member?, :shift, :size, - :snakeize_keys, :store, :unshift, :value?, :values - - - ### - # Methods that take a block and should return an instance (self or a new' - # instance) have been overridden to do so, rather than an' - # IIIF::V3::OrderedHash based on the internal hash - - SIMPLE_SELF_RETURNERS = %w[delete_if each each_key each_value keep_if] - - SIMPLE_SELF_RETURNERS.each do |method_name| - define_method(method_name) do |*arg, &block| - unless block.nil? # block_given? doesn't seem to work in this context - @data.send(method_name, *arg, &block) - return self - else - @data.send(method_name) - end - end - end - - # Clear is the only method that returns self but doesn't accept a block - def clear - @data.clear - return self - end - - # Returns a new instance of this class containing the contents of' - # another_obj. The argument can be any object that implements two - # methods: - # - # obj.each { |k,v| block } - # obj.has_key? - # - # If no block is specified, the value for entries with duplicate keys' - # will be those of the argument, but at the index of the original; all' - # other entries will be appended to the end. - # - # If a block is specified the value for each duplicate key is determined' - # by calling the block with the key, its value in hsh and its value in' - # another_obj. - def merge another_obj - new_instance = self.class.new - # self.clone # Would this be better? What happens to other attributes of the class? - if block_given? - self.each do |k,v| - if another_obj.has_key? k - new_instance[k] = yield(k, self[k], another_obj[k]) - else - new_instance[k] = v - end - end - else - self.each { |k,v| new_instance[k] = v } - another_obj.each { |k,v| new_instance[k] = v } - end - new_instance - end - - # Adds the entries from another obj to this one. The argument can be any - # object that implements two methods: - # - # obj.each { |k,v| block } - # obj.has_key? - # - # If no block is specified, the value for entries with duplicate keys' - # will be those of the argument, but at the index of the original; all' - # other entries will be appended to the end. - # - # If a block is specified the value for each duplicate key is determined' - # by calling the block with the key, its value in hsh and its value in' - # another_obj. - def merge! another_obj - if block_given? - self.each do |k,v| - if another_obj.has_key? k - self[k] = yield(k, self[k], another_obj[k]) - else - self[k] = v - end - end - else - self.each { |k,v| self[k] = v } - another_obj.each { |k,v| self[k] = v } - end - self - end - alias update merge! - - # Deletes entries for which the supplied block evaluates to true. - # Equivalent to #delete_if, but returns nil if there were no changes - def reject! - if block_given? - return_nil = true - @data.each do |k, v| - if yield(k, v) - @data.delete(k) - return_nil = false - end - end - return return_nil ? nil : self - else - return self.data.reject! - end - end - - # Returns a new instance consisting of entries for which the block returns - # true. Not that an enumerator is not available for the OrderedHash' - # implementation - def select - new_instance = self.class.new - if block_given? - @data.each { |k,v| new_instance.data[k] = v if yield(k,v) } - end - return new_instance - end - - # Deletes entries for which the supplied block evaluates to false. - # Equivalent to Hash#keep_if, but returns nil if no changes were made. - def select! - if block_given? - return_nil = true - @data.each do |k,v| - unless yield(k,v) - @data.delete(k) - return_nil = false - end - end - return nil if return_nil - end - self - end - - end - end -end diff --git a/lib/iiif/v3/ordered_hash.rb b/lib/iiif/v3/ordered_hash.rb deleted file mode 100644 index e0f25e2..0000000 --- a/lib/iiif/v3/ordered_hash.rb +++ /dev/null @@ -1,148 +0,0 @@ -require 'active_support/inflector' - -module IIIF - module V3 - class OrderedHash < ::Hash - - # Insert a new key and value at the suppplied index. - # - # Note that this is slightly different from Array#insert in that new - # entries must be added one at a time, i.e. insert(n, k, v, k, v...) is - # not supported. - # - # @param [Integer] index - # @param [Object] key - # @param [Object] value - def insert(index, key, value) - tmp = IIIF::V3::OrderedHash.new - index = self.length + 1 + index if index < 0 - if index < 0 - m = "Index #{index} is too small for current length (#{length})" - raise IndexError, m - end - if index > 0 - i=0 - self.each do |k,v| - tmp[k] = v - self.delete(k) - i+=1 - break if i == index - end - end - tmp[key] = value - tmp.merge!(self) # copy the remaining to tmp - self.clear # start over... - self.merge!(tmp) # now put them all back - self - end - - # Insert a key and value before an existing key or the first entry for' - # which the supplied block evaluates to true. The block takes precendence - # over the supplied key. - # Options:' - # * :existing_key (default: nil). If nil or not supplied then a block is required. - # * :new_key (required) - # * :value (required) - # @raise KeyError if the supplied existing key is not found, the new - # key exists, or the block never evaluates to true. - def insert_before(hsh, &block) - existing_key = hsh.fetch(:existing_key, nil) - new_key = hsh[:new_key] - value = hsh[:value] - if block_given? - self.insert_here(0, new_key, value, &block) - else - self.insert_here(0, new_key, value, existing_key) - end - end - - # Insert a key and value after an existing key or the first entry for' - # which the supplied block evaluates to true. The block takes precendence - # over the supplied key. - # Options:' - # * :existing_key (default: nil). If nil or not supplied then a block is required. - # * :new_key (required) - # * :value (required) - # @raise KeyError if the supplied existing key is not found, the new - # key exists, or the block never evaluates to true. - def insert_after(hsh, &block) - existing_key = hsh.fetch(:existing_key, nil) - new_key = hsh[:new_key] - value = hsh[:value] - if block_given? - self.insert_here(1, new_key, value, &block) - else - self.insert_here(1, new_key, value, existing_key) - end - end - - # Delete any keys that are empty arrays - def remove_empties - self.keys.each do |key| - if (self[key].kind_of?(Array) && self[key].empty?) || self[key].nil? - self.delete(key) - end - end - end - - # Covert snake_case keys to camelCase - def camelize_keys - self.keys.each_with_index do |key, i| - if key != key.camelize(:lower) - self.insert(i, key.camelize(:lower), self[key]) - self.delete(key) - end - end - self - end - - # Covert camelCase keys to snake_case - def snakeize_keys - self.keys.each_with_index do |key, i| - if key != key.underscore - self.insert(i, key.underscore, self[key]) - self.delete(key) - end - end - self - end - - - # Prepends an entry to the front of the object. - # Note that this is slightly different from Array#unshift in that new - # entries must be added one at a time, i.e. unshift([k,v],[k,v],...) is - # not currently supported. - def unshift k,v - self.insert(0, k, v) - self - end - - protected - def insert_here(where, new_key, value, existing_key=nil, &block) - idx = nil - if block_given? - self.each_with_index do |(k,v), i| - if yield(k, v) - idx = i - break - end - end - if idx.nil? - raise KeyError, "Supplied block never evaluates to true" - end - else - unless self.has_key?(existing_key) - raise KeyError, "Existing key '#{existing_key}' does not exist" - end - if self.has_key?(new_key) - raise KeyError, "Supplied new key '#{new_key}' already exists" - end - idx = self.keys.index(existing_key) + where - end - self.insert(idx, new_key, value) - self - end - - end - end -end diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index a4b8e10..57309dc 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -1,5 +1,5 @@ require_relative 'abstract_resource' -require_relative 'ordered_hash' +require_relative '../ordered_hash' require File.join(File.dirname(__FILE__), 'service') %w{ diff --git a/lib/iiif/v3/service.rb b/lib/iiif/v3/service.rb index 2819400..17bc569 100644 --- a/lib/iiif/v3/service.rb +++ b/lib/iiif/v3/service.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), 'hash_behaviours') +require File.join(File.dirname(__FILE__), '../hash_behaviours') require 'active_support/core_ext/class/subclasses' require 'active_support/ordered_hash' require 'active_support/inflector' @@ -7,7 +7,7 @@ module IIIF module V3 class Service - include IIIF::V3::HashBehaviours + include IIIF::HashBehaviours # Anything goes! SHOULD have id and profile, MAY have label # Consider subclassing this for typical services... @@ -21,7 +21,7 @@ def int_only_keys; %w{ }; end def numeric_only_keys; %w{ }; end def initialize(hsh={}) - @data = IIIF::V3::OrderedHash[hsh] + @data = IIIF::OrderedHash[hsh] self.define_methods_for_any_type_keys self.define_methods_for_array_only_keys self.define_methods_for_string_only_keys @@ -37,11 +37,11 @@ class << self def parse(s) ordered_hash = nil if s.kind_of?(String) && File.exists?(s) - ordered_hash = IIIF::V3::OrderedHash[JSON.parse(IO.read(s))] + ordered_hash = IIIF::OrderedHash[JSON.parse(IO.read(s))] elsif s.kind_of?(String) && !File.exists?(s) - ordered_hash = IIIF::V3::OrderedHash[JSON.parse(s)] + ordered_hash = IIIF::OrderedHash[JSON.parse(s)] elsif s.kind_of?(Hash) - ordered_hash = IIIF::V3::OrderedHash[s] + ordered_hash = IIIF::OrderedHash[s] else m = '#parse takes a path to a file, a JSON String, or a Hash, ' m += "argument was a #{s.class}." @@ -114,7 +114,7 @@ def to_ordered_hash(opts={}) self.validate end - export_hash = IIIF::V3::OrderedHash.new + export_hash = IIIF::OrderedHash.new if sort_json_ld_keys self.keys.select { |k| k.start_with?('@') }.sort!.each do |k| @@ -133,7 +133,7 @@ def to_ordered_hash(opts={}) export_hash[k] = self.data[k].to_ordered_hash(sub_opts) elsif self.data[k].kind_of?(Hash) - export_hash[k] = IIIF::V3::OrderedHash.new + export_hash[k] = IIIF::OrderedHash.new self.data[k].each do |sub_k, v| if v.respond_to?(:to_ordered_hash) @@ -161,7 +161,7 @@ def to_ordered_hash(opts={}) export_hash[k] << member.to_ordered_hash(sub_opts) elsif member.kind_of?(Hash) - hsh = IIIF::V3::OrderedHash.new + hsh = IIIF::OrderedHash.new export_hash[k] << hsh member.each do |sub_k,v| @@ -199,7 +199,7 @@ def to_ordered_hash(opts={}) export_hash end - def self.from_ordered_hash(hsh, default_klass=IIIF::V3::OrderedHash) + def self.from_ordered_hash(hsh, default_klass=IIIF::OrderedHash) # Create a new object (new_object) type = nil if hsh.has_key?('type') diff --git a/spec/integration/iiif/v3/service_spec.rb b/spec/integration/iiif/v3/service_spec.rb index d80931e..ccb21fd 100644 --- a/spec/integration/iiif/v3/service_spec.rb +++ b/spec/integration/iiif/v3/service_spec.rb @@ -24,9 +24,9 @@ s = described_class.parse(h) expect(s['label']).to eq 'Book 1' end - it 'IIIF::V3::OrderedHash' do + it 'IIIF::OrderedHash' do h = JSON.parse(IO.read(manifest_from_spec_path)) - oh = IIIF::V3::OrderedHash[h] + oh = IIIF::OrderedHash[h] s = described_class.parse(oh) expect(s['label']).to eq 'Book 1' end @@ -98,7 +98,7 @@ expect(parsed.class).to be expected_klass end it 'turns keys without "type" into an OrderedHash' do - expected_klass = IIIF::V3::OrderedHash + expected_klass = IIIF::OrderedHash parsed = described_class.from_ordered_hash(fixture) expect(parsed['some_other_thing'].class).to be expected_klass end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index cbc3a16..4cc73da 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -1,6 +1,6 @@ require 'active_support/inflector' require 'json' -require File.join(File.dirname(__FILE__), '../../../../lib/iiif/v3/hash_behaviours') +require File.join(File.dirname(__FILE__), '../../../../lib/iiif/hash_behaviours') describe IIIF::V3::AbstractResource do @@ -9,7 +9,7 @@ let(:manifest_from_spec_path) { File.join(fixtures_dir, 'v3/manifests/complete_from_spec.json') } let(:abstract_resource_subclass) do Class.new(IIIF::V3::AbstractResource) do - include IIIF::V3::HashBehaviours + include IIIF::HashBehaviours def initialize(hsh={}) hsh['type'] = 'a:SubClass' unless hsh.has_key?('type') diff --git a/spec/unit/iiif/v3/hash_behaviours_spec.rb b/spec/unit/iiif/v3/hash_behaviours_spec.rb deleted file mode 100644 index 878e0a9..0000000 --- a/spec/unit/iiif/v3/hash_behaviours_spec.rb +++ /dev/null @@ -1,568 +0,0 @@ -require File.join(File.dirname(__FILE__), '../../../spec_helper') -require 'active_support/ordered_hash' -describe IIIF::V3::HashBehaviours do - - let(:hash_like_class) do - Class.new do - include IIIF::V3::HashBehaviours - attr_accessor :data # Accessible for easier expects...not sure you'd do this in a real class - def initialize() - @data = IIIF::V3::OrderedHash.new - end - end - end - - # TODO: let(:init_data)...rather than repeating so much below - subject { hash_like_class.new } - - describe '#[]=' do - it 'assigns a new k and value to the node' do - subject['foo'] = 'bar' - expect(subject.data).to eq({'foo' => 'bar'}) - end - it 'always puts new entries at the end' do - subject['baz'] = 'qux' - subject['quux'] = 'corge' - subject['grault'] = 'garply' - expect(subject.data[subject.data.keys.last]).to eq 'garply' - end - it 'replaces keys that already exist in the same place' do - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - expect(subject.data.select {|k,v| k == 'plugh'}).to eq({'plugh'=>'wobble'}) - end - end - - describe '#[]' do - it 'retrieves the expected value' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - expect(subject['wibble']).to eq 'wobble' - end - it 'returns nil if the key is not found' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - expect(subject['flob']).to be_nil - end - end - - describe '#clear' do - it 'clears all properties' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - subject.clear - expect(subject.keys).to eq [] - end - it 'returns the instance on which it was called' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - expect(subject.clear).to eq subject - end - end - - describe '#delete' do - it 'removes an entry from the object' do - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - subject.delete('waldo') - expect(subject.data).to eq({'plugh' => 'xyzzy'}) - end - it 'returns the value of the entry that was removed' do - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.delete('waldo')).to eq 'fred' - end - it 'can take a block as well' do - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect { |b| subject.delete('wubble', &b) }.to yield_with_args - expect(subject.delete('foo') {|e| e.reverse }).to eq 'oof' - end - end - - describe '#delete_if' do - it 'can take a block' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect { |b| subject.delete_if(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) - end - it 'returns the instance' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect( subject.delete_if { |k,v| k.start_with?('w') } ).to eq subject - end - it 'works' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - subject.delete_if { |k,v| k.start_with?('w') } - expect(subject.data).to eq({'plugh' => 'xyzzy'}) - end - it 'returns an enumerator if no block is supplied' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.delete_if).to be_a Enumerator - end - end - - describe '#each' do - it 'yields' do - subject['plugh'] = 'xyzzy' - expect { |b| subject.each(&b) }.to yield_with_args - end - it 'returns the instance' do - subject.data['waldo'] = 'fred' - subject.data['plugh'] = 'xyzzy' - expect(subject.each { |k,v| nil }).to eq subject - end - it 'loops as expected' do - subject.data['wibble'] = 'foo' - subject.data['waldo'] = 'fred' - subject.data['plugh'] = 'xyzzy' - capped_keys = [] - subject.each { |k,v| capped_keys << k.capitalize } - expect(capped_keys).to eq ['Wibble', 'Waldo', 'Plugh'] - end - it 'returns an enumerator if no block is supplied' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.delete_if).to be_a Enumerator - end - end - - describe '#each_key' do - it 'yields' do - subject['plugh'] = 'xyzzy' - expect { |b| subject.each_key(&b) }.to yield_with_args - end - it 'returns the instance' do - subject.data['waldo'] = 'fred' - subject.data['plugh'] = 'xyzzy' - expect(subject.each_key { |k| nil }).to eq subject - end - it 'loops as expected' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - key_accumulator = [] - subject.each_key { |k| key_accumulator << k } - expect(key_accumulator).to eq ['wibble', 'waldo', 'plugh'] - end - it 'returns an enumerator if no block is supplied' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.each_key).to be_a Enumerator - end - end - - describe '#each_value' do - it 'yields' do - subject['plugh'] = 'xyzzy' - expect { |b| subject.each_value(&b) }.to yield_with_args - end - it 'returns the instance' do - subject.data['waldo'] = 'fred' - subject.data['plugh'] = 'xyzzy' - expect(subject.each_value { |v| nil }).to eq subject - end - it 'loops as expected' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - value_accumulator = [] - subject.each_value { |v| value_accumulator << v } - expect(value_accumulator).to eq ['foo', 'fred', 'xyzzy'] - end - it 'returns an enumerator if no block is supplied' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.each_value).to be_a Enumerator - end - - end - - describe '#empty' do - it 'returns true when there are no entries' do - expect(subject.empty?).to be_truthy - end - it 'returns false when we have data' do - subject['waldo'] = 'fred' - expect(subject.empty?).to be_falsey - end - end - - describe '#fetch' do - it 'retrieves the expected value' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - expect(subject.fetch('wibble')).to eq 'wobble' - end - it 'returns the default if the key is not found and one is supplied' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - expect(subject.fetch('flob', 'waldo')).to eq 'waldo' - end - it 'raises a KeyError if the key is not found and no default is supplied' do - expect { subject.fetch('flob') }.to raise_error KeyError - end - it 'can take a block as well' do - subject['wibble'] = 'wobble' - subject['wubble'] = 'fred' - expect(subject.fetch('wubble') {|e| e.capitalize }).to eq 'fred' # value takes precence - expect { |b| subject.fetch('foo', &b) }.to yield_with_args - expect(subject.fetch('foo') {|e| e.reverse }).to eq 'oof' - end - end - - describe '#has_key? (and aliases)' do - it 'is true when the key exists' do - subject['wibble'] = 'wobble' - expect(subject.has_key? 'wibble').to be_truthy - end - it 'is false when the key does not exist' do - expect(subject.has_key? 'wibble').to be_falsey - end - end - - describe '#has_value? (and aliases)' do - it 'is true when the value exists' do - subject['wibble'] = 'wobble' - expect(subject.has_value? 'wobble').to be_truthy - end - it 'is false when the value does not exist' do - expect(subject.has_value? 'wobble').to be_falsey - end - end - - describe '#keep_if' do - it 'can take a block' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect { |b| subject.keep_if(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) - end - it 'returns the instance' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect( subject.keep_if { |k,v| k.start_with?('w') } ).to eq subject - end - it 'works' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - subject.keep_if { |k,v| k.start_with?('w') } - expect(subject.data).to eq({'wibble'=>'foo', 'waldo'=>'fred'}) - end - end - - describe '#key' do - it 'is the key associated with a value' do - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - expect(subject.key 'wibble').to eq 'thud' - expect(subject.key 'wobble').to eq 'plugh' - end - it 'is nil if the value is not found' do - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - expect(subject.key 'foo').to be_nil - end - end - - describe '#keys' do - it 'is an array of all of the keys in the object' do - subject['foo'] = 'bar' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.keys).to eq ['foo', 'waldo', 'plugh'] - end - end - - describe '#length' do - it 'works' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - expect(subject.length).to eq 2 - end - end - - describe '#merge' do - it 'returns a new instance of the calling class' do - subject['wibble'] = 'foo' - another = hash_like_class.new - another['waldo'] = 'fred' - merged = subject.merge(another) - # clear them all to confirm we're not testing equality of anything other - # than that we have different instances - subject.data.clear - another.data.clear - merged.data.clear - expect(subject.merge(another).class).to eq subject.class # same class but' - expect(merged).to_not be subject # different instance - expect(merged).to_not be another # different instance - end - # it 'adds new entries to the end' do - # subject['wibble'] = 'foo' - # subject['plugh'] = 'xyzzy' - # another = hash_like_class.new - # another['waldo'] = 'fred' - # new_instance = subject.merge(another) - # expect(new_instance.data.last).to eq ['waldo', 'fred'] - # end - it 'retains the index position for existing entries, replacing the value' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = hash_like_class.new - another['plugh'] = 'fred' - new_instance = subject.merge(another) - expect(new_instance['plugh']).to eq 'fred' - expect(new_instance[new_instance.keys[1]]).to eq 'fred' - end - it 'takes a block' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = hash_like_class.new - another['plugh'] = 'fred' - # e.g. give a block that turns common keys into an Array - new_instance = subject.merge(another) { |k, old_val,new_val| [old_val, new_val] } - expect(new_instance['wibble']).to eq 'foo' - expect(new_instance['plugh']).to eq ['xyzzy', 'fred'] - expect(new_instance['foo']).to eq 'bar' - expect(new_instance.data).to eq({'wibble'=>'foo', 'plugh'=>['xyzzy', 'fred'], 'foo'=>'bar'}) - end - describe 'takes anything that implements `#each { |k,v| block }` and #has_key?' do - it 'returns a new instance of the calling class' do - subject['wibble'] = 'foo' - another = {'waldo' => 'fred'} - merged = subject.merge(another) - # clear them all to confirm we're not testing equality of anything other - # than that we have different instances - subject.data.clear - another.clear - merged.data.clear - expect(subject.merge(another).class).to eq subject.class # same class but' - expect(merged).to_not be subject # different instance - expect(merged).to_not be another # different instance - end - # it 'adds new entries to the end' do - # subject['wibble'] = 'foo' - # subject['plugh'] = 'xyzzy' - # another = {'waldo' => 'fred'} - # new_instance = subject.merge(another) - # expect(new_instance.data.last).to eq ['waldo', 'fred'] - # end - it 'retains the index position for existing entries, replacing the value' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = {'plugh' => 'fred' } - another['plugh'] = 'fred' - new_instance = subject.merge(another) - expect(new_instance['plugh']).to eq 'fred' - expect(new_instance[new_instance.keys[1]]).to eq 'fred' - end - it 'takes a block' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = {'plugh' => 'fred'} - # e.g. give a block that turns common keys into an Array - new_instance = subject.merge(another) { |k, old_val,new_val| [old_val, new_val] } - expect(new_instance['wibble']).to eq 'foo' - expect(new_instance['plugh']).to eq ['xyzzy', 'fred'] - expect(new_instance['foo']).to eq 'bar' - expect(new_instance.data).to eq({'wibble'=>'foo', 'plugh'=>['xyzzy', 'fred'], 'foo'=>'bar'}) - end - end - end - - describe '#merge!' do - it 'returns the instance on which is was called' do - subject['wibble'] = 'foo' - another = hash_like_class.new - another['waldo'] = 'fred' - expect(subject.merge!(another)).to eq subject # same instance - end - it 'adds new entries to the end' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - another = hash_like_class.new - another['waldo'] = 'fred' - subject.merge!(another) - expect(subject.data[subject.data.keys.last]).to eq 'fred' - end - it 'retains the index position for existing entries, replacing the value' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = hash_like_class.new - another['plugh'] = 'fred' - subject.merge!(another) - expect(subject['plugh']).to eq 'fred' - expect(subject.data[subject.keys[1]]).to eq 'fred' - end - it 'takes a block' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = hash_like_class.new - another['plugh'] = 'fred' - # e.g. give a block that turns common keys into an Array - subject.merge!(another) { |k, old_val,new_val| [old_val, new_val] } - expect(subject['wibble']).to eq 'foo' - expect(subject['plugh']).to eq ['xyzzy', 'fred'] - expect(subject['foo']).to eq 'bar' - expect(subject.data).to eq({'wibble'=>'foo', 'plugh'=>['xyzzy', 'fred'], 'foo'=>'bar'}) - end - describe 'takes anything that implements `#each { |k,v| block }` and #has_key?' do - it 'returns a new instance of the calling class' do - subject['wibble'] = 'foo' - another = {'waldo' => 'fred'} - expect(subject.merge!(another)).to eq subject # same instance - end - it 'adds new entries to the end' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - another = {'waldo' => 'fred'} - subject.merge!(another) - expect(subject.data[subject.data.keys.last]).to eq 'fred' - end - it 'retains the index position for existing entries, replacing the value' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = {'plugh' => 'fred' } - subject.merge!(another) - expect(subject['plugh']).to eq 'fred' - expect(subject.data[subject.keys[1]]).to eq 'fred' - end - it 'takes a block' do - subject['wibble'] = 'foo' - subject['plugh'] = 'xyzzy' - subject['foo'] = 'bar' - another = {'plugh' => 'fred'} - subject.merge!(another) { |k, old_val,new_val| "#{k}, #{old_val}, #{new_val}" } - expect(subject['wibble']).to eq 'foo' - expect(subject['plugh']).to eq 'plugh, xyzzy, fred' - expect(subject['foo']).to eq 'bar' - expect(subject.data).to eq({'wibble'=>'foo', 'plugh'=>'plugh, xyzzy, fred', 'foo'=>'bar'}) - end - end - end - - describe '#reject!' do - it 'can take a block' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect { |b| subject.reject!(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) - end - it 'returns the instance if there were changes' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect( subject.reject! { |k| k.start_with?('w') } ).to be subject - end - it 'returns nil if there were no changes' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect( subject.reject! { |k| k.start_with?('X') } ).to be_nil - end - it 'works' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - subject.reject! { |k| k.start_with?('w') } - expect(subject.data).to eq({'plugh' => 'xyzzy'}) - end - end - - describe '#select' do - it 'yields' do - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - subject['waldo'] = 'fred' - expect { |b| subject.select(&b) }.to yield_successive_args(['thud', 'wibble'], ['plugh', 'wobble'], ['waldo', 'fred']) - end - it 'returns a new instance of the class' do - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - subject['waldo'] = 'fred' - expect( subject.select{ |k,v| true }.class ).to eq subject.class - expect( subject.select{ |k,v| true } ).to_not eq subject - end - it 'selects but doesn\'t delete from the original instance' do - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - subject['waldo'] = 'fred' - expect( subject.select{ |k,v| k.include?('u') }.data ).to eq({'thud'=>'wibble', 'plugh'=>'wobble'}) - expect( subject.data ).to eq subject.data - end - end - - describe '#select!' do - it 'can take a block' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect { |b| subject.select!(&b) }.to yield_successive_args(['wibble', 'foo'], ['waldo', 'fred'], ['plugh', 'xyzzy']) - end - it 'returns nil if there were no changes' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['wobble'] = 'xyzzy' - expect( subject.select! { |k,v| k.start_with?('w') } ).to be_nil - end - it 'returns the instance if there were changes' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect( subject.select! { |k,v| k.start_with?('w') } ).to eq subject - end - it 'works' do - subject['wibble'] = 'foo' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - subject.select! { |k,v| k.start_with?('p') } - expect(subject.data).to eq({'plugh' => 'xyzzy'}) - end - end - - describe '#shift' do - it 'returns the first element in the hash without a param' do - subject['thud'] = 'wibble' - subject['plugh'] = 'wobble' - expect(subject.shift).to eq ['thud','wibble'] - expect(subject.data).to eq({'plugh' => 'wobble'}) - end - end - - describe 'store' do - it 'works as an alias for []=' do - subject.store('foo', 'bar') - expect(subject.data).to eq({'foo' => 'bar'}) - end - end - - describe '#values' do - it 'is an array of all of the keys in the object' do - subject['foo'] = 'bar' - subject['waldo'] = 'fred' - subject['plugh'] = 'xyzzy' - expect(subject.values).to eq ['bar', 'fred', 'xyzzy'] - end - end - -end diff --git a/spec/unit/iiif/v3/ordered_hash_spec.rb b/spec/unit/iiif/v3/ordered_hash_spec.rb deleted file mode 100644 index 3b1b438..0000000 --- a/spec/unit/iiif/v3/ordered_hash_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -describe IIIF::V3::OrderedHash do - - describe '#camelize_keys' do - before(:each) do - @uri = 'http://www.example.org/descriptions/book1.xml' - @within_uri = 'http://www.example.org/collections/books/' - subject['see_also'] = @uri - subject['within'] = @within_uri - end - it 'changes snake_case keys to camelCase' do - subject.camelize_keys # #send gets past protection - expect(subject.keys.include?('seeAlso')).to be_truthy - expect(subject.keys.include?('see_also')).to be_falsey - end - it 'keeps the right values' do - subject.camelize_keys - expect(subject['seeAlso']).to eq @uri - expect(subject['within']).to eq @within_uri - end - it 'keeps things in the same position' do - see_also_position = subject.keys.index('see_also') - within_position = subject.keys.index('within') - subject.camelize_keys - expect(subject.keys[see_also_position]).to eq 'seeAlso' - expect(subject.keys[within_position]).to eq 'within' - end - - end - - describe '#snakeize_keys' do - before(:each) do - @uri = 'http://www.example.org/descriptions/book1.xml' - @within_uri = 'http://www.example.org/collections/books/' - subject['seeAlso'] = @uri - subject['within'] = @within_uri - end - it 'changes camelCase keys to snake_case' do - subject.snakeize_keys - expect(subject.keys.include?('see_also')).to be_truthy - expect(subject.keys.include?('seeAlso')).to be_falsey - end - it 'keeps the right values' do - subject.snakeize_keys - expect(subject['see_also']).to eq @uri - expect(subject['within']).to eq @within_uri - end - it 'keeps things in the same position' do - see_also_position = subject.keys.index('seeAlso') - within_position = subject.keys.index('within') - subject.snakeize_keys - expect(subject.keys[see_also_position]).to eq 'see_also' - expect(subject.keys[within_position]).to eq 'within' - end - end - - describe 'insertion patches' do - - let (:init_data) { [ ['wubble', 'fred'], ['baz', 'qux'], ['grault','garply'] ] } - - subject do - hsh = described_class.new - init_data.each { |e| hsh[e[0]] = e[1] } - hsh - end - - describe '#insert' do - it 'inserts as expected' do - subject.insert(2, 'quux', 'corge') - expect(subject[subject.keys[0]]).to eq 'fred' - expect(subject[subject.keys[1]]).to eq 'qux' - expect(subject[subject.keys[2]]).to eq 'corge' - expect(subject[subject.keys[3]]).to eq 'garply' - end - it 'returns the instance' do - expect(subject.insert(1, 'quux','corge')).to eq subject - end - it 'raises IndexError if a negative index is too small' do - expect { subject.insert(-5, 'quux','corge') }.to raise_error IndexError - end - it 'puts index -1 on the end' do - subject.insert(-1, 'thud','wibble') - expect(subject[subject.keys.last]).to eq 'wibble' - end - end - - describe '#insert_before' do - it 'inserts in the expected place with a supplied key' do - subject.insert_before(existing_key: 'grault', new_key: 'quux', value: 'corge') - expect(subject.keys).to eq ['wubble','baz','quux','grault'] - end - it 'inserts in the expected place with a supplied block' do - subject.insert_before(new_key: 'quux', value: 'corge') { |k,v| k.start_with?('g') } - expect(subject.keys).to eq ['wubble','baz','quux','grault'] - end - it 'returns the instance' do - expect(subject.insert_before(existing_key: 'grault', new_key: 'quux', value: 'corge')).to be subject - end - describe 'raises KeyError' do - it 'when the supplied existing key is not found' do - expect { subject.insert_before(existing_key: 'foo', new_key: 'quux', value: 'corge') }.to raise_error KeyError - end - it 'when the supplied new key already exists' do - expect { subject.insert_before(existing_key: 'grault', new_key: 'wubble', value: 'corge') }.to raise_error KeyError - end - end - end - - describe '#insert_after' do - it 'inserts in the expected place with a supplied key' do - subject.insert_after(existing_key: 'baz', new_key: 'quux', value: 'corge') - expect(subject.keys).to eq ['wubble','baz','quux','grault'] - end - it 'inserts in the expected place with a supplied block' do - subject.insert_after(new_key: 'quux', value: 'corge') { |k,v| k.start_with?('g') } - expect(subject.keys).to eq ['wubble','baz','quux','grault'] - end - it 'returns the instance' do - expect(subject.insert_after(existing_key: 'baz', new_key: 'quux', value: 'corge')).to be subject - end - describe 'raises KeyError' do - it 'when the supplied existing key is not found' do - expect { subject.insert_after(existing_key: 'foo', new_key: 'quux', value: 'corge') }.to raise_error KeyError - end - it 'when the supplied new key already exists' do - expect { subject.insert_after(existing_key: 'grault', new_key: 'wubble', value: 'corge') }.to raise_error KeyError - end - end - end - - describe '#unshift' do - it 'adds an entry to the front of the object' do - subject.unshift('thud','wibble') - expect(subject[subject.keys[0]]).to eq 'wibble' - end - it 'returns the instance' do - expect(subject.unshift('thud','wibble')).to be subject - end - end - - describe '#remove_empties' do - it 'if they\'re arrays' do - subject[:wubble] = [] - subject.remove_empties - expect(subject.has_key?(:wubble)).to be_falsey - end - it 'if they\'re nil' do - subject[:wubble] = nil - subject.remove_empties - expect(subject.has_key?(:wubble)).to be_falsey - end - end - - end -end From 66db6723065244ee8ec53bdb83e860faa82b4fc8 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Sun, 2 Jul 2017 20:57:19 -0700 Subject: [PATCH 25/91] v3 presentation class: clean up require statements --- lib/iiif/v3/presentation.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 57309dc..36e6bd9 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -1,7 +1,8 @@ require_relative 'abstract_resource' require_relative '../ordered_hash' +require_relative 'service' -require File.join(File.dirname(__FILE__), 'service') +# NOTE: image_resource must follow resource due to inheritance %w{ annotation annotation_collection From d4a874d703af200abac98ed77f61df1a73903cd9 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Sun, 2 Jul 2017 21:03:18 -0700 Subject: [PATCH 26/91] v3 - reverse inheritance of service now inherits from abstract_resource --- lib/iiif/v3/abstract_resource.rb | 330 ++++++++++++++++- lib/iiif/v3/service.rb | 344 +----------------- ...vice_spec.rb => abstract_resource_spec.rb} | 36 +- 3 files changed, 348 insertions(+), 362 deletions(-) rename spec/integration/iiif/v3/{service_spec.rb => abstract_resource_spec.rb} (86%) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 1e95fb7..caee86a 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -1,11 +1,11 @@ -require File.join(File.dirname(__FILE__), 'service') +require_relative '../hash_behaviours' module IIIF module V3 - class AbstractResource < Service + class AbstractResource + include IIIF::HashBehaviours - # Every subclass should override the following five methods where - # appropriate, see Subclasses for how. + # Every subclass should override the following methods defining key value types, see Subclasses for how def required_keys %w{ type } end @@ -24,16 +24,18 @@ def array_only_keys end def abstract_resource_only_keys - super + [ { key: 'service', type: IIIF::V3::Service } ] + [ { key: 'service', type: IIIF::V3::Service } ] end def hash_only_keys %w{ } end - def int_only_keys %w{ } end + def numeric_only_keys + %w{ } + end # Not every subclass is allowed to have viewingDirect, but when it is, # it must be one of these values @@ -54,7 +56,85 @@ def initialize(hsh={}) if self.class == IIIF::V3::AbstractResource raise "#{self.class} is an abstract class. Please use one of its subclasses." end - super(hsh) + @data = IIIF::OrderedHash[hsh] + self.define_methods_for_any_type_keys + self.define_methods_for_array_only_keys + self.define_methods_for_string_only_keys + self.define_methods_for_int_only_keys + self.define_methods_for_numeric_only_keys + self.define_methods_for_abstract_resource_only_keys + self.snakeize_keys + end + + # Static methods / alternative constructors + class << self + # Parse from a file path, string, or existing hash + def parse(s) + ordered_hash = nil + if s.kind_of?(String) && File.exists?(s) + ordered_hash = IIIF::OrderedHash[JSON.parse(IO.read(s))] + elsif s.kind_of?(String) && !File.exists?(s) + ordered_hash = IIIF::OrderedHash[JSON.parse(s)] + elsif s.kind_of?(Hash) + ordered_hash = IIIF::OrderedHash[s] + else + m = '#parse takes a path to a file, a JSON String, or a Hash, ' + m += "argument was a #{s.class}." + if s.kind_of?(String) + m+= "If you were trying to point to a file, does it exist?" + end + raise ArgumentError, m + end + return IIIF::V3::Service.from_ordered_hash(ordered_hash) + end + end + + def validate + # TODO: + # * check for required keys + # * type check Array-only values + # * type check String-only values + # * type check Integer-only values + # * type check AbstractResource-only values + self.required_keys.each do |k| + unless self.has_key?(k) + m = "A(n) #{k} is required for each #{self.class}" + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + end + # Viewing Direction values + if self.has_key?('viewing_direction') + unless self.legal_viewing_direction_values.include?(self['viewing_direction']) + m = "viewingDirection must be one of #{legal_viewing_direction_values}" + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + # Viewing Hint values + if self.has_key?('viewing_hint') + unless self.legal_viewing_hint_values.include?(self['viewing_hint']) + m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + # Metadata is all hashes + if self.has_key?('metadata') + unless self['metadata'].all? { |entry| entry.kind_of?(Hash) } + m = 'All entries in the metadata list must be a type of Hash' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + + # Options + # * pretty: (true|false). Should the JSON be pretty-printed? (default: false) + # * All options available in #to_ordered_hash + def to_json(opts={}) + hsh = self.to_ordered_hash(opts) + if opts.fetch(:pretty, false) + JSON.pretty_generate(hsh) + else + hsh.to_json + end end # Options: @@ -68,8 +148,242 @@ def to_ordered_hash(opts={}) if include_context && !self.has_key?('@context') self['@context'] = IIIF::V3::Presentation::CONTEXT end - super(opts) + force = opts.fetch(:force, false) + sort_json_ld_keys = opts.fetch(:sort_json_ld_keys, true) + + unless force + self.validate + end + + export_hash = IIIF::OrderedHash.new + + if sort_json_ld_keys + self.keys.select { |k| k.start_with?('@') }.sort!.each do |k| + export_hash[k] = self.data[k] + end + end + + sub_opts = { + include_context: false, + sort_json_ld_keys: sort_json_ld_keys, + force: force + } + self.keys.each do |k| + unless sort_json_ld_keys && k.start_with?('@') + if self.data[k].respond_to?(:to_ordered_hash) #.respond_to?(:to_ordered_hash) + export_hash[k] = self.data[k].to_ordered_hash(sub_opts) + + elsif self.data[k].kind_of?(Hash) + export_hash[k] = IIIF::OrderedHash.new + self.data[k].each do |sub_k, v| + + if v.respond_to?(:to_ordered_hash) + export_hash[k][sub_k] = v.to_ordered_hash(sub_opts) + + elsif v.kind_of?(Array) + export_hash[k][sub_k] = [] + v.each do |member| + if member.respond_to?(:to_ordered_hash) + export_hash[k][sub_k] << member.to_ordered_hash(sub_opts) + else + export_hash[k][sub_k] << member + end + end + else + export_hash[k][sub_k] = v + end + end + + elsif self.data[k].kind_of?(Array) + export_hash[k] = [] + + self.data[k].each do |member| + if member.respond_to?(:to_ordered_hash) + export_hash[k] << member.to_ordered_hash(sub_opts) + + elsif member.kind_of?(Hash) + hsh = IIIF::OrderedHash.new + export_hash[k] << hsh + member.each do |sub_k,v| + + if v.respond_to?(:to_ordered_hash) + hsh[sub_k] = v.to_ordered_hash(sub_opts) + + elsif v.kind_of?(Array) + hsh[sub_k] = [] + + v.each do |sub_member| + if sub_member.respond_to?(:to_ordered_hash) + hsh[sub_k] << sub_member.to_ordered_hash(sub_opts) + else + hsh[sub_k] << sub_member + end + end + else + hsh[sub_k] = v + end + end + + else + export_hash[k] << member + # there are no nested arrays, right? + end + end + else + export_hash[k] = self.data[k] + end + + end + end + export_hash.remove_empties + export_hash.camelize_keys + export_hash + end + + def self.from_ordered_hash(hsh, default_klass=IIIF::OrderedHash) + # Create a new object (new_object) + type = nil + if hsh.has_key?('type') + type = IIIF::V3::AbstractResource.get_descendant_class_by_jld_type(hsh['type']) + end + new_object = type.nil? ? default_klass.new : type.new + + hsh.keys.each do |key| + new_key = key.underscore == key ? key : key.underscore + if new_key == 'service' + new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Service) + elsif new_key == 'body' + new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Resource) + elsif hsh[key].kind_of?(Hash) + new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key]) + elsif hsh[key].kind_of?(Array) + new_object[new_key] = [] + hsh[key].each do |member| + if new_key == 'service' + new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member, IIIF::V3::Service) + elsif member.kind_of?(Hash) + new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member) + else + new_object[new_key] << member + # Again, no nested arrays, right? + end + end + else + new_object[new_key] = hsh[key] + end + end + new_object + end + + protected + + def self.get_descendant_class_by_jld_type(type) + IIIF::V3::AbstractResource.all_known_subclasses.find do |klass| + klass.const_defined?(:TYPE) && klass.const_get(:TYPE) == type + end + end + + def self.all_known_subclasses + @all_known_subclasses ||= IIIF::V3::AbstractResource.descendants.reject(&:singleton_class?) + end + + def data=(hsh) + @data = hsh + end + + def data + @data + end + + def define_methods_for_any_type_keys + define_accessor_methods(*any_type_keys) + + # override the getter defined by define_accessor_methods to avoid returning + # an array for empty values. + any_type_keys.each do |key| + define_singleton_method(key) do + self.send('[]', key) + end + end + end + + def define_methods_for_array_only_keys + define_accessor_methods(*array_only_keys) do |key, arg| + unless arg.kind_of?(Array) + m = "#{key} must be an Array." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + + def define_methods_for_abstract_resource_only_keys + # keys in this case is an array of hashes with { key: 'k', type: Class } + abstract_resource_only_keys.each do |hsh| + key = hsh[:key] + type = hsh[:type] + + define_accessor_methods(key) do |key, arg| + unless arg.kind_of?(type) + m = "#{key} must be an #{type}." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + end + + def define_methods_for_string_only_keys + define_accessor_methods(*string_only_keys) do |key, arg| + unless arg.kind_of?(String) + m = "#{key} must be an String." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + + def define_methods_for_int_only_keys + define_accessor_methods(*int_only_keys) do |key, arg| + unless arg.kind_of?(Integer) && arg > 0 + m = "#{key} must be a positive Integer." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + + def define_methods_for_numeric_only_keys + define_accessor_methods(*numeric_only_keys) do |key, arg| + unless arg.kind_of?(Numeric) && arg > 0 + m = "#{key} must be a positive Integer or Float." + raise IIIF::Presentation::IllegalValueError, m + end + end end + + def define_accessor_methods(*keys, &validation) + keys.each do |key| + # Setter + define_singleton_method("#{key}=") do |arg| + validation.call(key, arg) if block_given? + self.send('[]=', key, arg) + end + if key.camelize(:lower) != key + define_singleton_method("#{key.camelize(:lower)}=") do |arg| + validation.call(key, arg) if block_given? + self.send('[]=', key, arg) + end + end + # Getter + define_singleton_method(key) do + self[key] ||= [] + self[key] + end + if key.camelize(:lower) != key + define_singleton_method(key.camelize(:lower)) do + self.send('[]', key) + end + end + end + end + end end end diff --git a/lib/iiif/v3/service.rb b/lib/iiif/v3/service.rb index 17bc569..0bc1e11 100644 --- a/lib/iiif/v3/service.rb +++ b/lib/iiif/v3/service.rb @@ -1,348 +1,18 @@ -require File.join(File.dirname(__FILE__), '../hash_behaviours') -require 'active_support/core_ext/class/subclasses' -require 'active_support/ordered_hash' -require 'active_support/inflector' -require 'json' +require_relative 'abstract_resource' module IIIF module V3 - class Service - include IIIF::HashBehaviours + class Service < AbstractResource - # Anything goes! SHOULD have id and profile, MAY have label - # Consider subclassing this for typical services... - def required_keys; %w{ }; end - def any_type_keys; %w{ }; end - def string_only_keys; %w{ }; end - def array_only_keys; %w{ }; end - def abstract_resource_only_keys; %w{ }; end - def hash_only_keys; %w{ }; end - def int_only_keys; %w{ }; end - def numeric_only_keys; %w{ }; end - - def initialize(hsh={}) - @data = IIIF::OrderedHash[hsh] - self.define_methods_for_any_type_keys - self.define_methods_for_array_only_keys - self.define_methods_for_string_only_keys - self.define_methods_for_int_only_keys - self.define_methods_for_numeric_only_keys - self.define_methods_for_abstract_resource_only_keys - self.snakeize_keys - end - - # Static methods / alternative constructors - class << self - # Parse from a file path, string, or existing hash - def parse(s) - ordered_hash = nil - if s.kind_of?(String) && File.exists?(s) - ordered_hash = IIIF::OrderedHash[JSON.parse(IO.read(s))] - elsif s.kind_of?(String) && !File.exists?(s) - ordered_hash = IIIF::OrderedHash[JSON.parse(s)] - elsif s.kind_of?(Hash) - ordered_hash = IIIF::OrderedHash[s] - else - m = '#parse takes a path to a file, a JSON String, or a Hash, ' - m += "argument was a #{s.class}." - if s.kind_of?(String) - m+= "If you were trying to point to a file, does it exist?" - end - raise ArgumentError, m - end - return IIIF::V3::Service.from_ordered_hash(ordered_hash) - end - end - - def validate - # TODO: - # * check for required keys - # * type check Array-only values - # * type check String-only values - # * type check Integer-only values - # * type check AbstractResource-only values - self.required_keys.each do |k| - unless self.has_key?(k) - m = "A(n) #{k} is required for each #{self.class}" - raise IIIF::V3::Presentation::MissingRequiredKeyError, m - end - end - # Viewing Direction values - if self.has_key?('viewing_direction') - unless self.legal_viewing_direction_values.include?(self['viewing_direction']) - m = "viewingDirection must be one of #{legal_viewing_direction_values}" - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - # Viewing Hint values - if self.has_key?('viewing_hint') - unless self.legal_viewing_hint_values.include?(self['viewing_hint']) - m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - # Metadata is all hashes - if self.has_key?('metadata') - unless self['metadata'].all? { |entry| entry.kind_of?(Hash) } - m = 'All entries in the metadata list must be a type of Hash' - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - end - - # Options - # * pretty: (true|false). Should the JSON be pretty-printed? (default: false) - # * All options available in #to_ordered_hash - def to_json(opts={}) - hsh = self.to_ordered_hash(opts) - if opts.fetch(:pretty, false) - JSON.pretty_generate(hsh) - else - hsh.to_json - end - end - - # Options: - # * force: (true|false). Skips validations. - # * sort_json_ld_keys: (true|false). Brings all properties starting with - # '@'. Default: true. to the top of the document and sorts them. - def to_ordered_hash(opts={}) - force = opts.fetch(:force, false) - sort_json_ld_keys = opts.fetch(:sort_json_ld_keys, true) - - unless force - self.validate - end - - export_hash = IIIF::OrderedHash.new - - if sort_json_ld_keys - self.keys.select { |k| k.start_with?('@') }.sort!.each do |k| - export_hash[k] = self.data[k] - end - end - - sub_opts = { - include_context: false, - sort_json_ld_keys: sort_json_ld_keys, - force: force - } - self.keys.each do |k| - unless sort_json_ld_keys && k.start_with?('@') - if self.data[k].respond_to?(:to_ordered_hash) #.respond_to?(:to_ordered_hash) - export_hash[k] = self.data[k].to_ordered_hash(sub_opts) - - elsif self.data[k].kind_of?(Hash) - export_hash[k] = IIIF::OrderedHash.new - self.data[k].each do |sub_k, v| - - if v.respond_to?(:to_ordered_hash) - export_hash[k][sub_k] = v.to_ordered_hash(sub_opts) - - elsif v.kind_of?(Array) - export_hash[k][sub_k] = [] - v.each do |member| - if member.respond_to?(:to_ordered_hash) - export_hash[k][sub_k] << member.to_ordered_hash(sub_opts) - else - export_hash[k][sub_k] << member - end - end - else - export_hash[k][sub_k] = v - end - end - - elsif self.data[k].kind_of?(Array) - export_hash[k] = [] - - self.data[k].each do |member| - if member.respond_to?(:to_ordered_hash) - export_hash[k] << member.to_ordered_hash(sub_opts) - - elsif member.kind_of?(Hash) - hsh = IIIF::OrderedHash.new - export_hash[k] << hsh - member.each do |sub_k,v| - - if v.respond_to?(:to_ordered_hash) - hsh[sub_k] = v.to_ordered_hash(sub_opts) - - elsif v.kind_of?(Array) - hsh[sub_k] = [] - - v.each do |sub_member| - if sub_member.respond_to?(:to_ordered_hash) - hsh[sub_k] << sub_member.to_ordered_hash(sub_opts) - else - hsh[sub_k] << sub_member - end - end - else - hsh[sub_k] = v - end - end - - else - export_hash[k] << member - # there are no nested arrays, right? - end - end - else - export_hash[k] = self.data[k] - end - - end - end - export_hash.remove_empties - export_hash.camelize_keys - export_hash + # service is the only class that doesn't need a type + def required_keys + super.reject {|el| el == 'type' } end - def self.from_ordered_hash(hsh, default_klass=IIIF::OrderedHash) - # Create a new object (new_object) - type = nil - if hsh.has_key?('type') - type = IIIF::V3::Service.get_descendant_class_by_jld_type(hsh['type']) - end - new_object = type.nil? ? default_klass.new : type.new - - hsh.keys.each do |key| - new_key = key.underscore == key ? key : key.underscore - if new_key == 'service' - new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key], IIIF::V3::Service) - elsif new_key == 'body' - new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Resource) - elsif hsh[key].kind_of?(Hash) - new_object[new_key] = IIIF::V3::Service.from_ordered_hash(hsh[key]) - elsif hsh[key].kind_of?(Array) - new_object[new_key] = [] - hsh[key].each do |member| - if new_key == 'service' - new_object[new_key] << IIIF::V3::Service.from_ordered_hash(member, IIIF::V3::Service) - elsif member.kind_of?(Hash) - new_object[new_key] << IIIF::V3::Service.from_ordered_hash(member) - else - new_object[new_key] << member - # Again, no nested arrays, right? - end - end - else - new_object[new_key] = hsh[key] - end - end - new_object - end - - protected - - def self.get_descendant_class_by_jld_type(type) - IIIF::V3::Service.all_service_subclasses.find do |klass| - klass.const_defined?(:TYPE) && klass.const_get(:TYPE) == type - end - end - - # All known subclasses of service. - def self.all_service_subclasses - @all_service_subclasses ||= IIIF::V3::Service.descendants.reject(&:singleton_class?) - end - - def data=(hsh) - @data = hsh - end - - def data - @data - end - - def define_methods_for_any_type_keys - define_accessor_methods(*any_type_keys) - - # override the getter defined by define_accessor_methods to avoid returning - # an array for empty values. - any_type_keys.each do |key| - define_singleton_method(key) do - self.send('[]', key) - end - end - end - - def define_methods_for_array_only_keys - define_accessor_methods(*array_only_keys) do |key, arg| - unless arg.kind_of?(Array) - m = "#{key} must be an Array." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - end - - def define_methods_for_abstract_resource_only_keys - # keys in this case is an array of hashes with { key: 'k', type: Class } - abstract_resource_only_keys.each do |hsh| - key = hsh[:key] - type = hsh[:type] - - define_accessor_methods(key) do |key, arg| - unless arg.kind_of?(type) - m = "#{key} must be an #{type}." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - end - end - - def define_methods_for_string_only_keys - define_accessor_methods(*string_only_keys) do |key, arg| - unless arg.kind_of?(String) - m = "#{key} must be an String." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - end - - def define_methods_for_int_only_keys - define_accessor_methods(*int_only_keys) do |key, arg| - unless arg.kind_of?(Integer) && arg > 0 - m = "#{key} must be a positive Integer." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - end - - def define_methods_for_numeric_only_keys - define_accessor_methods(*numeric_only_keys) do |key, arg| - unless arg.kind_of?(Numeric) && arg > 0 - m = "#{key} must be a positive Integer or Float." - raise IIIF::Presentation::IllegalValueError, m - end - end + def initialize(hsh={}) + super(hsh) end - def define_accessor_methods(*keys, &validation) - keys.each do |key| - # Setter - define_singleton_method("#{key}=") do |arg| - validation.call(key, arg) if block_given? - self.send('[]=', key, arg) - end - if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - validation.call(key, arg) if block_given? - self.send('[]=', key, arg) - end - end - # Getter - define_singleton_method(key) do - self[key] ||= [] - self[key] - end - if key.camelize(:lower) != key - define_singleton_method(key.camelize(:lower)) do - self.send('[]', key) - end - end - end - end end end end diff --git a/spec/integration/iiif/v3/service_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb similarity index 86% rename from spec/integration/iiif/v3/service_spec.rb rename to spec/integration/iiif/v3/abstract_resource_spec.rb index ccb21fd..0c76bbf 100644 --- a/spec/integration/iiif/v3/service_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -1,7 +1,7 @@ require 'active_support/inflector' require 'json' -describe IIIF::V3::Service do +describe IIIF::V3::AbstractResource do let(:fixtures_dir) { File.join(File.dirname(__FILE__), '../../../fixtures') } let(:manifest_from_spec_path) { File.join(fixtures_dir, 'v3/manifests/complete_from_spec.json') } @@ -104,7 +104,7 @@ end it 'turns services into Services' do - expected_klass = IIIF::V3::Service + expected_klass = IIIF::V3::Service parsed = described_class.from_ordered_hash(fixture) expect(parsed['service'].class).to be expected_klass end @@ -152,6 +152,8 @@ let(:logo_uri) { 'http://www.example.org/logos/institution1.jpg' } let(:within_uri) { 'http://www.example.org/collections/books/' } let(:see_also) { 'http://www.example.org/library/catalog/book1.xml' } + # NOTE: Using Service to test, as we can't initialize the abstract class + let(:instantiated_class) { IIIF::V3::Service.new } describe 'it puts the json-ld keys at the top' do let(:extra_props) { [ @@ -160,28 +162,28 @@ ['within','http://example.com/something'] ] } let(:sorted_ld_keys) { - subject.keys.select { |k| k.start_with?('@') }.sort! + instantiated_class.keys.select { |k| k.start_with?('@') }.sort! } before(:each) { extra_props.reverse.each do |k,v| - subject.unshift(k,v) + instantiated_class.unshift(k,v) end } it 'by default' do (0..extra_props.length-1).each do |i| - expect(subject.keys[i]).to eq(extra_props[i][0]) + expect(instantiated_class.keys[i]).to eq(extra_props[i][0]) end - oh = subject.to_ordered_hash + oh = instantiated_class.to_ordered_hash (0..sorted_ld_keys.length-1).each do |i| expect(oh.keys[i]).to eq(sorted_ld_keys[i]) end end it 'unless you say not to' do (0..extra_props.length-1).each do |i| - expect(subject.keys[i]).to eq(extra_props[i][0]) + expect(instantiated_class.keys[i]).to eq(extra_props[i][0]) end - oh = subject.to_ordered_hash(sort_json_ld_keys: false) + oh = instantiated_class.to_ordered_hash(sort_json_ld_keys: false) (0..extra_props.length-1).each do |i| expect(oh.keys[i]).to eq(extra_props[i][0]) end @@ -190,23 +192,23 @@ describe 'removes empty keys' do it 'if they\'re arrays' do - subject['logo'] = logo_uri - subject['within'] = [] - ordered_hash = subject.to_ordered_hash + instantiated_class['logo'] = logo_uri + instantiated_class['within'] = [] + ordered_hash = instantiated_class.to_ordered_hash expect(ordered_hash.has_key?('within')).to be_falsey end it 'if they\'re nil' do - subject['logo'] = logo_uri - subject['within'] = nil - ordered_hash = subject.to_ordered_hash + instantiated_class['logo'] = logo_uri + instantiated_class['within'] = nil + ordered_hash = instantiated_class.to_ordered_hash expect(ordered_hash.has_key?('within')).to be_falsey end end it 'converts snake_case keys to camelCase' do - subject['see_also'] = logo_uri - subject['within'] = within_uri - ordered_hash = subject.to_ordered_hash + instantiated_class['see_also'] = logo_uri + instantiated_class['within'] = within_uri + ordered_hash = instantiated_class.to_ordered_hash expect(ordered_hash.keys.include?('seeAlso')).to be_truthy end end From 6c0fd8d9d5b531551283788c9ee1c1deba0debbd Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:36:53 -0700 Subject: [PATCH 27/91] v3 refactor service class to be in presentation module --- lib/iiif/v3/abstract_resource.rb | 8 ++++---- lib/iiif/v3/presentation.rb | 3 ++- lib/iiif/v3/presentation/service.rb | 18 ++++++++++++++++++ lib/iiif/v3/service.rb | 18 ------------------ .../iiif/v3/abstract_resource_spec.rb | 6 +++--- .../iiif/v3/{ => presentation}/service_spec.rb | 6 +++--- 6 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 lib/iiif/v3/presentation/service.rb delete mode 100644 lib/iiif/v3/service.rb rename spec/unit/iiif/v3/{ => presentation}/service_spec.rb (74%) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index caee86a..35e61ad 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -24,7 +24,7 @@ def array_only_keys end def abstract_resource_only_keys - [ { key: 'service', type: IIIF::V3::Service } ] + [ { key: 'service', type: IIIF::V3::Presentation::Service } ] end def hash_only_keys @@ -85,7 +85,7 @@ def parse(s) end raise ArgumentError, m end - return IIIF::V3::Service.from_ordered_hash(ordered_hash) + return IIIF::V3::Presentation::Service.from_ordered_hash(ordered_hash) end end @@ -251,7 +251,7 @@ def self.from_ordered_hash(hsh, default_klass=IIIF::OrderedHash) hsh.keys.each do |key| new_key = key.underscore == key ? key : key.underscore if new_key == 'service' - new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Service) + new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Service) elsif new_key == 'body' new_object[new_key] = IIIF::V3::AbstractResource.from_ordered_hash(hsh[key], IIIF::V3::Presentation::Resource) elsif hsh[key].kind_of?(Hash) @@ -260,7 +260,7 @@ def self.from_ordered_hash(hsh, default_klass=IIIF::OrderedHash) new_object[new_key] = [] hsh[key].each do |member| if new_key == 'service' - new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member, IIIF::V3::Service) + new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member, IIIF::V3::Presentation::Service) elsif member.kind_of?(Hash) new_object[new_key] << IIIF::V3::AbstractResource.from_ordered_hash(member) else diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 36e6bd9..7c79bf6 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -1,8 +1,8 @@ require_relative 'abstract_resource' require_relative '../ordered_hash' -require_relative 'service' # NOTE: image_resource must follow resource due to inheritance +# NOTE: range must follow sequence due to inheritance %w{ annotation annotation_collection @@ -15,6 +15,7 @@ image_resource sequence range + service }.each do |f| require File.join(File.dirname(__FILE__), 'presentation', f) end diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb new file mode 100644 index 0000000..a6a5592 --- /dev/null +++ b/lib/iiif/v3/presentation/service.rb @@ -0,0 +1,18 @@ +module IIIF + module V3 + module Presentation + class Service < AbstractResource + + # service is the only class that doesn't need a type + def required_keys + super.reject {|el| el == 'type' } + end + + def initialize(hsh={}) + super(hsh) + end + + end + end + end +end diff --git a/lib/iiif/v3/service.rb b/lib/iiif/v3/service.rb deleted file mode 100644 index 0bc1e11..0000000 --- a/lib/iiif/v3/service.rb +++ /dev/null @@ -1,18 +0,0 @@ -require_relative 'abstract_resource' - -module IIIF - module V3 - class Service < AbstractResource - - # service is the only class that doesn't need a type - def required_keys - super.reject {|el| el == 'type' } - end - - def initialize(hsh={}) - super(hsh) - end - - end - end -end diff --git a/spec/integration/iiif/v3/abstract_resource_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb index 0c76bbf..59efc08 100644 --- a/spec/integration/iiif/v3/abstract_resource_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -104,7 +104,7 @@ end it 'turns services into Services' do - expected_klass = IIIF::V3::Service + expected_klass = IIIF::V3::Presentation::Service parsed = described_class.from_ordered_hash(fixture) expect(parsed['service'].class).to be expected_klass end @@ -115,7 +115,7 @@ File.open(fp,'w') do |f| f.write(parsed.to_json) end - from_file = IIIF::V3::Service.parse('/tmp/osullivan-spec.json') + from_file = IIIF::V3::Presentation::Service.parse('/tmp/osullivan-spec.json') File.delete(fp) # is this sufficient? expect(parsed.to_ordered_hash.to_a - from_file.to_ordered_hash.to_a).to eq [] @@ -153,7 +153,7 @@ let(:within_uri) { 'http://www.example.org/collections/books/' } let(:see_also) { 'http://www.example.org/library/catalog/book1.xml' } # NOTE: Using Service to test, as we can't initialize the abstract class - let(:instantiated_class) { IIIF::V3::Service.new } + let(:instantiated_class) { IIIF::V3::Presentation::Service.new } describe 'it puts the json-ld keys at the top' do let(:extra_props) { [ diff --git a/spec/unit/iiif/v3/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb similarity index 74% rename from spec/unit/iiif/v3/service_spec.rb rename to spec/unit/iiif/v3/presentation/service_spec.rb index ddfd329..06db7ab 100644 --- a/spec/unit/iiif/v3/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -1,8 +1,8 @@ -describe IIIF::V3::Service do +describe IIIF::V3::Presentation::Service do describe 'self#get_descendant_class_by_jld_type' do before do - class DummyClass < IIIF::V3::Service + class DummyClass < IIIF::V3::Presentation::Service TYPE = "Collection" def self.singleton_class? true @@ -18,7 +18,7 @@ def self.singleton_class? end context "when there are singleton classes which are returned" do it "gets the right class" do - allow(IIIF::V3::Service).to receive(:descendants).and_return([DummyClass, IIIF::V3::Presentation::Collection]) + allow(IIIF::V3::Presentation::Service).to receive(:descendants).and_return([DummyClass, IIIF::V3::Presentation::Collection]) klass = described_class.get_descendant_class_by_jld_type('Collection') expect(klass).to eq IIIF::V3::Presentation::Collection end From 8806078dfc809e5263e01a1f910ed8ec32900b85 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:42:34 -0700 Subject: [PATCH 28/91] v3 service clean up unnec initialize method (that only calls super) --- lib/iiif/v3/presentation/service.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb index a6a5592..de22add 100644 --- a/lib/iiif/v3/presentation/service.rb +++ b/lib/iiif/v3/presentation/service.rb @@ -8,10 +8,6 @@ def required_keys super.reject {|el| el == 'type' } end - def initialize(hsh={}) - super(hsh) - end - end end end From d041e8bd50deff2ed82da862091ba3c967cca48e Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:28:29 -0700 Subject: [PATCH 29/91] v3 image_resource doesn't need to require json --- lib/iiif/v3/presentation/image_resource.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 74d9f2e..2a77be1 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -1,5 +1,4 @@ require 'faraday' -require 'json' module IIIF module V3 From 168879313a94f77a64f2262f1f00ef9fb6920eeb Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:30:16 -0700 Subject: [PATCH 30/91] v3 abstract_resource: avoid shadowing var in define_methods_for_abstract_resource_only_key --- lib/iiif/v3/abstract_resource.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 35e61ad..b3f96c3 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -322,9 +322,9 @@ def define_methods_for_abstract_resource_only_keys key = hsh[:key] type = hsh[:type] - define_accessor_methods(key) do |key, arg| + define_accessor_methods(key) do |k, arg| unless arg.kind_of?(type) - m = "#{key} must be an #{type}." + m = "#{k} must be an #{type}." raise IIIF::V3::Presentation::IllegalValueError, m end end From ede7042016378a90bf0aa4fbcf3cf2a227155150 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:43:39 -0700 Subject: [PATCH 31/91] v3 clean up unnec methods (that only call super) --- lib/iiif/v3/presentation/annotation_collection.rb | 5 +---- lib/iiif/v3/presentation/canvas.rb | 4 ---- lib/iiif/v3/presentation/resource.rb | 3 --- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation_collection.rb b/lib/iiif/v3/presentation/annotation_collection.rb index 1950284..20c4674 100644 --- a/lib/iiif/v3/presentation/annotation_collection.rb +++ b/lib/iiif/v3/presentation/annotation_collection.rb @@ -26,10 +26,7 @@ def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' super(hsh) end - - def validate - super - end + end end end diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index ab53d61..e7e9d31 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -11,10 +11,6 @@ def required_keys super + %w{ id label } end - def any_type_keys - super + %w{ } - end - def array_only_keys super + %w{ content } end diff --git a/lib/iiif/v3/presentation/resource.rb b/lib/iiif/v3/presentation/resource.rb index 767e273..97eac92 100644 --- a/lib/iiif/v3/presentation/resource.rb +++ b/lib/iiif/v3/presentation/resource.rb @@ -15,9 +15,6 @@ def numeric_only_keys super + %w{ duration } end - def initialize(hsh={}) - super(hsh) - end end end end From 2d5940671d09f5654791b77c66e8b221d31dceb3 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:48:05 -0700 Subject: [PATCH 32/91] v3: make sure all validate methods call super when present --- lib/iiif/v3/presentation/annotation_page.rb | 3 ++- lib/iiif/v3/presentation/canvas.rb | 2 +- lib/iiif/v3/presentation/collection.rb | 5 +++-- lib/iiif/v3/presentation/manifest.rb | 3 +-- lib/iiif/v3/presentation/range.rb | 3 ++- lib/iiif/v3/presentation/sequence.rb | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation_page.rb b/lib/iiif/v3/presentation/annotation_page.rb index 882afd6..22dabb2 100644 --- a/lib/iiif/v3/presentation/annotation_page.rb +++ b/lib/iiif/v3/presentation/annotation_page.rb @@ -19,7 +19,8 @@ def initialize(hsh={}) end def validate - # Each member or resources must be a kind of Annotation + super + # TODO: Each member or resources must be a kind of Annotation end end diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index e7e9d31..75b9e1d 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -30,8 +30,8 @@ def initialize(hsh={}) end def validate - # all members of content are of type AnnotationPage super + # TODO: all members of content are of type AnnotationPage end end end diff --git a/lib/iiif/v3/presentation/collection.rb b/lib/iiif/v3/presentation/collection.rb index 35971a6..af126ba 100644 --- a/lib/iiif/v3/presentation/collection.rb +++ b/lib/iiif/v3/presentation/collection.rb @@ -23,8 +23,9 @@ def initialize(hsh={}) end def validate - # each member of collections and manifests must be a Hash - # each member of collections and manifests MUST have id, type, and label + super + # TODO: each member of collections and manifests must be a Hash + # TODO: each member of collections and manifests MUST have id, type, and label end end end diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index f4c7115..1490c4d 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -27,9 +27,8 @@ def initialize(hsh={}) end def validate - # TODO: check types of sequences and structure members - super + # TODO: check types of sequences and structure members end end end diff --git a/lib/iiif/v3/presentation/range.rb b/lib/iiif/v3/presentation/range.rb index 14d712a..a3fee96 100644 --- a/lib/iiif/v3/presentation/range.rb +++ b/lib/iiif/v3/presentation/range.rb @@ -23,7 +23,8 @@ def initialize(hsh={}) end def validate - # Values of the members array must be canvas or range + super + # TODO: Values of the members array must be canvas or range end end end diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index ae0bc52..2c239bc 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -23,9 +23,9 @@ def initialize(hsh={}) end def validate - # * Must be at least one canvas - # * All members of canvases must be a kind of Canvas super + # TODO: Must be at least one canvas + # TODO: All members of canvases must be a kind of Canvas end end end From ce0f6b7feae0d950960f0e061329ca88ab2da1c6 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:54:23 -0700 Subject: [PATCH 33/91] v3 abstract_resource: add define_methods_for_hash_only_keys; fix comments in validate method --- lib/iiif/v3/abstract_resource.rb | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index b3f96c3..e057d4c 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -58,8 +58,9 @@ def initialize(hsh={}) end @data = IIIF::OrderedHash[hsh] self.define_methods_for_any_type_keys - self.define_methods_for_array_only_keys self.define_methods_for_string_only_keys + self.define_methods_for_array_only_keys + self.define_methods_for_hash_only_keys self.define_methods_for_int_only_keys self.define_methods_for_numeric_only_keys self.define_methods_for_abstract_resource_only_keys @@ -90,18 +91,15 @@ def parse(s) end def validate - # TODO: - # * check for required keys - # * type check Array-only values - # * type check String-only values - # * type check Integer-only values - # * type check AbstractResource-only values self.required_keys.each do |k| unless self.has_key?(k) m = "A(n) #{k} is required for each #{self.class}" raise IIIF::V3::Presentation::MissingRequiredKeyError, m end end + + # note that xxx_only key values are checked via, e.g. self.define_methods_for_array_only_keys + # Viewing Direction values if self.has_key?('viewing_direction') unless self.legal_viewing_direction_values.include?(self['viewing_direction']) @@ -316,8 +314,17 @@ def define_methods_for_array_only_keys end end + def define_methods_for_hash_only_keys + define_accessor_methods(*hash_only_keys) do |key, arg| + unless arg.kind_of?(Hash) + m = "#{key} must be a Hash." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + def define_methods_for_abstract_resource_only_keys - # keys in this case is an array of hashes with { key: 'k', type: Class } + # values in this case: an array of hashes with { key: 'k', type: Class } abstract_resource_only_keys.each do |hsh| key = hsh[:key] type = hsh[:type] From 630be259c5daab1d11d8006900999d34fbae99c1 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 3 Jul 2017 00:44:03 -0700 Subject: [PATCH 34/91] .gitignore: add pry_history --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9422037..d63a280 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ coverage/ pkg/ Gemfile.lock +.pry_history From 568919786fd282dd65135f620ce69a1508cc6c63 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 11:37:20 -0700 Subject: [PATCH 35/91] v3 abstract_resource_spec: move test for get_descendant_class_by_jld_type here --- spec/unit/iiif/v3/abstract_resource_spec.rb | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 4cc73da..b3c5e45 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -1,7 +1,6 @@ require 'active_support/inflector' require 'json' -require File.join(File.dirname(__FILE__), '../../../../lib/iiif/hash_behaviours') - +require_relative '../../../../lib/iiif/hash_behaviours' describe IIIF::V3::AbstractResource do @@ -117,7 +116,6 @@ def required_keys end describe 'runs the validations' do - # Test this here because there's nothing to validate on the superclass (Subject) let(:error) { IIIF::V3::Presentation::MissingRequiredKeyError } before(:each) { subject.delete('id') } it 'raises exceptions' do @@ -129,4 +127,29 @@ def required_keys end end + describe '*get_descendant_class_by_jld_type' do + before do + class DummyClass < IIIF::V3::AbstractResource + TYPE = "Collection" + def self.singleton_class? + true + end + end + end + after do + Object.send(:remove_const, :DummyClass) + end + it 'gets the right class' do + klass = described_class.get_descendant_class_by_jld_type('Canvas') + expect(klass).to eq IIIF::V3::Presentation::Canvas + end + context "when there are singleton classes which are returned" do + it "gets the right class" do + allow(IIIF::V3::AbstractResource).to receive(:descendants).and_return([DummyClass, IIIF::V3::Presentation::Collection]) + klass = described_class.get_descendant_class_by_jld_type('Collection') + expect(klass).to eq IIIF::V3::Presentation::Collection + end + end + end + end From d656b54c0ceedff12d5436f32ae160c261622fc9 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 16:09:56 -0700 Subject: [PATCH 36/91] v3 rename shared_examples file for consistency and brevity --- .../{numeric_only_keys_spec.rb => numeric_only_keys.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/unit/iiif/v3/presentation/shared_examples/{numeric_only_keys_spec.rb => numeric_only_keys.rb} (100%) diff --git a/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys_spec.rb b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb similarity index 100% rename from spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys_spec.rb rename to spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb From 04258ae5579957a1dcd3bc77f494a1c22495a2d8 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 17:18:52 -0700 Subject: [PATCH 37/91] v3 abstract_resource: numeric_only_keys throws V3 error --- lib/iiif/v3/abstract_resource.rb | 2 +- .../iiif/v3/presentation/shared_examples/numeric_only_keys.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index e057d4c..a9c95c0 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -360,7 +360,7 @@ def define_methods_for_numeric_only_keys define_accessor_methods(*numeric_only_keys) do |key, arg| unless arg.kind_of?(Numeric) && arg > 0 m = "#{key} must be a positive Integer or Float." - raise IIIF::Presentation::IllegalValueError, m + raise IIIF::V3::Presentation::IllegalValueError, m end end end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb index 14dbb97..b21b642 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb @@ -19,10 +19,10 @@ end end it 'raises an exception when attempting to set it to something other than an Integer' do - expect { subject.send("#{prop}=", 'Foo') }.to raise_error IIIF::Presentation::IllegalValueError + expect { subject.send("#{prop}=", 'Foo') }.to raise_error IIIF::V3::Presentation::IllegalValueError end it 'raises an exception when attempting to set it to a negative number' do - expect { subject.send("#{prop}=", -1.0) }.to raise_error IIIF::Presentation::IllegalValueError + expect { subject.send("#{prop}=", -1.0) }.to raise_error IIIF::V3::Presentation::IllegalValueError end end From c6122b1fc0a3c51f73fd40daf82bc219707d2c90 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 17:22:36 -0700 Subject: [PATCH 38/91] v3 abstract_resource: add uri_only_keys --- lib/iiif/v3/abstract_resource.rb | 13 ++++++++ .../shared_examples/uri_only_keys.rb | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index a9c95c0..a903051 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -36,6 +36,9 @@ def int_only_keys def numeric_only_keys %w{ } end + def uri_only_keys + %w{ } + end # Not every subclass is allowed to have viewingDirect, but when it is, # it must be one of these values @@ -64,6 +67,7 @@ def initialize(hsh={}) self.define_methods_for_int_only_keys self.define_methods_for_numeric_only_keys self.define_methods_for_abstract_resource_only_keys + self.define_methods_for_uri_only_keys self.snakeize_keys end @@ -365,6 +369,15 @@ def define_methods_for_numeric_only_keys end end + def define_methods_for_uri_only_keys + define_accessor_methods(*uri_only_keys) do |key, arg| + unless arg.kind_of?(String) && arg =~ URI::regexp + m = "#{key} must be a String containing a URI." + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + def define_accessor_methods(*keys, &validation) keys.each do |key| # Setter diff --git a/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb new file mode 100644 index 0000000..270081c --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb @@ -0,0 +1,31 @@ +shared_examples 'it has the appropriate methods for uri-only keys v3' do + + described_class.new.uri_only_keys.each do |prop| + + describe "#{prop}=" do + it "sets #{prop}" do + ex = 'http://example.org/foo' + subject.send("#{prop}=", ex) + expect(subject[prop]).to eq ex + end + it 'raises an exception when attempting to set it to something other than a String' do + expect { subject.send("#{prop}=", ['Foo']) }.to raise_error IIIF::V3::Presentation::IllegalValueError + expect { subject.send("#{prop}=", nil) }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + it 'raises an exception when attempting to set it to something other than a parseable URI' do + expect { subject.send("#{prop}=", 'Not a URI') }.to raise_error IIIF::V3::Presentation::IllegalValueError + expect { subject.send("#{prop}=", '') }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end + + describe "#{prop}" do + it "gets #{prop}" do + ex = 'bar' + subject[prop] = ex + expect(subject.send(prop)).to eq ex + end + end + + end + +end From 550ff22cb24cc8861a67e81cec7bedc822f8b264 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 17:23:56 -0700 Subject: [PATCH 39/91] v3: add shared_examples for hash_only_keys --- .../shared_examples/hash_only_keys.rb | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb diff --git a/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb new file mode 100644 index 0000000..d5f2269 --- /dev/null +++ b/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb @@ -0,0 +1,40 @@ +shared_examples 'it has the appropriate methods for hash-only keys v3' do + + described_class.new.hash_only_keys.each do |prop| + + describe "#{prop}=" do + it "sets #{prop}" do + ex = {'label' => 'XYZ', 'fooBar' => 'bar'} + subject.send("#{prop}=", ex) + expect(subject[prop]).to eq ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}=" do + ex = [{'label' => 'XYZ'}] + subject.send("#{prop.camelize(:lower)}=", ex) + expect(subject[prop]).to eq ex + end + end + it 'raises an exception when attempting to set it to something other than a Hash' do + expect { subject.send("#{prop}=", ['Foo']) }.to raise_error IIIF::V3::Presentation::IllegalValueError + end + end + + describe "#{prop}" do + it "gets #{prop}" do + ex = {'label' => 'XYZ', 'fooBar' => 'bar'} + subject[prop] = ex + expect(subject.send(prop)).to eq ex + end + if prop.camelize(:lower) != prop + it "is aliased as ##{prop.camelize(:lower)}" do + ex = {'fooBar' => 'bar'} + subject[prop] = ex + expect(subject.send("#{prop.camelize(:lower)}")).to eq ex + end + end + end + + end + +end From acb111bf7231a66828b3fe9dab2c4628f674ada0 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 17:25:34 -0700 Subject: [PATCH 40/91] v3: add abstract_resource spec for *define_methods_for_xxx_keys --- ...stract_resource_define_methods_for_spec.rb | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb diff --git a/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb b/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb new file mode 100644 index 0000000..cf7064c --- /dev/null +++ b/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb @@ -0,0 +1,78 @@ +class AbstractResourceSubClass < IIIF::V3::AbstractResource + TYPE = 'Ignore' + # need a property here for type of key not already initialized in AbstractResource + def array_only_keys + super + %w{ my_array } + end + def hash_only_keys + super + %w{ my_hash } + end + def int_only_keys + super + %w{ my_int } + end + def numeric_only_keys + super + %w{ my_num } + end + def uri_only_keys + super + %w{ my_uri } + end + + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end +end + +describe AbstractResourceSubClass do + + describe '*define_methods_for_abstract_resource_only_keys' do + it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' + end + describe "*define_methods_for_any_type_keys" do + # shared_example expects fixed_values; these are roughly based on Stanford purl code + # (see https://github.com/sul-dlss/purl/blob/master/app/models/iiif3_presentation_manifest.rb) + let(:fixed_values) do + { + 'label' => 'foo', + 'description' => 'bar', + 'thumbnail' => IIIF::V3::Presentation::ImageResource.new( + 'type' => 'Image', + 'id' => "http://example.org/full/!400,400/0/default.jpg", + 'format' => 'image/jpeg' + ), + 'attribution' => ['foo'], + 'logo' => { + 'id' => 'https://example.org/default.jpg', + 'service' => IIIF::V3::Presentation::Service.new( + '@context' => 'http://iiif.io/api/image/2/context.json', + '@id' => 'http://example.org/1', + 'id' => 'http://example.org/1', + 'profile' => 'http://example.org/whatever' + ) + }, + 'see_also' => { + 'id' => 'http://example.org/whatever', + 'format' => 'application/mods+xml' + }, + 'related' => ['no', 'idea'], + 'within' => {'foo' => 'bar'} + } + end + it_behaves_like 'it has the appropriate methods for any-type keys v3' + end + describe "*define_methods_for_array_only_keys" do + it_behaves_like 'it has the appropriate methods for array-only keys v3' + end + describe "*define_methods_for_int_only_keys" do + it_behaves_like 'it has the appropriate methods for integer-only keys v3' + end + describe "*define_methods_for_numeric_only_keys" do + it_behaves_like 'it has the appropriate methods for numeric-only keys v3' + end + describe "*define_methods_for_string_only_keys" do + it_behaves_like 'it has the appropriate methods for string-only keys v3' + end + describe "*define_methods_for_uri_only_keys" do + it_behaves_like 'it has the appropriate methods for uri-only keys v3' + end +end From 11cd7e1334b464dc6dfb1ea0cff30370aa883cad Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 17:30:53 -0700 Subject: [PATCH 41/91] v3: remove unused require from shared_examples --- spec/unit/iiif/v3/presentation/resource_spec.rb | 2 +- .../presentation/shared_examples/abstract_resource_only_keys.rb | 2 -- spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb | 2 -- .../iiif/v3/presentation/shared_examples/array_only_keys.rb | 2 -- spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb | 2 -- .../iiif/v3/presentation/shared_examples/numeric_only_keys.rb | 2 -- .../iiif/v3/presentation/shared_examples/string_only_keys.rb | 2 -- 7 files changed, 1 insertion(+), 13 deletions(-) diff --git a/spec/unit/iiif/v3/presentation/resource_spec.rb b/spec/unit/iiif/v3/presentation/resource_spec.rb index a004f40..6279b0b 100644 --- a/spec/unit/iiif/v3/presentation/resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/resource_spec.rb @@ -1,6 +1,6 @@ describe IIIF::V3::Presentation::Resource do - describe "#{described_class}.define_methods_for_abstract_resource_only_keys" do + describe "#{described_class}.abstract_resource_only_keys" do it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb index 037a7d5..8d4cd88 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb @@ -1,5 +1,3 @@ -require 'set' - shared_examples 'it has the appropriate methods for abstract_resource_only_keys v3' do described_class.new.abstract_resource_only_keys.each do |entry| diff --git a/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb index 930855a..ab99eee 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb @@ -1,5 +1,3 @@ -require 'set' - shared_examples 'it has the appropriate methods for any-type keys v3' do described_class.new.any_type_keys.each do |prop| diff --git a/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb index 199801c..5ef0160 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb @@ -1,5 +1,3 @@ -require 'set' - shared_examples 'it has the appropriate methods for array-only keys v3' do described_class.new.array_only_keys.each do |prop| diff --git a/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb index 069d008..5f4a9ae 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb @@ -1,5 +1,3 @@ -require 'set' - shared_examples 'it has the appropriate methods for integer-only keys v3' do described_class.new.int_only_keys.each do |prop| diff --git a/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb index b21b642..a0e27b3 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb @@ -1,5 +1,3 @@ -require 'set' - shared_examples 'it has the appropriate methods for numeric-only keys v3' do described_class.new.numeric_only_keys.each do |prop| diff --git a/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb index 065a94e..0e819b3 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb @@ -1,5 +1,3 @@ -require 'set' - shared_examples 'it has the appropriate methods for string-only keys v3' do described_class.new.string_only_keys.each do |prop| From 3567f1b410f86866eae91029e9e0eec7278f6c80 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 6 Jul 2017 16:18:26 -0700 Subject: [PATCH 42/91] v3 add viewing_direction to string_only_keys in abstract_resource --- lib/iiif/v3/abstract_resource.rb | 2 +- lib/iiif/v3/presentation/annotation_collection.rb | 7 +++---- lib/iiif/v3/presentation/manifest.rb | 4 ---- lib/iiif/v3/presentation/sequence.rb | 2 +- spec/unit/iiif/v3/presentation/sequence_spec.rb | 2 +- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index a903051..d2acd70 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -16,7 +16,7 @@ def any_type_keys # these are allowed on all classes end def string_only_keys - %w{ viewing_hint } # should any of the any_type_keys be here? + %w{ viewing_hint viewing_direction } end def array_only_keys diff --git a/lib/iiif/v3/presentation/annotation_collection.rb b/lib/iiif/v3/presentation/annotation_collection.rb index 20c4674..dbb6dc4 100644 --- a/lib/iiif/v3/presentation/annotation_collection.rb +++ b/lib/iiif/v3/presentation/annotation_collection.rb @@ -17,16 +17,15 @@ def array_only_keys super + %w{ content } end - def string_only_keys - # first and last are actually uris - super + %w{ viewing_direction, first, last } + def uri_only_keys + super + %w{ first last } end def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' super(hsh) end - + end end end diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index 1490c4d..329bca7 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -9,10 +9,6 @@ def required_keys super + %w{ id label } end - def string_only_keys - super + %w{ viewing_direction } - end - def array_only_keys super + %w{ sequences structures } end diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index 2c239bc..f37c233 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -10,7 +10,7 @@ def array_only_keys end def string_only_keys - super + %w{ start_canvas viewing_direction } + super + %w{ start_canvas } end def legal_viewing_hint_values diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index 2103c48..c522020 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -71,7 +71,7 @@ def initialize(hsh={}) describe '#string_only_keys' do it 'accumulates from the superclass' do - expect(subject.string_only_keys).to eq %w{ viewing_hint start_canvas viewing_direction } + expect(subject.string_only_keys).to eq %w{ viewing_hint viewing_direction start_canvas } end end From 4e6cc4757aa24537dff5101d87c7161557e3f552 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 6 Jul 2017 16:29:52 -0700 Subject: [PATCH 43/91] v3 abstract_resource_spec improvements --- lib/iiif/v3/abstract_resource.rb | 9 ++--- spec/unit/iiif/v3/abstract_resource_spec.rb | 37 ++++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index d2acd70..2b70488 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -102,7 +102,8 @@ def validate end end - # note that xxx_only key values are checked via, e.g. self.define_methods_for_array_only_keys + # Note: self.define_methods_for_xxx_only_keys does NOT provide validation + # when key values are assigned directly with hash syntax, e.g. my_image_resource['format']= 'image/jpeg' # Viewing Direction values if self.has_key?('viewing_direction') @@ -114,12 +115,12 @@ def validate # Viewing Hint values if self.has_key?('viewing_hint') unless self.legal_viewing_hint_values.include?(self['viewing_hint']) - m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}." + m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}" raise IIIF::V3::Presentation::IllegalValueError, m end end # Metadata is all hashes - if self.has_key?('metadata') + if self.has_key?('metadata') && self['metadata'].kind_of?(Array) unless self['metadata'].all? { |entry| entry.kind_of?(Hash) } m = 'All entries in the metadata list must be a type of Hash' raise IIIF::V3::Presentation::IllegalValueError, m @@ -345,7 +346,7 @@ def define_methods_for_abstract_resource_only_keys def define_methods_for_string_only_keys define_accessor_methods(*string_only_keys) do |key, arg| unless arg.kind_of?(String) - m = "#{key} must be an String." + m = "#{key} must be a String." raise IIIF::V3::Presentation::IllegalValueError, m end end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index b3c5e45..667750b 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -20,13 +20,18 @@ def required_keys end end end - subject do instance = abstract_resource_subclass.new instance['id'] = 'http://example.com/prefix/manifest/123' instance end + describe '#required_keys' do + it 'accumulate' do + expect(subject.required_keys).to eq %w{ type id } + end + end + describe '#initialize' do it 'raises an error if you try to instantiate AbstractResource' do expect { IIIF::V3::AbstractResource.new }.to raise_error(RuntimeError) @@ -41,7 +46,29 @@ def required_keys end end - describe 'A nested object (e.g. self[\'metdata\'])' do + describe '#validate' do + it 'raises MissingRequiredKeyError if required key is missing' do + subject.required_keys.each { |k| subject.delete(k) } + expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + end + it 'raises IllegalValueError for bad viewing_direction' do + subject['viewing_direction'] = 'foo' + exp_err_msg = "viewingDirection must be one of #{subject.legal_viewing_direction_values}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for bad viewing_hint' do + subject['viewing_hint'] = 'foo' + exp_err_msg = "viewingHint for #{subject.class} must be one of #{subject.legal_viewing_hint_values}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for metadata entry that is not a Hash' do + subject['metadata'] = [{ 'foo' => 'bar' }, 'error', { 'bar' => 'foo' }] + exp_err_msg = "All entries in the metadata list must be a type of Hash" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + end + + describe 'A nested object (e.g. self[\'metadata\'])' do it 'returns [] if not set' do expect(subject.metadata).to eq([]) end @@ -90,12 +117,6 @@ def required_keys end end - describe '#required_keys' do - it 'accumulates' do - expect(subject.required_keys).to eq %w{ type id } - end - end - describe '#to_ordered_hash' do describe 'adds the @context' do before(:each) { subject.delete('@context') } From 2301bd9392eb25ff69062f99c033a2ded32faa4f Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 13:33:32 -0700 Subject: [PATCH 44/91] v3 refactor: abstract_resource rename 'arg' to 'val' in context of key, val pairs --- lib/iiif/v3/abstract_resource.rb | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 2b70488..9bc7d9a 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -311,8 +311,8 @@ def define_methods_for_any_type_keys end def define_methods_for_array_only_keys - define_accessor_methods(*array_only_keys) do |key, arg| - unless arg.kind_of?(Array) + define_accessor_methods(*array_only_keys) do |key, val| + unless val.kind_of?(Array) m = "#{key} must be an Array." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -320,8 +320,8 @@ def define_methods_for_array_only_keys end def define_methods_for_hash_only_keys - define_accessor_methods(*hash_only_keys) do |key, arg| - unless arg.kind_of?(Hash) + define_accessor_methods(*hash_only_keys) do |key, val| + unless val.kind_of?(Hash) m = "#{key} must be a Hash." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -334,8 +334,8 @@ def define_methods_for_abstract_resource_only_keys key = hsh[:key] type = hsh[:type] - define_accessor_methods(key) do |k, arg| - unless arg.kind_of?(type) + define_accessor_methods(key) do |k, val| + unless val.kind_of?(type) m = "#{k} must be an #{type}." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -344,8 +344,8 @@ def define_methods_for_abstract_resource_only_keys end def define_methods_for_string_only_keys - define_accessor_methods(*string_only_keys) do |key, arg| - unless arg.kind_of?(String) + define_accessor_methods(*string_only_keys) do |key, val| + unless val.kind_of?(String) m = "#{key} must be a String." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -353,8 +353,8 @@ def define_methods_for_string_only_keys end def define_methods_for_int_only_keys - define_accessor_methods(*int_only_keys) do |key, arg| - unless arg.kind_of?(Integer) && arg > 0 + define_accessor_methods(*int_only_keys) do |key, val| + unless val.kind_of?(Integer) && val > 0 m = "#{key} must be a positive Integer." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -362,8 +362,8 @@ def define_methods_for_int_only_keys end def define_methods_for_numeric_only_keys - define_accessor_methods(*numeric_only_keys) do |key, arg| - unless arg.kind_of?(Numeric) && arg > 0 + define_accessor_methods(*numeric_only_keys) do |key, val| + unless val.kind_of?(Numeric) && val > 0 m = "#{key} must be a positive Integer or Float." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -371,8 +371,8 @@ def define_methods_for_numeric_only_keys end def define_methods_for_uri_only_keys - define_accessor_methods(*uri_only_keys) do |key, arg| - unless arg.kind_of?(String) && arg =~ URI::regexp + define_accessor_methods(*uri_only_keys) do |key, val| + unless val.kind_of?(String) && val =~ URI::regexp m = "#{key} must be a String containing a URI." raise IIIF::V3::Presentation::IllegalValueError, m end @@ -382,14 +382,14 @@ def define_methods_for_uri_only_keys def define_accessor_methods(*keys, &validation) keys.each do |key| # Setter - define_singleton_method("#{key}=") do |arg| - validation.call(key, arg) if block_given? - self.send('[]=', key, arg) + define_singleton_method("#{key}=") do |val| + validation.call(key, val) if block_given? + self.send('[]=', key, val) end if key.camelize(:lower) != key - define_singleton_method("#{key.camelize(:lower)}=") do |arg| - validation.call(key, arg) if block_given? - self.send('[]=', key, arg) + define_singleton_method("#{key.camelize(:lower)}=") do |val| + validation.call(key, val) if block_given? + self.send('[]=', key, val) end end # Getter From 6fe4ec35f2f3b6cada1886c06646cb74b332d5a8 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 13:35:38 -0700 Subject: [PATCH 45/91] v3 abstract_resource add some constants with properties groupings --- lib/iiif/v3/abstract_resource.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 9bc7d9a..d330e62 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -5,7 +5,13 @@ module V3 class AbstractResource include IIIF::HashBehaviours - # Every subclass should override the following methods defining key value types, see Subclasses for how + # properties used by content resources only + CONTENT_RESOURCE_PROPERTIES = %w{ format height width duration } + + # used by Collection, AnnotationCollection + PAGING_PROPERTIES = %w{ first last next prev total start_index } + + # subclasses should override required_keys as appropriate, e.g. super + %w{ id } def required_keys %w{ type } end From 2ade9f70549c1be576124c4c2f2a03164d1387ef Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 13:42:22 -0700 Subject: [PATCH 46/91] v3 refactor: abstract_resource implement prohibited_keys method --- lib/iiif/v3/abstract_resource.rb | 12 ++++++++++++ lib/iiif/v3/presentation.rb | 1 + spec/unit/iiif/v3/abstract_resource_spec.rb | 9 +++++++++ 3 files changed, 22 insertions(+) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index d330e62..48cd643 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -16,6 +16,11 @@ def required_keys %w{ type } end + # subclasses should override prohibited_keys as appropriate, e.g. super + PAGING_PROPERTIES + def prohibited_keys + %w{ } + end + def any_type_keys # these are allowed on all classes %w{ label description thumbnail attribution logo see_also related within } @@ -108,6 +113,13 @@ def validate end end + self.prohibited_keys.each do |k| + if self.has_key?(k) + m = "#{k} is a prohibited key in #{self.class}" + raise IIIF::V3::Presentation::ProhibitedKeyError, m + end + end + # Note: self.define_methods_for_xxx_only_keys does NOT provide validation # when key values are assigned directly with hash syntax, e.g. my_image_resource['format']= 'image/jpeg' diff --git a/lib/iiif/v3/presentation.rb b/lib/iiif/v3/presentation.rb index 7c79bf6..eb3686c 100644 --- a/lib/iiif/v3/presentation.rb +++ b/lib/iiif/v3/presentation.rb @@ -29,6 +29,7 @@ module Presentation ] class MissingRequiredKeyError < StandardError; end + class ProhibitedKeyError < StandardError; end class IllegalValueError < StandardError; end end end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 667750b..6a070ac 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -18,6 +18,10 @@ def initialize(hsh={}) def required_keys super + %w{ id } end + + def prohibited_keys + super + %w{ verboten } + end end end subject do @@ -51,6 +55,11 @@ def required_keys subject.required_keys.each { |k| subject.delete(k) } expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError end + it 'raises ProhibitedKeyError if prohibited key is present' do + subject['verboten'] = 666 + exp_err_msg = "verboten is a prohibited key in #{subject.class}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::ProhibitedKeyError, exp_err_msg) + end it 'raises IllegalValueError for bad viewing_direction' do subject['viewing_direction'] = 'foo' exp_err_msg = "viewingDirection must be one of #{subject.legal_viewing_direction_values}" From 79916f2ba6417b3c86a6678c4246e483e04ba130 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 13:43:16 -0700 Subject: [PATCH 47/91] v3 abstract_resource_define_methods_for_spec: add hash only methods --- .../v3/abstract_resource_define_methods_for_spec.rb | 13 ++++++++----- .../presentation/shared_examples/hash_only_keys.rb | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb b/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb index cf7064c..07d395c 100644 --- a/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb @@ -2,19 +2,19 @@ class AbstractResourceSubClass < IIIF::V3::AbstractResource TYPE = 'Ignore' # need a property here for type of key not already initialized in AbstractResource def array_only_keys - super + %w{ my_array } + super + %w{ array my_array } end def hash_only_keys - super + %w{ my_hash } + super + %w{ hash my_hash } end def int_only_keys - super + %w{ my_int } + super + %w{ int my_int } end def numeric_only_keys - super + %w{ my_num } + super + %w{ num my_num } end def uri_only_keys - super + %w{ my_uri } + super + %w{ uri my_uri } end def initialize(hsh={}) @@ -63,6 +63,9 @@ def initialize(hsh={}) describe "*define_methods_for_array_only_keys" do it_behaves_like 'it has the appropriate methods for array-only keys v3' end + describe "*define_methods_for_hash_only_keys" do + it_behaves_like 'it has the appropriate methods for hash-only keys v3' + end describe "*define_methods_for_int_only_keys" do it_behaves_like 'it has the appropriate methods for integer-only keys v3' end diff --git a/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb b/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb index d5f2269..b4c16a7 100644 --- a/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +++ b/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb @@ -10,7 +10,7 @@ end if prop.camelize(:lower) != prop it "is aliased as ##{prop.camelize(:lower)}=" do - ex = [{'label' => 'XYZ'}] + ex = {'label' => 'XYZ'} subject.send("#{prop.camelize(:lower)}=", ex) expect(subject[prop]).to eq ex end From 5507156e48ac6b85946f8dca310a013e8d6da4dc Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 13:48:28 -0700 Subject: [PATCH 48/91] v3 refactor: abstract_resource - beef up xxx_only_keys inclusions (affects sequence due to 'items') --- lib/iiif/v3/abstract_resource.rb | 21 +++++++++----- .../v3/presentation/annotation_collection.rb | 4 --- lib/iiif/v3/presentation/sequence.rb | 8 ++---- .../iiif/v3/abstract_resource_spec.rb | 9 +++--- .../iiif/v3/presentation/sequence_spec.rb | 28 ++++++------------- 5 files changed, 29 insertions(+), 41 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 48cd643..38b58ba 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -21,17 +21,21 @@ def prohibited_keys %w{ } end - def any_type_keys # these are allowed on all classes - %w{ label description thumbnail attribution logo see_also - related within } + # NOTE: keys associated with a single resource type are not included below in xxx_keys methods: + # those single resource types should include additional keys by overriding xxx_keys as appropriate + + def any_type_keys + # values *may* be multivalued + # NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers" + %w{ label description id attribution logo related rendering see_also within } end def string_only_keys - %w{ viewing_hint viewing_direction } + %w{ nav_date type format viewing_direction viewing_hint start_canvas } end def array_only_keys - %w{ metadata rights } + %w{ metadata rights thumbnail first last next prev items } end def abstract_resource_only_keys @@ -41,12 +45,15 @@ def abstract_resource_only_keys def hash_only_keys %w{ } end + def int_only_keys - %w{ } + %w{ height width total start_index } end + def numeric_only_keys - %w{ } + %w{ duration } end + def uri_only_keys %w{ } end diff --git a/lib/iiif/v3/presentation/annotation_collection.rb b/lib/iiif/v3/presentation/annotation_collection.rb index dbb6dc4..ee9a756 100644 --- a/lib/iiif/v3/presentation/annotation_collection.rb +++ b/lib/iiif/v3/presentation/annotation_collection.rb @@ -17,10 +17,6 @@ def array_only_keys super + %w{ content } end - def uri_only_keys - super + %w{ first last } - end - def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' super(hsh) diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index f37c233..9b7b132 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -5,12 +5,8 @@ class Sequence < IIIF::V3::AbstractResource TYPE = 'Sequence' - def array_only_keys - super + %w{ canvases } - end - - def string_only_keys - super + %w{ start_canvas } + def required_keys + super + %w{ items } end def legal_viewing_hint_values diff --git a/spec/integration/iiif/v3/abstract_resource_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb index 59efc08..e597129 100644 --- a/spec/integration/iiif/v3/abstract_resource_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -69,7 +69,7 @@ "viewingHint":"paged", "startCanvas": "http://www.example.org/iiif/book1/canvas/p2", - "canvases": [ + "items": [ { "id": "http://example.com/canvas", "type": "Canvas", @@ -128,12 +128,11 @@ expect(s.class).to be expected_klass end end - it 'turns each member of sequences/canvaes in an instance of Canvas' do - expected_klass = IIIF::V3::Presentation::Canvas + it 'turns each member of items into an instance of Canvas' do parsed = described_class.from_ordered_hash(fixture) parsed['sequences'].each do |s| - s.canvases.each do |c| - expect(c.class).to be expected_klass + s.items.each do |c| + expect(c.class).to be IIIF::V3::Presentation::Canvas end end end diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index c522020..3bcb069 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -1,5 +1,11 @@ describe IIIF::V3::Presentation::Sequence do + describe '#required_keys' do + it 'accumulates from the superclass' do + expect(subject.required_keys).to eq %w{ type items } + end + end + let(:subclass_subject) do Class.new(IIIF::V3::Presentation::Sequence) do def initialize(hsh={}) @@ -39,7 +45,7 @@ def initialize(hsh={}) 'within' => 'http://www.example.org/collections/books/', # Sequence 'metadata' => [{'label'=>'Author', 'value'=>'Anne Author'}], - 'canvases' => [{ + 'items' => [{ 'id' => 'http://www.example.org/iiif/book1/canvas/p1', 'type' => 'Canvas', 'label' => 'p. 1', @@ -63,24 +69,6 @@ def initialize(hsh={}) end end - describe '#required_keys' do - it 'accumulates from the superclass' do - expect(subject.required_keys).to eq %w{ type } - end - end - - describe '#string_only_keys' do - it 'accumulates from the superclass' do - expect(subject.string_only_keys).to eq %w{ viewing_hint viewing_direction start_canvas } - end - end - - describe '#array_only_keys' do - it 'accumulates from the superclass' do - expect(subject.array_only_keys).to eq %w{ metadata rights canvases } - end - end - describe "#{described_class}.define_methods_for_array_only_keys" do it_behaves_like 'it has the appropriate methods for array-only keys v3' end @@ -96,10 +84,12 @@ def initialize(hsh={}) describe '#validate' do it 'raises an error if viewing_hint isn\'t an allowable value' do subject['viewing_hint'] = 'foo' + subject['items'] = [IIIF::V3::Presentation::Canvas.new] expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError end it 'raises an error if viewing_directon isn\'t an allowable value' do subject['viewing_direction'] = 'foo-to-bar' + subject['items'] = [IIIF::V3::Presentation::Canvas.new] expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError end end From 81732dd42721e27f2ab03ad1b4773692b71e8690 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 14:27:51 -0700 Subject: [PATCH 49/91] v3 abstract_resource: improve validation for metadata and thumbnail properties --- lib/iiif/v3/abstract_resource.rb | 27 ++++++++++++++++++--- spec/unit/iiif/v3/abstract_resource_spec.rb | 23 ++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 38b58ba..e7b4003 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -144,12 +144,33 @@ def validate raise IIIF::V3::Presentation::IllegalValueError, m end end - # Metadata is all hashes - if self.has_key?('metadata') && self['metadata'].kind_of?(Array) + # Metadata is Array; each entry is a Hash containing (only) 'label' and 'value' properties + if self.has_key?('metadata') && self['metadata'] unless self['metadata'].all? { |entry| entry.kind_of?(Hash) } - m = 'All entries in the metadata list must be a type of Hash' + m = 'metadata must be an Array with Hash members' raise IIIF::V3::Presentation::IllegalValueError, m end + self['metadata'].each do |entry| + md_keys = entry.keys + unless md_keys.size == 2 && md_keys.include?('label') && md_keys.include?('value') + m = "metadata members must be a Hash of keys 'label' and 'value'" + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end + # thumbnail is Array; each entry is a Hash containing (at least) 'id' and 'type' keys + if self.has_key?('thumbnail') && self['thumbnail'] + unless self['thumbnail'].all? { |entry| entry.kind_of?(Hash) } + m = 'thumbnail must be an Array with Hash members' + raise IIIF::V3::Presentation::IllegalValueError, m + end + self['thumbnail'].each do |entry| + thumb_keys = entry.keys + unless thumb_keys.include?('id') && thumb_keys.include?('type') + m = 'thumbnail members must be a Hash including keys "id" and "type"' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end end end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 6a070ac..6c73759 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -71,8 +71,27 @@ def prohibited_keys expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end it 'raises IllegalValueError for metadata entry that is not a Hash' do - subject['metadata'] = [{ 'foo' => 'bar' }, 'error', { 'bar' => 'foo' }] - exp_err_msg = "All entries in the metadata list must be a type of Hash" + subject['metadata'] = ['error'] + exp_err_msg = "metadata must be an Array with Hash members" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for metadata entry that does not contain exactly "label" and "value"' do + subject['metadata'] = [{ 'label' => 'bar', 'value' => 'foo' }] + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + subject['metadata'] = [{ 'label' => 'bar', 'bar' => 'foo' }] + exp_err_msg = "metadata members must be a Hash of keys 'label' and 'value'" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for thumbnail entry that is not a Hash' do + subject['thumbnail'] = ['error'] + exp_err_msg = "thumbnail must be an Array with Hash members" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for thumbnail entry that does not contain "id" and "type"' do + subject['thumbnail'] = [{ 'id' => 'bar', 'type' => 'foo', 'random' => 'xxx' }] + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + subject['thumbnail'] = [{ 'id' => 'bar' }] + exp_err_msg = 'thumbnail members must be a Hash including keys "id" and "type"' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end From 93dd74105637394c88a020697bf991a910e82a82 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 17:09:58 -0700 Subject: [PATCH 50/91] v3 abstract_resource: improve validation for navDate property --- lib/iiif/v3/abstract_resource.rb | 16 +++++- spec/unit/iiif/v3/abstract_resource_spec.rb | 61 ++++++++++++++------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index e7b4003..ce75632 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -158,7 +158,7 @@ def validate end end end - # thumbnail is Array; each entry is a Hash containing (at least) 'id' and 'type' keys + # Thumbnail is Array; each entry is a Hash containing (at least) 'id' and 'type' keys if self.has_key?('thumbnail') && self['thumbnail'] unless self['thumbnail'].all? { |entry| entry.kind_of?(Hash) } m = 'thumbnail must be an Array with Hash members' @@ -172,6 +172,20 @@ def validate end end end + # NavDate (navigation date) + if self.has_key?('nav_date') + begin + Date.strptime(self['nav_date'], '%Y-%m-%dT%H:%M:%SZ') + rescue ArgumentError + m = "nav_date must be of form YYYY-MM-DDThh:mm:ssZ" + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + + # TODO: rights - confusing; Array of hashes? including id which must be a URI? + # rights + + # TODO: rendering - A label and the format of the rendering resource must be supplied end # Options diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 6c73759..e7a0852 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -70,29 +70,48 @@ def prohibited_keys exp_err_msg = "viewingHint for #{subject.class} must be one of #{subject.legal_viewing_hint_values}" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end - it 'raises IllegalValueError for metadata entry that is not a Hash' do - subject['metadata'] = ['error'] - exp_err_msg = "metadata must be an Array with Hash members" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) - end - it 'raises IllegalValueError for metadata entry that does not contain exactly "label" and "value"' do - subject['metadata'] = [{ 'label' => 'bar', 'value' => 'foo' }] - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) - subject['metadata'] = [{ 'label' => 'bar', 'bar' => 'foo' }] - exp_err_msg = "metadata members must be a Hash of keys 'label' and 'value'" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'metadata' do + it 'raises IllegalValueError for entry that is not a Hash' do + subject['metadata'] = ['error'] + exp_err_msg = "metadata must be an Array with Hash members" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'does not raise error for entry that contains exactly "label" and "value"' do + subject['metadata'] = [{ 'label' => 'bar', 'value' => 'foo' }] + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + end + it 'raises IllegalValueError for entry that does not contain exactly "label" and "value"' do + subject['metadata'] = [{ 'label' => 'bar', 'bar' => 'foo' }] + exp_err_msg = "metadata members must be a Hash of keys 'label' and 'value'" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end - it 'raises IllegalValueError for thumbnail entry that is not a Hash' do - subject['thumbnail'] = ['error'] - exp_err_msg = "thumbnail must be an Array with Hash members" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'thumbnail' do + it 'raises IllegalValueError for entry that is not a Hash' do + subject['thumbnail'] = ['error'] + exp_err_msg = "thumbnail must be an Array with Hash members" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'does not raise error for entry with "id" and "type"' do + subject['thumbnail'] = [{ 'id' => 'bar', 'type' => 'foo', 'random' => 'xxx' }] + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + end + it 'raises IllegalValueError for entry that does not contain "id" and "type"' do + subject['thumbnail'] = [{ 'id' => 'bar' }] + exp_err_msg = 'thumbnail members must be a Hash including keys "id" and "type"' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end - it 'raises IllegalValueError for thumbnail entry that does not contain "id" and "type"' do - subject['thumbnail'] = [{ 'id' => 'bar', 'type' => 'foo', 'random' => 'xxx' }] - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) - subject['thumbnail'] = [{ 'id' => 'bar' }] - exp_err_msg = 'thumbnail members must be a Hash including keys "id" and "type"' - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'nav_date' do + it 'does not raise error for value of form YYYY-MM-DDThh:mm:ssZ' do + subject['nav_date'] = '1991-01-02T13:04:27Z' + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + end + it 'raises IllegalValueError for value not of form YYYY-MM-DDThh:mm:ssZ' do + subject['nav_date'] = '1991-01-02T13:04:27+0500' + exp_err_msg = 'nav_date must be of form YYYY-MM-DDThh:mm:ssZ' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end end From 8ba757db2a65dfa920881fa9520c8973ffbcada5 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 17:32:47 -0700 Subject: [PATCH 51/91] v3 abstract_resource: improve validation for rights property --- lib/iiif/v3/abstract_resource.rb | 32 +++++++++++++++------ spec/unit/iiif/v3/abstract_resource_spec.rb | 22 ++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index ce75632..cfd3e01 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -181,9 +181,20 @@ def validate raise IIIF::V3::Presentation::IllegalValueError, m end end - - # TODO: rights - confusing; Array of hashes? including id which must be a URI? - # rights + # rights is Array; each entry is a Hash containing 'id' with a URI value + if self.has_key?('rights') + unless self['rights'].all? { |entry| entry.kind_of?(Hash) } + m = 'rights must be an Array with Hash members' + raise IIIF::V3::Presentation::IllegalValueError, m + end + self['rights'].each do |entry| + unless entry.keys.include?('id') + m = 'rights members must be a Hash including "id"' + raise IIIF::V3::Presentation::IllegalValueError, m + end + validate_uri(entry['id'], 'id') # raises IllegalValueError + end + end # TODO: rendering - A label and the format of the rendering resource must be supplied end @@ -431,12 +442,7 @@ def define_methods_for_numeric_only_keys end def define_methods_for_uri_only_keys - define_accessor_methods(*uri_only_keys) do |key, val| - unless val.kind_of?(String) && val =~ URI::regexp - m = "#{key} must be a String containing a URI." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end + define_accessor_methods(*uri_only_keys) { |key, val| validate_uri(val, key) } end def define_accessor_methods(*keys, &validation) @@ -465,6 +471,14 @@ def define_accessor_methods(*keys, &validation) end end + private + def validate_uri(val, key) + unless val.kind_of?(String) && val =~ URI::regexp + m = "#{key} value must be a String containing a URI" + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end end end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index e7a0852..b7b99f0 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -113,6 +113,28 @@ def prohibited_keys expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end + describe 'rights' do + #rights - confusing; Array of hashes? including id which must be a URI? + it 'raises IllegalValueError for entry that is not a Hash' do + subject['rights'] = ['error'] + exp_err_msg = "rights must be an Array with Hash members" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'does not raise error for entry with "id" that is URI' do + subject['rights'] = [{ 'id' => 'http://example.org/rights', 'format' => 'text/html' }] + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + end + it 'raises IllegalValueError for entry with "id" that is not URI' do + subject['rights'] = [{ 'id' => 'bar', 'format' => 'text/html' }] + exp_err_msg = "id value must be a String containing a URI" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for entry that does not contain "id"' do + subject['rights'] = [{ 'whoops' => 'http://example.org/rights' }] + exp_err_msg = 'rights members must be a Hash including "id"' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + end end describe 'A nested object (e.g. self[\'metadata\'])' do From 093685b54e6920ac7ce32f7de3880ab8deaaac84 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 17:41:55 -0700 Subject: [PATCH 52/91] v3 abstract_resource: improve validation for rendering property --- lib/iiif/v3/abstract_resource.rb | 25 ++++++++++++++++----- spec/unit/iiif/v3/abstract_resource_spec.rb | 17 +++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index cfd3e01..d5a15df 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -27,7 +27,7 @@ def prohibited_keys def any_type_keys # values *may* be multivalued # NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers" - %w{ label description id attribution logo related rendering see_also within } + %w{ label description id attribution logo related see_also within } end def string_only_keys @@ -35,7 +35,7 @@ def string_only_keys end def array_only_keys - %w{ metadata rights thumbnail first last next prev items } + %w{ metadata rights thumbnail rendering first last next prev items } end def abstract_resource_only_keys @@ -127,8 +127,9 @@ def validate end end - # Note: self.define_methods_for_xxx_only_keys does NOT provide validation - # when key values are assigned directly with hash syntax, e.g. my_image_resource['format']= 'image/jpeg' + # Note: self.define_methods_for_xxx_only_keys provides some validation at assignment time + # currently, there is NO validation when key values are assigned directly with hash syntax, + # e.g. my_image_resource['format'] = 'image/jpeg' # Viewing Direction values if self.has_key?('viewing_direction') @@ -195,8 +196,20 @@ def validate validate_uri(entry['id'], 'id') # raises IllegalValueError end end - - # TODO: rendering - A label and the format of the rendering resource must be supplied + # rendering is Array; each entry is a Hash containing 'label' and 'format' keys + if self.has_key?('rendering') && self['rendering'] + unless self['rendering'].all? { |entry| entry.kind_of?(Hash) } + m = 'rendering must be an Array with Hash members' + raise IIIF::V3::Presentation::IllegalValueError, m + end + self['rendering'].each do |entry| + rendering_keys = entry.keys + unless rendering_keys.include?('label') && rendering_keys.include?('format') + m = 'rendering members must be a Hash including keys "label" and "format"' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + end end # Options diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index b7b99f0..52436a2 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -114,7 +114,6 @@ def prohibited_keys end end describe 'rights' do - #rights - confusing; Array of hashes? including id which must be a URI? it 'raises IllegalValueError for entry that is not a Hash' do subject['rights'] = ['error'] exp_err_msg = "rights must be an Array with Hash members" @@ -135,6 +134,22 @@ def prohibited_keys expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end + describe 'rendering' do + it 'raises IllegalValueError for entry that is not a Hash' do + subject['rendering'] = ['error'] + exp_err_msg = "rendering must be an Array with Hash members" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'does not raise error for entry with "label" and "format"' do + subject['rendering'] = [{ 'label' => 'bar', 'format' => 'foo', 'random' => 'xxx' }] + expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + end + it 'raises IllegalValueError for entry that does not contain "label" and "format"' do + subject['rendering'] = [{ 'label' => 'bar' }] + exp_err_msg = 'rendering members must be a Hash including keys "label" and "format"' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + end end describe 'A nested object (e.g. self[\'metadata\'])' do From 65d7d879d5cf2593d4a1335c9598a42100d99487 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 7 Jul 2017 20:41:18 -0700 Subject: [PATCH 53/91] v3 abstract_resource_spec: validate tests tweak --- spec/unit/iiif/v3/abstract_resource_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 52436a2..8332b8a 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -78,7 +78,7 @@ def prohibited_keys end it 'does not raise error for entry that contains exactly "label" and "value"' do subject['metadata'] = [{ 'label' => 'bar', 'value' => 'foo' }] - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for entry that does not contain exactly "label" and "value"' do subject['metadata'] = [{ 'label' => 'bar', 'bar' => 'foo' }] @@ -94,7 +94,7 @@ def prohibited_keys end it 'does not raise error for entry with "id" and "type"' do subject['thumbnail'] = [{ 'id' => 'bar', 'type' => 'foo', 'random' => 'xxx' }] - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for entry that does not contain "id" and "type"' do subject['thumbnail'] = [{ 'id' => 'bar' }] @@ -105,7 +105,7 @@ def prohibited_keys describe 'nav_date' do it 'does not raise error for value of form YYYY-MM-DDThh:mm:ssZ' do subject['nav_date'] = '1991-01-02T13:04:27Z' - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for value not of form YYYY-MM-DDThh:mm:ssZ' do subject['nav_date'] = '1991-01-02T13:04:27+0500' @@ -121,7 +121,7 @@ def prohibited_keys end it 'does not raise error for entry with "id" that is URI' do subject['rights'] = [{ 'id' => 'http://example.org/rights', 'format' => 'text/html' }] - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for entry with "id" that is not URI' do subject['rights'] = [{ 'id' => 'bar', 'format' => 'text/html' }] @@ -142,7 +142,7 @@ def prohibited_keys end it 'does not raise error for entry with "label" and "format"' do subject['rendering'] = [{ 'label' => 'bar', 'format' => 'foo', 'random' => 'xxx' }] - expect { subject.validate }.not_to raise_error(IIIF::V3::Presentation::IllegalValueError) + expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for entry that does not contain "label" and "format"' do subject['rendering'] = [{ 'label' => 'bar' }] From 6347dcadb7f4847bb2ba0e11954bf75959560e19 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 5 Jul 2017 18:49:22 -0700 Subject: [PATCH 54/91] v3 service class: improve specificity, validation and write better tests --- lib/iiif/v3/presentation/service.rb | 31 +++- .../iiif/v3/abstract_resource_spec.rb | 1 + .../unit/iiif/v3/presentation/service_spec.rb | 156 ++++++++++++++++-- 3 files changed, 170 insertions(+), 18 deletions(-) diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb index de22add..ccbc035 100644 --- a/lib/iiif/v3/presentation/service.rb +++ b/lib/iiif/v3/presentation/service.rb @@ -1,13 +1,42 @@ module IIIF module V3 module Presentation + # See http://prezi3.iiif.io/api/annex/services for more info class Service < AbstractResource - # service is the only class that doesn't need a type + # constants included here for convenience + IIIF_IMAGE_V2_CONTEXT = 'http://iiif.io/api/image/2/context.json'.freeze + IIIF_IMAGE_V2_LEVEL1_PROFILE = 'http://iiif.io/api/image/2/level1.json'.freeze + IIIF_AUTHENTICATION_V1_LOGIN_PROFILE = 'http://iiif.io/api/auth/1/login'.freeze + IIIF_AUTHENTICATION_V1_TOKEN_PROFILE = 'http://iiif.io/api/auth/1/token'.freeze + + # service class doesn't require type def required_keys super.reject {|el| el == 'type' } end + def prohibited_keys + super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + + %w{ nav_date viewing_direction start_canvas content_annotation } + end + + def uri_only_keys + super + %w{ @context id @id } + end + + def validate + super + if IIIF_IMAGE_V2_CONTEXT == self['@context'] + unless self.has_key?('@id') + m = "@id is required for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + unless self.has_key?('profile') + m = "profile is required for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + end + end end end end diff --git a/spec/integration/iiif/v3/abstract_resource_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb index e597129..c366a9f 100644 --- a/spec/integration/iiif/v3/abstract_resource_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -49,6 +49,7 @@ "label": "My Manifest", "service": { "@context": "http://iiif.io/api/image/2/context.json", + "@id":"http://www.example.org/images/book1-page1", "id":"http://www.example.org/images/book1-page1", "profile":"http://iiif.io/api/image/2/profiles/level2.json" }, diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index 06db7ab..58cad1e 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -1,28 +1,150 @@ describe IIIF::V3::Presentation::Service do - describe 'self#get_descendant_class_by_jld_type' do - before do - class DummyClass < IIIF::V3::Presentation::Service - TYPE = "Collection" - def self.singleton_class? - true - end + describe '#required_keys' do + it '"type" is not required' do + expect(subject.required_keys).not_to include('type') + end + end + + describe '#prohibited_keys' do + keys = IIIF::V3::Presentation::Service::CONTENT_RESOURCE_PROPERTIES + + IIIF::V3::Presentation::Service::PAGING_PROPERTIES + + %w{ + nav_date + viewing_direction + start_canvas + content_annotation + } + keys.each do |k| + it "#{k} is prohibited" do + expect(subject.prohibited_keys).to include(k) end end - after do - Object.send(:remove_const, :DummyClass) + end + + describe '#uri_only_keys' do + it '@context' do + expect(subject.uri_only_keys).to include('@context') + end + it '@id' do + expect(subject.uri_only_keys).to include('@id') + end + it 'id' do + expect(subject.uri_only_keys).to include('id') end - it 'gets the right class' do - klass = described_class.get_descendant_class_by_jld_type('Canvas') - expect(klass).to eq IIIF::V3::Presentation::Canvas + end + + let(:id_uri) { "https://example.org/image1" } + + describe '#initialize' do + it 'assigns hash values passed in' do + label_val = 'foo' + inner_service_id = 'http://example.org/whatever' + inner_service_profile = 'http://iiif.io/api/auth/1/token' + inner_service_val = described_class.new({ + 'id' => inner_service_id, + 'profile' => inner_service_profile + }) + service_obj = described_class.new({ + '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'id' => id_uri, + 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE, + 'label' => label_val, + 'service' => [inner_service_val] + }) + expect(service_obj.keys.size).to eq 5 + expect(service_obj['id']).to eq id_uri + expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + expect(service_obj['label']).to eq label_val + expect(service_obj['service'][0]['id']).to eq inner_service_id + expect(service_obj['service'][0]['profile']).to eq inner_service_profile end - context "when there are singleton classes which are returned" do - it "gets the right class" do - allow(IIIF::V3::Presentation::Service).to receive(:descendants).and_return([DummyClass, IIIF::V3::Presentation::Collection]) - klass = described_class.get_descendant_class_by_jld_type('Collection') - expect(klass).to eq IIIF::V3::Presentation::Collection + it 'allows both "id" and "@id" as keys' do + id_uri = "https://stacks.stanford.edu/image/iiif/wy534zh7137%2FSULAIR_rosette" + service_obj = described_class.new({ + 'id' => id_uri, + '@id' => id_uri + }) + expect(service_obj.keys.size).to eq 2 + expect(service_obj['id']).to eq id_uri + expect(service_obj['@id']).to eq id_uri + end + it 'allows non-URI profile value' do + expect{ + described_class.new({ + "profile" => [ + "http://iiif.io/api/image/2/level2.json", + { + "formats" => [ "gif", "pdf" ], + "qualities" => [ "color", "gray" ], + "supports" => [ "canonicalLinkHeader", "rotationArbitrary", "http://example.com/feature" ] + } + ] + }) + }.not_to raise_error + end + end + + describe '#validate' do + describe '@context = IIIF_IMAGE_API_V2_CONTEXT' do + it 'must have a "@id"' do + service_obj = described_class.new({ + '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'id' => id_uri, + 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + }) + exp_err_msg = '@id is required for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' + expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) + end + it 'must have a profile' do + service_obj = described_class.new({ + '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + '@id' => id_uri + }) + exp_err_msg = 'profile is required for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' + expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) end end end + describe '"integration" tests' do + describe 'realistic examples from Stanford purl manifests' do + it 'iiif image v2 service' do + service_obj = described_class.new({ + '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'id' => id_uri, + '@id' => id_uri, + 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + }) + expect(service_obj.keys.size).to eq 4 + expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') + expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['id']).to eq id_uri + expect(service_obj['@id']).to eq id_uri + expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + end + it 'login service' do + service_obj = IIIF::V3::Presentation::Service.new( + 'id' => 'https://example.org/auth/iiif', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, + 'label' => 'label value', + 'service' => [{ + 'id' => 'https://example.org/image/iiif/token', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + }] + ) + expect(service_obj.keys.size).to eq 4 + expect(service_obj.keys).to include('id', 'profile', 'label', 'service') + expect(service_obj['id']).to eq 'https://example.org/auth/iiif' + expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE + expect(service_obj['label']).to eq 'label value' + inner_service = service_obj['service'][0] + expect(inner_service.keys.size).to eq 2 + expect(inner_service.keys).to include('id', 'profile') + expect(inner_service['id']).to eq 'https://example.org/image/iiif/token' + expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + end + end + end end From 89b48e7995280a7db382797dac169ce9e1ba8f5b Mon Sep 17 00:00:00 2001 From: Johnathan Martin Date: Tue, 11 Jul 2017 17:30:14 -0700 Subject: [PATCH 55/91] additional validation for id and @id consistency, corresponding test. two minor service_spec simplifications. --- lib/iiif/v3/presentation/service.rb | 4 + .../unit/iiif/v3/presentation/service_spec.rb | 101 +++++++++--------- 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb index ccbc035..e278100 100644 --- a/lib/iiif/v3/presentation/service.rb +++ b/lib/iiif/v3/presentation/service.rb @@ -31,6 +31,10 @@ def validate m = "@id is required for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" raise IIIF::V3::Presentation::MissingRequiredKeyError, m end + if self.has_key?('id') && (self['@id'] != self['id']) + m = "id and @id values must match for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" + raise IIIF::V3::Presentation::IllegalValueError, m + end unless self.has_key?('profile') m = "profile is required for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" raise IIIF::V3::Presentation::MissingRequiredKeyError, m diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index 58cad1e..4be33bd 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -7,18 +7,16 @@ end describe '#prohibited_keys' do - keys = IIIF::V3::Presentation::Service::CONTENT_RESOURCE_PROPERTIES + - IIIF::V3::Presentation::Service::PAGING_PROPERTIES + - %w{ - nav_date - viewing_direction - start_canvas - content_annotation - } - keys.each do |k| - it "#{k} is prohibited" do - expect(subject.prohibited_keys).to include(k) - end + it 'contains the expected key names' do + keys = described_class::CONTENT_RESOURCE_PROPERTIES + + described_class::PAGING_PROPERTIES + + %w{ + nav_date + viewing_direction + start_canvas + content_annotation + } + expect(subject.prohibited_keys).to include(*keys) end end @@ -105,46 +103,53 @@ exp_err_msg = 'profile is required for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) end - end - end - - describe '"integration" tests' do - describe 'realistic examples from Stanford purl manifests' do - it 'iiif image v2 service' do + it 'must have matching values for "@id" and "id" if both are specified' do service_obj = described_class.new({ '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, - 'id' => id_uri, '@id' => id_uri, - 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE - }) - expect(service_obj.keys.size).to eq 4 - expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') - expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT - expect(service_obj['id']).to eq id_uri - expect(service_obj['@id']).to eq id_uri - expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE - end - it 'login service' do - service_obj = IIIF::V3::Presentation::Service.new( - 'id' => 'https://example.org/auth/iiif', - 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, - 'label' => 'label value', - 'service' => [{ - 'id' => 'https://example.org/image/iiif/token', - 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE - }] - ) - expect(service_obj.keys.size).to eq 4 - expect(service_obj.keys).to include('id', 'profile', 'label', 'service') - expect(service_obj['id']).to eq 'https://example.org/auth/iiif' - expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE - expect(service_obj['label']).to eq 'label value' - inner_service = service_obj['service'][0] - expect(inner_service.keys.size).to eq 2 - expect(inner_service.keys).to include('id', 'profile') - expect(inner_service['id']).to eq 'https://example.org/image/iiif/token' - expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + 'id' => "#{id_uri}/foo" + }) + exp_err_msg = 'id and @id values must match for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' + expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end end + + describe 'realistic examples from Stanford purl manifests' do + it 'iiif image v2 service' do + service_obj = described_class.new({ + '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'id' => id_uri, + '@id' => id_uri, + 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + }) + expect(service_obj.keys.size).to eq 4 + expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') + expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['id']).to eq id_uri + expect(service_obj['@id']).to eq id_uri + expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + end + it 'login service' do + service_obj = described_class.new( + 'id' => 'https://example.org/auth/iiif', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, + 'label' => 'label value', + 'service' => [{ + 'id' => 'https://example.org/image/iiif/token', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + }] + ) + expect(service_obj.keys.size).to eq 4 + expect(service_obj.keys).to include('id', 'profile', 'label', 'service') + expect(service_obj['id']).to eq 'https://example.org/auth/iiif' + expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE + expect(service_obj['label']).to eq 'label value' + inner_service = service_obj['service'][0] + expect(inner_service.keys.size).to eq 2 + expect(inner_service.keys).to include('id', 'profile') + expect(inner_service['id']).to eq 'https://example.org/image/iiif/token' + expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + end + end end From 590a1224bf682351a8089c03f49ddc9034885366 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 17 Jul 2017 16:22:26 -0700 Subject: [PATCH 56/91] v3 service spec - add example from spec --- .../unit/iiif/v3/presentation/service_spec.rb | 91 ++++++++++++------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index 4be33bd..dc06231 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -115,41 +115,64 @@ end end - describe 'realistic examples from Stanford purl manifests' do - it 'iiif image v2 service' do - service_obj = described_class.new({ - '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, - 'id' => id_uri, - '@id' => id_uri, - 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE - }) - expect(service_obj.keys.size).to eq 4 - expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') - expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT - expect(service_obj['id']).to eq id_uri - expect(service_obj['@id']).to eq id_uri - expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + describe 'realistic examples' do + describe 'from Stanford purl manifests' do + it 'iiif image v2 service' do + service_obj = described_class.new({ + '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'id' => id_uri, + '@id' => id_uri, + 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + }) + expect(service_obj.keys.size).to eq 4 + expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') + expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['id']).to eq id_uri + expect(service_obj['@id']).to eq id_uri + expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + end + it 'login service' do + service_obj = described_class.new( + 'id' => 'https://example.org/auth/iiif', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, + 'label' => 'label value', + 'service' => [{ + 'id' => 'https://example.org/image/iiif/token', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + }] + ) + expect(service_obj.keys.size).to eq 4 + expect(service_obj.keys).to include('id', 'profile', 'label', 'service') + expect(service_obj['id']).to eq 'https://example.org/auth/iiif' + expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE + expect(service_obj['label']).to eq 'label value' + inner_service = service_obj['service'][0] + expect(inner_service.keys.size).to eq 2 + expect(inner_service.keys).to include('id', 'profile') + expect(inner_service['id']).to eq 'https://example.org/image/iiif/token' + expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + end end - it 'login service' do - service_obj = described_class.new( - 'id' => 'https://example.org/auth/iiif', - 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, - 'label' => 'label value', - 'service' => [{ - 'id' => 'https://example.org/image/iiif/token', - 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE - }] - ) - expect(service_obj.keys.size).to eq 4 - expect(service_obj.keys).to include('id', 'profile', 'label', 'service') - expect(service_obj['id']).to eq 'https://example.org/auth/iiif' - expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE - expect(service_obj['label']).to eq 'label value' - inner_service = service_obj['service'][0] - expect(inner_service.keys.size).to eq 2 - expect(inner_service.keys).to include('id', 'profile') - expect(inner_service['id']).to eq 'https://example.org/image/iiif/token' - expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do + it 'iiif image v2' do + service_obj = described_class.new({ + "@context" => "http://iiif.io/api/image/2/context.json", + "id" => "http://example.org/images/book1-page2", + "@id" => "http://example.org/images/book1-page2", + "profile" => "http://iiif.io/api/image/2/level1.json", + "height" => 8000, + "width" => 6000, + "tiles" => [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] + }) + expect(service_obj.keys.size).to eq 7 + expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['id']).to eq 'http://example.org/images/book1-page2' + expect(service_obj['@id']).to eq 'http://example.org/images/book1-page2' + expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE + expect(service_obj['height']).to eq 8000 + expect(service_obj['width']).to eq 6000 + expect(service_obj['tiles']).to eq [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] + end end end end From ae1212f62de024090e49680ef15f23246dcb08e8 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 17 Jul 2017 10:18:22 -0700 Subject: [PATCH 57/91] v3 abstract_resource: viewing_hint multivalued and can also be a URI --- lib/iiif/v3/abstract_resource.rb | 19 ++++++++------- spec/unit/iiif/v3/abstract_resource_spec.rb | 26 +++++++++++++++++---- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index d5a15df..101c5a7 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -27,11 +27,11 @@ def prohibited_keys def any_type_keys # values *may* be multivalued # NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers" - %w{ label description id attribution logo related see_also within } + %w{ label description id attribution logo viewing_hint related see_also within } end def string_only_keys - %w{ nav_date type format viewing_direction viewing_hint start_canvas } + %w{ nav_date type format viewing_direction start_canvas } end def array_only_keys @@ -138,12 +138,15 @@ def validate raise IIIF::V3::Presentation::IllegalValueError, m end end - # Viewing Hint values + # Viewing Hint can be an Array ("Any resource type may have one or more viewing hints") if self.has_key?('viewing_hint') - unless self.legal_viewing_hint_values.include?(self['viewing_hint']) - m = "viewingHint for #{self.class} must be one of #{self.legal_viewing_hint_values}" - raise IIIF::V3::Presentation::IllegalValueError, m - end + viewing_hint_val = self['viewing_hint'] + [*viewing_hint_val].each { |vh_val| + unless self.legal_viewing_hint_values.include?(vh_val) || (vh_val.kind_of?(String) && vh_val =~ URI::regexp) + m = "viewingHint for #{self.class} must be one or more of #{self.legal_viewing_hint_values} or a URI" + raise IIIF::V3::Presentation::IllegalValueError, m + end + } end # Metadata is Array; each entry is a Hash containing (only) 'label' and 'value' properties if self.has_key?('metadata') && self['metadata'] @@ -196,7 +199,7 @@ def validate validate_uri(entry['id'], 'id') # raises IllegalValueError end end - # rendering is Array; each entry is a Hash containing 'label' and 'format' keys + # rendering is Array; each entry is a Hash containing 'label' and 'format' keys if self.has_key?('rendering') && self['rendering'] unless self['rendering'].all? { |entry| entry.kind_of?(Hash) } m = 'rendering must be an Array with Hash members' diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 8332b8a..5211176 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -22,6 +22,10 @@ def required_keys def prohibited_keys super + %w{ verboten } end + + def legal_viewing_hint_values + %w{ viewing_hint1 viewing_hint2 } + end end end subject do @@ -65,10 +69,24 @@ def prohibited_keys exp_err_msg = "viewingDirection must be one of #{subject.legal_viewing_direction_values}" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end - it 'raises IllegalValueError for bad viewing_hint' do - subject['viewing_hint'] = 'foo' - exp_err_msg = "viewingHint for #{subject.class} must be one of #{subject.legal_viewing_hint_values}" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'viewing_hint' do + it 'can be a uri' do + subject['viewing_hint'] = 'https://example.org/viewiing_hint' + expect { subject.validate }.not_to raise_error + end + it 'can be a member of legal_viewing_hint_values' do + subject['viewing_hint'] = subject.legal_viewing_hint_values.first + expect { subject.validate }.not_to raise_error + end + it 'raises IllegalValueError for bad viewing_hint' do + subject['viewing_hint'] = 'foo' + exp_err_msg = "viewingHint for #{subject.class} must be one or more of #{subject.legal_viewing_hint_values} or a URI" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'can have multiple values' do + subject['viewing_hint'] = [subject.legal_viewing_hint_values.first, subject.legal_viewing_hint_values.last] + expect { subject.validate }.not_to raise_error + end end describe 'metadata' do it 'raises IllegalValueError for entry that is not a Hash' do From ddb383b314ce0ce466d9ab06bd374ebe49fa5f49 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 17 Jul 2017 17:18:17 -0700 Subject: [PATCH 58/91] v3 abstract_resource: startCanvase must be a URI --- lib/iiif/v3/abstract_resource.rb | 4 ++++ spec/unit/iiif/v3/abstract_resource_spec.rb | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 101c5a7..040f252 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -213,6 +213,10 @@ def validate end end end + # startCanvas is a String with a URI value + if self.has_key?('start_canvas') && self['start_canvas'].kind_of?(String) + validate_uri(self['start_canvas'], 'startCanvas') # raises IllegalValueError + end end # Options diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 5211176..be48b46 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -168,6 +168,13 @@ def legal_viewing_hint_values expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end + describe 'startCanvas' do + it 'raises IllegalValueError for entry that is not URI' do + subject.startCanvas = 'foo' + exp_err_msg = "startCanvas value must be a String containing a URI" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + end end describe 'A nested object (e.g. self[\'metadata\'])' do From 2968a9fbb83954ae108dfc19165588d336f02ec9 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 19 Jul 2017 11:57:56 -0700 Subject: [PATCH 59/91] v3 service_spec: improve realistic examples --- .../unit/iiif/v3/presentation/service_spec.rb | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index dc06231..efc4a36 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -124,6 +124,7 @@ '@id' => id_uri, 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE }) + expect{service_obj.validate}.not_to raise_error expect(service_obj.keys.size).to eq 4 expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT @@ -132,26 +133,58 @@ expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE end it 'login service' do + token_service = described_class.new({ + 'id' => 'https://example.org/image/iiif/token', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + }) + expect{token_service.validate}.not_to raise_error + expect(token_service.class).to eq described_class + expect(token_service.keys.size).to eq 2 + expect(token_service.keys).to include('id', 'profile') + expect(token_service['id']).to eq 'https://example.org/image/iiif/token' + expect(token_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE service_obj = described_class.new( 'id' => 'https://example.org/auth/iiif', 'profile' => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, 'label' => 'label value', - 'service' => [{ - 'id' => 'https://example.org/image/iiif/token', - 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE - }] + 'service' => [token_service] ) + expect{service_obj.validate}.not_to raise_error expect(service_obj.keys.size).to eq 4 expect(service_obj.keys).to include('id', 'profile', 'label', 'service') expect(service_obj['id']).to eq 'https://example.org/auth/iiif' expect(service_obj['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE expect(service_obj['label']).to eq 'label value' inner_service = service_obj['service'][0] + expect(inner_service.class).to eq described_class + expect{inner_service.validate}.not_to raise_error expect(inner_service.keys.size).to eq 2 expect(inner_service.keys).to include('id', 'profile') expect(inner_service['id']).to eq 'https://example.org/image/iiif/token' expect(inner_service['profile']).to eq described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE end + it 'triply nested service' do + inner = described_class.new({ + 'id' => 'https://example.org/image/iiif/token', + 'profile' => described_class::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + }) + expect{inner.validate}.not_to raise_error + middle = described_class.new({ + "id" => "https://example.org/auth/iiif", + "profile" => described_class::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE, + "label" => "Stanford-affiliated? Login to view", + "service" => [inner] + }) + expect{middle.validate}.not_to raise_error + outer = described_class.new({ + "@context" => described_class::IIIF_IMAGE_V2_CONTEXT, + "@id" => "https://example.org/iiif/yy816tv6021_img_1", + "id" => "https://example.org/iiif/yy816tv6021_img_1", + "profile" => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE, + "service" => [middle] + }) + expect{outer.validate}.not_to raise_error + end end describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do it 'iiif image v2' do @@ -172,6 +205,8 @@ expect(service_obj['height']).to eq 8000 expect(service_obj['width']).to eq 6000 expect(service_obj['tiles']).to eq [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] + # TODO: note that this won't validate because "height is a prohibited key in IIIF::V3::Presentation::Service" + # expect{service_obj.validate}.not_to raise_error end end end From 0f5eafb871a535df37fd205a9052f6da88b5b14b Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 17 Jul 2017 23:41:53 -0700 Subject: [PATCH 60/91] v3 canvas: improve specificity, validation and tests (WIP) --- lib/iiif/v3/presentation/canvas.rb | 46 ++- spec/unit/iiif/v3/presentation/canvas_spec.rb | 314 ++++++++++++++++-- 2 files changed, 319 insertions(+), 41 deletions(-) diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index 75b9e1d..d14f6bf 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -3,25 +3,22 @@ module V3 module Presentation class Canvas < IIIF::V3::AbstractResource - # TODO (?) a simple 'Image Canvas' constructor. - - TYPE = 'Canvas' + TYPE = 'Canvas'.freeze def required_keys super + %w{ id label } end - def array_only_keys - super + %w{ content } + def prohibited_keys + super + PAGING_PROPERTIES + %w{ viewing_direction format nav_date start_canvas content_annotations } end - # TODO: test and validate - def int_only_keys - super + %w{ width height } + def array_only_keys + super + %w{ content } end def legal_viewing_hint_values - super + %w{ non-paged } + super + %w{ paged continuous non-paged facing-pages auto-advance } end def initialize(hsh={}) @@ -31,7 +28,36 @@ def initialize(hsh={}) def validate super - # TODO: all members of content are of type AnnotationPage + + id_uri = URI.parse(self['id']) + unless self['id'] =~ /^https?:/ && id_uri.fragment.nil? + err_msg = "id must be an http(s) URI without a fragment for #{self.class}" + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + + content = self['content'] + if content && content.any? + unless content.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::AnnotationPage) } + err_msg = 'All entries in the content list must be a IIIF::V3::Presentation::AnnotationPage' + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + end + + # "A canvas MUST have exactly one width and one height, or exactly one duration. + # It may have width, height and duration."" + height = self['height'] + width = self['width'] + extent_err_msg = "#{self.class} must have (a height and a width) and/or a duration" + if (!!height ^ !!width) # this is an exclusive or: forces height and width to boolean + raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg + end + duration = self['duration'] + unless (height && width) || duration + raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg + end + + # TODO: Content must not be associated with space or time outside of the Canvas’s dimensions, + # such as at coordinates below 0,0, greater than the height or width, before 0 seconds, or after the duration. end end end diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index 056f05a..e7d5b08 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -1,51 +1,303 @@ describe IIIF::V3::Presentation::Canvas do - let(:fixed_values) do - { - "@context" => [ - "http://iiif.io/api/presentation/3/context.json", - "http://www.w3.org/ns/anno.jsonld" - ], - "id" => "http://www.example.org/iiif/book1/canvas/p1", - "type" => "Canvas", - "label" => "p. 1", - "height" => 1000, - "width" => 750, - "content" => [ ] - } + describe '#required_keys' do + %w{ type id label }.each do |k| + it k do + expect(subject.required_keys).to include(k) + end + end + end + + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::PAGING_PROPERTIES + + %w{ + viewing_direction + format + nav_date + start_canvas + content_annotations + } + expect(subject.prohibited_keys).to include(*keys) + end + end + + describe '#array_only_keys' do + it 'content' do + expect(subject.array_only_keys).to include('content') + end end + describe '#legal_viewing_hint_values' do + it 'contains the expected values' do + expect(subject.legal_viewing_hint_values).to contain_exactly('paged', 'continuous', 'non-paged', 'facing-pages', 'auto-advance') + end + end describe '#initialize' do - it 'sets type' do + it 'sets type to Canvas by default' do expect(subject['type']).to eq 'Canvas' end + it 'allows subclasses to override type' do + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new + expect(sub['type']).to eq 'a:SubClass' + end end - describe "#{described_class}.int_only_keys" do - it_behaves_like 'it has the appropriate methods for integer-only keys v3' - end + describe '#validate' do + let(:exp_id_err_msg) { "id must be an http(s) URI without a fragment for #{described_class}" } + before(:each) do + subject['id'] = 'http://www.example.org/my_canvas' + subject['label'] = 'foo' + end + it 'raises an IllegalValueError if id is not URI' do + subject['id'] = 'foo' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) + end + it 'raises an IllegalValueError if id is not http(s)' do + subject['id'] = 'ftp://www.example.org' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) + end + it 'raises an IllegalValueError if id has a fragment' do + subject['id'] = 'http://www.example.org#foo' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) + end - describe "#{described_class}.numeric_only_keys" do - it_behaves_like 'it has the appropriate methods for numeric-only keys v3' - end + let(:exp_extent_err_msg) { "#{described_class} must have (a height and a width) and/or a duration" } + it 'raises an IllegalValueError if height is a string' do + subject['height'] = 'foo' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) + end + it 'raises an IllegalValueError if height but no width' do + subject['height'] = 666 + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) + end + it 'raises an IllegalValueError if width but no height' do + subject['width'] = 666 + subject['duration'] = 66.6 + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) + end + it 'raises an IllegalValueError if no width, height or duration' do + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) + end + it 'allows width, height and duration' do + subject['width'] = 666 + subject['height'] = 666 + subject['duration'] = 66.6 + expect { subject.validate }.not_to raise_error + end - describe "#{described_class}.define_methods_for_string_only_keys" do - it_behaves_like 'it has the appropriate methods for string-only keys v3' + it 'raises IllegalValueError for content entry that is not an AnnotationPage' do + subject['content'] = [IIIF::V3::Presentation::AnnotationPage.new, IIIF::V3::Presentation::Annotation.new] + exp_err_msg = "All entries in the content list must be a IIIF::V3::Presentation::AnnotationPage" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end - describe "#{described_class}.define_methods_for_array_only_keys" do - it_behaves_like 'it has the appropriate methods for array-only keys v3' - end + describe 'realistic examples' do + let(:minimal_canvas_object) { described_class.new({ + "id" => "http://example.org/iiif/book1/canvas/c1", + 'label' => "so minimal it's not here", + "height" => 1000, + "width" => 1000 + })} + describe 'minimal canvas' do + it 'validates' do + expect{minimal_canvas_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(minimal_canvas_object.type).to eq described_class::TYPE + expect(minimal_canvas_object.id).to eq "http://example.org/iiif/book1/canvas/c1" + expect(minimal_canvas_object.label).to eq "so minimal it's not here" + expect(minimal_canvas_object.height).to eq 1000 + expect(minimal_canvas_object.width).to eq 1000 + end + end + describe 'minimal with empty content' do + let(:canvas_object) { + minimal_canvas_object['content'] = [] + minimal_canvas_object + } + it 'validates' do + expect{canvas_object.validate}.not_to raise_error + end + it 'has additional values' do + expect(canvas_object.content).to eq [] + end + end + let(:anno_page) { IIIF::V3::Presentation::AnnotationPage.new( + "id" => "http://example.org/iiif/book1/page/p1/1", + 'items' => [] + ) } + describe 'minimal with content' do + let(:canvas_object) { + minimal_canvas_object['content'] = [anno_page, anno_page] + minimal_canvas_object + } + it 'validates' do + expect{canvas_object.validate}.not_to raise_error + end + it 'has content value' do + expect(canvas_object.content.size).to eq 2 + expect(canvas_object.content).to eq [anno_page, anno_page] + end + end - describe "#{described_class}.define_methods_for_any_type_keys" do - it_behaves_like 'it has the appropriate methods for any-type keys v3' - end + ex5 = { + "id" => "http://example.org/iiif/book1/canvas/p2", + "type" => "Canvas", + "label" => "p. 2", + "height" =>1000, + "width" =>750, + "images" => [ + { + "type" => "Annotation", + "motivation" => "painting", + "resource" =>{ + "id" => "http://example.org/images/book1-page2/full/1500,2000/0/default.jpg", + "type" => "dctypes:Image", + "format" => "image/jpeg", + "height" =>2000, + "width" =>1500, + "service" => { + "@context" => "http://iiif.io/api/image/2/context.json", + "id" => "http://example.org/images/book1-page2", + "profile" => "http://iiif.io/api/image/2/level1.json", + "height" =>8000, + "width" =>6000, + "tiles" => [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] + } + }, + "on" => "http://example.org/iiif/book1/canvas/p2" + } + ], + "otherContent" => [ + { + "id" => "http://example.org/iiif/book1/list/p2", + "type" => "AnnotationList", + "within" => "http://example.org/iiif/book1/layer/l1" + } + ] + } + + describe 'video object' do + let(:canvas_for_video) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/1", + "label" => "Associate multiple Video representations as Choice", + "height" => 1000, + "width" => 1000, + "duration" => 100, + "content" => [anno_page] + }) } + it 'validates' do + expect{canvas_for_video.validate}.not_to raise_error + end + it 'height, width, duration' do + expect(canvas_for_video.height).to eq 1000 + expect(canvas_for_video.width).to eq 1000 + expect(canvas_for_video.duration).to eq 100 + end + end - describe "#legal_viewing_hint_values" do - it "should not error" do - expect{subject.legal_viewing_hint_values}.not_to raise_error + describe 'audio object' do + let(:canvas_for_audio) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2", + "label" => "Track 2", + "description" => "foo", + "duration" => 45, + "content" => [anno_page] + })} + it 'validates' do + expect{canvas_for_audio.validate}.not_to raise_error + end + it 'duration' do + expect(canvas_for_audio.duration).to eq 45 + end + it 'description' do + expect(canvas_for_audio.description).to eq 'foo' + end end + + ex_3d_tom_crane = { + "id" =>"http://tomcrane.github.io/scratch/manifests/3/canvas/3d", + "type" =>"Canvas", + "thumbnail" =>"http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", + "width" =>10000, + "height" =>10000, + "depth" =>10000, + "label" =>"A stage for an object", + "content" =>[ + { + "id" =>"...", + "type" =>"AnnotationPage", + "items" =>[ + { + "id" =>"http://tomcrane.github.io/scratch/manifests/3/3d/anno1", + "type" =>"Annotation", + "motivation" =>"painting", + "body" =>{ + "id" =>"http://files.universalviewer.io/manifests/nelis/animal-skull/animal-skull.json", + "type" =>"PhysicalObject", + "format" =>"application/vnd.threejs+json", + "label" =>"Animal Skull" + }, + "target" =>"http://tomcrane.github.io/scratch/manifests/3/canvas/3d" + } + ] + } + ] +} + + # file + # Audio + # video + # 3d object + # document + # citation + # image + # book + + stanford_1 = { + "type" => "Canvas", + "id" => "https://purl.stanford.edu/bc592pz8308/iiif3/canvas/0001", + "label" => "image", + "height" => 7579, + "width" => 10108, + "content" => [ + { + "type" => "AnnotationPage", + "id" => "https://purl.stanford.edu/bc592pz8308/iiif3/annotation_page/0001", + "items" => [ + { + "type" => "Annotation", + "motivation" => "painting", + "id" => "https://purl.stanford.edu/bc592pz8308/iiif3/annotation/0001", + "body" => { + "type" => "Image", + "id" => "https://stacks.stanford.edu/image/iiif/bc592pz8308%2Fbc592pz8308_05_0001/full/full/0/default.jpg", + "format" => "image/jpeg", + "height" => 7579, + "width" => 10108, + "service" => { + "@context" => "http://iiif.io/api/image/2/context.json", + "@id" => "https://stacks.stanford.edu/image/iiif/bc592pz8308%2Fbc592pz8308_05_0001", + "id" => "https://stacks.stanford.edu/image/iiif/bc592pz8308%2Fbc592pz8308_05_0001", + "profile" => "http://iiif.io/api/image/2/level1.json" + } + }, + "target" => "https://purl.stanford.edu/bc592pz8308/iiif3/canvas/0001" + } + ] + } + ] + } + end end From 9e3ecbbb4beae1f7fcce677378e66c9a2fd5c5eb Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 18 Jul 2017 18:50:06 -0700 Subject: [PATCH 61/91] v3 canvas: add depth property for 3d objects --- lib/iiif/v3/presentation/canvas.rb | 4 + spec/unit/iiif/v3/presentation/canvas_spec.rb | 93 ++++++------------- 2 files changed, 32 insertions(+), 65 deletions(-) diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index d14f6bf..7ab6f83 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -13,6 +13,10 @@ def prohibited_keys super + PAGING_PROPERTIES + %w{ viewing_direction format nav_date start_canvas content_annotations } end + def int_only_keys + super + %w{ depth } + end + def array_only_keys super + %w{ content } end diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index e7d5b08..ad68f3a 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -22,6 +22,12 @@ end end + describe '#int_only_keys' do + it 'depth (for 3d objects)' do + expect(subject.int_only_keys).to include('depth') + end + end + describe '#array_only_keys' do it 'content' do expect(subject.array_only_keys).to include('content') @@ -149,42 +155,27 @@ def initialize(hsh={}) end end - ex5 = { - "id" => "http://example.org/iiif/book1/canvas/p2", - "type" => "Canvas", - "label" => "p. 2", - "height" =>1000, - "width" =>750, - "images" => [ - { - "type" => "Annotation", - "motivation" => "painting", - "resource" =>{ - "id" => "http://example.org/images/book1-page2/full/1500,2000/0/default.jpg", - "type" => "dctypes:Image", - "format" => "image/jpeg", - "height" =>2000, - "width" =>1500, - "service" => { - "@context" => "http://iiif.io/api/image/2/context.json", - "id" => "http://example.org/images/book1-page2", - "profile" => "http://iiif.io/api/image/2/level1.json", - "height" =>8000, - "width" =>6000, - "tiles" => [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] - } - }, - "on" => "http://example.org/iiif/book1/canvas/p2" - } - ], - "otherContent" => [ - { - "id" => "http://example.org/iiif/book1/list/p2", - "type" => "AnnotationList", - "within" => "http://example.org/iiif/book1/layer/l1" - } - ] - } + describe '3d object' do + let(:canvas_3d_object) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/3d", + "thumbnail" => [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", + 'type' => 'Image'}], + "width" => 10000, + "height" => 10000, + "depth" => 10000, + "label" => "A stage for an object", + "content" => [anno_page] + })} + it 'validates' do + expect{canvas_3d_object.validate}.not_to raise_error + end + it 'thumbnail' do + expect(canvas_3d_object.thumbnail).to eq [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", 'type' => 'Image'}] + end + it 'depth' do + expect(canvas_3d_object.depth).to eq 10000 + end + end describe 'video object' do let(:canvas_for_video) { described_class.new({ @@ -224,35 +215,7 @@ def initialize(hsh={}) end end - ex_3d_tom_crane = { - "id" =>"http://tomcrane.github.io/scratch/manifests/3/canvas/3d", - "type" =>"Canvas", - "thumbnail" =>"http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", - "width" =>10000, - "height" =>10000, - "depth" =>10000, - "label" =>"A stage for an object", - "content" =>[ - { - "id" =>"...", - "type" =>"AnnotationPage", - "items" =>[ - { - "id" =>"http://tomcrane.github.io/scratch/manifests/3/3d/anno1", - "type" =>"Annotation", - "motivation" =>"painting", - "body" =>{ - "id" =>"http://files.universalviewer.io/manifests/nelis/animal-skull/animal-skull.json", - "type" =>"PhysicalObject", - "format" =>"application/vnd.threejs+json", - "label" =>"Animal Skull" - }, - "target" =>"http://tomcrane.github.io/scratch/manifests/3/canvas/3d" - } - ] - } - ] -} + # TODO: still need Stanford examples # file # Audio From 2e3c5d9c1371ed0485afcb401e0572c48604a051 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 19 Jul 2017 00:05:38 -0700 Subject: [PATCH 62/91] v3 canvas spec: add realistic examples from stanford --- spec/unit/iiif/v3/presentation/canvas_spec.rb | 167 +++++++++--------- 1 file changed, 86 insertions(+), 81 deletions(-) diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index ad68f3a..c657ac8 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -155,6 +155,75 @@ def initialize(hsh={}) end end + describe 'file object' do + describe 'without extent info' do + let(:file_object) { described_class.new({ + "id" => "https://example.org/bd742gh0511/iiif3/canvas/bd742gh0511_1", + "label" => "File 1", + "content" => [anno_page] + })} + it 'validates' do + expect{file_object.validate}.not_to raise_error + end + end + end + + describe 'image object' do + describe 'without extent info' do + let(:image_object) { described_class.new({ + "id" => "https://example.org/yv090xk3108/iiif3/canvas/yv090xk3108_1", + "label" => "image", + "content" => [anno_page] + })} + it 'validates' do + expect{image_object.validate}.not_to raise_error + end + end + describe 'with extent given' do + let(:image_object) { described_class.new({ + "id" => "https://example.org/yy816tv6021/iiif3/canvas/yy816tv6021_3", + "label" => "Image of media (1 of 2)", + "height" => 3456, + "width" => 5184, + "content" => [anno_page] + })} + it 'validates' do + expect{image_object.validate}.not_to raise_error + end + end + end + + describe 'audio object' do + describe 'without duration' do + let(:canvas_for_audio) { described_class.new({ + "id" => "https://example.org/xk681bt2506/iiif3/canvas/xk681bt2506_1", + "label" => "Audio file 1", + "content" => [anno_page] + })} + it 'validates' do + expect{canvas_for_audio.validate}.not_to raise_error + end + end + describe 'digerati example' do + let(:canvas_for_audio) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2", + "label" => "Track 2", + "description" => "foo", + "duration" => 45, + "content" => [anno_page] + })} + it 'validates' do + expect{canvas_for_audio.validate}.not_to raise_error + end + it 'duration' do + expect(canvas_for_audio.duration).to eq 45 + end + it 'description' do + expect(canvas_for_audio.description).to eq 'foo' + end + end + end + describe '3d object' do let(:canvas_3d_object) { described_class.new({ "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/3d", @@ -178,89 +247,25 @@ def initialize(hsh={}) end describe 'video object' do - let(:canvas_for_video) { described_class.new({ - "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/1", - "label" => "Associate multiple Video representations as Choice", - "height" => 1000, - "width" => 1000, - "duration" => 100, - "content" => [anno_page] - }) } - it 'validates' do - expect{canvas_for_video.validate}.not_to raise_error - end - it 'height, width, duration' do - expect(canvas_for_video.height).to eq 1000 - expect(canvas_for_video.width).to eq 1000 - expect(canvas_for_video.duration).to eq 100 - end - end - - describe 'audio object' do - let(:canvas_for_audio) { described_class.new({ - "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2", - "label" => "Track 2", - "description" => "foo", - "duration" => 45, - "content" => [anno_page] - })} - it 'validates' do - expect{canvas_for_audio.validate}.not_to raise_error - end - it 'duration' do - expect(canvas_for_audio.duration).to eq 45 - end - it 'description' do - expect(canvas_for_audio.description).to eq 'foo' + describe 'with extent info' do + let(:canvas_for_video) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/1", + "label" => "Associate multiple Video representations as Choice", + "height" => 1000, + "width" => 1000, + "duration" => 100, + "content" => [anno_page] + }) } + it 'validates' do + expect{canvas_for_video.validate}.not_to raise_error + end + it 'height, width, duration' do + expect(canvas_for_video.height).to eq 1000 + expect(canvas_for_video.width).to eq 1000 + expect(canvas_for_video.duration).to eq 100 + end end end - - # TODO: still need Stanford examples - - # file - # Audio - # video - # 3d object - # document - # citation - # image - # book - - stanford_1 = { - "type" => "Canvas", - "id" => "https://purl.stanford.edu/bc592pz8308/iiif3/canvas/0001", - "label" => "image", - "height" => 7579, - "width" => 10108, - "content" => [ - { - "type" => "AnnotationPage", - "id" => "https://purl.stanford.edu/bc592pz8308/iiif3/annotation_page/0001", - "items" => [ - { - "type" => "Annotation", - "motivation" => "painting", - "id" => "https://purl.stanford.edu/bc592pz8308/iiif3/annotation/0001", - "body" => { - "type" => "Image", - "id" => "https://stacks.stanford.edu/image/iiif/bc592pz8308%2Fbc592pz8308_05_0001/full/full/0/default.jpg", - "format" => "image/jpeg", - "height" => 7579, - "width" => 10108, - "service" => { - "@context" => "http://iiif.io/api/image/2/context.json", - "@id" => "https://stacks.stanford.edu/image/iiif/bc592pz8308%2Fbc592pz8308_05_0001", - "id" => "https://stacks.stanford.edu/image/iiif/bc592pz8308%2Fbc592pz8308_05_0001", - "profile" => "http://iiif.io/api/image/2/level1.json" - } - }, - "target" => "https://purl.stanford.edu/bc592pz8308/iiif3/canvas/0001" - } - ] - } - ] - } - end end From 9e25cb13b2a767a27fbb011e3c1ba6aca2c1831d Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 19 Jul 2017 00:14:30 -0700 Subject: [PATCH 63/91] v3 canvas: relax requirement for extent info --- lib/iiif/v3/presentation/canvas.rb | 14 +++++++++----- spec/unit/iiif/v3/presentation/canvas_spec.rb | 6 +++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index 7ab6f83..f2d2f1b 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -51,14 +51,18 @@ def validate # It may have width, height and duration."" height = self['height'] width = self['width'] - extent_err_msg = "#{self.class} must have (a height and a width) and/or a duration" if (!!height ^ !!width) # this is an exclusive or: forces height and width to boolean + extent_err_msg = "#{self.class} requires both height and width or neither" raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg end - duration = self['duration'] - unless (height && width) || duration - raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg - end + # NOTE: relaxing requirement for (exactly one width and one height, and/or exactly one duration) + # as Stanford has objects (such as txt files) for which this makes no sense + # (see sul-dlss/purl/issues/169) + # duration = self['duration'] + # unless (height && width) || duration + # extent_err_msg = "#{self.class} must have (a height and a width) and/or a duration" + # raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg + # end # TODO: Content must not be associated with space or time outside of the Canvas’s dimensions, # such as at coordinates below 0,0, greater than the height or width, before 0 seconds, or after the duration. diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index c657ac8..7a0b066 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -75,7 +75,9 @@ def initialize(hsh={}) expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) end - let(:exp_extent_err_msg) { "#{described_class} must have (a height and a width) and/or a duration" } + # let(:exp_extent_err_msg) { "#{described_class} must have (a height and a width) and/or a duration" } + # (see sul-dlss/purl/issues/169) + let(:exp_extent_err_msg) { "#{described_class} requires both height and width or neither" } it 'raises an IllegalValueError if height is a string' do subject['height'] = 'foo' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) @@ -90,6 +92,8 @@ def initialize(hsh={}) expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) end it 'raises an IllegalValueError if no width, height or duration' do + # (see sul-dlss/purl/issues/169) + skip('while this is in the current v3 spec, it does not make sense for some content (e.g. txt files)') expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) end it 'allows width, height and duration' do From a445807157ec1b7b21e4f4020cfae14b700f55ae Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 19 Jul 2017 16:13:27 -0700 Subject: [PATCH 64/91] v3 abstract_resource: thumbnail entries can be ImageResource objects --- lib/iiif/v3/abstract_resource.rb | 8 ++++---- spec/unit/iiif/v3/abstract_resource_spec.rb | 22 ++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 040f252..dba87c9 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -162,16 +162,16 @@ def validate end end end - # Thumbnail is Array; each entry is a Hash containing (at least) 'id' and 'type' keys + # Thumbnail is Array; each entry is a Hash or ImageResource containing (at least) 'id' and 'type' keys if self.has_key?('thumbnail') && self['thumbnail'] - unless self['thumbnail'].all? { |entry| entry.kind_of?(Hash) } - m = 'thumbnail must be an Array with Hash members' + unless self['thumbnail'].all? { |entry| entry.kind_of?(IIIF::V3::Presentation::ImageResource) || entry.kind_of?(Hash) } + m = 'thumbnail must be an Array with Hash or ImageResource members' raise IIIF::V3::Presentation::IllegalValueError, m end self['thumbnail'].each do |entry| thumb_keys = entry.keys unless thumb_keys.include?('id') && thumb_keys.include?('type') - m = 'thumbnail members must be a Hash including keys "id" and "type"' + m = 'thumbnail members must include keys "id" and "type"' raise IIIF::V3::Presentation::IllegalValueError, m end end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index be48b46..6d9bd9d 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -105,19 +105,27 @@ def legal_viewing_hint_values end end describe 'thumbnail' do - it 'raises IllegalValueError for entry that is not a Hash' do + let (:exp_basic_err_msg) { "thumbnail must be an Array with Hash or ImageResource members" } + let (:exp_keys_msg) { 'thumbnail members must include keys "id" and "type"' } + it 'raises IllegalValueError for entry that is not a Hash or ImageResource object' do subject['thumbnail'] = ['error'] - exp_err_msg = "thumbnail must be an Array with Hash members" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_basic_err_msg) end - it 'does not raise error for entry with "id" and "type"' do + it 'does not raise error for entry Hash with "id" and "type"' do subject['thumbnail'] = [{ 'id' => 'bar', 'type' => 'foo', 'random' => 'xxx' }] expect { subject.validate }.not_to raise_error end - it 'raises IllegalValueError for entry that does not contain "id" and "type"' do + it 'raises IllegalValueError for entry Hash that does not contain "id" and "type"' do subject['thumbnail'] = [{ 'id' => 'bar' }] - exp_err_msg = 'thumbnail members must be a Hash including keys "id" and "type"' - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_keys_msg) + end + it 'does not raise error for entry ImageResource with "id"' do + subject['thumbnail'] = [IIIF::V3::Presentation::ImageResource.new({ 'id' => 'bar', 'label' => 'xxx' })] + expect { subject.validate }.not_to raise_error + end + it 'raises IllegalValueError for entry ImageResource without id' do + subject['thumbnail'] = [IIIF::V3::Presentation::ImageResource.new] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_keys_msg) end end describe 'nav_date' do From 549cae11bd7ce08e7b3ca7b7074f66113f87e620 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 20 Jul 2017 12:25:18 -0700 Subject: [PATCH 65/91] v3 service: fix typo content_annotationS --- lib/iiif/v3/presentation/service.rb | 2 +- spec/unit/iiif/v3/presentation/service_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb index e278100..0b698de 100644 --- a/lib/iiif/v3/presentation/service.rb +++ b/lib/iiif/v3/presentation/service.rb @@ -17,7 +17,7 @@ def required_keys def prohibited_keys super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + - %w{ nav_date viewing_direction start_canvas content_annotation } + %w{ nav_date viewing_direction start_canvas content_annotations } end def uri_only_keys diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index efc4a36..462b77c 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -14,7 +14,7 @@ nav_date viewing_direction start_canvas - content_annotation + content_annotations } expect(subject.prohibited_keys).to include(*keys) end From ac1e3e3df5ce8c32f480681b468ea4bc1279c31e Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 20 Jul 2017 21:46:06 -0700 Subject: [PATCH 66/91] v3 service - make profile a known key --- lib/iiif/v3/presentation/service.rb | 4 ++++ spec/unit/iiif/v3/presentation/service_spec.rb | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb index 0b698de..c3866d2 100644 --- a/lib/iiif/v3/presentation/service.rb +++ b/lib/iiif/v3/presentation/service.rb @@ -24,6 +24,10 @@ def uri_only_keys super + %w{ @context id @id } end + def any_type_keys + super + %w{ profile } + end + def validate super if IIIF_IMAGE_V2_CONTEXT == self['@context'] diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index 462b77c..790a22d 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -32,6 +32,12 @@ end end + describe '#any_type_keys' do + it 'profile' do + expect(subject.any_type_keys).to include('profile') + end + end + let(:id_uri) { "https://example.org/image1" } describe '#initialize' do From 739296e6e88e715fe256f631b6e816f82e1dc7e8 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 20 Jul 2017 12:27:27 -0700 Subject: [PATCH 67/91] v3 resource: improve specificity, validation and tests --- lib/iiif/v3/presentation/resource.rb | 19 +- .../iiif/v3/presentation/resource_spec.rb | 171 +++++++++++++++++- 2 files changed, 177 insertions(+), 13 deletions(-) diff --git a/lib/iiif/v3/presentation/resource.rb b/lib/iiif/v3/presentation/resource.rb index 97eac92..21d270d 100644 --- a/lib/iiif/v3/presentation/resource.rb +++ b/lib/iiif/v3/presentation/resource.rb @@ -1,20 +1,29 @@ module IIIF module V3 module Presentation + # class for generic content resource class Resource < IIIF::V3::AbstractResource def required_keys - %w{ id } + super + %w{ id } end - def string_only_keys - super + %w{ format } + def prohibited_keys + super + PAGING_PROPERTIES + %w{ nav_date viewing_direction start_canvas content_annotations} end - def numeric_only_keys - super + %w{ duration } + def uri_only_keys + super + %w{ id } end + def validate + super + + unless self['id'] =~ /^https?:/ + err_msg = "id must be an http(s) URI for #{self.class}" + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + end end end end diff --git a/spec/unit/iiif/v3/presentation/resource_spec.rb b/spec/unit/iiif/v3/presentation/resource_spec.rb index 6279b0b..40fe449 100644 --- a/spec/unit/iiif/v3/presentation/resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/resource_spec.rb @@ -1,19 +1,174 @@ describe IIIF::V3::Presentation::Resource do + describe '#required_keys' do + it 'id' do + expect(subject.required_keys).to include('id') + end + it 'type' do + expect(subject.required_keys).to include('type') + end + end + + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::PAGING_PROPERTIES + + %w{ + nav_date + viewing_direction + start_canvas + content_annotations + } + expect(subject.prohibited_keys).to include(*keys) + end + end - describe "#{described_class}.abstract_resource_only_keys" do - it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' + describe '#uri_only_keys' do + it 'id' do + expect(subject.uri_only_keys).to include('id') + end end - describe "#{described_class}.int_only_keys" do - it_behaves_like 'it has the appropriate methods for integer-only keys v3' + describe '#initialize' do + it 'allows subclasses to override type' do + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new + expect(sub['type']).to eq 'a:SubClass' + end end - describe "#{described_class}.numeric_only_keys" do - it_behaves_like 'it has the appropriate methods for numeric-only keys v3' + describe '#validate' do + it 'raises an IllegalValueError if id is not URI' do + subject['id'] = 'foo' + subject['type'] = 'image/jpeg' + exp_err_msg = "id must be an http(s) URI for #{described_class}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises an IllegalValueError if id is not http' do + subject['id'] = 'ftp://www.example.org' + subject['type'] = 'image/jpeg' + exp_err_msg = "id must be an http(s) URI for #{described_class}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end - describe "#{described_class}.define_methods_for_string_only_keys" do - it_behaves_like 'it has the appropriate methods for string-only keys v3' + describe 'realistic examples' do + describe 'non-image examples from http://prezi3.iiif.io/api/presentation/3.0' do + describe 'audio' do + let(:file_id) { 'http://example.org/iiif/book1/res/music.mp3' } + let(:file_type) { 'dctypes:Sound' } + let(:file_mimetype) { 'audio/mpeg' } + let(:resource_object) { IIIF::V3::Presentation::Resource.new({ + 'id' => file_id, + 'type' => file_type, + 'format' => file_mimetype + })} + it 'validates' do + expect{resource_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(resource_object.id).to eq file_id + end + it 'has expected other values' do + expect(resource_object.type).to eq file_type + expect(resource_object.format).to eq file_mimetype + expect(resource_object.service).to eq [] + end + end + describe 'text' do + let(:file_id) { 'http://example.org/iiif/book1/res/tei-text-p1.xml' } + let(:file_type) { 'dctypes:Text' } + let(:file_mimetype) { 'application/tei+xml' } + let(:resource_object) { IIIF::V3::Presentation::Resource.new({ + 'id' => file_id, + 'type' => file_type, + 'format' => file_mimetype + })} + it 'validates' do + expect{resource_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(resource_object.id).to eq file_id + end + it 'has expected other values' do + expect(resource_object.type).to eq file_type + expect(resource_object.format).to eq file_mimetype + expect(resource_object.service).to eq [] + end + end + + end + describe 'stanford' do + describe 'non-image resource per purl code' do + let(:file_id) { 'https://example.org/file/abc666/ocr.txt' } + let(:file_type) { 'Document' } + let(:file_mimetype) { 'text/plain' } + let(:resource_object) { + resource = IIIF::V3::Presentation::Resource.new + resource['id'] = file_id + resource['type'] = file_type + resource.format = file_mimetype + resource + } + describe 'world visible' do + it 'validates' do + expect{resource_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(resource_object.id).to eq file_id + end + it 'has expected other values' do + expect(resource_object.type).to eq file_type + expect(resource_object.format).to eq file_mimetype + expect(resource_object.service).to eq [] + end + end + describe 'requires login' do + # let(:auth_token_service) { + # IIIF::V3::Presentation::Service.new({ + # 'id' => 'https://example.org/image/iiif/token', + # 'profile' => IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + # })} + let(:service_label) { 'login message' } + let(:token_service_id) { 'https://example.org/iiif/token' } + let(:login_service) { + IIIF::V3::Presentation::Service.new( + 'id' => 'https://example.org/auth/iiif', + 'profile' => 'http://iiif.io/api/auth/1/login', + 'label' => service_label, + 'service' => [{ + 'id' => token_service_id, + 'profile' => 'http://iiif.io/api/auth/1/token' + }] + ) + } + let(:resource_object_w_login) { + resource = resource_object + resource.service = login_service + resource + } + it 'validates' do + expect{resource_object_w_login.validate}.not_to raise_error + end + it 'has expected service value' do + service_obj = resource_object_w_login.service + expect(service_obj.class).to eq IIIF::V3::Presentation::Service + expect(service_obj.keys.size).to eq 4 + expect(service_obj.id).to eq 'https://example.org/auth/iiif' + expect(service_obj.profile).to eq IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE + expect(service_obj.label).to eq service_label + expect(service_obj.service.class).to eq Array + expect(service_obj.service.size).to eq 1 + expect(service_obj.service.first.keys.size).to eq 2 + expect(service_obj.service.first['id']).to eq token_service_id + expect(service_obj.service.first['profile']).to eq IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + end + end + end + end end end From a5545f4a6e43a9ce8cc6cb00a990ed1af9da1c07 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 20 Jul 2017 22:40:53 -0700 Subject: [PATCH 68/91] v3 image_resource: improve specificity, validation and tests --- lib/iiif/v3/presentation/image_resource.rb | 12 +- .../v3/presentation/image_resource_spec.rb | 184 +++++++++++++++++- 2 files changed, 185 insertions(+), 11 deletions(-) diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 2a77be1..7ca6a49 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -5,11 +5,7 @@ module V3 module Presentation class ImageResource < Resource - TYPE = 'Image' - - def int_only_keys - super + %w{ width height } - end + TYPE = 'Image'.freeze def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' @@ -17,9 +13,9 @@ def initialize(hsh={}) end class << self - IMAGE_API_DEFAULT_PARAMS = '/full/!200,200/0/default.jpg' - IMAGE_API_CONTEXT = 'http://iiif.io/api/image/2/context.json' - DEFAULT_FORMAT = 'image/jpeg' + IMAGE_API_DEFAULT_PARAMS = '/full/!200,200/0/default.jpg'.freeze + IMAGE_API_CONTEXT = 'http://iiif.io/api/image/2/context.json'.freeze + DEFAULT_FORMAT = 'image/jpeg'.freeze # Create a new ImageResource that includes a IIIF Image API Service # See http://iiif.io/api/presentation/2.0/#image-resources # diff --git a/spec/unit/iiif/v3/presentation/image_resource_spec.rb b/spec/unit/iiif/v3/presentation/image_resource_spec.rb index 43d241f..69ece68 100644 --- a/spec/unit/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/image_resource_spec.rb @@ -6,8 +6,186 @@ end end - describe "#{described_class}.int_only_keys" do - it_behaves_like 'it has the appropriate methods for integer-only keys v3' - end + describe 'realistic examples' do + let(:img_id) { 'https://example.org/image/iiif/abc666' } + let(:image_v2_service) { + IIIF::V3::Presentation::Service.new( + '@context' => 'http://iiif.io/api/image/2/context.json', + '@id' => img_id, + 'id' => img_id, + 'profile' => 'http://iiif.io/api/image/2/level1.json' + ) + } + let(:img_mimetype) { 'image/jpeg' } + let(:width) { 999 } + let(:height) { 666 } + describe 'stanford' do + describe 'thumbnail per purl code' do + let(:thumb_id) { "#{img_id}/full/!400,400/0/default.jpg" } + let(:thumb_object) { + thumb = IIIF::V3::Presentation::ImageResource.new + thumb['type'] = 'Image' + thumb['id'] = thumb_id + thumb.format = img_mimetype + thumb.service = image_v2_service + thumb + } + it 'validates' do + expect{thumb_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(thumb_object.type).to eq 'Image' + expect(thumb_object.id).to eq thumb_id + end + it 'has expected additional values' do + expect(thumb_object.format).to eq img_mimetype + expect(thumb_object.service).to eq image_v2_service + end + end + describe 'full size per purl code' do + let(:full_id) { "#{img_id}/full/full/0/default.jpg" } + let(:image_object) { + img = IIIF::V3::Presentation::ImageResource.new + img['id'] = full_id + img.format = img_mimetype + img.height = height + img.width = width + img.service = image_v2_service + img + } + describe 'world visible' do + it 'validates' do + expect{image_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(image_object.type).to eq 'Image' + expect(image_object.id).to eq full_id + end + it 'has expected additional values' do + expect(image_object.format).to eq img_mimetype + expect(image_object.height).to eq height + expect(image_object.width).to eq width + expect(image_object.service).to eq image_v2_service + end + it 'has expected service value' do + img_service_obj = image_object.service + expect(img_service_obj.class).to eq IIIF::V3::Presentation::Service + expect(img_service_obj.keys.size).to eq 4 + expect(img_service_obj.id).to eq img_id + expect(img_service_obj['@id']).to eq img_id + expect(img_service_obj.profile).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE + expect(img_service_obj['@context']).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT + end + end + describe 'requires login' do + let(:service_label) { 'login message' } + let(:token_service_id) { 'https://example.org/iiif/token' } + let(:login_service) { + IIIF::V3::Presentation::Service.new( + 'id' => 'https://example.org/auth/iiif', + 'profile' => 'http://iiif.io/api/auth/1/login', + 'label' => service_label, + 'service' => [{ + 'id' => token_service_id, + 'profile' => 'http://iiif.io/api/auth/1/token' + }] + ) + } + let(:image_object_w_login) { + img = image_object + img.service['service'] = [login_service] + img + } + it 'validates' do + expect{image_object_w_login.validate}.not_to raise_error + end + it 'has expected service value' do + img_service_obj = image_object_w_login.service + expect(img_service_obj.class).to eq IIIF::V3::Presentation::Service + expect(img_service_obj.keys.size).to eq 5 + expect(img_service_obj.id).to eq img_id + expect(img_service_obj['@id']).to eq img_id + expect(img_service_obj.profile).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE + expect(img_service_obj['@context']).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT + expect(img_service_obj.service.class).to eq Array + expect(img_service_obj.service.size).to eq 1 + + login_service_obj = img_service_obj.service.first + expect(login_service_obj.keys.size).to eq 4 + expect(login_service.id).to eq 'https://example.org/auth/iiif' + expect(login_service.profile).to eq IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_LOGIN_PROFILE + expect(login_service.label).to eq service_label + expect(login_service.service.class).to eq Array + expect(login_service.service.size).to eq 1 + token_service_obj = login_service_obj.service.first + expect(token_service_obj['id']).to eq token_service_id + expect(token_service_obj['profile']).to eq IIIF::V3::Presentation::Service::IIIF_AUTHENTICATION_V1_TOKEN_PROFILE + end + end + end + end + describe 'image examples from http://prezi3.iiif.io/api/presentation/3.0' do + let(:image_object) { + IIIF::V3::Presentation::ImageResource.new({ + 'id' => "#{img_id}/full/full/0/default.jpg", + 'type' => 'dctypes:Image', + 'format' => img_mimetype, + 'height' => height, + 'width' => width, + 'service' => image_v2_service + }) + } + describe 'simpler' do + it 'validates' do + expect{image_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(image_object.id).to eq "#{img_id}/full/full/0/default.jpg" + expect(image_object.type).to eq 'dctypes:Image' + end + it 'has expected additional values' do + expect(image_object.format).to eq img_mimetype + expect(image_object.height).to eq height + expect(image_object.width).to eq width + expect(image_object.service).to eq image_v2_service + end + end + describe 'height and width in service' do + # { + # "id": "http://example.org/images/book1-page2/full/1500,2000/0/default.jpg", + # "type": "dctypes:Image", + # "format": "image/jpeg", + # "height":2000, + # "width":1500, + # "service": { + # "@context": "http://iiif.io/api/image/2/context.json", + # "id": "http://example.org/images/book1-page2", + # "profile": "http://iiif.io/api/image/2/level1.json", + # "height":8000, + # "width":6000, + # "tiles": [{"width": 512, "scaleFactors": [1,2,4,8,16]}] + # } + # } + let(:img_obj) { + img = image_object + img.service['height'] = 6666 + img.service['width'] = 9999 + img.service['tiles'] = [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] + img + } + it 'validates' do + expect{img_obj.validate}.not_to raise_error + end + it 'has expected service value' do + service_obj = img_obj.service + expect(service_obj.class).to eq IIIF::V3::Presentation::Service + expect(service_obj.keys.size).to eq 7 + expect(service_obj['height']).to eq 6666 + expect(service_obj['width']).to eq 9999 + expect(service_obj['tiles']).to eq [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] + end + end + end + end end From 9cd7923415ecabce89b978a0381aa5c30c59de3e Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 24 Jul 2017 21:45:01 -0700 Subject: [PATCH 69/91] v3 abstract_resource: validate calls validate_uri for uri_only_keys --- lib/iiif/v3/abstract_resource.rb | 9 ++++++++- spec/unit/iiif/v3/abstract_resource_spec.rb | 4 ++-- spec/unit/iiif/v3/presentation/resource_spec.rb | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index dba87c9..d507af3 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -127,6 +127,13 @@ def validate end end + self.uri_only_keys.each do |k| + if self[k] + vals = *self[k] + vals.each { |val| validate_uri(val, k) } + end + end + # Note: self.define_methods_for_xxx_only_keys provides some validation at assignment time # currently, there is NO validation when key values are assigned directly with hash syntax, # e.g. my_image_resource['format'] = 'image/jpeg' @@ -494,7 +501,7 @@ def define_accessor_methods(*keys, &validation) private def validate_uri(val, key) unless val.kind_of?(String) && val =~ URI::regexp - m = "#{key} value must be a String containing a URI" + m = "#{key} value must be a String containing a URI for #{self.class}" raise IIIF::V3::Presentation::IllegalValueError, m end end diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 6d9bd9d..849cdfb 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -151,7 +151,7 @@ def legal_viewing_hint_values end it 'raises IllegalValueError for entry with "id" that is not URI' do subject['rights'] = [{ 'id' => 'bar', 'format' => 'text/html' }] - exp_err_msg = "id value must be a String containing a URI" + exp_err_msg = "id value must be a String containing a URI for #{subject.class}" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end it 'raises IllegalValueError for entry that does not contain "id"' do @@ -179,7 +179,7 @@ def legal_viewing_hint_values describe 'startCanvas' do it 'raises IllegalValueError for entry that is not URI' do subject.startCanvas = 'foo' - exp_err_msg = "startCanvas value must be a String containing a URI" + exp_err_msg = "startCanvas value must be a String containing a URI for #{subject.class}" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end diff --git a/spec/unit/iiif/v3/presentation/resource_spec.rb b/spec/unit/iiif/v3/presentation/resource_spec.rb index 40fe449..d916212 100644 --- a/spec/unit/iiif/v3/presentation/resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/resource_spec.rb @@ -44,7 +44,7 @@ def initialize(hsh={}) it 'raises an IllegalValueError if id is not URI' do subject['id'] = 'foo' subject['type'] = 'image/jpeg' - exp_err_msg = "id must be an http(s) URI for #{described_class}" + exp_err_msg = "id value must be a String containing a URI for #{described_class}" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end it 'raises an IllegalValueError if id is not http' do From 3a697dc30c289b2dfaaee4e57366f3d66e6bd1fb Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 25 Jul 2017 09:54:15 -0700 Subject: [PATCH 70/91] v3 abstract_resource: beef up validate_uri slightly --- lib/iiif/v3/abstract_resource.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index d507af3..38554a5 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -500,7 +500,7 @@ def define_accessor_methods(*keys, &validation) private def validate_uri(val, key) - unless val.kind_of?(String) && val =~ URI::regexp + unless val.kind_of?(String) && val =~ /\A#{URI::regexp}\z/ m = "#{key} value must be a String containing a URI for #{self.class}" raise IIIF::V3::Presentation::IllegalValueError, m end From 86a46e3a5337e5c9b9589d00936c5f2d510b7ffa Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 25 Jul 2017 18:14:29 -0700 Subject: [PATCH 71/91] v3 choice: improve specificity and tests --- lib/iiif/v3/presentation/choice.rb | 12 +- spec/unit/iiif/v3/presentation/choice_spec.rb | 115 +++++++++++++++++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/lib/iiif/v3/presentation/choice.rb b/lib/iiif/v3/presentation/choice.rb index 693923f..092477d 100644 --- a/lib/iiif/v3/presentation/choice.rb +++ b/lib/iiif/v3/presentation/choice.rb @@ -3,7 +3,16 @@ module V3 module Presentation class Choice < IIIF::V3::AbstractResource - TYPE = 'Choice' + TYPE = 'Choice'.freeze + + def prohibited_keys + super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + + %w{ nav_date viewing_direction start_canvas content_annotations } + end + + def any_type_keys + super + %w{ default } + end def string_only_keys super + %w{ choice_hint } @@ -29,7 +38,6 @@ def initialize(hsh={}) def validate super - # time mode values if self.has_key?('choice_hint') unless self.legal_choice_hint_values.include?(self['choice_hint']) m = "choiceHint for #{self.class} must be one of #{self.legal_choice_hint_values}." diff --git a/spec/unit/iiif/v3/presentation/choice_spec.rb b/spec/unit/iiif/v3/presentation/choice_spec.rb index 0ffc7f3..0f76f8e 100644 --- a/spec/unit/iiif/v3/presentation/choice_spec.rb +++ b/spec/unit/iiif/v3/presentation/choice_spec.rb @@ -1,17 +1,120 @@ describe IIIF::V3::Presentation::Choice do - describe "#{described_class}.define_methods_for_array_only_keys" do - it_behaves_like 'it has the appropriate methods for array-only keys v3' + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::PAGING_PROPERTIES + + described_class::CONTENT_RESOURCE_PROPERTIES + + %w{ + nav_date + viewing_direction + start_canvas + content_annotations + } + expect(subject.prohibited_keys).to include(*keys) + end end - describe "#{described_class}.define_methods_for_string_only_keys" do - it_behaves_like 'it has the appropriate methods for string-only keys v3' + describe '#any_type_keys' do + it 'default' do + expect(subject.any_type_keys).to include('default') + end + end + + describe '#string_only_keys' do + it 'choice_hint' do + expect(subject.string_only_keys).to include('choice_hint') + end + end + + describe '#array_only_keys' do + it 'items' do + expect(subject.array_only_keys).to include('items') + end + end + + describe '#legal_choice_hint_values' do + it 'contains the expected values' do + expect(subject.legal_choice_hint_values).to contain_exactly('client', 'user') + end + end + + describe '#legal_viewing_hint_values' do + it 'contains none' do + expect(subject.legal_viewing_hint_values).to contain_exactly('none') + end + end + + describe '#initialize' do + it 'sets type to Choice by default' do + expect(subject['type']).to eq 'Choice' + end + it 'allows subclasses to override type' do + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new + expect(sub['type']).to eq 'a:SubClass' + end + it 'allows type to be passed in' do + my_choice = described_class.new('type' => 'bar') + expect(my_choice.type).to eq 'bar' + end end describe '#validate' do - it 'raises an error if choice_hint isn\'t an allowable value' do + it 'raises an IllegalValueError if choice_hint isn\'t an allowable value' do + exp_err_msg = "choiceHint for #{described_class} must be one of [\"client\", \"user\"]." subject['choice_hint'] = 'foo' - expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, exp_err_msg + end + end + + describe 'realistic examples' do + describe 'from digerati' do + let(:item_type) { 'Video' } + let(:item1_id) { 'http://example.org/foo.mp4f' } + let(:item1_mime) { 'video/mp4' } + let(:item1_res) { IIIF::V3::Presentation::Resource.new( + 'id' => item1_id, + 'type' => item_type, + 'format' => item1_mime + )} + let(:item2_id) { 'http://example.org/foo.webm' } + let(:item2_mime) { 'video/webm' } + let(:item2_res) { IIIF::V3::Presentation::Resource.new( + 'id' => item2_id, + 'type' => item_type, + 'format' => item2_mime + )} + let(:choice) { IIIF::V3::Presentation::Choice.new( + 'choiceHint' => 'client', + 'items' => [item1_res, item2_res] + )} + it 'validates' do + expect{choice.validate}.not_to raise_error + end + it 'has expected required values' do + expect(choice['type']).to eq 'Choice' + end + it 'has expected additional values' do + expect(choice.id).to be_nil + expect(choice['choice_hint']).to eq 'client' + expect(choice.choiceHint).to eq 'client' + expect(choice['items']).to eq [item1_res, item2_res] + first = choice['items'].first + expect(first.keys.size).to eq 3 + expect(first['id']).to eq item1_id + expect(first['type']).to eq item_type + expect(first['format']).to eq item1_mime + second = choice['items'].last + expect(second.keys.size).to eq 3 + expect(second['id']).to eq item2_id + expect(second['type']).to eq item_type + expect(second['format']).to eq item2_mime + end end end end From d096e469be5821885791f7e7b71a9e67ee51a91a Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 24 Jul 2017 16:05:52 -0700 Subject: [PATCH 72/91] v3 annotation: increase specificity, improve validation and tests --- lib/iiif/v3/presentation/annotation.rb | 52 ++- .../iiif/v3/presentation/annotation_spec.rb | 404 +++++++++++++++++- 2 files changed, 445 insertions(+), 11 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb index d19fe71..69a88fa 100644 --- a/lib/iiif/v3/presentation/annotation.rb +++ b/lib/iiif/v3/presentation/annotation.rb @@ -3,22 +3,35 @@ module V3 module Presentation class Annotation < IIIF::V3::AbstractResource - TYPE = 'Annotation' + TYPE = 'Annotation'.freeze def required_keys - super + %w{ motivation } + super + %w{ id motivation target } end - def abstract_resource_only_keys - super + [ { key: 'body', type: IIIF::V3::Presentation::Resource } ] + def prohibited_keys + super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + + %w{ nav_date viewing_direction start_canvas content_annotations } + end + + def any_type_keys + super + %w{ body } + end + + def uri_only_keys + super + %w{ id } end def string_only_keys - super + %w{ time_mode } + super + %w{ motivation time_mode } end def legal_time_mode_values - %w{ trim scale loop } + %w{ trim scale loop }.freeze + end + + def legal_viewing_hint_values + super + %w{ none } end def initialize(hsh={}) @@ -30,7 +43,32 @@ def initialize(hsh={}) def validate super - # time mode values + if self.has_key?('body') && self['body'].kind_of?(IIIF::V3::Presentation::ImageResource) + img_res_class_str = "IIIF::V3::Presentation::ImageResource" + + unless self.motivation == 'painting' + m = "#{self.class} motivation must be 'painting' when body is a kind of #{img_res_class_str}" + raise IIIF::V3::Presentation::IllegalValueError, m + end + + body_resource = self['body'] + body_id = body_resource['id'] + if body_id && body_id =~ /^https?:/ + validate_uri(body_id, 'anno body ImageResource id') # can raise IllegalValueError + else + m = "when #{self.class} body is a kind of #{img_res_class_str}, ImageResource id must be an http(s) URI" + raise IIIF::V3::Presentation::IllegalValueError, m + end + + body_service = *body_resource['service'] + body_service_context = *body_resource['service']['@context'] + expected_context = IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT + unless body_service && body_service_context && body_service_context.include?(expected_context) + m = "when #{self.class} body is a kind of #{img_res_class_str}, ImageResource's service @context must include #{expected_context}" + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + if self.has_key?('time_mode') unless self.legal_time_mode_values.include?(self['time_mode']) m = "timeMode for #{self.class} must be one of #{self.legal_time_mode_values}." diff --git a/spec/unit/iiif/v3/presentation/annotation_spec.rb b/spec/unit/iiif/v3/presentation/annotation_spec.rb index 11e541a..3be6ec3 100644 --- a/spec/unit/iiif/v3/presentation/annotation_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_spec.rb @@ -1,13 +1,409 @@ describe IIIF::V3::Presentation::Annotation do - describe "#{described_class}.define_methods_for_abstract_resource_only_keys" do - it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' + let(:content_id) { 'http://example.org/iiif/book1/res/tei-text-p1.xml' } + let(:content_type) { 'dctypes:Text' } + let(:mimetype) { 'application/tei+xml' } + let(:image_2_api_service) { IIIF::V3::Presentation::Service.new({ + '@context' => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT, + 'id' => content_id, + '@id' => content_id, + 'profile' => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE + })} + let(:img_content_resource) { IIIF::V3::Presentation::ImageResource.new( + 'id' => content_id, + 'type' => content_type, + 'format' => mimetype, + 'service' => image_2_api_service + )} + + describe '#required_keys' do + %w{ type id motivation target }.each do |k| + it k do + expect(subject.required_keys).to include(k) + end + end + end + + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::PAGING_PROPERTIES + + described_class::CONTENT_RESOURCE_PROPERTIES + + %w{ + nav_date + viewing_direction + start_canvas + content_annotations + } + expect(subject.prohibited_keys).to include(*keys) + end + end + + describe '#any_type_keys' do + it 'body' do + expect(subject.any_type_keys).to include('body') + end + end + + describe '#uri_only_keys' do + it 'id' do + expect(subject.uri_only_keys).to include('id') + end + end + + describe '#string_only_keys' do + it 'time_mode' do + expect(subject.string_only_keys).to include('time_mode') + end + end + + describe '#legal_time_mode_values' do + it 'contains the expected values' do + expect(subject.legal_time_mode_values).to contain_exactly('trim', 'scale', 'loop') + end + end + + describe '#legal_viewing_hint_values' do + it 'contains none' do + expect(subject.legal_viewing_hint_values).to contain_exactly('none') + end + end + + describe '#initialize' do + it 'sets type to Annotation by default' do + expect(subject['type']).to eq 'Annotation' + end + it 'allows subclasses to override type' do + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new + expect(sub['type']).to eq 'a:SubClass' + end + it 'sets motivation to painting by default' do + expect(subject['motivation']).to eq 'painting' + end + it 'allows motivation to be passed in' do + my_anno = described_class.new('motivation' => 'foo') + expect(my_anno.motivation).to eq 'foo' + end + it 'allows type to be passed in' do + my_anno = described_class.new('type' => 'bar') + expect(my_anno.type).to eq 'bar' + end end describe '#validate' do - it 'raises an error if time_mode isn\'t an allowable value' do + before(:each) do + subject['id'] = 'http://example.org/iiif/anno/1s' + subject['target'] = 'foo' + end + it 'raises IllegalValueError if id is not URI' do + exp_err_msg = "id value must be a String containing a URI for #{described_class}" + subject['id'] = 'foo' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + + it 'raises IllegalValueError if time_mode isn\'t an allowable value' do + exp_err_msg = "timeMode for #{described_class} must be one of [\"trim\", \"scale\", \"loop\"]." subject['time_mode'] = 'foo' - expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, exp_err_msg + end + + describe 'body is a kind of IIIF::V3::Presentation::ImageResource' do + let(:img_body_anno) { + subject['id'] = 'http://example.org/iiif/anno/1s' + subject['target'] = 'foo' + subject['body'] = img_content_resource + subject + } + it 'raises IllegalValueError if motivation isn\'t "painting"' do + exp_err_msg = "#{described_class} motivation must be 'painting' when body is a kind of IIIF::V3::Presentation::ImageResource" + img_body_anno['motivation'] = 'foo' + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, exp_err_msg + end + let(:img_resource_without_id) { + IIIF::V3::Presentation::ImageResource.new( + 'type' => content_type, + 'format' => mimetype + )} + let(:http_uri_err_msg) { + "when #{described_class} body is a kind of IIIF::V3::Presentation::ImageResource, ImageResource id must be an http(s) URI" + } + it 'raises IllegalValueError if no id field in ImageResource' do + img_body_anno.body = img_resource_without_id + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg + end + it 'raises IllegalValueError if id in ImageResource isn\'t URI' do + img_resource_without_id['id'] = 'foo' + img_body_anno.body = img_resource_without_id + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg + end + it 'raises IllegalValueError if id in ImageResource isn\'t http(s) URI' do + img_resource_without_id['id'] = 'ftp://example.com/somewhere' + img_body_anno.body = img_resource_without_id + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg + end + + let(:service_context_err_msg) { + "when #{described_class} body is a kind of IIIF::V3::Presentation::ImageResource, ImageResource's service @context must include http://iiif.io/api/image/2/context.json" + } + it 'raises IllegalValueError if no @context field in ImageResource service' do + img_content_resource['service']['@context'] = nil + img_body_anno.body = img_content_resource + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, service_context_err_msg + end + it 'raises IllegalValueError if @context in ImageResource service doesn\'t include reference to IIIF Image API context doc' do + img_content_resource['service']['@context'] = 'http://example.com/context.json' + img_body_anno.body = img_content_resource + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, service_context_err_msg + img_content_resource['service']['@context'] = ['http://example.com/context.json', 'http://example.com/context2.json'] + img_body_anno.body = img_content_resource + expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, service_context_err_msg + end + it 'does not raise error if @context in ImageResource service includes reference to IIIF Image API context doc' do + img_content_resource['service']['@context'] = ['http://example.com/context.json', IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT] + img_body_anno.body = img_content_resource + expect { img_body_anno.validate }.not_to raise_error + end end end + + describe 'realistic examples' do + let(:anno_id) { 'http://example.org/iiif/annoation/abc666'} + let(:target_id) { 'http://example.org/iiif/canvas/abc666'} + + describe 'stanford (purl code)' do + let(:anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = img_content_resource + anno + } + it 'validates' do + expect{anno.validate}.not_to raise_error + end + it 'has expected required values' do + expect(anno.id).to eq anno_id + expect(anno['type']).to eq 'Annotation' + expect(anno['motivation']).to eq 'painting' + expect(anno['target']).to eq target_id + end + it 'has expected additional values' do + expect(anno['body']).to eq img_content_resource + end + end + + describe 'from http://prezi3.iiif.io/api/presentation/3.0' do + describe 'body is image_resource with height and width' do + let(:img_type) { 'dctypes:Image' } + let(:img_mime) { 'image/jpeg' } + let(:img_h) { 2000 } + let(:img_w) { 1500 } + let(:img_hw_resource) { IIIF::V3::Presentation::ImageResource.new( + 'id' => content_id, + 'type' => img_type, + 'format' => img_mime, + 'height' => img_h, + 'width' => img_w, + 'service' => image_2_api_service + )} + let(:my_anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = img_hw_resource + anno + } + it 'validates' do + expect{my_anno.validate}.not_to raise_error + end + it 'has expected additional values' do + expect(my_anno['body']).to eq img_hw_resource + expect(my_anno['body']['height']).to eq img_h + expect(my_anno['body']['width']).to eq img_w + end + + describe 'and service with height and width and tiles' do + let(:tiles_val) { [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] } + let(:service) { + s = image_2_api_service + s['height'] = 8000 + s['width'] = 6000 + s['tiles'] = tiles_val + s + } + it 'validates' do + img_hw_resource['service'] = service + expect{my_anno.validate}.not_to raise_error + end + it "body['service'] has expected additional values'" do + expect(my_anno['body']['service']).to eq service + expect(my_anno['body']['service']['height']).to eq 8000 + expect(my_anno['body']['service']['width']).to eq 6000 + expect(my_anno['body']['service']['tiles']).to eq tiles_val + end + end + end + end + + describe 'from digerati' do + describe 'anno body is audio' do + let(:body_id) { 'http://example.org/iiif/foo2.mp3' } + let(:body_type) { 'Audio' } + let(:audio_res) { IIIF::V3::Presentation::Resource.new( + 'id' => body_id, + 'type' => body_type + )} + let(:my_anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = audio_res + anno + } + it 'validates' do + expect{my_anno.validate}.not_to raise_error + end + it 'has expected required values' do + expect(my_anno['type']).to eq 'Annotation' + expect(my_anno.id).to eq anno_id + expect(my_anno['motivation']).to eq 'painting' + expect(my_anno['target']).to eq target_id + end + it 'has expected additional values' do + expect(my_anno['body']).to eq audio_res + expect(my_anno['body']['type']).to eq body_type + expect(my_anno['body']['id']).to eq body_id + end + end + + describe 'anno body is video' do + let(:body_id) { 'http://example.org/foo.webm' } + let(:body_type) { 'Video' } + let(:body_mime) { 'video/webm' } + let(:video_res) { IIIF::V3::Presentation::Resource.new( + 'id' => body_id, + 'type' => body_type, + 'format' => body_mime + )} + let(:my_anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = video_res + anno + } + it 'validates' do + expect{my_anno.validate}.not_to raise_error + end + it 'has expected body values' do + expect(my_anno['body']).to eq video_res + expect(my_anno['body']['type']).to eq body_type + expect(my_anno['body']['id']).to eq body_id + expect(my_anno['body']['format']).to eq body_mime + end + end + + describe 'anno body is 3d object' do + let(:body_id) { 'http://files.universalviewer.io/manifests/nelis/animal-skull/animal-skull.json' } + let(:body_type) { 'PhysicalObject' } + let(:body_mime) { 'application/vnd.threejs+json' } + let(:body_label) { 'Animal Skull' } + let(:body_res) { IIIF::V3::Presentation::Resource.new( + 'id' => body_id, + 'type' => body_type, + 'format' => body_mime, + 'label' => body_label + )} + let(:my_anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = body_res + anno + } + it 'validates' do + expect{my_anno.validate}.not_to raise_error + end + it 'has expected body values' do + expect(my_anno['body']).to eq body_res + expect(my_anno['body']['type']).to eq body_type + expect(my_anno['body']['id']).to eq body_id + expect(my_anno['body']['format']).to eq body_mime + expect(my_anno['body']['label']).to eq body_label + end + end + + describe 'anno body is pdf' do + let(:body_id) { 'http://example.org/iiif/some-document.pdf' } + let(:body_type) { 'Document' } + let(:body_mime) { 'application/pdf' } + let(:body_res) { IIIF::V3::Presentation::Resource.new( + 'id' => body_id, + 'type' => body_type, + 'format' => body_mime + )} + let(:my_anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = body_res + anno + } + it 'validates' do + expect{my_anno.validate}.not_to raise_error + end + it 'has expected body values' do + expect(my_anno['body']).to eq body_res + expect(my_anno['body']['type']).to eq body_type + expect(my_anno['body']['id']).to eq body_id + expect(my_anno['body']['format']).to eq body_mime + end + end + + describe 'anno body is choice (of 2 videos)' do + let(:body_type) { 'Video' } + let(:body1_id) { 'http://example.org/foo.mp4f' } + let(:body1_mime) { 'video/mp4; codec..xxxxx' } + let(:body1_res) { IIIF::V3::Presentation::Resource.new( + 'id' => body1_id, + 'type' => body_type, + 'format' => body1_mime + )} + let(:body2_id) { 'http://example.org/foo.webm' } + let(:body2_mime) { 'video/webm' } + let(:body2_res) { IIIF::V3::Presentation::Resource.new( + 'id' => body2_id, + 'type' => body_type, + 'format' => body2_mime + )} + let(:body_res) { IIIF::V3::Presentation::Choice.new( + 'items' => [body1_res, body2_res], + 'choiceHint' => 'client' + )} + let(:my_anno) { + anno = described_class.new + anno['id'] = anno_id + anno['target'] = target_id + anno.body = body_res + anno + } + it 'validates' do + expect{my_anno.validate}.not_to raise_error + end + it 'has expected body values' do + expect(my_anno['body']).to eq body_res + expect(my_anno['body'].keys.size).to eq 3 + expect(my_anno['body']['type']).to eq 'Choice' + expect(my_anno['body'].choiceHint).to eq 'client' + expect(my_anno['body']['items']).to eq [body1_res, body2_res] + end + end + end + end + end From 9a102d804c8f2b134d0d9e067a2cb389f76f08db Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Tue, 25 Jul 2017 19:22:21 -0700 Subject: [PATCH 73/91] v3 anno_page: improve specificity, validation and tests --- lib/iiif/v3/presentation/annotation_page.rb | 32 ++++- .../v3/presentation/annotation_page_spec.rb | 128 +++++++++++++++++- 2 files changed, 154 insertions(+), 6 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation_page.rb b/lib/iiif/v3/presentation/annotation_page.rb index 22dabb2..07c29a3 100644 --- a/lib/iiif/v3/presentation/annotation_page.rb +++ b/lib/iiif/v3/presentation/annotation_page.rb @@ -3,14 +3,27 @@ module V3 module Presentation class AnnotationPage < IIIF::V3::AbstractResource - TYPE = 'AnnotationPage' + TYPE = 'AnnotationPage'.freeze def required_keys super + %w{ id } end + def prohibited_keys + super + CONTENT_RESOURCE_PROPERTIES + + %w{ first last total nav_date viewing_direction start_canvas content_annotations } + end + + def uri_only_keys + super + %w{ id } + end + def array_only_keys; - super + %w{ items }; + super + %w{ items } + end + + def legal_viewing_hint_values + super + %w{ none } end def initialize(hsh={}) @@ -20,9 +33,20 @@ def initialize(hsh={}) def validate super - # TODO: Each member or resources must be a kind of Annotation - end + unless self['id'] =~ /^https?:/ + err_msg = "id must be an http(s) URI for #{self.class}" + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + + items = self['items'] + if items && items.any? + unless items.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Annotation) } + err_msg = 'All entries in the items list must be a IIIF::V3::Presentation::Annotation' + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + end + end end end end diff --git a/spec/unit/iiif/v3/presentation/annotation_page_spec.rb b/spec/unit/iiif/v3/presentation/annotation_page_spec.rb index 6ae7f00..5df72df 100644 --- a/spec/unit/iiif/v3/presentation/annotation_page_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_page_spec.rb @@ -1,7 +1,131 @@ describe IIIF::V3::Presentation::AnnotationPage do - describe "#{described_class}.define_methods_for_array_only_keys" do - it_behaves_like 'it has the appropriate methods for array-only keys v3' + describe '#required_keys' do + it 'id' do + expect(subject.required_keys).to include('id') + end end + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::CONTENT_RESOURCE_PROPERTIES + + %w{ + first + last + total + nav_date + viewing_direction + start_canvas + content_annotations + } + expect(subject.prohibited_keys).to include(*keys) + end + end + + describe '#uri_only_keys' do + it 'id' do + expect(subject.uri_only_keys).to include('id') + end + end + + describe '#array_only_keys' do + it 'items' do + expect(subject.array_only_keys).to include('items') + end + end + + describe '#legal_viewing_hint_values' do + it 'contains none' do + expect(subject.legal_viewing_hint_values).to contain_exactly('none') + end + end + + describe '#initialize' do + it 'sets type to AnnotationPage by default' do + expect(subject['type']).to eq 'AnnotationPage' + end + it 'allows subclasses to override type' do + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new + expect(sub['type']).to eq 'a:SubClass' + end + it 'allows type to be passed in' do + ap = described_class.new('type' => 'bar') + expect(ap.type).to eq 'bar' + end + end + + describe '#validate' do + it 'raises IllegalValueError if id is not URI' do + exp_err_msg = "id value must be a String containing a URI for #{described_class}" + subject['id'] = 'foo' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError if id is not http(s)' do + subject['id'] = 'ftp://www.example.org' + exp_err_msg = "id must be an http(s) URI for #{described_class}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for items entry that is not an Annotation' do + subject['id'] = 'http://example.com/iiif3/annotation_page/666' + subject['items'] = [IIIF::V3::Presentation::ImageResource.new, IIIF::V3::Presentation::Annotation.new] + exp_err_msg = "All entries in the items list must be a IIIF::V3::Presentation::Annotation" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + end + + describe 'realistic examples' do + let(:ap_id) { 'http://example.com/iiif3/annotation_page/666' } + let(:anno) { IIIF::V3::Presentation::Annotation.new( + 'id' => 'http://example.com/anno/666', + 'target' => 'http://example.com/canvas/abc' + )} + + describe 'stanford (purl code)' do + let(:anno_page) { + anno_page = described_class.new + anno_page['id'] = ap_id + anno_page.items << anno + anno_page + } + it 'validates' do + expect{anno_page.validate}.not_to raise_error + end + it 'has expected required values' do + expect(anno_page.id).to eq ap_id + expect(anno_page['type']).to eq 'AnnotationPage' + end + it 'has expected additional values' do + expect(anno_page.items).to eq [anno] + end + end + + describe 'two items' do + let(:anno2) { IIIF::V3::Presentation::Annotation.new( + 'id' => 'http://example.com/anno/333', + 'target' => 'http://example.com/canvas/abc' + )} + let(:anno_page) { + anno_page = described_class.new + anno_page['id'] = ap_id + anno_page.items = [anno, anno2] + anno_page + } + it 'validates' do + expect{anno_page.validate}.not_to raise_error + end + it 'has expected required values' do + expect(anno_page.id).to eq ap_id + expect(anno_page['type']).to eq 'AnnotationPage' + end + it 'has expected additional values' do + expect(anno_page.items).to eq [anno, anno2] + end + end + end end From f80d41112de64d30857c0e1130b42c6b492ba284 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 26 Jul 2017 16:22:55 -0700 Subject: [PATCH 74/91] canvas_spec: add test mimicing stanford purl code --- spec/unit/iiif/v3/presentation/canvas_spec.rb | 245 ++++++++++-------- 1 file changed, 144 insertions(+), 101 deletions(-) diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index 7a0b066..ff836b0 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -111,19 +111,24 @@ def initialize(hsh={}) end describe 'realistic examples' do + let(:canvas_id) { 'http://example.org/iiif/book1/canvas/c1' } let(:minimal_canvas_object) { described_class.new({ - "id" => "http://example.org/iiif/book1/canvas/c1", + "id" => canvas_id, 'label' => "so minimal it's not here", - "height" => 1000, - "width" => 1000 + 'height' => 1000, + 'width' => 1000 })} + let(:anno_page) { IIIF::V3::Presentation::AnnotationPage.new( + "id" => "http://example.org/iiif/book1/page/p1/1", + 'items' => [] + ) } describe 'minimal canvas' do it 'validates' do expect{minimal_canvas_object.validate}.not_to raise_error end it 'has expected required values' do expect(minimal_canvas_object.type).to eq described_class::TYPE - expect(minimal_canvas_object.id).to eq "http://example.org/iiif/book1/canvas/c1" + expect(minimal_canvas_object.id).to eq canvas_id expect(minimal_canvas_object.label).to eq "so minimal it's not here" expect(minimal_canvas_object.height).to eq 1000 expect(minimal_canvas_object.width).to eq 1000 @@ -137,14 +142,10 @@ def initialize(hsh={}) it 'validates' do expect{canvas_object.validate}.not_to raise_error end - it 'has additional values' do + it 'has empty array for content' do expect(canvas_object.content).to eq [] end end - let(:anno_page) { IIIF::V3::Presentation::AnnotationPage.new( - "id" => "http://example.org/iiif/book1/page/p1/1", - 'items' => [] - ) } describe 'minimal with content' do let(:canvas_object) { minimal_canvas_object['content'] = [anno_page, anno_page] @@ -157,119 +158,161 @@ def initialize(hsh={}) expect(canvas_object.content.size).to eq 2 expect(canvas_object.content).to eq [anno_page, anno_page] end - end - describe 'file object' do - describe 'without extent info' do - let(:file_object) { described_class.new({ - "id" => "https://example.org/bd742gh0511/iiif3/canvas/bd742gh0511_1", - "label" => "File 1", - "content" => [anno_page] - })} - it 'validates' do - expect{file_object.validate}.not_to raise_error + describe 'stanford (purl code)' do + let(:canvas_object) { + c = described_class.new + c['id'] = canvas_id + c.label = 'label' + c.content << anno_page + c + } + describe 'non-image' do + it 'validates' do + expect{canvas_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(canvas_object.type).to eq described_class::TYPE + expect(canvas_object.id).to eq canvas_id + expect(canvas_object.label).to eq "label" + end + it 'has expected additional values' do + expect(canvas_object.content).to eq [anno_page] + end + end + describe 'image' do + let(:img_canvas) { + canvas_object.height = 666 + canvas_object.width = 888 + canvas_object + } + it 'validates' do + expect{img_canvas.validate}.not_to raise_error + end + it 'has expected required values' do + expect(img_canvas.type).to eq described_class::TYPE + expect(img_canvas.id).to eq canvas_id + expect(img_canvas.label).to eq "label" + expect(img_canvas.height).to eq 666 + expect(img_canvas.width).to eq 888 + end + it 'has expected additional values' do + expect(img_canvas.content).to eq [anno_page] + end end end - end - describe 'image object' do - describe 'without extent info' do - let(:image_object) { described_class.new({ - "id" => "https://example.org/yv090xk3108/iiif3/canvas/yv090xk3108_1", - "label" => "image", - "content" => [anno_page] - })} - it 'validates' do - expect{image_object.validate}.not_to raise_error + describe 'file object' do + describe 'without extent info' do + let(:file_object) { described_class.new({ + "id" => "https://example.org/bd742gh0511/iiif3/canvas/bd742gh0511_1", + "label" => "File 1", + "content" => [anno_page] + })} + it 'validates' do + expect{file_object.validate}.not_to raise_error + end end end - describe 'with extent given' do - let(:image_object) { described_class.new({ - "id" => "https://example.org/yy816tv6021/iiif3/canvas/yy816tv6021_3", - "label" => "Image of media (1 of 2)", - "height" => 3456, - "width" => 5184, - "content" => [anno_page] - })} - it 'validates' do - expect{image_object.validate}.not_to raise_error + + describe 'image object' do + describe 'without extent info' do + let(:image_object) { described_class.new({ + "id" => "https://example.org/yv090xk3108/iiif3/canvas/yv090xk3108_1", + "label" => "image", + "content" => [anno_page] + })} + it 'validates' do + expect{image_object.validate}.not_to raise_error + end + end + describe 'with extent given' do + let(:image_object) { described_class.new({ + "id" => "https://example.org/yy816tv6021/iiif3/canvas/yy816tv6021_3", + "label" => "Image of media (1 of 2)", + "height" => 3456, + "width" => 5184, + "content" => [anno_page] + })} + it 'validates' do + expect{image_object.validate}.not_to raise_error + end end end - end - describe 'audio object' do - describe 'without duration' do - let(:canvas_for_audio) { described_class.new({ - "id" => "https://example.org/xk681bt2506/iiif3/canvas/xk681bt2506_1", - "label" => "Audio file 1", - "content" => [anno_page] - })} - it 'validates' do - expect{canvas_for_audio.validate}.not_to raise_error + describe 'audio object' do + describe 'without duration' do + let(:canvas_for_audio) { described_class.new({ + "id" => "https://example.org/xk681bt2506/iiif3/canvas/xk681bt2506_1", + "label" => "Audio file 1", + "content" => [anno_page] + })} + it 'validates' do + expect{canvas_for_audio.validate}.not_to raise_error + end + end + describe 'digerati example' do + let(:canvas_for_audio) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2", + "label" => "Track 2", + "description" => "foo", + "duration" => 45, + "content" => [anno_page] + })} + it 'validates' do + expect{canvas_for_audio.validate}.not_to raise_error + end + it 'duration' do + expect(canvas_for_audio.duration).to eq 45 + end + it 'description' do + expect(canvas_for_audio.description).to eq 'foo' + end end end - describe 'digerati example' do - let(:canvas_for_audio) { described_class.new({ - "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2", - "label" => "Track 2", - "description" => "foo", - "duration" => 45, + + describe '3d object' do + let(:canvas_3d_object) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/3d", + "thumbnail" => [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", + 'type' => 'Image'}], + "width" => 10000, + "height" => 10000, + "depth" => 10000, + "label" => "A stage for an object", "content" => [anno_page] })} it 'validates' do - expect{canvas_for_audio.validate}.not_to raise_error + expect{canvas_3d_object.validate}.not_to raise_error end - it 'duration' do - expect(canvas_for_audio.duration).to eq 45 + it 'thumbnail' do + expect(canvas_3d_object.thumbnail).to eq [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", 'type' => 'Image'}] end - it 'description' do - expect(canvas_for_audio.description).to eq 'foo' + it 'depth' do + expect(canvas_3d_object.depth).to eq 10000 end end - end - describe '3d object' do - let(:canvas_3d_object) { described_class.new({ - "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/3d", - "thumbnail" => [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", - 'type' => 'Image'}], - "width" => 10000, - "height" => 10000, - "depth" => 10000, - "label" => "A stage for an object", - "content" => [anno_page] - })} - it 'validates' do - expect{canvas_3d_object.validate}.not_to raise_error - end - it 'thumbnail' do - expect(canvas_3d_object.thumbnail).to eq [{'id' => "http://files.universalviewer.io/manifests/nelis/animal-skull/thumb.jpg", 'type' => 'Image'}] - end - it 'depth' do - expect(canvas_3d_object.depth).to eq 10000 - end - end - - describe 'video object' do - describe 'with extent info' do - let(:canvas_for_video) { described_class.new({ - "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/1", - "label" => "Associate multiple Video representations as Choice", - "height" => 1000, - "width" => 1000, - "duration" => 100, - "content" => [anno_page] - }) } - it 'validates' do - expect{canvas_for_video.validate}.not_to raise_error - end - it 'height, width, duration' do - expect(canvas_for_video.height).to eq 1000 - expect(canvas_for_video.width).to eq 1000 - expect(canvas_for_video.duration).to eq 100 + describe 'video object' do + describe 'with extent info' do + let(:canvas_for_video) { described_class.new({ + "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/1", + "label" => "Associate multiple Video representations as Choice", + "height" => 1000, + "width" => 1000, + "duration" => 100, + "content" => [anno_page] + }) } + it 'validates' do + expect{canvas_for_video.validate}.not_to raise_error + end + it 'height, width, duration' do + expect(canvas_for_video.height).to eq 1000 + expect(canvas_for_video.width).to eq 1000 + expect(canvas_for_video.duration).to eq 100 + end end end end end - end From e01fc7a1c167dd2cf1318e6b80d59162dc332a6f Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 26 Jul 2017 16:45:16 -0700 Subject: [PATCH 75/91] v3 annotation: make target an any_type_key so anno.target works --- lib/iiif/v3/presentation/annotation.rb | 2 +- spec/unit/iiif/v3/presentation/annotation_spec.rb | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb index 69a88fa..5f0116f 100644 --- a/lib/iiif/v3/presentation/annotation.rb +++ b/lib/iiif/v3/presentation/annotation.rb @@ -15,7 +15,7 @@ def prohibited_keys end def any_type_keys - super + %w{ body } + super + %w{ body target } end def uri_only_keys diff --git a/spec/unit/iiif/v3/presentation/annotation_spec.rb b/spec/unit/iiif/v3/presentation/annotation_spec.rb index 3be6ec3..18fedeb 100644 --- a/spec/unit/iiif/v3/presentation/annotation_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_spec.rb @@ -42,6 +42,9 @@ it 'body' do expect(subject.any_type_keys).to include('body') end + it 'target' do + expect(subject.any_type_keys).to include('target') + end end describe '#uri_only_keys' do From 82bbffb450606ddbcbbe6710b0fe311961292ac9 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 26 Jul 2017 16:45:58 -0700 Subject: [PATCH 76/91] v3 canvas.validate: ensure annotation target matches canvas id --- lib/iiif/v3/presentation/canvas.rb | 9 +++++ spec/unit/iiif/v3/presentation/canvas_spec.rb | 35 ++++++++++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/iiif/v3/presentation/canvas.rb b/lib/iiif/v3/presentation/canvas.rb index f2d2f1b..e0d1760 100644 --- a/lib/iiif/v3/presentation/canvas.rb +++ b/lib/iiif/v3/presentation/canvas.rb @@ -45,6 +45,15 @@ def validate err_msg = 'All entries in the content list must be a IIIF::V3::Presentation::AnnotationPage' raise IIIF::V3::Presentation::IllegalValueError, err_msg end + content.each do |anno_page| + annos = anno_page['items'] + if annos && annos.any? + unless annos.all? { |anno| anno.target == self.id } + err_msg = 'URI of the canvas must be repeated in the target field of included Annotations' + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + end + end end # "A canvas MUST have exactly one width and one height, or exactly one duration. diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index ff836b0..4c88e5b 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -57,20 +57,21 @@ def initialize(hsh={}) end describe '#validate' do + let(:canvas_id) { 'http://example.org/iiif/book1/canvas/c1' } let(:exp_id_err_msg) { "id must be an http(s) URI without a fragment for #{described_class}" } before(:each) do - subject['id'] = 'http://www.example.org/my_canvas' + subject['id'] = canvas_id subject['label'] = 'foo' end - it 'raises an IllegalValueError if id is not URI' do + it 'raises IllegalValueError if id is not URI' do subject['id'] = 'foo' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) end - it 'raises an IllegalValueError if id is not http(s)' do + it 'raises IllegalValueError if id is not http(s)' do subject['id'] = 'ftp://www.example.org' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) end - it 'raises an IllegalValueError if id has a fragment' do + it 'raises IllegalValueError if id has a fragment' do subject['id'] = 'http://www.example.org#foo' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_id_err_msg) end @@ -78,20 +79,20 @@ def initialize(hsh={}) # let(:exp_extent_err_msg) { "#{described_class} must have (a height and a width) and/or a duration" } # (see sul-dlss/purl/issues/169) let(:exp_extent_err_msg) { "#{described_class} requires both height and width or neither" } - it 'raises an IllegalValueError if height is a string' do + it 'raises IllegalValueError if height is a string' do subject['height'] = 'foo' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) end - it 'raises an IllegalValueError if height but no width' do + it 'raises IllegalValueError if height but no width' do subject['height'] = 666 expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) end - it 'raises an IllegalValueError if width but no height' do + it 'raises IllegalValueError if width but no height' do subject['width'] = 666 subject['duration'] = 66.6 expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) end - it 'raises an IllegalValueError if no width, height or duration' do + it 'raises IllegalValueError if no width, height or duration' do # (see sul-dlss/purl/issues/169) skip('while this is in the current v3 spec, it does not make sense for some content (e.g. txt files)') expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_extent_err_msg) @@ -108,6 +109,22 @@ def initialize(hsh={}) exp_err_msg = "All entries in the content list must be a IIIF::V3::Presentation::AnnotationPage" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end + + it 'IllegalValueError for content with annotation target not the canvas id' do + anno = IIIF::V3::Presentation::Annotation.new( + 'id' => 'http://example.com/anno/666', + 'target' => canvas_id) + anno_page = IIIF::V3::Presentation::AnnotationPage.new( + 'id' => "http://example.org/iiif/book1/page/p1/1", + 'items' => [anno]) + subject['content'] = [anno_page] + + expect { subject.validate }.not_to raise_error + + anno['target'] = 'http://example.com/canvas/abc' + exp_err_msg = 'URI of the canvas must be repeated in the target field of included Annotations' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end describe 'realistic examples' do @@ -121,7 +138,7 @@ def initialize(hsh={}) let(:anno_page) { IIIF::V3::Presentation::AnnotationPage.new( "id" => "http://example.org/iiif/book1/page/p1/1", 'items' => [] - ) } + )} describe 'minimal canvas' do it 'validates' do expect{minimal_canvas_object.validate}.not_to raise_error From 129f622f7a7f6591516b0268ccfb8a8019995b07 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Mon, 17 Jul 2017 16:54:08 -0700 Subject: [PATCH 77/91] v3 sequence: improve specificity, validation and tests --- lib/iiif/v3/presentation/sequence.rb | 22 +- .../iiif/v3/presentation/sequence_spec.rb | 220 ++++++++++++------ 2 files changed, 168 insertions(+), 74 deletions(-) diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index 9b7b132..0c57c71 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -3,12 +3,16 @@ module V3 module Presentation class Sequence < IIIF::V3::AbstractResource - TYPE = 'Sequence' + TYPE = 'Sequence'.freeze def required_keys super + %w{ items } end + def prohibited_keys + super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w{ nav_date content_annotations } + end + def legal_viewing_hint_values %w{ individuals paged continuous auto-advance } end @@ -20,8 +24,20 @@ def initialize(hsh={}) def validate super - # TODO: Must be at least one canvas - # TODO: All members of canvases must be a kind of Canvas + + unless self['items'].size >= 1 + m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + + unless self['items'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } + m = 'All entries in the items list must be a IIIF::V3::Presentation::Canvas' + raise IIIF::V3::Presentation::IllegalValueError, m + end + + # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it + + # TODO: All external Sequences must have a dereference-able http(s) URI end end end diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index 3bcb069..3b41a44 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -1,62 +1,35 @@ describe IIIF::V3::Presentation::Sequence do describe '#required_keys' do - it 'accumulates from the superclass' do - expect(subject.required_keys).to eq %w{ type items } + %w{ type items }.each do |k| + it k do + expect(subject.required_keys).to include(k) + end end end - let(:subclass_subject) do - Class.new(IIIF::V3::Presentation::Sequence) do - def initialize(hsh={}) - hsh = { 'type' => 'a:SubClass' } - super(hsh) - end + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::CONTENT_RESOURCE_PROPERTIES + + described_class::PAGING_PROPERTIES + + %w{ + nav_date + content_annotations + } + expect(subject.prohibited_keys).to include(*keys) end end - let(:fixed_values) do - { - 'type' => 'Sequence', - 'id' => 'http://example.com/prefix/sequence/456', - 'label' => 'Book 1', - 'description' => 'A longer description of this example book. It should give some real information.', - 'thumbnail' => { - 'id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg', - 'service'=> { - '@context' => 'http://iiif.io/api/image/2/context.json', - 'id' => 'http://www.example.org/images/book1-page1', - 'profile' => 'http://iiif.io/api/image/2/level1.json' - } - }, - 'attribution' => 'Provided by Example Organization', - 'rights' => [{'id' => 'http://www.example.org/license.html'}], - 'logo' => 'http://www.example.org/logos/institution1.jpg', - 'see_also' => 'http://www.example.org/library/catalog/book1.xml', - 'service' => { - '@context' => 'http://example.org/ns/jsonld/context.json', - 'id' => 'http://example.org/service/example', - 'profile' => 'http://example.org/docs/example-service.html' - }, - 'related' => { - 'id' => 'http://www.example.org/videos/video-book1.mpg', - 'format' => 'video/mpeg' - }, - 'within' => 'http://www.example.org/collections/books/', - # Sequence - 'metadata' => [{'label'=>'Author', 'value'=>'Anne Author'}], - 'items' => [{ - 'id' => 'http://www.example.org/iiif/book1/canvas/p1', - 'type' => 'Canvas', - 'label' => 'p. 1', - 'height' => 1000, - 'width' => 750, - 'content'=> [] - }], - 'viewing_hint' => 'paged', - 'start_canvas' => 'http://www.example.org/iiif/book1/canvas/p2', - 'viewing_direction' => 'right-to-left', - } + describe '#array_only_keys' do + it 'items' do + expect(subject.array_only_keys).to include('items') + end + end + + describe '#legal_viewing_hint_values' do + it 'contains the expected values' do + expect(subject.legal_viewing_hint_values).to contain_exactly('individuals', 'paged', 'continuous', 'auto-advance') + end end describe '#initialize' do @@ -64,35 +37,140 @@ def initialize(hsh={}) expect(subject['type']).to eq 'Sequence' end it 'allows subclasses to override type' do - sub = subclass_subject.new + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new expect(sub['type']).to eq 'a:SubClass' end end - describe "#{described_class}.define_methods_for_array_only_keys" do - it_behaves_like 'it has the appropriate methods for array-only keys v3' - end - - describe "#{described_class}.define_methods_for_string_only_keys" do - it_behaves_like 'it has the appropriate methods for string-only keys v3' - end - - describe "#{described_class}.define_methods_for_any_type_keys" do - it_behaves_like 'it has the appropriate methods for any-type keys v3' - end - describe '#validate' do - it 'raises an error if viewing_hint isn\'t an allowable value' do - subject['viewing_hint'] = 'foo' - subject['items'] = [IIIF::V3::Presentation::Canvas.new] - expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + it 'raises MissingRequiredKeyError for items as empty Array' do + subject['items'] = [] + exp_err_msg = "The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) end - it 'raises an error if viewing_directon isn\'t an allowable value' do - subject['viewing_direction'] = 'foo-to-bar' - subject['items'] = [IIIF::V3::Presentation::Canvas.new] - expect { subject.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError + it 'raises IllegalValueError for items entry that is not a Canvas' do + subject['items'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new] + exp_err_msg = "All entries in the items list must be a IIIF::V3::Presentation::Canvas" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end + describe 'realistic examples' do + let!(:canvas_object) { IIIF::V3::Presentation::Canvas.new({ + "id" => "https://example.org/abc666/iiif3/canvas/0001", + "label" => "p. 1", + "height" => 7579, + "width" => 10108, + "content" => [] + })} + describe 'minimal sequence' do + let!(:sequence_object) { described_class.new({ + "items" => [canvas_object] + })} + it 'validates' do + expect{sequence_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(sequence_object.type).to eq 'Sequence' + expect(sequence_object.items.size).to be 1 + expect(sequence_object.items.first).to eq canvas_object + end + end + + describe 'example from Stanford purl' do + let!(:sequence_object) {IIIF::V3::Presentation::Sequence.new({ + "id" => "https://example.org/abc666#sequence-1", + "label" => "Current order", + "type" => "Sequence", + "items" => [canvas_object] + })} + it 'validates' do + expect{sequence_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(sequence_object.type).to eq 'Sequence' + expect(sequence_object.items.size).to be 1 + expect(sequence_object.items.first).to eq canvas_object + end + it 'has expected string values' do + expect(sequence_object.id).to eq "https://example.org/abc666#sequence-1" + expect(sequence_object.label).to eq "Current order" + end + end + describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do + let!(:sequence_object) { described_class.new({ + "id" => "http://example.org/iiif/book1/sequence/normal", + "label" => {"en" => "Current Page Order"}, + "viewingDirection" => "left-to-right", + "viewingHint" => ["paged"], + "startCanvas" => canvas_object.id, + "items" => [canvas_object] + })} + it 'validates' do + expect{sequence_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(sequence_object.type).to eq 'Sequence' + expect(sequence_object.items.size).to be 1 + expect(sequence_object.items.first).to eq canvas_object + end + it 'has expected string values' do + expect(sequence_object.id).to eq "http://example.org/iiif/book1/sequence/normal" + expect(sequence_object.viewingDirection).to eq "left-to-right" + expect(sequence_object.startCanvas).to eq "https://example.org/abc666/iiif3/canvas/0001" + end + it 'has expected additional content' do + expect(sequence_object.viewingHint).to eq ["paged"] + expect(sequence_object.label).to eq ({"en" => "Current Page Order"}) + end + end + + describe 'another example' do + let!(:sequence_object) { described_class.new({ + "id" => "http://example.com/prefix/sequence/456", + 'label' => 'Book 1', + 'description' => 'A longer description of this example book. It should give some real information.', + 'thumbnail' => [{ + 'id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg', + 'type' => 'Image', + 'service'=> { + '@context' => 'http://iiif.io/api/image/2/context.json', + 'id' => 'http://www.example.org/images/book1-page1', + 'profile' => 'http://iiif.io/api/image/2/level1.json' + } + }], + 'attribution' => 'Provided by Example Organization', + 'rights' => [{'id' => 'http://www.example.org/license.html'}], + 'logo' => 'http://www.example.org/logos/institution1.jpg', + 'see_also' => 'http://www.example.org/library/catalog/book1.xml', + 'service' => { + '@context' => 'http://example.org/ns/jsonld/context.json', + 'id' => 'http://example.org/service/example', + 'profile' => 'http://example.org/docs/example-service.html' + }, + 'related' => { + 'id' => 'http://www.example.org/videos/video-book1.mpg', + 'format' => 'video/mpeg' + }, + 'within' => 'http://www.example.org/collections/books/', + # Sequence + 'metadata' => [{'label'=>'Author', 'value'=>'Anne Author'}], + "items" => [canvas_object], + 'start_canvas' => 'http://www.example.org/iiif/book1/canvas/p2', + "viewingDirection" => "left-to-right", + "viewingHint" => ["paged"], + "startCanvas" => canvas_object.id, + })} + it 'validates' do + expect{sequence_object.validate}.not_to raise_error + end + end + end end From 32ae943217b5f229a16166ed907ec1bd97a79906 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 26 Jul 2017 18:19:27 -0700 Subject: [PATCH 78/91] v3 sequence: allow 'canvases' as well as 'items' as UniversalViewer only works with the former --- lib/iiif/v3/presentation/sequence.rb | 41 +++++++++--- .../iiif/v3/presentation/sequence_spec.rb | 62 +++++++++++++------ 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/lib/iiif/v3/presentation/sequence.rb b/lib/iiif/v3/presentation/sequence.rb index 0c57c71..825bfab 100644 --- a/lib/iiif/v3/presentation/sequence.rb +++ b/lib/iiif/v3/presentation/sequence.rb @@ -5,14 +5,22 @@ class Sequence < IIIF::V3::AbstractResource TYPE = 'Sequence'.freeze - def required_keys - super + %w{ items } - end + # NOTE: relaxing requirement for items as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + # def required_keys + # super + %w{ items } + # end def prohibited_keys super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w{ nav_date content_annotations } end + # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + def array_only_keys + super + %w{ canvases } + end + def legal_viewing_hint_values %w{ individuals paged continuous auto-advance } end @@ -25,20 +33,33 @@ def initialize(hsh={}) def validate super - unless self['items'].size >= 1 - m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' + # Canvas object list + # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + unless (self['items'] && self['items'].any?) || + (self['canvases'] && self['canvases'].any?) + m = 'The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' raise IIIF::V3::Presentation::MissingRequiredKeyError, m end - - unless self['items'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } - m = 'All entries in the items list must be a IIIF::V3::Presentation::Canvas' - raise IIIF::V3::Presentation::IllegalValueError, m - end + validate_canvas_list(self['items']) if self['items'] + validate_canvas_list(self['canvases']) if self['canvases'] # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it # TODO: All external Sequences must have a dereference-able http(s) URI end + + def validate_canvas_list(canvas_array) + unless canvas_array.size >= 1 + m = 'The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + + unless canvas_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } + m = 'All entries in the (items or canvases) list must be a IIIF::V3::Presentation::Canvas' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end end end end diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index 3b41a44..9e6a79f 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -1,7 +1,10 @@ describe IIIF::V3::Presentation::Sequence do describe '#required_keys' do - %w{ type items }.each do |k| + # NOTE: relaxing requirement for items as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + # %w{ type items }.each do |k| + %w{ type }.each do |k| it k do expect(subject.required_keys).to include(k) end @@ -24,6 +27,11 @@ it 'items' do expect(subject.array_only_keys).to include('items') end + # NOTE: also allowing sequences as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + it 'canvases' do + expect(subject.array_only_keys).to include('canvases') + end end describe '#legal_viewing_hint_values' do @@ -49,15 +57,30 @@ def initialize(hsh={}) end describe '#validate' do - it 'raises MissingRequiredKeyError for items as empty Array' do - subject['items'] = [] - exp_err_msg = "The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) + let(:req_key_msg) { "The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)" } + let(:bad_val_msg) { "All entries in the (items or canvases) list must be a IIIF::V3::Presentation::Canvas" } + it 'raises MissingRequiredKeyError if no items or canvases key' do + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg) end - it 'raises IllegalValueError for items entry that is not a Canvas' do - subject['items'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new] - exp_err_msg = "All entries in the items list must be a IIIF::V3::Presentation::Canvas" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'items' do + it 'raises MissingRequiredKeyError for items as empty Array' do + subject['items'] = [] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg) + end + it 'raises IllegalValueError for items entry that is not a Canvas' do + subject['items'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg) + end + end + describe 'canvases' do + it 'raises MissingRequiredKeyError for canvases as empty Array' do + subject['items'] = [] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg) + end + it 'raises IllegalValueError for canvases entry that is not a Canvas' do + subject['canvases'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg) + end end end @@ -84,22 +107,25 @@ def initialize(hsh={}) end describe 'example from Stanford purl' do - let!(:sequence_object) {IIIF::V3::Presentation::Sequence.new({ - "id" => "https://example.org/abc666#sequence-1", - "label" => "Current order", - "type" => "Sequence", - "items" => [canvas_object] - })} + let(:seq_id) { 'https://example.org/abc666#sequence-1' } + let(:sequence_object) { + s = described_class.new({ + "id" => seq_id, + "label" => "Current order" + }) + s.canvases << canvas_object + s + } it 'validates' do expect{sequence_object.validate}.not_to raise_error end it 'has expected required values' do expect(sequence_object.type).to eq 'Sequence' - expect(sequence_object.items.size).to be 1 - expect(sequence_object.items.first).to eq canvas_object + expect(sequence_object.canvases.size).to be 1 + expect(sequence_object.canvases.first).to eq canvas_object end it 'has expected string values' do - expect(sequence_object.id).to eq "https://example.org/abc666#sequence-1" + expect(sequence_object.id).to eq seq_id expect(sequence_object.label).to eq "Current order" end end From 9ded7f25039d1f2b2f914a3b14dcf9ecd8dc6f42 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 26 Jul 2017 18:20:19 -0700 Subject: [PATCH 79/91] v3 sequence_spec: viewingDirection is now part of stanford sequence building --- .../iiif/v3/presentation/sequence_spec.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/unit/iiif/v3/presentation/sequence_spec.rb b/spec/unit/iiif/v3/presentation/sequence_spec.rb index 9e6a79f..b09d068 100644 --- a/spec/unit/iiif/v3/presentation/sequence_spec.rb +++ b/spec/unit/iiif/v3/presentation/sequence_spec.rb @@ -128,6 +128,26 @@ def initialize(hsh={}) expect(sequence_object.id).to eq seq_id expect(sequence_object.label).to eq "Current order" end + describe 'with viewingDirection' do + let(:sequence_vd) { + sequence_object.viewingDirection = 'left-to-right' + sequence_object + } + it 'validates' do + expect{sequence_vd.validate}.not_to raise_error + end + it 'has expected required values' do + expect(sequence_vd.type).to eq 'Sequence' + expect(sequence_vd.canvases.size).to be 1 + expect(sequence_vd.canvases.first).to eq canvas_object + end + it 'has expected string values' do + expect(sequence_vd.id).to eq seq_id + expect(sequence_vd.label).to eq "Current order" + expect(sequence_vd.viewing_direction).to eq 'left-to-right' + expect(sequence_vd.viewingDirection).to eq 'left-to-right' + end + end end describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do From b5d265a3e1312cf5036d9ef56032dc18716639ec Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 6 Jul 2017 16:01:35 -0700 Subject: [PATCH 80/91] v3 manifest: improve specificity, validation and tests --- lib/iiif/v3/presentation/manifest.rb | 59 ++- .../iiif/v3/abstract_resource_spec.rb | 14 +- .../iiif/v3/presentation/manifest_spec.rb | 347 +++++++++++++++--- 3 files changed, 349 insertions(+), 71 deletions(-) diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index 329bca7..5cb801d 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -3,18 +3,26 @@ module V3 module Presentation class Manifest < IIIF::V3::AbstractResource - TYPE = 'Manifest' + TYPE = 'Manifest'.freeze def required_keys - super + %w{ id label } + super + %w{ id label items } + end + + def prohibited_keys + super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w{ start_canvas content_annotation } + end + + def uri_only_keys + super + %w{ id } end def array_only_keys - super + %w{ sequences structures } + super + %w{ items structures } end def legal_viewing_hint_values - %w{ individuals paged continuous auto-advance none } + %w{ individuals paged continuous auto-advance } end def initialize(hsh={}) @@ -23,8 +31,47 @@ def initialize(hsh={}) end def validate - super - # TODO: check types of sequences and structure members + super # also checks navDate format + + unless self['id'] =~ /^https?:/ + err_msg = "id must be an http(s) URI for #{self.class}" + raise IIIF::V3::Presentation::IllegalValueError, err_msg + end + + unless self['items'].size >= 1 + m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end + + unless self['items'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Sequence) } + m = 'All entries in the items list must be a IIIF::V3::Presentation::Sequence' + raise IIIF::V3::Presentation::IllegalValueError, m + end + + default_sequence = self['items'].first + unless default_sequence['items'] && default_sequence['items'].size >=1 && + default_sequence['items'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } + m = 'The default Sequence (the first entry of "items") must be written out in full within the Manifest file' + raise IIIF::V3::Presentation::IllegalValueError, m + end + + if self['items'].size > 1 + unless self['items'].all? { |entry| entry['label'] } + m = 'If there are multiple Sequences in a manifest then they must each have at least one label' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end + + # TODO: when embedding a sequence without any extensions within a manifest, the sequence must not have the @context field. + + # TODO: AnnotationLists must not be embedded within the manifest + + if self['structures'] + unless self['structures'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Range)} + m = 'All entries in the structures list must be a IIIF::V3::Presentation::Range' + raise IIIF::V3::Presentation::IllegalValueError, m + end + end end end end diff --git a/spec/integration/iiif/v3/abstract_resource_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb index c366a9f..e4a60a1 100644 --- a/spec/integration/iiif/v3/abstract_resource_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -60,7 +60,7 @@ "id": "http://www.example.org/library/catalog/book1.marc", "format": "application/marc" }, - "sequences": [ + "items": [ { "id":"http://www.example.org/iiif/book1/sequence/normal", "type": "Sequence", @@ -122,16 +122,15 @@ expect(parsed.to_ordered_hash.to_a - from_file.to_ordered_hash.to_a).to eq [] expect(from_file.to_ordered_hash.to_a - parsed.to_ordered_hash.to_a).to eq [] end - it 'turns each member of "sequences" into an instance of Sequence' do - expected_klass = IIIF::V3::Presentation::Sequence + it 'turns each member of "items" into an instance of Sequence' do parsed = described_class.from_ordered_hash(fixture) - parsed['sequences'].each do |s| - expect(s.class).to be expected_klass + parsed['items'].each do |s| + expect(s.class).to be IIIF::V3::Presentation::Sequence end end - it 'turns each member of items into an instance of Canvas' do + it 'turns each member of sequences/items into an instance of Canvas' do parsed = described_class.from_ordered_hash(fixture) - parsed['sequences'].each do |s| + parsed['items'].each do |s| s.items.each do |c| expect(c.class).to be IIIF::V3::Presentation::Canvas end @@ -145,7 +144,6 @@ parsed = described_class.from_ordered_hash(fixture) expect(parsed['label']).to eq 'My Manifest' end - end describe '#to_ordered_hash' do diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index fa1ff9a..29cf686 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -1,43 +1,43 @@ describe IIIF::V3::Presentation::Manifest do - let(:subclass_subject) do - Class.new(IIIF::V3::Presentation::Manifest) do - def initialize(hsh={}) - hsh = { 'type' => 'a:SubClass' } - super(hsh) + describe '#required_keys' do + %w{ type id label items }.each do |k| + it k do + expect(subject.required_keys).to include(k) end end end - let(:fixed_values) do - { - 'type' => 'a:SubClass', - 'id' => 'http://example.com/prefix/manifest/123', - 'label' => 'Book 1', - 'description' => 'A longer description of this example book. It should give some real information.', - 'thumbnail' => { - 'id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg', - 'service'=> { - '@context' => 'http://iiif.io/api/image/2/context.json', - 'id' => 'http://www.example.org/images/book1-page1', - 'profile' => 'http://iiif.io/api/image/2/level1.json' + describe '#prohibited_keys' do + it 'contains the expected key names' do + keys = described_class::CONTENT_RESOURCE_PROPERTIES + + described_class::PAGING_PROPERTIES + + %w{ + start_canvas + content_annotation } - }, - 'attribution' => 'Provided by Example Organization', - 'rights' => 'http://www.example.org/license.html', - 'logo' => 'http://www.example.org/logos/institution1.jpg', - 'see_also' => 'http://www.example.org/library/catalog/book1.xml', - 'service' => { - '@context' => 'http://example.org/ns/jsonld/context.json', - 'id' => 'http://example.org/service/example', - 'profile' => 'http://example.org/docs/example-service.html' - }, - 'related' => { - 'id' => 'http://www.example.org/videos/video-book1.mpg', - 'format' => 'video/mpeg' - }, - 'within' => 'http://www.example.org/collections/books/', - } + expect(subject.prohibited_keys).to include(*keys) + end + end + + describe '#uri_only_keys' do + it 'id' do + expect(subject.uri_only_keys).to include('id') + end + end + + describe '#array_only_keys' do + %w{ items structures}.each do |k| + it k do + expect(subject.array_only_keys).to include(k) + end + end + end + + describe '#legal_viewing_hint_values' do + it 'contains the expected values' do + expect(subject.legal_viewing_hint_values).to contain_exactly('individuals', 'paged', 'continuous', 'auto-advance') + end end describe '#initialize' do @@ -45,43 +45,276 @@ def initialize(hsh={}) expect(subject['type']).to eq 'Manifest' end it 'allows subclasses to override type' do - sub = subclass_subject.new + subclass = Class.new(described_class) do + def initialize(hsh={}) + hsh = { 'type' => 'a:SubClass' } + super(hsh) + end + end + sub = subclass.new expect(sub['type']).to eq 'a:SubClass' end end - describe '#required_keys' do - it 'accumulates' do - expect(subject.required_keys).to eq %w{ type id label } - end - end + let(:manifest_id) { 'http://www.example.org/iiif/book1/manifest' } describe '#validate' do - it 'raises an error if there is no id' do + it 'raises an IllegalValueError if id is not http' do subject.label = 'Book 1' - expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + subject['id'] = 'ftp://www.example.org' + subject['items'] = [IIIF::V3::Presentation::Sequence.new] + exp_err_msg = "id must be an http(s) URI for #{described_class}" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end - it 'raises an error if there is no label' do - subject['id'] = 'http://www.example.org/iiif/book1/manifest' - expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + it 'raises MissingRequiredKeyError for items entry without values' do + subject['id'] = manifest_id + subject.label = 'Book 1' + subject['items'] = [] + exp_err_msg = "The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) end - it 'raises an error if there is no type' do - subject.delete('type') + it 'raises IllegalValueError for items entry that is not a Sequence' do + subject['id'] = manifest_id subject.label = 'Book 1' - subject['id'] = 'http://www.example.org/iiif/book1/manifest' - expect { subject.validate }.to raise_error IIIF::V3::Presentation::MissingRequiredKeyError + subject['items'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] + exp_err_msg = "All entries in the items list must be a IIIF::V3::Presentation::Sequence" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for default Sequence that is not written out' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [] + subject['items'] = [seq] + exp_err_msg = 'The default Sequence (the first entry of "items") must be written out in full within the Manifest file' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq1 = IIIF::V3::Presentation::Sequence.new + seq1['items'] = [IIIF::V3::Presentation::Canvas.new] + seq2 = IIIF::V3::Presentation::Sequence.new + seq2['label'] = 'label2' + subject['items'] = [seq1, seq2] + exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises IllegalValueError for structures entry that is not a Range' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [IIIF::V3::Presentation::Canvas.new] + subject['items'] = [seq] + subject['structures'] = [IIIF::V3::Presentation::Sequence.new] + exp_err_msg = "All entries in the structures list must be a IIIF::V3::Presentation::Range" + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end + it 'raises no error when structures entry is a Range' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [IIIF::V3::Presentation::Canvas.new] + subject['items'] = [seq] + subject['structures'] = [IIIF::V3::Presentation::Range.new] + expect { subject.validate }.not_to raise_error end end - describe "#{described_class}.define_methods_for_array_only_keys" do - it_behaves_like 'it has the appropriate methods for array-only keys v3' - end - - describe "#{described_class}.define_methods_for_string_only_keys" do - it_behaves_like 'it has the appropriate methods for string-only keys v3' - end + describe 'realistic examples' do + let!(:canvas_object) { IIIF::V3::Presentation::Canvas.new({ + "type" => "Canvas", + "id" => "https://example.org/abc666/iiif3/canvas/0001", + "label" => "image", + "height" => 7579, + "width" => 10108, + "content" => [] + })} + let!(:default_sequence_object) {IIIF::V3::Presentation::Sequence.new({ + "id" => "https://example.org/abc666#sequence-1", + "label" => "Current order", + "type" => "Sequence", + "items" => [canvas_object] + })} + describe 'realistic(?) minimal manifest' do + let!(:manifest_object) { described_class.new({ + "@context" => [ + "http://www.w3.org/ns/anno.jsonld", + "http://iiif.io/api/presentation/3/context.json" + ], + "id" => "https://example.org/abc666/iiif3/manifest", + "type" => "Manifest", + "label" => "blah", + "attribution" => "bleah", + "description" => "blargh", + "items" => [default_sequence_object] + })} + it 'validates' do + expect{manifest_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(manifest_object.type).to eq 'Manifest' + expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" + expect(manifest_object.label).to eq "blah" + expect(manifest_object.items.size).to be 1 + expect(manifest_object.items.first).to eq default_sequence_object + end + it 'has expected context' do + expect(manifest_object['@context'].size).to be 2 + expect(manifest_object['@context']).to include(*IIIF::V3::Presentation::CONTEXT) + end + it 'has expected string values' do + expect(manifest_object.attribution).to eq "bleah" + expect(manifest_object.description).to eq "blargh" + end + end - describe "#{described_class}.define_methods_for_any_type_keys" do - it_behaves_like 'it has the appropriate methods for any-type keys v3' + describe 'realistic example from Stanford purl manifests' do + let!(:manifest_object) { described_class.new({ + "id" => "https://example.org/abc666/iiif3/manifest", + "type" => "Manifest", + "label" => "blah", + "attribution" => "bleah", + "description" => "blargh", + "items" => [default_sequence_object], + "logo" => { + "id" => "https://example.org/logo/full/400,/0/default.jpg", + "service" => { + "@context" => "http://iiif.io/api/image/2/context.json", + "@id" => "https://example.org/logo", + "id" => "https://example.org/logo", + "profile" => "http://iiif.io/api/image/2/level1.json" + } + }, + "seeAlso" => { + "id" => "https://example.org/abc666.mods", + "format" => "application/mods+xml" + }, + "metadata" => [ + { + "label" => "Type", + "value" => "map" + }, + { + "label" => "Rights", + "value" => "stuff" + } + ], + "thumbnail" => [{ + "type" => "Image", + "id" => "https://example.org/image/iiif/abc666_05_0001/full/!400,400/0/default.jpg", + "format" => "image/jpeg", + "service" => { + "@context" => "http://iiif.io/api/image/2/context.json", + "@id" => "https://example.org/image/iiif/abc666_05_0001", + "id" => "https://example.org/image/iiif/abc666_05_0001", + "profile" => "http://iiif.io/api/image/2/level1.json" + } + }] + })} + it 'validates' do + expect{manifest_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(manifest_object.type).to eq 'Manifest' + expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" + expect(manifest_object.label).to eq "blah" + expect(manifest_object.items.size).to be 1 + expect(manifest_object.items.first).to eq default_sequence_object + end + it 'has expected string values' do + expect(manifest_object.attribution).to eq "bleah" + expect(manifest_object.description).to eq "blargh" + end + it 'has expected additional content' do + expect(manifest_object.logo.keys.size).to be 2 + expect(manifest_object.seeAlso.keys.size).to be 2 + expect(manifest_object.metadata.size).to be 2 + expect(manifest_object.thumbnail.size).to be 1 + end + end + describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do + let!(:range_object) { IIIF::V3::Presentation::Range.new({ + "id" => "http://example.org/iiif/book1/range/top", + "label" => "home, home on the", + "type" => "Range", + "viewingHint" => ["top"] + }) + } + let!(:manifest_object) { described_class.new({ + "@context" => [ + "http://www.w3.org/ns/anno.jsonld", + "http://iiif.io/api/presentation/3/context.json" + ], + "id" => "http://example.org/iiif/book1/manifest", + "label" => {"en" => ["Book 1"]}, + "metadata" => [ + {"label" => {"en" => ["Author"]}, + "value" => {"@none" => ["Anne Author"]}}, + {"label" => {"en" => ["Published"]}, + "value" => { + "en" => ["Paris, circa 1400"], + "fr" => ["Paris, environ 1400"]} + }, + {"label" => {"en" => ["Notes"]}, + "value" => {"en" => ["Text of note 1", "Text of note 2"]}}, + {"label" => {"en" => ["Source"]}, + "value" => {"@none" => ["From: Some Collection"]}} + ], + "description" => {"en" => ["A longer description of this example book. It should give some real information."]}, + "thumbnail" => [{ + "id" => "http://example.org/images/book1-page1/full/80,100/0/default.jpg", + "type" => "Image", + "service" => { + "@context" => "http://iiif.io/api/image/2/context.json", + "id" => "http://example.org/images/book1-page1", + "profile" => ["http://iiif.io/api/image/2/level1.json"] + } + }], + "viewingDirection" => "right-to-left", + "viewingHint" => ["paged"], + "navDate" => "1856-01-01T00:00:00Z", + "rights" => [{ + "id" =>"http://example.org/license.html", + "format" => "text/html"}], + "attribution" => {"en" => ["Provided by Example Organization"]}, + "logo" => { + "id" => "http://example.org/logos/institution1.jpg", + "service" => { + "@context" => "http://iiif.io/api/image/2/context.json", + "id" => "http://example.org/service/inst1", + "profile" => ["http://iiif.io/api/image/2/profiles/level2.json"] + } + }, + "related" => [{ + "id" => "http://example.org/videos/video-book1.mpg", + "format" => "video/mpeg" + }], + "service" => [{ + "@context" => "http://example.org/ns/jsonld/context.json", + "id" => "http://example.org/service/example", + "profile" => ["http://example.org/docs/example-service.html"] + }], + "seeAlso" => [{ + "id" => "http://example.org/library/catalog/book1.xml", + "format" => "text/xml", + "profile" => ["http://example.org/profiles/bibliographic"] + }], + "rendering" => [{ + "id" => "http://example.org/iiif/book1.pdf", + "label" => {"en" => ["Download as PDF"]}, + "format" => "application/pdf" + }], + "within" => [{ + "id" => "http://example.org/collections/books/", + "type" => "Collection" + }], + "items" => [default_sequence_object], + "structures" => [range_object] + })} + it 'validates' do + expect{manifest_object.validate}.not_to raise_error + end + end end end From 090664779625f7be0f87d220d8b1e2efe905155f Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Wed, 19 Jul 2017 23:30:36 -0700 Subject: [PATCH 81/91] v3 manifest: allow 'sequences' as well as 'items' as UniversalViewer only works with the former --- lib/iiif/v3/presentation/manifest.rb | 68 +++-- .../iiif/v3/presentation/manifest_spec.rb | 283 ++++++++++++++---- 2 files changed, 272 insertions(+), 79 deletions(-) diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index 5cb801d..9e4f5ff 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -6,7 +6,10 @@ class Manifest < IIIF::V3::AbstractResource TYPE = 'Manifest'.freeze def required_keys - super + %w{ id label items } + # NOTE: relaxing requirement for items as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + # super + %w{ id label items } + super + %w{ id label } end def prohibited_keys @@ -18,7 +21,10 @@ def uri_only_keys end def array_only_keys - super + %w{ items structures } + # NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + # super + %w{ items structures } + super + %w{ items structures sequences } end def legal_viewing_hint_values @@ -38,37 +44,55 @@ def validate raise IIIF::V3::Presentation::IllegalValueError, err_msg end - unless self['items'].size >= 1 - m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' + # Sequence object list + # NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + unless (self['items'] && self['items'].any?) || + (self['sequences'] && self['sequences'].any?) + m = 'The (items or sequences) list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' raise IIIF::V3::Presentation::MissingRequiredKeyError, m end + validate_sequence_list(self['items']) if self['items'] + validate_sequence_list(self['sequences']) if self['sequences'] - unless self['items'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Sequence) } - m = 'All entries in the items list must be a IIIF::V3::Presentation::Sequence' - raise IIIF::V3::Presentation::IllegalValueError, m - end + # TODO: when embedding a sequence without any extensions within a manifest, the sequence must not have the @context field. - default_sequence = self['items'].first - unless default_sequence['items'] && default_sequence['items'].size >=1 && - default_sequence['items'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } - m = 'The default Sequence (the first entry of "items") must be written out in full within the Manifest file' - raise IIIF::V3::Presentation::IllegalValueError, m - end + # TODO: AnnotationLists must not be embedded within the manifest - if self['items'].size > 1 - unless self['items'].all? { |entry| entry['label'] } - m = 'If there are multiple Sequences in a manifest then they must each have at least one label' + if self['structures'] + unless self['structures'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Range)} + m = 'All entries in the structures list must be a IIIF::V3::Presentation::Range' raise IIIF::V3::Presentation::IllegalValueError, m end end + end - # TODO: when embedding a sequence without any extensions within a manifest, the sequence must not have the @context field. + # NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + def validate_sequence_list(sequence_array) + unless sequence_array.size >= 1 + m = 'The (items or sequences) list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' + raise IIIF::V3::Presentation::MissingRequiredKeyError, m + end - # TODO: AnnotationLists must not be embedded within the manifest + unless sequence_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Sequence) } + m = 'All entries in the (items or sequences) list must be a IIIF::V3::Presentation::Sequence' + raise IIIF::V3::Presentation::IllegalValueError, m + end - if self['structures'] - unless self['structures'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Range)} - m = 'All entries in the structures list must be a IIIF::V3::Presentation::Range' + default_sequence = sequence_array.first + # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + canvas_array = default_sequence['items'] || default_sequence['canvases'] + unless canvas_array && canvas_array.size >= 1 && + canvas_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } + m = 'The default Sequence (the first entry of (items or sequences)) must be written out in full within the Manifest file' + raise IIIF::V3::Presentation::IllegalValueError, m + end + + if sequence_array.size > 1 + unless sequence_array.all? { |entry| entry['label'] } + m = 'If there are multiple Sequences in a manifest then they must each have at least one label' raise IIIF::V3::Presentation::IllegalValueError, m end end diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index 29cf686..40be724 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -1,7 +1,10 @@ describe IIIF::V3::Presentation::Manifest do describe '#required_keys' do - %w{ type id label items }.each do |k| + # NOTE: relaxing requirement for items as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + # %w{ type id label items }.each do |k| + %w{ type id label }.each do |k| it k do expect(subject.required_keys).to include(k) end @@ -27,7 +30,9 @@ end describe '#array_only_keys' do - %w{ items structures}.each do |k| + # NOTE: also allowing sequences as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + %w{ items sequences structures}.each do |k| it k do expect(subject.array_only_keys).to include(k) end @@ -66,40 +71,139 @@ def initialize(hsh={}) exp_err_msg = "id must be an http(s) URI for #{described_class}" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end - it 'raises MissingRequiredKeyError for items entry without values' do - subject['id'] = manifest_id - subject.label = 'Book 1' - subject['items'] = [] - exp_err_msg = "The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) - end - it 'raises IllegalValueError for items entry that is not a Sequence' do + + let(:seq_list_err) { 'The (items or sequences) list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' } + let(:seq_entry_err) { 'All entries in the (items or sequences) list must be a IIIF::V3::Presentation::Sequence' } + let(:def_seq_err) { 'The default Sequence (the first entry of (items or sequences)) must be written out in full within the Manifest file' } + + it 'raises MissingRequiredKeyError if no items or sequences key' do subject['id'] = manifest_id subject.label = 'Book 1' - subject['items'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] - exp_err_msg = "All entries in the items list must be a IIIF::V3::Presentation::Sequence" - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) end - it 'raises IllegalValueError for default Sequence that is not written out' do - subject['id'] = manifest_id - subject.label = 'Book 1' - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [] - subject['items'] = [seq] - exp_err_msg = 'The default Sequence (the first entry of "items") must be written out in full within the Manifest file' - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'items' do + it 'raises MissingRequiredKeyError for items entry without values' do + subject['id'] = manifest_id + subject.label = 'Book 1' + subject['items'] = [] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) + end + it 'raises IllegalValueError for items entry that is not a Sequence' do + subject['id'] = manifest_id + subject.label = 'Book 1' + subject['items'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, seq_entry_err) + end + describe 'raises IllegalValueError for default Sequence that is not written out' do + it 'Sequence has "items"' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [] + subject['items'] = [seq] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) + end + # NOTE: also allowing canvases as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + it 'Sequence has "canvases"' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['canvases'] = [] + subject['items'] = [seq] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) + end + end + it 'raises no error for Sequence with populated "items"' do + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [IIIF::V3::Presentation::Canvas.new] + subject['items'] = [seq] + subject['id'] = manifest_id + subject.label = 'Book 1' + expect { subject.validate }.not_to raise_error + end + it 'raises no error for Sequence with populated "canvases"' do + seq = IIIF::V3::Presentation::Sequence.new + seq['canvases'] = [IIIF::V3::Presentation::Canvas.new] + subject['items'] = [seq] + subject['id'] = manifest_id + subject.label = 'Book 1' + expect { subject.validate }.not_to raise_error + end + it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq1 = IIIF::V3::Presentation::Sequence.new + seq1['items'] = [IIIF::V3::Presentation::Canvas.new] + seq2 = IIIF::V3::Presentation::Sequence.new + seq2['label'] = 'label2' + subject['items'] = [seq1, seq2] + exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end - it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do - subject['id'] = manifest_id - subject.label = 'Book 1' - seq1 = IIIF::V3::Presentation::Sequence.new - seq1['items'] = [IIIF::V3::Presentation::Canvas.new] - seq2 = IIIF::V3::Presentation::Sequence.new - seq2['label'] = 'label2' - subject['items'] = [seq1, seq2] - exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + describe 'sequences' do + it 'raises MissingRequiredKeyError for sequences entry without values' do + subject['id'] = manifest_id + subject.label = 'Book 1' + subject['sequences'] = [] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) + end + it 'raises IllegalValueError for sequences entry that is not a Sequence' do + subject['id'] = manifest_id + subject.label = 'Book 1' + subject['sequences'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, seq_entry_err) + end + describe 'raises IllegalValueError for default Sequence that is not written out' do + it 'Sequence has "items"' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [] + subject['sequences'] = [seq] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) + end + # NOTE: also allowing canvases as Universal Viewer currently only accepts canvases + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + it 'Sequence has "canvases"' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['canvases'] = [] + subject['sequences'] = [seq] + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) + end + end + it 'raises no error for Sequence with populated "items"' do + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [IIIF::V3::Presentation::Canvas.new] + subject['sequences'] = [seq] + subject['id'] = manifest_id + subject.label = 'Book 1' + expect { subject.validate }.not_to raise_error + end + it 'raises no error for Sequence with populated "canvases"' do + seq = IIIF::V3::Presentation::Sequence.new + seq['canvases'] = [IIIF::V3::Presentation::Canvas.new] + subject['sequences'] = [seq] + subject['id'] = manifest_id + subject.label = 'Book 1' + expect { subject.validate }.not_to raise_error + end + it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq1 = IIIF::V3::Presentation::Sequence.new + seq1['items'] = [IIIF::V3::Presentation::Canvas.new] + seq2 = IIIF::V3::Presentation::Sequence.new + seq2['label'] = 'label2' + subject['sequences'] = [seq1, seq2] + exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) + end end + it 'raises IllegalValueError for structures entry that is not a Range' do subject['id'] = manifest_id subject.label = 'Book 1' @@ -123,7 +227,6 @@ def initialize(hsh={}) describe 'realistic examples' do let!(:canvas_object) { IIIF::V3::Presentation::Canvas.new({ - "type" => "Canvas", "id" => "https://example.org/abc666/iiif3/canvas/0001", "label" => "image", "height" => 7579, @@ -133,7 +236,6 @@ def initialize(hsh={}) let!(:default_sequence_object) {IIIF::V3::Presentation::Sequence.new({ "id" => "https://example.org/abc666#sequence-1", "label" => "Current order", - "type" => "Sequence", "items" => [canvas_object] })} describe 'realistic(?) minimal manifest' do @@ -143,7 +245,6 @@ def initialize(hsh={}) "http://iiif.io/api/presentation/3/context.json" ], "id" => "https://example.org/abc666/iiif3/manifest", - "type" => "Manifest", "label" => "blah", "attribution" => "bleah", "description" => "blargh", @@ -170,26 +271,39 @@ def initialize(hsh={}) end describe 'realistic example from Stanford purl manifests' do + let!(:logo_service) { IIIF::V3::Presentation::Service.new({ + "@context" => "http://iiif.io/api/image/2/context.json", + "@id" => "https://example.org/logo", + "id" => "https://example.org/logo", + "profile" => "http://iiif.io/api/image/2/level1.json" + })} + let!(:thumbnail_image_service) { IIIF::V3::Presentation::Service.new({ + "@context" => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT, + "@id" => "https://example.org/image/iiif/abc666_05_0001", + "id" => "https://example.org/image/iiif/abc666_05_0001", + "profile" => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE + })} + let!(:thumbnail_image) { IIIF::V3::Presentation::ImageResource.new({ + "id" => "https://example.org/image/iiif/abc666_05_0001/full/!400,400/0/default.jpg", + "format" => "image/jpeg", + "service" => thumbnail_image_service + })} let!(:manifest_object) { described_class.new({ "id" => "https://example.org/abc666/iiif3/manifest", - "type" => "Manifest", "label" => "blah", "attribution" => "bleah", "description" => "blargh", - "items" => [default_sequence_object], + "sequences" => [default_sequence_object], "logo" => { "id" => "https://example.org/logo/full/400,/0/default.jpg", - "service" => { - "@context" => "http://iiif.io/api/image/2/context.json", - "@id" => "https://example.org/logo", - "id" => "https://example.org/logo", - "profile" => "http://iiif.io/api/image/2/level1.json" - } + "service" => logo_service }, "seeAlso" => { "id" => "https://example.org/abc666.mods", "format" => "application/mods+xml" }, + "viewingHint" => "paged", + "viewingDirection" => "right-to-left", "metadata" => [ { "label" => "Type", @@ -200,27 +314,22 @@ def initialize(hsh={}) "value" => "stuff" } ], - "thumbnail" => [{ - "type" => "Image", - "id" => "https://example.org/image/iiif/abc666_05_0001/full/!400,400/0/default.jpg", - "format" => "image/jpeg", - "service" => { - "@context" => "http://iiif.io/api/image/2/context.json", - "@id" => "https://example.org/image/iiif/abc666_05_0001", - "id" => "https://example.org/image/iiif/abc666_05_0001", - "profile" => "http://iiif.io/api/image/2/level1.json" - } - }] + "thumbnail" => [thumbnail_image] })} - it 'validates' do + it 'thumbnail image object validates' do + expect{thumbnail_image.validate}.not_to raise_error + end + it 'manifest validates' do expect{manifest_object.validate}.not_to raise_error end it 'has expected required values' do expect(manifest_object.type).to eq 'Manifest' expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" expect(manifest_object.label).to eq "blah" - expect(manifest_object.items.size).to be 1 - expect(manifest_object.items.first).to eq default_sequence_object + # NOTE: using sequences as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + expect(manifest_object.sequences.size).to be 1 + expect(manifest_object.sequences.first).to eq default_sequence_object end it 'has expected string values' do expect(manifest_object.attribution).to eq "bleah" @@ -232,12 +341,72 @@ def initialize(hsh={}) expect(manifest_object.metadata.size).to be 2 expect(manifest_object.thumbnail.size).to be 1 end + + describe 'from stanford purl CODE' do + let!(:seq_object) { + s = IIIF::V3::Presentation::Sequence.new({ + 'id' => 'https://example.org/abc666#sequence-1', + 'label' => 'Current order' + }) + s.viewingDirection = 'left-to-right' + s.canvases << canvas_object + s + } + let!(:manifest_data) { + { + "id" => "https://example.org/abc666/iiif3/manifest", + "label" => "blah", + "attribution" => "bleah", + "logo" => { + "id" => "https://example.org/logo/full/400,/0/default.jpg", + "service" => logo_service + }, + "seeAlso" => { + "id" => "https://example.org/abc666.mods", + "format" => "application/mods+xml" + } + } + } + let!(:manifest_object) { + m = described_class.new manifest_data + m.viewingHint = 'paged' + m.metadata = [ + { 'label' => 'title', 'value' => 'who wants to know?' }, + { 'label' => 'PublishDate', 'value' => 'sometime' } + ] + m.description = 'blargh' + m.thumbnail = [thumbnail_image] + m.sequences << seq_object + m + } + it 'manifest validates' do + expect{manifest_object.validate}.not_to raise_error + end + it 'has expected required values' do + expect(manifest_object.type).to eq 'Manifest' + expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" + expect(manifest_object.label).to eq "blah" + # NOTE: using sequences as Universal Viewer currently only accepts sequences + # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 + expect(manifest_object.sequences.size).to be 1 + expect(manifest_object.sequences.first).to eq seq_object + end + it 'has expected string values' do + expect(manifest_object.attribution).to eq "bleah" + expect(manifest_object.description).to eq "blargh" + end + it 'has expected additional content' do + expect(manifest_object.logo.keys.size).to be 2 + expect(manifest_object.seeAlso.keys.size).to be 2 + expect(manifest_object.metadata.size).to be 2 + expect(manifest_object.thumbnail.size).to be 1 + end + end end describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do let!(:range_object) { IIIF::V3::Presentation::Range.new({ "id" => "http://example.org/iiif/book1/range/top", "label" => "home, home on the", - "type" => "Range", "viewingHint" => ["top"] }) } From 584663503719470866236af8849ed49d9e4cacdf Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Thu, 27 Jul 2017 17:33:37 -0700 Subject: [PATCH 82/91] v3 add some validation notes as comments to Range, Collection and AnnotationCollection --- .../v3/presentation/annotation_collection.rb | 12 ++++++++++- lib/iiif/v3/presentation/collection.rb | 21 ++++++++++++++++++- lib/iiif/v3/presentation/range.rb | 9 +++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation_collection.rb b/lib/iiif/v3/presentation/annotation_collection.rb index ee9a756..469c9fa 100644 --- a/lib/iiif/v3/presentation/annotation_collection.rb +++ b/lib/iiif/v3/presentation/annotation_collection.rb @@ -3,7 +3,7 @@ module V3 module Presentation class AnnotationCollection < IIIF::V3::AbstractResource - TYPE = 'AnnotationCollection' + TYPE = 'AnnotationCollection'.freeze def required_keys super + %w{ id } @@ -17,6 +17,16 @@ def array_only_keys super + %w{ content } end + # TODO: paging properties + # Collection, AnnotationCollection, (formerly layer --> AnnotationPage???) allow; forbidden o.w. + # --- + # first, last, next, prev + # id is URI, but may have other info + # total, startIndex + # The value must be a non-negative integer. + # + # don't forget to validate + def initialize(hsh={}) hsh['type'] = TYPE unless hsh.has_key? 'type' super(hsh) diff --git a/lib/iiif/v3/presentation/collection.rb b/lib/iiif/v3/presentation/collection.rb index af126ba..76f4f89 100644 --- a/lib/iiif/v3/presentation/collection.rb +++ b/lib/iiif/v3/presentation/collection.rb @@ -3,7 +3,7 @@ module V3 module Presentation class Collection < IIIF::V3::AbstractResource - TYPE = 'Collection' + TYPE = 'Collection'.freeze def required_keys super + %w{ id label } @@ -13,6 +13,16 @@ def array_only_keys super + %w{ collections manifests } end + # TODO: navDate (collection or manifest only) - The value must be an xsd:dateTime literal in UTC, expressed in the form “YYYY-MM-DDThh:mm:ssZ”; There must be at most one navDate associated with any given resource. + + # TODO: paging properties + # Collection, AnnotationCollection, (formerly layer --> AnnotationPage???) allow; forbidden o.w. + # --- + # first, last, next, prev + # id is URI, but may have other info + # total, startIndex + # The value must be a non-negative integer. + def legal_viewing_hint_values %w{ auto-advance together } end @@ -26,6 +36,15 @@ def validate super # TODO: each member of collections and manifests must be a Hash # TODO: each member of collections and manifests MUST have id, type, and label + # TODO: navDate (collection or manifest only) - The value must be an xsd:dateTime literal in UTC, expressed in the form “YYYY-MM-DDThh:mm:ssZ”; There must be at most one navDate associated with any given resource. + + # TODO: paging properties + # Collection, AnnotationCollection, (formerly layer --> AnnotationPage???) allow; forbidden o.w. + # --- + # first, last, next, prev + # id is URI, but may have other info + # total, startIndex + # The value must be a non-negative integer. end end end diff --git a/lib/iiif/v3/presentation/range.rb b/lib/iiif/v3/presentation/range.rb index a3fee96..8742b73 100644 --- a/lib/iiif/v3/presentation/range.rb +++ b/lib/iiif/v3/presentation/range.rb @@ -1,14 +1,18 @@ module IIIF module V3 module Presentation + # Ranges are linked or embedded within the manifest in a structures field class Range < Sequence - TYPE = 'Range' + TYPE = 'Range'.freeze def required_keys super + %w{ id label } end + # TODO: contentAnnotations: links to AnnotationCollection + # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it + def array_only_keys super + %w{ members } end @@ -24,7 +28,10 @@ def initialize(hsh={}) def validate super + # TODO: Ranges must have URIs and they should be http(s) URIs. # TODO: Values of the members array must be canvas or range + # TODO: contentAnnotations: links to AnnotationCollection + # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it end end end From a234762773afb69e9c1a757e4f6b813441a54e28 Mon Sep 17 00:00:00 2001 From: Naomi Dushay Date: Fri, 28 Jul 2017 12:43:12 -0700 Subject: [PATCH 83/91] v3 manifest_spec: add test for single sequence without label --- spec/unit/iiif/v3/presentation/manifest_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index 40be724..6875811 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -202,6 +202,14 @@ def initialize(hsh={}) exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end + it 'raises no error for single Sequences without "label"' do + subject['id'] = manifest_id + subject.label = 'Book 1' + seq = IIIF::V3::Presentation::Sequence.new + seq['items'] = [IIIF::V3::Presentation::Canvas.new] + subject['sequences'] = [seq] + expect { subject.validate }.not_to raise_error + end end it 'raises IllegalValueError for structures entry that is not a Range' do From d250e13c1519059634804dda1d493c6291954023 Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 27 Mar 2020 07:55:06 -0600 Subject: [PATCH 84/91] Update to use Faraday adapter syntax for setting Net::HTTP --- lib/iiif/v3/presentation/image_resource.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 7ca6a49..40cc3d4 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -99,7 +99,7 @@ def create_image_api_image_resource(params={}) def get_info(svc_id) conn = Faraday.new("#{svc_id}/info.json") do |c| c.use Faraday::Response::RaiseError - c.use Faraday::Adapter::NetHttp + c.adapter :net_http end resp = conn.get # raises exceptions that indicate HTTP problems JSON.parse(resp.body) From b7ae2f86fead7e9e88b2efdac757d0bf36178790 Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 27 Mar 2020 09:21:34 -0600 Subject: [PATCH 85/91] Update `label` to be a required type of `Hash` See: https://iiif.io/api/presentation/3.0/change-log/#133-use-language-map-pattern-for-label-value-summary --- lib/iiif/v3/abstract_resource.rb | 4 +- .../v3/manifests/complete_from_spec.json | 2 +- .../iiif/v3/abstract_resource_spec.rb | 8 +-- spec/unit/iiif/presentation/manifest_spec.rb | 2 +- spec/unit/iiif/v3/abstract_resource_spec.rb | 2 +- spec/unit/iiif/v3/presentation/canvas_spec.rb | 18 +++--- .../iiif/v3/presentation/manifest_spec.rb | 62 +++++++++---------- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 38554a5..8681e56 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -27,7 +27,7 @@ def prohibited_keys def any_type_keys # values *may* be multivalued # NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers" - %w{ label description id attribution logo viewing_hint related see_also within } + %w{ description id attribution logo viewing_hint related see_also within } end def string_only_keys @@ -43,7 +43,7 @@ def abstract_resource_only_keys end def hash_only_keys - %w{ } + %w{ label } end def int_only_keys diff --git a/spec/fixtures/v3/manifests/complete_from_spec.json b/spec/fixtures/v3/manifests/complete_from_spec.json index 5e47642..dc7ab0c 100644 --- a/spec/fixtures/v3/manifests/complete_from_spec.json +++ b/spec/fixtures/v3/manifests/complete_from_spec.json @@ -5,7 +5,7 @@ ], "id": "http://www.example.org/iiif/book1/manifest", "type": "Manifest", - "label": "Book 1", + "label": { "en": [ "Book 1" ] }, "metadata": [ { "label": "Author", diff --git a/spec/integration/iiif/v3/abstract_resource_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb index e4a60a1..114a365 100644 --- a/spec/integration/iiif/v3/abstract_resource_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -9,26 +9,26 @@ describe 'self.parse' do it 'works from a file' do s = described_class.parse(manifest_from_spec_path) - expect(s['label']).to eq 'Book 1' + expect(s['label']['en']).to include 'Book 1' end it 'works from a string of JSON' do file = File.open(manifest_from_spec_path, 'rb') json_string = file.read file.close s = described_class.parse(json_string) - expect(s['label']).to eq 'Book 1' + expect(s['label']['en']).to include 'Book 1' end describe 'works from a hash' do it 'plain old' do h = JSON.parse(IO.read(manifest_from_spec_path)) s = described_class.parse(h) - expect(s['label']).to eq 'Book 1' + expect(s['label']['en']).to include 'Book 1' end it 'IIIF::OrderedHash' do h = JSON.parse(IO.read(manifest_from_spec_path)) oh = IIIF::OrderedHash[h] s = described_class.parse(oh) - expect(s['label']).to eq 'Book 1' + expect(s['label']['en']).to include 'Book 1' end end it 'turns camels to snakes' do diff --git a/spec/unit/iiif/presentation/manifest_spec.rb b/spec/unit/iiif/presentation/manifest_spec.rb index 4aa60a8..3dd5b98 100644 --- a/spec/unit/iiif/presentation/manifest_spec.rb +++ b/spec/unit/iiif/presentation/manifest_spec.rb @@ -14,7 +14,7 @@ def initialize(hsh={}) 'type' => 'a:SubClass', 'id' => 'http://example.com/prefix/manifest/123', 'context' => IIIF::Presentation::CONTEXT, - 'label' => 'Book 1', + 'label' => {'en' => ['Book 1']}, 'description' => 'A longer description of this example book. It should give some real information.', 'thumbnail' => { '@id' => 'http://www.example.org/images/book1-page1/full/80,100/0/default.jpg', diff --git a/spec/unit/iiif/v3/abstract_resource_spec.rb b/spec/unit/iiif/v3/abstract_resource_spec.rb index 849cdfb..b19a470 100644 --- a/spec/unit/iiif/v3/abstract_resource_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_spec.rb @@ -50,7 +50,7 @@ def legal_viewing_hint_values it 'can take any old hash' do hsh = JSON.parse(IO.read(manifest_from_spec_path)) new_instance = abstract_resource_subclass.new(hsh) - expect(new_instance['label']).to eq 'Book 1' + expect(new_instance['label']['en']).to include 'Book 1' end end diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index 4c88e5b..3fd1282 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -131,7 +131,7 @@ def initialize(hsh={}) let(:canvas_id) { 'http://example.org/iiif/book1/canvas/c1' } let(:minimal_canvas_object) { described_class.new({ "id" => canvas_id, - 'label' => "so minimal it's not here", + 'label' => {"en" => ["so minimal it's not here"]}, 'height' => 1000, 'width' => 1000 })} @@ -146,7 +146,7 @@ def initialize(hsh={}) it 'has expected required values' do expect(minimal_canvas_object.type).to eq described_class::TYPE expect(minimal_canvas_object.id).to eq canvas_id - expect(minimal_canvas_object.label).to eq "so minimal it's not here" + expect(minimal_canvas_object.label['en']).to include "so minimal it's not here" expect(minimal_canvas_object.height).to eq 1000 expect(minimal_canvas_object.width).to eq 1000 end @@ -180,7 +180,7 @@ def initialize(hsh={}) let(:canvas_object) { c = described_class.new c['id'] = canvas_id - c.label = 'label' + c.label = {'en' => ['label']} c.content << anno_page c } @@ -191,7 +191,7 @@ def initialize(hsh={}) it 'has expected required values' do expect(canvas_object.type).to eq described_class::TYPE expect(canvas_object.id).to eq canvas_id - expect(canvas_object.label).to eq "label" + expect(canvas_object.label['en']).to include "label" end it 'has expected additional values' do expect(canvas_object.content).to eq [anno_page] @@ -209,7 +209,7 @@ def initialize(hsh={}) it 'has expected required values' do expect(img_canvas.type).to eq described_class::TYPE expect(img_canvas.id).to eq canvas_id - expect(img_canvas.label).to eq "label" + expect(img_canvas.label['en']).to include "label" expect(img_canvas.height).to eq 666 expect(img_canvas.width).to eq 888 end @@ -223,7 +223,7 @@ def initialize(hsh={}) describe 'without extent info' do let(:file_object) { described_class.new({ "id" => "https://example.org/bd742gh0511/iiif3/canvas/bd742gh0511_1", - "label" => "File 1", + "label" => {"en" => ["File 1"]}, "content" => [anno_page] })} it 'validates' do @@ -236,7 +236,7 @@ def initialize(hsh={}) describe 'without extent info' do let(:image_object) { described_class.new({ "id" => "https://example.org/yv090xk3108/iiif3/canvas/yv090xk3108_1", - "label" => "image", + "label" => {"en" => ["image"]}, "content" => [anno_page] })} it 'validates' do @@ -246,7 +246,7 @@ def initialize(hsh={}) describe 'with extent given' do let(:image_object) { described_class.new({ "id" => "https://example.org/yy816tv6021/iiif3/canvas/yy816tv6021_3", - "label" => "Image of media (1 of 2)", + "label" => {"en" => ["Image of media (1 of 2)"]}, "height" => 3456, "width" => 5184, "content" => [anno_page] @@ -261,7 +261,7 @@ def initialize(hsh={}) describe 'without duration' do let(:canvas_for_audio) { described_class.new({ "id" => "https://example.org/xk681bt2506/iiif3/canvas/xk681bt2506_1", - "label" => "Audio file 1", + "label" => {"en" => ["Audio file 1"]}, "content" => [anno_page] })} it 'validates' do diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index 6875811..22c5e0d 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -65,7 +65,7 @@ def initialize(hsh={}) describe '#validate' do it 'raises an IllegalValueError if id is not http' do - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} subject['id'] = 'ftp://www.example.org' subject['items'] = [IIIF::V3::Presentation::Sequence.new] exp_err_msg = "id must be an http(s) URI for #{described_class}" @@ -78,26 +78,26 @@ def initialize(hsh={}) it 'raises MissingRequiredKeyError if no items or sequences key' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) end describe 'items' do it 'raises MissingRequiredKeyError for items entry without values' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} subject['items'] = [] expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) end it 'raises IllegalValueError for items entry that is not a Sequence' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} subject['items'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, seq_entry_err) end describe 'raises IllegalValueError for default Sequence that is not written out' do it 'Sequence has "items"' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['items'] = [] subject['items'] = [seq] @@ -107,7 +107,7 @@ def initialize(hsh={}) # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 it 'Sequence has "canvases"' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['canvases'] = [] subject['items'] = [seq] @@ -119,7 +119,7 @@ def initialize(hsh={}) seq['items'] = [IIIF::V3::Presentation::Canvas.new] subject['items'] = [seq] subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} expect { subject.validate }.not_to raise_error end it 'raises no error for Sequence with populated "canvases"' do @@ -127,16 +127,16 @@ def initialize(hsh={}) seq['canvases'] = [IIIF::V3::Presentation::Canvas.new] subject['items'] = [seq] subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq1 = IIIF::V3::Presentation::Sequence.new seq1['items'] = [IIIF::V3::Presentation::Canvas.new] seq2 = IIIF::V3::Presentation::Sequence.new - seq2['label'] = 'label2' + seq2['label'] = {'en' => ['label2']} subject['items'] = [seq1, seq2] exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) @@ -145,20 +145,20 @@ def initialize(hsh={}) describe 'sequences' do it 'raises MissingRequiredKeyError for sequences entry without values' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} subject['sequences'] = [] expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) end it 'raises IllegalValueError for sequences entry that is not a Sequence' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} subject['sequences'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, seq_entry_err) end describe 'raises IllegalValueError for default Sequence that is not written out' do it 'Sequence has "items"' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['items'] = [] subject['sequences'] = [seq] @@ -168,7 +168,7 @@ def initialize(hsh={}) # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 it 'Sequence has "canvases"' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['canvases'] = [] subject['sequences'] = [seq] @@ -180,7 +180,7 @@ def initialize(hsh={}) seq['items'] = [IIIF::V3::Presentation::Canvas.new] subject['sequences'] = [seq] subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} expect { subject.validate }.not_to raise_error end it 'raises no error for Sequence with populated "canvases"' do @@ -188,23 +188,23 @@ def initialize(hsh={}) seq['canvases'] = [IIIF::V3::Presentation::Canvas.new] subject['sequences'] = [seq] subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} expect { subject.validate }.not_to raise_error end it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq1 = IIIF::V3::Presentation::Sequence.new seq1['items'] = [IIIF::V3::Presentation::Canvas.new] seq2 = IIIF::V3::Presentation::Sequence.new - seq2['label'] = 'label2' + seq2['label'] = {'en' => ['label2']} subject['sequences'] = [seq1, seq2] exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end it 'raises no error for single Sequences without "label"' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['items'] = [IIIF::V3::Presentation::Canvas.new] subject['sequences'] = [seq] @@ -214,7 +214,7 @@ def initialize(hsh={}) it 'raises IllegalValueError for structures entry that is not a Range' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['items'] = [IIIF::V3::Presentation::Canvas.new] subject['items'] = [seq] @@ -224,7 +224,7 @@ def initialize(hsh={}) end it 'raises no error when structures entry is a Range' do subject['id'] = manifest_id - subject.label = 'Book 1' + subject.label = {'en' => ['Book 1']} seq = IIIF::V3::Presentation::Sequence.new seq['items'] = [IIIF::V3::Presentation::Canvas.new] subject['items'] = [seq] @@ -236,14 +236,14 @@ def initialize(hsh={}) describe 'realistic examples' do let!(:canvas_object) { IIIF::V3::Presentation::Canvas.new({ "id" => "https://example.org/abc666/iiif3/canvas/0001", - "label" => "image", + "label" => {'en' => ['image']}, "height" => 7579, "width" => 10108, "content" => [] })} let!(:default_sequence_object) {IIIF::V3::Presentation::Sequence.new({ "id" => "https://example.org/abc666#sequence-1", - "label" => "Current order", + "label" => {'en' => ['Current order']}, "items" => [canvas_object] })} describe 'realistic(?) minimal manifest' do @@ -253,7 +253,7 @@ def initialize(hsh={}) "http://iiif.io/api/presentation/3/context.json" ], "id" => "https://example.org/abc666/iiif3/manifest", - "label" => "blah", + "label" => {"en" => ["blah"]}, "attribution" => "bleah", "description" => "blargh", "items" => [default_sequence_object] @@ -264,7 +264,7 @@ def initialize(hsh={}) it 'has expected required values' do expect(manifest_object.type).to eq 'Manifest' expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" - expect(manifest_object.label).to eq "blah" + expect(manifest_object.label['en']).to include "blah" expect(manifest_object.items.size).to be 1 expect(manifest_object.items.first).to eq default_sequence_object end @@ -298,7 +298,7 @@ def initialize(hsh={}) })} let!(:manifest_object) { described_class.new({ "id" => "https://example.org/abc666/iiif3/manifest", - "label" => "blah", + "label" => {"en" => ["blah"]}, "attribution" => "bleah", "description" => "blargh", "sequences" => [default_sequence_object], @@ -333,7 +333,7 @@ def initialize(hsh={}) it 'has expected required values' do expect(manifest_object.type).to eq 'Manifest' expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" - expect(manifest_object.label).to eq "blah" + expect(manifest_object.label['en']).to include "blah" # NOTE: using sequences as Universal Viewer currently only accepts sequences # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 expect(manifest_object.sequences.size).to be 1 @@ -354,7 +354,7 @@ def initialize(hsh={}) let!(:seq_object) { s = IIIF::V3::Presentation::Sequence.new({ 'id' => 'https://example.org/abc666#sequence-1', - 'label' => 'Current order' + "label" => {"en" => ["Current order"]}, }) s.viewingDirection = 'left-to-right' s.canvases << canvas_object @@ -363,7 +363,7 @@ def initialize(hsh={}) let!(:manifest_data) { { "id" => "https://example.org/abc666/iiif3/manifest", - "label" => "blah", + "label" => {"en" => ["blah"]}, "attribution" => "bleah", "logo" => { "id" => "https://example.org/logo/full/400,/0/default.jpg", @@ -393,7 +393,7 @@ def initialize(hsh={}) it 'has expected required values' do expect(manifest_object.type).to eq 'Manifest' expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" - expect(manifest_object.label).to eq "blah" + expect(manifest_object.label['en']).to include "blah" # NOTE: using sequences as Universal Viewer currently only accepts sequences # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 expect(manifest_object.sequences.size).to be 1 @@ -414,7 +414,7 @@ def initialize(hsh={}) describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do let!(:range_object) { IIIF::V3::Presentation::Range.new({ "id" => "http://example.org/iiif/book1/range/top", - "label" => "home, home on the", + "label" => {"en" => ["home, home on the"]}, "viewingHint" => ["top"] }) } From 76ef0afc683084e8577ddc22a1034cf595754278 Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 10 Apr 2020 07:31:03 -0600 Subject: [PATCH 86/91] Remove Sequence in favor of Ranges, items, and behavior value sequence See: https://iiif.io/api/presentation/3.0/change-log/#141-remove-sequence-in-favor-of-ranges-items-and-behavior-value-sequence --- lib/iiif/v3/presentation/manifest.rb | 39 +--- .../iiif/v3/abstract_resource_spec.rb | 32 ++-- .../iiif/v3/presentation/manifest_spec.rb | 169 ++---------------- 3 files changed, 37 insertions(+), 203 deletions(-) diff --git a/lib/iiif/v3/presentation/manifest.rb b/lib/iiif/v3/presentation/manifest.rb index 9e4f5ff..15ef19b 100644 --- a/lib/iiif/v3/presentation/manifest.rb +++ b/lib/iiif/v3/presentation/manifest.rb @@ -44,16 +44,12 @@ def validate raise IIIF::V3::Presentation::IllegalValueError, err_msg end - # Sequence object list - # NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences - # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - unless (self['items'] && self['items'].any?) || - (self['sequences'] && self['sequences'].any?) - m = 'The (items or sequences) list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' + # Items object list + unless self&.[]('items')&.any? + m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' raise IIIF::V3::Presentation::MissingRequiredKeyError, m end - validate_sequence_list(self['items']) if self['items'] - validate_sequence_list(self['sequences']) if self['sequences'] + validate_items_list(self['items']) if self['items'] # TODO: when embedding a sequence without any extensions within a manifest, the sequence must not have the @context field. @@ -69,33 +65,16 @@ def validate # NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - def validate_sequence_list(sequence_array) - unless sequence_array.size >= 1 - m = 'The (items or sequences) list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' + def validate_items_list(items_array) + unless items_array.size >= 1 + m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' raise IIIF::V3::Presentation::MissingRequiredKeyError, m end - unless sequence_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Sequence) } - m = 'All entries in the (items or sequences) list must be a IIIF::V3::Presentation::Sequence' - raise IIIF::V3::Presentation::IllegalValueError, m - end - - default_sequence = sequence_array.first - # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases - # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - canvas_array = default_sequence['items'] || default_sequence['canvases'] - unless canvas_array && canvas_array.size >= 1 && - canvas_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } - m = 'The default Sequence (the first entry of (items or sequences)) must be written out in full within the Manifest file' + unless items_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) } + m = 'All entries in the items list must be a IIIF::V3::Presentation::Canvas' raise IIIF::V3::Presentation::IllegalValueError, m end - - if sequence_array.size > 1 - unless sequence_array.all? { |entry| entry['label'] } - m = 'If there are multiple Sequences in a manifest then they must each have at least one label' - raise IIIF::V3::Presentation::IllegalValueError, m - end - end end end end diff --git a/spec/integration/iiif/v3/abstract_resource_spec.rb b/spec/integration/iiif/v3/abstract_resource_spec.rb index 114a365..fa8686f 100644 --- a/spec/integration/iiif/v3/abstract_resource_spec.rb +++ b/spec/integration/iiif/v3/abstract_resource_spec.rb @@ -62,28 +62,16 @@ }, "items": [ { - "id":"http://www.example.org/iiif/book1/sequence/normal", - "type": "Sequence", - "label": "Current Page Order", - - "viewingDirection":"left-to-right", - "viewingHint":"paged", - "startCanvas": "http://www.example.org/iiif/book1/canvas/p2", - - "items": [ + "id": "http://example.com/canvas", + "type": "Canvas", + "width": 10, + "height": 20, + "label": "My Canvas", + "content": [ { - "id": "http://example.com/canvas", - "type": "Canvas", - "width": 10, - "height": 20, - "label": "My Canvas", - "content": [ - { - "id": "http://example.com/content", - "type": "AnnotationPage", - "motivation": "painting" - } - ] + "id": "http://example.com/content", + "type": "AnnotationPage", + "motivation": "painting" } ] } @@ -125,7 +113,7 @@ it 'turns each member of "items" into an instance of Sequence' do parsed = described_class.from_ordered_hash(fixture) parsed['items'].each do |s| - expect(s.class).to be IIIF::V3::Presentation::Sequence + expect(s.class).to be IIIF::V3::Presentation::Canvas end end it 'turns each member of sequences/items into an instance of Canvas' do diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index 22c5e0d..529036c 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -72,152 +72,39 @@ def initialize(hsh={}) expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end - let(:seq_list_err) { 'The (items or sequences) list must have at least one entry (and it must be a IIIF::V3::Presentation::Sequence)' } - let(:seq_entry_err) { 'All entries in the (items or sequences) list must be a IIIF::V3::Presentation::Sequence' } - let(:def_seq_err) { 'The default Sequence (the first entry of (items or sequences)) must be written out in full within the Manifest file' } + let(:item_list_err) { 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)' } + let(:item_entry_err) { 'All entries in the items list must be a IIIF::V3::Presentation::Canvas' } it 'raises MissingRequiredKeyError if no items or sequences key' do subject['id'] = manifest_id subject.label = {'en' => ['Book 1']} - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, item_list_err) end describe 'items' do it 'raises MissingRequiredKeyError for items entry without values' do subject['id'] = manifest_id subject.label = {'en' => ['Book 1']} subject['items'] = [] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, item_list_err) end it 'raises IllegalValueError for items entry that is not a Sequence' do subject['id'] = manifest_id subject.label = {'en' => ['Book 1']} subject['items'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, seq_entry_err) + expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, item_entry_err) end - describe 'raises IllegalValueError for default Sequence that is not written out' do - it 'Sequence has "items"' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [] - subject['items'] = [seq] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) - end - # NOTE: also allowing canvases as Universal Viewer currently only accepts canvases - # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - it 'Sequence has "canvases"' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['canvases'] = [] - subject['items'] = [seq] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) - end - end - it 'raises no error for Sequence with populated "items"' do - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [IIIF::V3::Presentation::Canvas.new] - subject['items'] = [seq] - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - expect { subject.validate }.not_to raise_error - end - it 'raises no error for Sequence with populated "canvases"' do - seq = IIIF::V3::Presentation::Sequence.new - seq['canvases'] = [IIIF::V3::Presentation::Canvas.new] - subject['items'] = [seq] - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - expect { subject.validate }.not_to raise_error - end - it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq1 = IIIF::V3::Presentation::Sequence.new - seq1['items'] = [IIIF::V3::Presentation::Canvas.new] - seq2 = IIIF::V3::Presentation::Sequence.new - seq2['label'] = {'en' => ['label2']} - subject['items'] = [seq1, seq2] - exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) - end - end - describe 'sequences' do - it 'raises MissingRequiredKeyError for sequences entry without values' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - subject['sequences'] = [] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, seq_list_err) - end - it 'raises IllegalValueError for sequences entry that is not a Sequence' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - subject['sequences'] = [IIIF::V3::Presentation::Sequence.new, IIIF::V3::Presentation::Canvas.new] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, seq_entry_err) - end - describe 'raises IllegalValueError for default Sequence that is not written out' do - it 'Sequence has "items"' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [] - subject['sequences'] = [seq] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) - end - # NOTE: also allowing canvases as Universal Viewer currently only accepts canvases - # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - it 'Sequence has "canvases"' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['canvases'] = [] - subject['sequences'] = [seq] - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, def_seq_err) - end - end - it 'raises no error for Sequence with populated "items"' do - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [IIIF::V3::Presentation::Canvas.new] - subject['sequences'] = [seq] + it 'raises no error items populated with canvases' do + subject['items'] = [IIIF::V3::Presentation::Canvas.new] subject['id'] = manifest_id subject.label = {'en' => ['Book 1']} expect { subject.validate }.not_to raise_error end - it 'raises no error for Sequence with populated "canvases"' do - seq = IIIF::V3::Presentation::Sequence.new - seq['canvases'] = [IIIF::V3::Presentation::Canvas.new] - subject['sequences'] = [seq] - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - expect { subject.validate }.not_to raise_error - end - it 'raises IllegalValueError for Sequences without "label" if there are multiple Sequences' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq1 = IIIF::V3::Presentation::Sequence.new - seq1['items'] = [IIIF::V3::Presentation::Canvas.new] - seq2 = IIIF::V3::Presentation::Sequence.new - seq2['label'] = {'en' => ['label2']} - subject['sequences'] = [seq1, seq2] - exp_err_msg = 'If there are multiple Sequences in a manifest then they must each have at least one label' - expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) - end - it 'raises no error for single Sequences without "label"' do - subject['id'] = manifest_id - subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [IIIF::V3::Presentation::Canvas.new] - subject['sequences'] = [seq] - expect { subject.validate }.not_to raise_error - end end it 'raises IllegalValueError for structures entry that is not a Range' do subject['id'] = manifest_id subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [IIIF::V3::Presentation::Canvas.new] - subject['items'] = [seq] + subject['items'] = [IIIF::V3::Presentation::Canvas.new] subject['structures'] = [IIIF::V3::Presentation::Sequence.new] exp_err_msg = "All entries in the structures list must be a IIIF::V3::Presentation::Range" expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) @@ -225,9 +112,7 @@ def initialize(hsh={}) it 'raises no error when structures entry is a Range' do subject['id'] = manifest_id subject.label = {'en' => ['Book 1']} - seq = IIIF::V3::Presentation::Sequence.new - seq['items'] = [IIIF::V3::Presentation::Canvas.new] - subject['items'] = [seq] + subject['items'] = [IIIF::V3::Presentation::Canvas.new] subject['structures'] = [IIIF::V3::Presentation::Range.new] expect { subject.validate }.not_to raise_error end @@ -241,11 +126,6 @@ def initialize(hsh={}) "width" => 10108, "content" => [] })} - let!(:default_sequence_object) {IIIF::V3::Presentation::Sequence.new({ - "id" => "https://example.org/abc666#sequence-1", - "label" => {'en' => ['Current order']}, - "items" => [canvas_object] - })} describe 'realistic(?) minimal manifest' do let!(:manifest_object) { described_class.new({ "@context" => [ @@ -256,7 +136,7 @@ def initialize(hsh={}) "label" => {"en" => ["blah"]}, "attribution" => "bleah", "description" => "blargh", - "items" => [default_sequence_object] + "items" => [canvas_object] })} it 'validates' do expect{manifest_object.validate}.not_to raise_error @@ -266,7 +146,7 @@ def initialize(hsh={}) expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" expect(manifest_object.label['en']).to include "blah" expect(manifest_object.items.size).to be 1 - expect(manifest_object.items.first).to eq default_sequence_object + expect(manifest_object.items.first).to eq canvas_object end it 'has expected context' do expect(manifest_object['@context'].size).to be 2 @@ -301,7 +181,7 @@ def initialize(hsh={}) "label" => {"en" => ["blah"]}, "attribution" => "bleah", "description" => "blargh", - "sequences" => [default_sequence_object], + "items" => [canvas_object], "logo" => { "id" => "https://example.org/logo/full/400,/0/default.jpg", "service" => logo_service @@ -334,10 +214,8 @@ def initialize(hsh={}) expect(manifest_object.type).to eq 'Manifest' expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" expect(manifest_object.label['en']).to include "blah" - # NOTE: using sequences as Universal Viewer currently only accepts sequences - # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - expect(manifest_object.sequences.size).to be 1 - expect(manifest_object.sequences.first).to eq default_sequence_object + expect(manifest_object.items.size).to be 1 + expect(manifest_object.items.first).to eq canvas_object end it 'has expected string values' do expect(manifest_object.attribution).to eq "bleah" @@ -351,15 +229,6 @@ def initialize(hsh={}) end describe 'from stanford purl CODE' do - let!(:seq_object) { - s = IIIF::V3::Presentation::Sequence.new({ - 'id' => 'https://example.org/abc666#sequence-1', - "label" => {"en" => ["Current order"]}, - }) - s.viewingDirection = 'left-to-right' - s.canvases << canvas_object - s - } let!(:manifest_data) { { "id" => "https://example.org/abc666/iiif3/manifest", @@ -384,7 +253,7 @@ def initialize(hsh={}) ] m.description = 'blargh' m.thumbnail = [thumbnail_image] - m.sequences << seq_object + m.items << canvas_object m } it 'manifest validates' do @@ -394,10 +263,8 @@ def initialize(hsh={}) expect(manifest_object.type).to eq 'Manifest' expect(manifest_object.id).to eq "https://example.org/abc666/iiif3/manifest" expect(manifest_object.label['en']).to include "blah" - # NOTE: using sequences as Universal Viewer currently only accepts sequences - # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167 - expect(manifest_object.sequences.size).to be 1 - expect(manifest_object.sequences.first).to eq seq_object + expect(manifest_object.items.size).to be 1 + expect(manifest_object.items.first).to eq canvas_object end it 'has expected string values' do expect(manifest_object.attribution).to eq "bleah" @@ -486,7 +353,7 @@ def initialize(hsh={}) "id" => "http://example.org/collections/books/", "type" => "Collection" }], - "items" => [default_sequence_object], + "items" => [canvas_object], "structures" => [range_object] })} it 'validates' do From ffae448c3f3b778a7d306c5a316e659e80e86f12 Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 10 Apr 2020 08:17:29 -0600 Subject: [PATCH 87/91] Rename attribution to requiredStatement, allow label+value https://iiif.io/api/presentation/3.0/change-log/#122-rename-viewinghint-to-behavior --- lib/iiif/v3/abstract_resource.rb | 4 +-- .../iiif/v3/presentation/manifest_spec.rb | 28 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 8681e56..4bfe389 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -27,7 +27,7 @@ def prohibited_keys def any_type_keys # values *may* be multivalued # NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers" - %w{ description id attribution logo viewing_hint related see_also within } + %w{ description id logo viewing_hint related see_also within } end def string_only_keys @@ -43,7 +43,7 @@ def abstract_resource_only_keys end def hash_only_keys - %w{ label } + %w{ label requiredStatement } end def int_only_keys diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index 529036c..c5d2e4b 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -134,7 +134,10 @@ def initialize(hsh={}) ], "id" => "https://example.org/abc666/iiif3/manifest", "label" => {"en" => ["blah"]}, - "attribution" => "bleah", + "requiredStatement" => { + "label": { "en": [ "Attribution" ] }, + "value": { "en": [ "bleah" ] }, + }, "description" => "blargh", "items" => [canvas_object] })} @@ -153,9 +156,11 @@ def initialize(hsh={}) expect(manifest_object['@context']).to include(*IIIF::V3::Presentation::CONTEXT) end it 'has expected string values' do - expect(manifest_object.attribution).to eq "bleah" expect(manifest_object.description).to eq "blargh" end + it 'has other values' do + expect(manifest_object['required_statement'][:value][:en].first).to eq 'bleah' + end end describe 'realistic example from Stanford purl manifests' do @@ -179,7 +184,10 @@ def initialize(hsh={}) let!(:manifest_object) { described_class.new({ "id" => "https://example.org/abc666/iiif3/manifest", "label" => {"en" => ["blah"]}, - "attribution" => "bleah", + "requiredStatement" => { + "label": { "en": [ "Attribution" ] }, + "value": { "en": [ "bleah" ] }, + }, "description" => "blargh", "items" => [canvas_object], "logo" => { @@ -218,10 +226,10 @@ def initialize(hsh={}) expect(manifest_object.items.first).to eq canvas_object end it 'has expected string values' do - expect(manifest_object.attribution).to eq "bleah" expect(manifest_object.description).to eq "blargh" end it 'has expected additional content' do + expect(manifest_object['required_statement'][:value][:en].first).to eq 'bleah' expect(manifest_object.logo.keys.size).to be 2 expect(manifest_object.seeAlso.keys.size).to be 2 expect(manifest_object.metadata.size).to be 2 @@ -233,7 +241,10 @@ def initialize(hsh={}) { "id" => "https://example.org/abc666/iiif3/manifest", "label" => {"en" => ["blah"]}, - "attribution" => "bleah", + "requiredStatement" => { + "label": { "en": [ "Attribution" ] }, + "value": { "en": [ "bleah" ] }, + }, "logo" => { "id" => "https://example.org/logo/full/400,/0/default.jpg", "service" => logo_service @@ -267,10 +278,10 @@ def initialize(hsh={}) expect(manifest_object.items.first).to eq canvas_object end it 'has expected string values' do - expect(manifest_object.attribution).to eq "bleah" expect(manifest_object.description).to eq "blargh" end it 'has expected additional content' do + expect(manifest_object['required_statement'][:value][:en].first).to eq 'bleah' expect(manifest_object.logo.keys.size).to be 2 expect(manifest_object.seeAlso.keys.size).to be 2 expect(manifest_object.metadata.size).to be 2 @@ -321,7 +332,10 @@ def initialize(hsh={}) "rights" => [{ "id" =>"http://example.org/license.html", "format" => "text/html"}], - "attribution" => {"en" => ["Provided by Example Organization"]}, + "requiredStatement" => { + "label": { "en": [ "Attribution" ] }, + "value": { "en": [ "bleah" ] }, + }, "logo" => { "id" => "http://example.org/logos/institution1.jpg", "service" => { From 1ec21e7b1ea677ea386e6ee67f558c80077f66b1 Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 10 Apr 2020 08:26:38 -0600 Subject: [PATCH 88/91] Context must not be present on any included resources --- lib/iiif/v3/presentation/annotation.rb | 8 ---- lib/iiif/v3/presentation/image_resource.rb | 2 - lib/iiif/v3/presentation/service.rb | 14 +++---- .../v3/presentation/image_resource_spec.rb | 4 -- .../iiif/v3/presentation/annotation_spec.rb | 23 ----------- .../v3/presentation/image_resource_spec.rb | 1 - .../iiif/v3/presentation/manifest_spec.rb | 8 ++-- .../unit/iiif/v3/presentation/service_spec.rb | 41 ++++++++++--------- 8 files changed, 31 insertions(+), 70 deletions(-) diff --git a/lib/iiif/v3/presentation/annotation.rb b/lib/iiif/v3/presentation/annotation.rb index 5f0116f..0d1e76f 100644 --- a/lib/iiif/v3/presentation/annotation.rb +++ b/lib/iiif/v3/presentation/annotation.rb @@ -59,14 +59,6 @@ def validate m = "when #{self.class} body is a kind of #{img_res_class_str}, ImageResource id must be an http(s) URI" raise IIIF::V3::Presentation::IllegalValueError, m end - - body_service = *body_resource['service'] - body_service_context = *body_resource['service']['@context'] - expected_context = IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT - unless body_service && body_service_context && body_service_context.include?(expected_context) - m = "when #{self.class} body is a kind of #{img_res_class_str}, ImageResource's service @context must include #{expected_context}" - raise IIIF::V3::Presentation::IllegalValueError, m - end end if self.has_key?('time_mode') diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 40cc3d4..793cc63 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -14,7 +14,6 @@ def initialize(hsh={}) class << self IMAGE_API_DEFAULT_PARAMS = '/full/!200,200/0/default.jpg'.freeze - IMAGE_API_CONTEXT = 'http://iiif.io/api/image/2/context.json'.freeze DEFAULT_FORMAT = 'image/jpeg'.freeze # Create a new ImageResource that includes a IIIF Image API Service # See http://iiif.io/api/presentation/2.0/#image-resources @@ -80,7 +79,6 @@ def create_image_api_image_resource(params={}) resource.service.merge!(remote_info) resource.service['id'] ||= resource.service.delete('@id') else - resource.service['@context'] = IMAGE_API_CONTEXT resource.service['id'] = service_id if profile.nil? if remote_info['profile'].kind_of?(Array) diff --git a/lib/iiif/v3/presentation/service.rb b/lib/iiif/v3/presentation/service.rb index c3866d2..a36cbfd 100644 --- a/lib/iiif/v3/presentation/service.rb +++ b/lib/iiif/v3/presentation/service.rb @@ -5,7 +5,7 @@ module Presentation class Service < AbstractResource # constants included here for convenience - IIIF_IMAGE_V2_CONTEXT = 'http://iiif.io/api/image/2/context.json'.freeze + IIIF_IMAGE_V2_TYPE = 'ImageService2'.freeze IIIF_IMAGE_V2_LEVEL1_PROFILE = 'http://iiif.io/api/image/2/level1.json'.freeze IIIF_AUTHENTICATION_V1_LOGIN_PROFILE = 'http://iiif.io/api/auth/1/login'.freeze IIIF_AUTHENTICATION_V1_TOKEN_PROFILE = 'http://iiif.io/api/auth/1/token'.freeze @@ -30,17 +30,17 @@ def any_type_keys def validate super - if IIIF_IMAGE_V2_CONTEXT == self['@context'] - unless self.has_key?('@id') - m = "@id is required for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" + if IIIF_IMAGE_V2_TYPE == self['type'] || IIIF_IMAGE_V2_TYPE == self['@type'] + unless self.has_key?('id') || self.has_key?('@id') + m = "id or @id values are required for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}" raise IIIF::V3::Presentation::MissingRequiredKeyError, m end - if self.has_key?('id') && (self['@id'] != self['id']) - m = "id and @id values must match for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" + if self.has_key?('id') && self.has_key?('@id') && (self['@id'] != self['id']) + m = "id and @id values must match for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}" raise IIIF::V3::Presentation::IllegalValueError, m end unless self.has_key?('profile') - m = "profile is required for IIIF::V3::Presentation::Service with @context #{IIIF_IMAGE_V2_CONTEXT}" + m = "profile should be present for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}" raise IIIF::V3::Presentation::MissingRequiredKeyError, m end end diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb index f257cf4..686c331 100644 --- a/spec/integration/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -29,14 +29,11 @@ it 'when copy_info is false' do opts = { service_id: valid_service_id } resource = described_class.create_image_api_image_resource(opts) - # expect(resource['@context']).to eq 'http://iiif.io/api/presentation/2/context.json' - # @context is only added when we call to_json... expect(resource['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg' expect(resource['type']).to eq 'Image' expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 - expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json' expect(resource.service['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' expect(resource.service['profile']).to eq 'http://iiif.io/api/image/2/level2.json' end @@ -49,7 +46,6 @@ expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 - expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json' expect(resource.service['profile']).to eq [ 'http://iiif.io/api/image/2/level2.json', { diff --git a/spec/unit/iiif/v3/presentation/annotation_spec.rb b/spec/unit/iiif/v3/presentation/annotation_spec.rb index 18fedeb..7bea098 100644 --- a/spec/unit/iiif/v3/presentation/annotation_spec.rb +++ b/spec/unit/iiif/v3/presentation/annotation_spec.rb @@ -4,7 +4,6 @@ let(:content_type) { 'dctypes:Text' } let(:mimetype) { 'application/tei+xml' } let(:image_2_api_service) { IIIF::V3::Presentation::Service.new({ - '@context' => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT, 'id' => content_id, '@id' => content_id, 'profile' => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE @@ -149,28 +148,6 @@ def initialize(hsh={}) img_body_anno.body = img_resource_without_id expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, http_uri_err_msg end - - let(:service_context_err_msg) { - "when #{described_class} body is a kind of IIIF::V3::Presentation::ImageResource, ImageResource's service @context must include http://iiif.io/api/image/2/context.json" - } - it 'raises IllegalValueError if no @context field in ImageResource service' do - img_content_resource['service']['@context'] = nil - img_body_anno.body = img_content_resource - expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, service_context_err_msg - end - it 'raises IllegalValueError if @context in ImageResource service doesn\'t include reference to IIIF Image API context doc' do - img_content_resource['service']['@context'] = 'http://example.com/context.json' - img_body_anno.body = img_content_resource - expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, service_context_err_msg - img_content_resource['service']['@context'] = ['http://example.com/context.json', 'http://example.com/context2.json'] - img_body_anno.body = img_content_resource - expect { img_body_anno.validate }.to raise_error IIIF::V3::Presentation::IllegalValueError, service_context_err_msg - end - it 'does not raise error if @context in ImageResource service includes reference to IIIF Image API context doc' do - img_content_resource['service']['@context'] = ['http://example.com/context.json', IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT] - img_body_anno.body = img_content_resource - expect { img_body_anno.validate }.not_to raise_error - end end end diff --git a/spec/unit/iiif/v3/presentation/image_resource_spec.rb b/spec/unit/iiif/v3/presentation/image_resource_spec.rb index 69ece68..e38f00a 100644 --- a/spec/unit/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/image_resource_spec.rb @@ -106,7 +106,6 @@ expect(img_service_obj.id).to eq img_id expect(img_service_obj['@id']).to eq img_id expect(img_service_obj.profile).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE - expect(img_service_obj['@context']).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT expect(img_service_obj.service.class).to eq Array expect(img_service_obj.service.size).to eq 1 diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index c5d2e4b..19555eb 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -165,13 +165,12 @@ def initialize(hsh={}) describe 'realistic example from Stanford purl manifests' do let!(:logo_service) { IIIF::V3::Presentation::Service.new({ - "@context" => "http://iiif.io/api/image/2/context.json", "@id" => "https://example.org/logo", + "@type" => "ImageService2", "id" => "https://example.org/logo", "profile" => "http://iiif.io/api/image/2/level1.json" })} let!(:thumbnail_image_service) { IIIF::V3::Presentation::Service.new({ - "@context" => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT, "@id" => "https://example.org/image/iiif/abc666_05_0001", "id" => "https://example.org/image/iiif/abc666_05_0001", "profile" => IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE @@ -321,8 +320,8 @@ def initialize(hsh={}) "id" => "http://example.org/images/book1-page1/full/80,100/0/default.jpg", "type" => "Image", "service" => { - "@context" => "http://iiif.io/api/image/2/context.json", "id" => "http://example.org/images/book1-page1", + "type" => "ImageService2", "profile" => ["http://iiif.io/api/image/2/level1.json"] } }], @@ -339,8 +338,8 @@ def initialize(hsh={}) "logo" => { "id" => "http://example.org/logos/institution1.jpg", "service" => { - "@context" => "http://iiif.io/api/image/2/context.json", "id" => "http://example.org/service/inst1", + "type" => "ImageService2", "profile" => ["http://iiif.io/api/image/2/profiles/level2.json"] } }, @@ -349,7 +348,6 @@ def initialize(hsh={}) "format" => "video/mpeg" }], "service" => [{ - "@context" => "http://example.org/ns/jsonld/context.json", "id" => "http://example.org/service/example", "profile" => ["http://example.org/docs/example-service.html"] }], diff --git a/spec/unit/iiif/v3/presentation/service_spec.rb b/spec/unit/iiif/v3/presentation/service_spec.rb index 790a22d..c4e3533 100644 --- a/spec/unit/iiif/v3/presentation/service_spec.rb +++ b/spec/unit/iiif/v3/presentation/service_spec.rb @@ -50,7 +50,7 @@ 'profile' => inner_service_profile }) service_obj = described_class.new({ - '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'type' => described_class::IIIF_IMAGE_V2_TYPE, 'id' => id_uri, 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE, 'label' => label_val, @@ -58,7 +58,7 @@ }) expect(service_obj.keys.size).to eq 5 expect(service_obj['id']).to eq id_uri - expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['type']).to eq described_class::IIIF_IMAGE_V2_TYPE expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE expect(service_obj['label']).to eq label_val expect(service_obj['service'][0]['id']).to eq inner_service_id @@ -91,31 +91,32 @@ end describe '#validate' do - describe '@context = IIIF_IMAGE_API_V2_CONTEXT' do - it 'must have a "@id"' do + describe 'type = IIIF_IMAGE_API_V2_TYPE' do + it 'must have a "@id" or "id"' do service_obj = described_class.new({ - '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, - 'id' => id_uri, + 'type' => described_class::IIIF_IMAGE_V2_TYPE, + 'nope' => id_uri, 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE }) - exp_err_msg = '@id is required for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' + exp_err_msg = 'id or @id values are required for IIIF::V3::Presentation::Service with type or @type ImageService2' expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) end - it 'must have a profile' do + it 'should have a profile' do service_obj = described_class.new({ - '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + '@type' => described_class::IIIF_IMAGE_V2_TYPE, '@id' => id_uri }) - exp_err_msg = 'profile is required for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' + exp_err_msg = 'profile should be present for IIIF::V3::Presentation::Service with type or @type ImageService2' expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, exp_err_msg) end - it 'must have matching values for "@id" and "id" if both are specified' do + it 'should have matching values for "@id" and "id" if both are specified' do service_obj = described_class.new({ - '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + '@type' => described_class::IIIF_IMAGE_V2_TYPE, '@id' => id_uri, - 'id' => "#{id_uri}/foo" + 'id' => "#{id_uri}/foo", + 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE }) - exp_err_msg = 'id and @id values must match for IIIF::V3::Presentation::Service with @context http://iiif.io/api/image/2/context.json' + exp_err_msg = 'id and @id values must match for IIIF::V3::Presentation::Service with type or @type ImageService2' expect{service_obj.validate}.to raise_error(IIIF::V3::Presentation::IllegalValueError, exp_err_msg) end end @@ -125,15 +126,15 @@ describe 'from Stanford purl manifests' do it 'iiif image v2 service' do service_obj = described_class.new({ - '@context' => described_class::IIIF_IMAGE_V2_CONTEXT, + 'type' => described_class::IIIF_IMAGE_V2_TYPE, 'id' => id_uri, '@id' => id_uri, 'profile' => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE }) expect{service_obj.validate}.not_to raise_error expect(service_obj.keys.size).to eq 4 - expect(service_obj.keys).to include('@context', '@id', 'id', 'profile') - expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj.keys).to include('type', '@id', 'id', 'profile') + expect(service_obj['type']).to eq described_class::IIIF_IMAGE_V2_TYPE expect(service_obj['id']).to eq id_uri expect(service_obj['@id']).to eq id_uri expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE @@ -183,7 +184,7 @@ }) expect{middle.validate}.not_to raise_error outer = described_class.new({ - "@context" => described_class::IIIF_IMAGE_V2_CONTEXT, + "@type" => described_class::IIIF_IMAGE_V2_TYPE, "@id" => "https://example.org/iiif/yy816tv6021_img_1", "id" => "https://example.org/iiif/yy816tv6021_img_1", "profile" => described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE, @@ -195,7 +196,7 @@ describe 'example from http://prezi3.iiif.io/api/presentation/3.0' do it 'iiif image v2' do service_obj = described_class.new({ - "@context" => "http://iiif.io/api/image/2/context.json", + "type" => described_class::IIIF_IMAGE_V2_TYPE, "id" => "http://example.org/images/book1-page2", "@id" => "http://example.org/images/book1-page2", "profile" => "http://iiif.io/api/image/2/level1.json", @@ -204,7 +205,7 @@ "tiles" => [{"width" => 512, "scaleFactors" => [1,2,4,8,16]}] }) expect(service_obj.keys.size).to eq 7 - expect(service_obj['@context']).to eq described_class::IIIF_IMAGE_V2_CONTEXT + expect(service_obj['type']).to eq described_class::IIIF_IMAGE_V2_TYPE expect(service_obj['id']).to eq 'http://example.org/images/book1-page2' expect(service_obj['@id']).to eq 'http://example.org/images/book1-page2' expect(service_obj['profile']).to eq described_class::IIIF_IMAGE_V2_LEVEL1_PROFILE From cd1d6c539f071b94a39d3dd9b01a1eb0db3f4762 Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 10 Apr 2020 08:41:11 -0600 Subject: [PATCH 89/91] A service must be an array --- lib/iiif/v3/presentation/image_resource.rb | 15 ++++++++------- .../iiif/v3/presentation/image_resource_spec.rb | 16 ++++++++-------- .../iiif/v3/presentation/image_resource_spec.rb | 15 +++++++-------- spec/unit/iiif/v3/presentation/resource_spec.rb | 4 ++-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/iiif/v3/presentation/image_resource.rb b/lib/iiif/v3/presentation/image_resource.rb index 793cc63..4337bcf 100644 --- a/lib/iiif/v3/presentation/image_resource.rb +++ b/lib/iiif/v3/presentation/image_resource.rb @@ -74,22 +74,23 @@ def create_image_api_image_resource(params={}) resource.format = format resource.width = width.nil? ? remote_info['width'] : width resource.height = height.nil? ? remote_info['height'] : height - resource.service = Service.new + resource_service = Service.new if copy_info - resource.service.merge!(remote_info) - resource.service['id'] ||= resource.service.delete('@id') + resource_service.merge!(remote_info) + resource_service['id'] ||= resource_service.delete('@id') else - resource.service['id'] = service_id + resource_service['id'] = service_id if profile.nil? if remote_info['profile'].kind_of?(Array) - resource.service['profile'] = remote_info['profile'][0] + resource_service['profile'] = remote_info['profile'][0] else - resource.service['profile'] = remote_info['profile'] + resource_service['profile'] = remote_info['profile'] end else - resource.service['profile'] = profile + resource_service['profile'] = profile end end + resource.service = [resource_service] return resource end diff --git a/spec/integration/iiif/v3/presentation/image_resource_spec.rb b/spec/integration/iiif/v3/presentation/image_resource_spec.rb index 686c331..89751c5 100644 --- a/spec/integration/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/integration/iiif/v3/presentation/image_resource_spec.rb @@ -34,8 +34,8 @@ expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 - expect(resource.service['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' - expect(resource.service['profile']).to eq 'http://iiif.io/api/image/2/level2.json' + expect(resource.service.first['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' + expect(resource.service.first['profile']).to eq 'http://iiif.io/api/image/2/level2.json' end it 'copies over all teh infos (when copy_info is true)' do opts = { service_id: valid_service_id, copy_info: true } @@ -46,7 +46,7 @@ expect(resource.format).to eq "image/jpeg" expect(resource.width).to eq 3047 expect(resource.height).to eq 7200 - expect(resource.service['profile']).to eq [ + expect(resource.service.first['profile']).to eq [ 'http://iiif.io/api/image/2/level2.json', { 'supports' => [ @@ -57,11 +57,11 @@ 'formats'=>['jpg', 'png', 'gif', 'webp'] } ] - expect(resource.service['tiles']).to eq [ { + expect(resource.service.first['tiles']).to eq [ { 'width' => 1024, 'scaleFactors' => [ 1, 2, 4, 8, 16, 32 ] } ] - expect(resource.service['sizes']).to eq [ + expect(resource.service.first['sizes']).to eq [ {'width' => 96, 'height' => 225 }, {'width' => 191, 'height' => 450 }, {'width' => 381, 'height' => 900 }, @@ -69,8 +69,8 @@ {'width' => 1524, 'height' => 3600 }, {'width' => 3047, 'height' => 7200 } ] - expect(resource.service['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' - expect(resource.service).not_to have_key('@id') + expect(resource.service.first['id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2' + expect(resource.service.first).not_to have_key('@id') end end @@ -97,7 +97,7 @@ profile = 'http://iiif.io/api/image/2/level1.json' opts = { service_id: valid_service_id, profile: profile} resource = described_class.create_image_api_image_resource(opts) - expect(resource.service['profile']).to eq profile + expect(resource.service.first['profile']).to eq profile end end diff --git a/spec/unit/iiif/v3/presentation/image_resource_spec.rb b/spec/unit/iiif/v3/presentation/image_resource_spec.rb index e38f00a..980d240 100644 --- a/spec/unit/iiif/v3/presentation/image_resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/image_resource_spec.rb @@ -27,7 +27,7 @@ thumb['type'] = 'Image' thumb['id'] = thumb_id thumb.format = img_mimetype - thumb.service = image_v2_service + thumb.service = [image_v2_service] thumb } it 'validates' do @@ -39,7 +39,7 @@ end it 'has expected additional values' do expect(thumb_object.format).to eq img_mimetype - expect(thumb_object.service).to eq image_v2_service + expect(thumb_object.service.first).to eq image_v2_service end end describe 'full size per purl code' do @@ -50,7 +50,7 @@ img.format = img_mimetype img.height = height img.width = width - img.service = image_v2_service + img.service = [image_v2_service] img } describe 'world visible' do @@ -65,16 +65,15 @@ expect(image_object.format).to eq img_mimetype expect(image_object.height).to eq height expect(image_object.width).to eq width - expect(image_object.service).to eq image_v2_service + expect(image_object.service.first).to eq image_v2_service end it 'has expected service value' do - img_service_obj = image_object.service + img_service_obj = image_object.service.first expect(img_service_obj.class).to eq IIIF::V3::Presentation::Service expect(img_service_obj.keys.size).to eq 4 expect(img_service_obj.id).to eq img_id expect(img_service_obj['@id']).to eq img_id expect(img_service_obj.profile).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_LEVEL1_PROFILE - expect(img_service_obj['@context']).to eq IIIF::V3::Presentation::Service::IIIF_IMAGE_V2_CONTEXT end end describe 'requires login' do @@ -93,14 +92,14 @@ } let(:image_object_w_login) { img = image_object - img.service['service'] = [login_service] + img.service.first['service'] = [login_service] img } it 'validates' do expect{image_object_w_login.validate}.not_to raise_error end it 'has expected service value' do - img_service_obj = image_object_w_login.service + img_service_obj = image_object_w_login.service.first expect(img_service_obj.class).to eq IIIF::V3::Presentation::Service expect(img_service_obj.keys.size).to eq 5 expect(img_service_obj.id).to eq img_id diff --git a/spec/unit/iiif/v3/presentation/resource_spec.rb b/spec/unit/iiif/v3/presentation/resource_spec.rb index d916212..4dd5e48 100644 --- a/spec/unit/iiif/v3/presentation/resource_spec.rb +++ b/spec/unit/iiif/v3/presentation/resource_spec.rb @@ -147,14 +147,14 @@ def initialize(hsh={}) } let(:resource_object_w_login) { resource = resource_object - resource.service = login_service + resource.service = [login_service] resource } it 'validates' do expect{resource_object_w_login.validate}.not_to raise_error end it 'has expected service value' do - service_obj = resource_object_w_login.service + service_obj = resource_object_w_login.service.first expect(service_obj.class).to eq IIIF::V3::Presentation::Service expect(service_obj.keys.size).to eq 4 expect(service_obj.id).to eq 'https://example.org/auth/iiif' From 34826cee2ec1205281c33c0b07f11d2fbeefbaae Mon Sep 17 00:00:00 2001 From: Jack Reed Date: Fri, 10 Apr 2020 09:24:32 -0600 Subject: [PATCH 90/91] Rename description to summary https://iiif.io/api/presentation/3.0/change-log/#126-rename-description-to-summary --- lib/iiif/v3/abstract_resource.rb | 26 +++---------------- ...stract_resource_define_methods_for_spec.rb | 3 --- spec/unit/iiif/v3/presentation/canvas_spec.rb | 8 +++--- .../iiif/v3/presentation/manifest_spec.rb | 20 +++++--------- 4 files changed, 15 insertions(+), 42 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index 4bfe389..e68069a 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -27,7 +27,7 @@ def prohibited_keys def any_type_keys # values *may* be multivalued # NOTE: for id: "Resources that do not require URIs [for ids] may be assigned blank node identifiers" - %w{ description id logo viewing_hint related see_also within } + %w{ id logo viewing_hint related see_also within } end def string_only_keys @@ -35,15 +35,11 @@ def string_only_keys end def array_only_keys - %w{ metadata rights thumbnail rendering first last next prev items } - end - - def abstract_resource_only_keys - [ { key: 'service', type: IIIF::V3::Presentation::Service } ] + %w{ metadata rights thumbnail rendering first last next prev items service } end def hash_only_keys - %w{ label requiredStatement } + %w{ label requiredStatement summary } end def int_only_keys @@ -84,7 +80,6 @@ def initialize(hsh={}) self.define_methods_for_hash_only_keys self.define_methods_for_int_only_keys self.define_methods_for_numeric_only_keys - self.define_methods_for_abstract_resource_only_keys self.define_methods_for_uri_only_keys self.snakeize_keys end @@ -426,21 +421,6 @@ def define_methods_for_hash_only_keys end end - def define_methods_for_abstract_resource_only_keys - # values in this case: an array of hashes with { key: 'k', type: Class } - abstract_resource_only_keys.each do |hsh| - key = hsh[:key] - type = hsh[:type] - - define_accessor_methods(key) do |k, val| - unless val.kind_of?(type) - m = "#{k} must be an #{type}." - raise IIIF::V3::Presentation::IllegalValueError, m - end - end - end - end - def define_methods_for_string_only_keys define_accessor_methods(*string_only_keys) do |key, val| unless val.kind_of?(String) diff --git a/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb b/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb index 07d395c..cad3e7b 100644 --- a/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +++ b/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb @@ -25,9 +25,6 @@ def initialize(hsh={}) describe AbstractResourceSubClass do - describe '*define_methods_for_abstract_resource_only_keys' do - it_behaves_like 'it has the appropriate methods for abstract_resource_only_keys v3' - end describe "*define_methods_for_any_type_keys" do # shared_example expects fixed_values; these are roughly based on Stanford purl code # (see https://github.com/sul-dlss/purl/blob/master/app/models/iiif3_presentation_manifest.rb) diff --git a/spec/unit/iiif/v3/presentation/canvas_spec.rb b/spec/unit/iiif/v3/presentation/canvas_spec.rb index 3fd1282..fad931d 100644 --- a/spec/unit/iiif/v3/presentation/canvas_spec.rb +++ b/spec/unit/iiif/v3/presentation/canvas_spec.rb @@ -272,7 +272,9 @@ def initialize(hsh={}) let(:canvas_for_audio) { described_class.new({ "id" => "http://tomcrane.github.io/scratch/manifests/3/canvas/2", "label" => "Track 2", - "description" => "foo", + 'summary' => { + 'en' => ['foo'] + }, "duration" => 45, "content" => [anno_page] })} @@ -282,8 +284,8 @@ def initialize(hsh={}) it 'duration' do expect(canvas_for_audio.duration).to eq 45 end - it 'description' do - expect(canvas_for_audio.description).to eq 'foo' + it 'summary' do + expect(canvas_for_audio.summary['en'].first).to eq 'foo' end end end diff --git a/spec/unit/iiif/v3/presentation/manifest_spec.rb b/spec/unit/iiif/v3/presentation/manifest_spec.rb index 19555eb..a9274f4 100644 --- a/spec/unit/iiif/v3/presentation/manifest_spec.rb +++ b/spec/unit/iiif/v3/presentation/manifest_spec.rb @@ -138,7 +138,7 @@ def initialize(hsh={}) "label": { "en": [ "Attribution" ] }, "value": { "en": [ "bleah" ] }, }, - "description" => "blargh", + 'summary' => { 'en' => ['blargh'] }, "items" => [canvas_object] })} it 'validates' do @@ -155,11 +155,9 @@ def initialize(hsh={}) expect(manifest_object['@context'].size).to be 2 expect(manifest_object['@context']).to include(*IIIF::V3::Presentation::CONTEXT) end - it 'has expected string values' do - expect(manifest_object.description).to eq "blargh" - end it 'has other values' do expect(manifest_object['required_statement'][:value][:en].first).to eq 'bleah' + expect(manifest_object.summary['en'].first).to eq 'blargh' end end @@ -187,7 +185,7 @@ def initialize(hsh={}) "label": { "en": [ "Attribution" ] }, "value": { "en": [ "bleah" ] }, }, - "description" => "blargh", + 'summary' => { 'en' => ['blargh'] }, "items" => [canvas_object], "logo" => { "id" => "https://example.org/logo/full/400,/0/default.jpg", @@ -224,11 +222,9 @@ def initialize(hsh={}) expect(manifest_object.items.size).to be 1 expect(manifest_object.items.first).to eq canvas_object end - it 'has expected string values' do - expect(manifest_object.description).to eq "blargh" - end it 'has expected additional content' do expect(manifest_object['required_statement'][:value][:en].first).to eq 'bleah' + expect(manifest_object.summary['en'].first).to eq 'blargh' expect(manifest_object.logo.keys.size).to be 2 expect(manifest_object.seeAlso.keys.size).to be 2 expect(manifest_object.metadata.size).to be 2 @@ -261,7 +257,7 @@ def initialize(hsh={}) { 'label' => 'title', 'value' => 'who wants to know?' }, { 'label' => 'PublishDate', 'value' => 'sometime' } ] - m.description = 'blargh' + m.summary = { 'en' => ['blargh'] } m.thumbnail = [thumbnail_image] m.items << canvas_object m @@ -276,11 +272,9 @@ def initialize(hsh={}) expect(manifest_object.items.size).to be 1 expect(manifest_object.items.first).to eq canvas_object end - it 'has expected string values' do - expect(manifest_object.description).to eq "blargh" - end it 'has expected additional content' do expect(manifest_object['required_statement'][:value][:en].first).to eq 'bleah' + expect(manifest_object.summary['en'].first).to eq 'blargh' expect(manifest_object.logo.keys.size).to be 2 expect(manifest_object.seeAlso.keys.size).to be 2 expect(manifest_object.metadata.size).to be 2 @@ -315,7 +309,7 @@ def initialize(hsh={}) {"label" => {"en" => ["Source"]}, "value" => {"@none" => ["From: Some Collection"]}} ], - "description" => {"en" => ["A longer description of this example book. It should give some real information."]}, + "summary" => {"en" => ["A longer description of this example book. It should give some real information."]}, "thumbnail" => [{ "id" => "http://example.org/images/book1-page1/full/80,100/0/default.jpg", "type" => "Image", From 0548b1685bd75821a6c3064dcff0afd501e87153 Mon Sep 17 00:00:00 2001 From: Justin Coyne Date: Thu, 8 Jun 2023 09:26:50 -0500 Subject: [PATCH 91/91] Use File.exist? for ruby 3.2 compatibility --- lib/iiif/v3/abstract_resource.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iiif/v3/abstract_resource.rb b/lib/iiif/v3/abstract_resource.rb index e68069a..76fd0c0 100644 --- a/lib/iiif/v3/abstract_resource.rb +++ b/lib/iiif/v3/abstract_resource.rb @@ -89,9 +89,9 @@ class << self # Parse from a file path, string, or existing hash def parse(s) ordered_hash = nil - if s.kind_of?(String) && File.exists?(s) + if s.kind_of?(String) && File.exist?(s) ordered_hash = IIIF::OrderedHash[JSON.parse(IO.read(s))] - elsif s.kind_of?(String) && !File.exists?(s) + elsif s.kind_of?(String) && !File.exist?(s) ordered_hash = IIIF::OrderedHash[JSON.parse(s)] elsif s.kind_of?(Hash) ordered_hash = IIIF::OrderedHash[s]