Skip to content

Commit

Permalink
Merge pull request #25 from sul-dlss/v3-canvas-tests
Browse files Browse the repository at this point in the history
V3 canvas: improve specificity, validation and tests
  • Loading branch information
tingulfsen authored Jul 19, 2017
2 parents 1cf02c0 + ef2adb8 commit e22658a
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 40 deletions.
52 changes: 43 additions & 9 deletions lib/iiif/v3/presentation/canvas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ 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 }
super + %w{ depth }
end

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={})
Expand All @@ -31,7 +32,40 @@ 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']
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
# 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.
end
end
end
Expand Down
286 changes: 255 additions & 31 deletions spec/unit/iiif/v3/presentation/canvas_spec.rb
Original file line number Diff line number Diff line change
@@ -1,50 +1,274 @@
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" => [ ]
}
end
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 '#initialize' do
it 'sets type' do
expect(subject['type']).to eq 'Canvas'
describe '#int_only_keys' do
it 'depth (for 3d objects)' do
expect(subject.int_only_keys).to include('depth')
end
end

describe "#{described_class}.int_only_keys" do
it_behaves_like 'it has the appropriate methods for integer-only keys v3'
describe '#array_only_keys' do
it 'content' do
expect(subject.array_only_keys).to include('content')
end
end

describe "#{described_class}.numeric_only_keys" do
it_behaves_like 'it has the appropriate methods for numeric-only keys v3'
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 "#{described_class}.define_methods_for_string_only_keys" do
it_behaves_like 'it has the appropriate methods for string-only keys v3'
describe '#initialize' 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}.define_methods_for_array_only_keys" do
it_behaves_like 'it has the appropriate methods for array-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

# 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)
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
# (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
subject['width'] = 666
subject['height'] = 666
subject['duration'] = 66.6
expect { subject.validate }.not_to raise_error
end

describe "#{described_class}.define_methods_for_any_type_keys" do
it_behaves_like 'it has the appropriate methods for any-type 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 "#legal_viewing_hint_values" do
it "should not error" do
expect{subject.legal_viewing_hint_values}.not_to raise_error
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 '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",
"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
end
end
end
end

Expand Down

0 comments on commit e22658a

Please sign in to comment.