diff --git a/.gitignore b/.gitignore index 418addeb67..8f5027b6af 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,6 @@ jetty # Coverage /coverage - - # ignore link check report /spec/support/link-check-report.txt @@ -42,6 +40,11 @@ jetty *.csv /*.xml /*.pbcore + +# Local Gemfiles Gemfile.me Gemfile.lock.me +# Local VSCode settings +.vscode + diff --git a/Dockerfile b/Dockerfile index 6754aeb771..50ffe83350 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,25 +3,66 @@ WORKDIR /usr/src/app RUN /bin/echo -e "deb http://archive.debian.org/debian stretch main\ndeb http://archive.debian.org/debian-security stretch/updates main\n" > /etc/apt/sources.list +# Install non-ruby dependencies RUN apt update && apt install -y nodejs curl libcurl3 libcurl3-openssl-dev openjdk-8-jdk -COPY Gemfile Gemfile.lock ./ +# Copy source code to container +COPY . . -RUN bundle install + +############################ +# Development Build Stage +############################ +FROM base as development + +# Set the RAILS_ENV to production. This affects several things in Rails. +ENV RAILS_ENV=development + +# Update the bundle from Gemfile to pull in any newer versions not committed to +# Gemfile.lock yet. +RUN bundle update + +# Install fresh jetty instance +RUN bundle exec rake jetty:clean EXPOSE 3000 -CMD bundle exec rake jetty:clean && bundle exec rake jetty:config && bundle exec rake jetty:start && bundle exec bundle exec rake db:migrate RAILS_ENV=development && bundle exec rails s -b 0.0.0.0 +# Run several commmands to start the development server: +# 1. bundle exec rake jetty:config +# Copies jetty configuration from config/jetty.yml to jetty instance, +# which is installed in the 'base' build stage. +# 2. bundle exec rake jetty:start +# Starts jetty server +# 3. bundle exec rake db:migrate +# Runs databae migrations, if any need to be run. +# 4. bundle exec rails s -b 0.0.0.0 +# Starts the Rails server. +CMD bundle exec rake jetty:config \ + bundle exec rake jetty:start && \ + bundle exec rake db:migrate && \ + bundle exec rails s -b 0.0.0.0 + +############################ +# Production Build Stage +############################ FROM base as production +# Set the RAILS_ENV to production. This affects several things in Rails. +ENV RAILS_ENV=production + +# TODO: is this needed? RUN apt-get autoremove -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* +# Update the bundle from Gemfile.lock (don't update the Bundle in production) +RUN bundle install -COPY . . - -RUN bundle exec rake jetty:clean && bundle exec rake jetty:config - -CMD bundle exec rake jetty:start && bundle exec rake db:migrate RAILS_ENV=development && bundle exec rails s -b 0.0.0.0 +# Run commands atomically to start production AAPB web application: +# 1. bundle exec rake jetty:start +# Starts the jetty server +# 2. bundle exec rails s -b 0.0.0.0 +# Starts the Rails server. +CMD bundle exec rake jetty:start && \ + bundle exec rails s -b 0.0.0.0 diff --git a/Gemfile b/Gemfile index 2b8bcc65f7..a6bbe03336 100644 --- a/Gemfile +++ b/Gemfile @@ -92,3 +92,4 @@ gem 'bigdecimal', '1.4.4' gem 'httparty' gem 'pbcore', '~> 0.2.0' +gem 'json-schema', '~> 2.8.0' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index bdc34a1299..403b4b5cf5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,6 +171,8 @@ GEM jquery-ui-rails (6.0.1) railties (>= 3.2.16) json (1.8.6) + json-schema (2.8.1) + addressable (>= 2.4) kaminari (1.2.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.1) @@ -371,6 +373,7 @@ DEPENDENCIES jettywrapper jquery-rails jquery-ui-rails + json-schema (~> 2.8.0) launchy libv8 (~> 3.16.14.13) maxminddb @@ -401,4 +404,4 @@ RUBY VERSION ruby 2.4.4p296 BUNDLED WITH - 1.17.3 \ No newline at end of file + 1.17.3 diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 18711a5af6..0ef9f52547 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -280,6 +280,9 @@ def show format.mods do render text: PBCorePresenter.new(xml).to_mods end + format.iiif do + render json: PBCorePresenter.new(xml).iiif_manifest + end end end diff --git a/app/models/iiif_manifest.rb b/app/models/iiif_manifest.rb new file mode 100644 index 0000000000..486f8fb0f7 --- /dev/null +++ b/app/models/iiif_manifest.rb @@ -0,0 +1,86 @@ +module IIIFManifest + def iiif_manifest + { + "@context" => "http://iiif.io/api/presentation/3/context.json", + "id" => "#{aapb_host}/#{id}.iiif", + "type" => "Manifest", + "label" => i18n_titles, + "metadata" => i18n_metadata, + "homepage" => [{ + "id" => "#{aapb_host}/catalog/#{id}", + "type" => "Text", + "label" => i18n_titles, + "format" => "text/html" }], + "summary" => i18n_descriptions, + "items" => [ + { + "id" => "#{aapb_host}/iiif/#{id}/canvas", + "type" => "Canvas", + "duration" => duration_seconds, + # height and witdth would be required for video content + "items" => [ + { + "id" => "#{aapb_host}/iiif/#{id}/annotationpage/1", + "type" => "AnnotationPage", + "items" => [ + { + "id" => "#{aapb_host}/iiif/#{id}/annotation/1", + "type" => "Annotation", + "motivation" => "painting", + "body" => { + "id" => location, # TODO: URI for media file, player will consume this and expect bits. Redirects ok. + "type" => media_type, # TODO: map to "Sound" or "Video" + "format" => media_format, + "duration" => duration_seconds # TODO: just ensure it's in seconds + }, + "target" => "#{aapb_host}/iiif/canvas/1" # IMPORTANT: this has to be the ame as the 'id' property of the parent canvas + } + ] + } + ] + } + ] + }.to_json + end + + def i18n_titles + titles.map { |title| { "en" => [title] } } + end + + def i18n_descriptions + descriptions.map { |description| { "en" => [description] } } + end + + def i18n_metadata + metadata.map do |key, value| + { + "label" => { + "en" => [key] + }, + "value" => { + "en" => [value] + } + } + end + end + + def metadata + { 'id' => id } + end + + def duration_seconds + duration.to_s.split(":").map(&:to_f).inject(0) { |a, e| a * 60 + e }.round(3) + end + + def location + URI.join(aapb_host, 'media', id).to_s + end + + def aapb_host + 'https://americanarchive.org' + end + + def media_format + digital_instantiations.map(&:format).compact.first + end +end diff --git a/app/models/pb_core_instantiation.rb b/app/models/pb_core_instantiation.rb index 7a6f39d144..f3accd10e7 100644 --- a/app/models/pb_core_instantiation.rb +++ b/app/models/pb_core_instantiation.rb @@ -60,7 +60,7 @@ def colors end def format - @format ||= read_format + @format ||= (digital || physical) end def annotations @@ -71,6 +71,14 @@ def display_text_fields @display_text ||= { identifier: identifier_display, format: format, generation: generations, color: colors, duration: duration }.compact end + def digital + optional('instantiationDigital') + end + + def physical + optional('instantiationPhysical') + end + private def optional(xpath) @@ -91,10 +99,4 @@ def optional_element_attribute(xpath, attribute) match = REXML::XPath.match(@rexml, xpath).first.attributes[attribute.to_s] match ? match : nil end - - def read_format - return optional('instantiationDigital') unless optional('instantiationDigital').nil? - return optional('instantiationPhysical') unless optional('instantiationPhysical').nil? - nil - end end diff --git a/app/models/pb_core_presenter.rb b/app/models/pb_core_presenter.rb index 3bd1f3327e..c94321132b 100644 --- a/app/models/pb_core_presenter.rb +++ b/app/models/pb_core_presenter.rb @@ -10,6 +10,7 @@ require_relative '../../lib/html_scrubber' require_relative 'xml_backed' require_relative 'to_mods' +require_relative 'iiif_manifest' require_relative 'pb_core_instantiation' require_relative 'pb_core_name_role_affiliation' require_relative 'organization' @@ -25,6 +26,7 @@ class PBCorePresenter # rubocop:disable Style/EmptyLineBetweenDefs include XmlBacked include ToMods + include IIIFManifest include ApplicationHelper include IdHelper @@ -92,6 +94,15 @@ def instantiations PBCoreInstantiation.new(rexml) end end + + def digital_instantiations + instantiations.select(&:digital) + end + + def physical_instantiations + instantiations.select(&:physical) + end + def instantiations_display @instantiations_display ||= instantiations.reject { |ins| ins.organization == 'American Archive of Public Broadcasting' } end @@ -555,7 +566,7 @@ def text :text, :to_solr, :contribs, :img_src, :media_srcs, :captions_src, :transcript_src, :rights_code, :access_level, :access_types, :title, :ci_ids, :display_ids, - :instantiations, :outside_urls, + :instantiations, :digital_instantiations, :physical_instantiations, :digital, :physical, :outside_urls, :reference_urls, :exhibits, :top_exhibits, :special_collections, :access_level_description, :img_height, :img_width, :player_aspect_ratio, :seconds, :player_specs, :transcript_status, :transcript_content, :constructed_transcript_src, :verify_transcript_src, diff --git a/config/application.rb b/config/application.rb index cea5f4489d..d76848c61a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,7 +22,6 @@ class Application < Rails::Application config.autoload_paths << Rails.root.join('lib') config.autoload_paths << Rails.root.join('lib', 'middleware') - config.middleware.use('RedirectMiddleware') config.middleware.insert_before(0, 'Rack::Cors') do allow do origins '*' diff --git a/config/environments/production.rb b/config/environments/production.rb index d1fbde082a..30ec3acb29 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -51,7 +51,7 @@ # config.log_tags = [ :subdomain, :uuid ] # Only keep up to 10 log files of ~ 1MB each. - config.logger = ActiveSupport::Logger.new('/var/www/aapb/current/log/production.log', 10, 1.megabytes) + config.logger = ActiveSupport::Logger.new(File.expand_path('../../../log/production.log', __FILE__), 10, 1.megabytes) # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -82,4 +82,6 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + config.middleware.use('RedirectMiddleware') end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 4ceae3f630..f21e191271 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -8,3 +8,4 @@ Mime::Type.register 'text/plain', :txt Mime::Type.register 'text/plain', :srt Mime::Type.register 'text/vtt', :vtt +Mime::Type.register 'application/json', :iiif diff --git a/lib/iiif_3_0.json b/lib/iiif_3_0.json new file mode 100644 index 0000000000..ea304cd0d7 --- /dev/null +++ b/lib/iiif_3_0.json @@ -0,0 +1,1147 @@ +{ + "$schema": "http://json-schema.org/schema#", + "$comment": "IIIF basic types", + "types": { + "id": { + "type": "string", + "format": "uri", + "pattern": "^http.*$", + "title": "Id must be present and must be a URI" + }, + "lngString": { + "title": "Language string, must have a language and value must be an array.", + "type": "object", + "patternProperties": { + "^[a-zA-Z-][a-zA-Z-]*$": { + "type": "array", + "items": { "type": "string"} + }, + "^none$": { + "type": "array", + "items": { "type": "string"} + } + }, + "additionalProperties": false + }, + "dimension": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "keyValueString": { + "type": "object", + "properties": { + "label": {"$ref": "#/types/lngString" }, + "value": {"$ref": "#/types/lngString" } + }, + "required": ["label", "value"] + }, + "BCP47": { + "anyOf": [ + { + "type":"string", + "pattern": "^[a-zA-Z-][a-zA-Z-]*$" + }, + { + "type":"string", + "pattern": "^none$" + } + ] + }, + "format": { + "type": "string", + "pattern": "^[a-z][a-z]*/.*$" + }, + "class": { + "title": "Classes MUST have an id and type property and MAY have a label.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { "type": "string" }, + "label": { "$ref": "#/types/lngString" } + }, + "required": ["id", "type"] + }, + "duration": { + "type": "number", + "exclusiveMinimum": 0 + }, + "external": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "format": { "$ref": "#/types/format" }, + "profile": { + "type": "string" + } + } + } + ] + } + }, + "reference": { + "type": "object", + "additionalProperties": true, + "title": "Reference", + "description":"Id. type, label but not items are required", + "properties": { + "id": { "$ref": "#/types/id" }, + "label": {"$ref": "#/types/lngString" }, + "type": { + "type": "string", + "pattern": "^Manifest$|^AnnotationPage$|^Collection$|^AnnotationCollection$|^Canvas$|^Range$" + }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + } + }, + "required": ["id", "type"], + "not": { "required": [ "items" ] } + } + }, + + "$comment": "IIIF Classes", + "classes": { + "metadata": { + "type": "array", + "items": { + "$ref": "#/types/keyValueString" + } + }, + "homepage": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "format": { "$ref": "#/types/format" }, + "language": { + "type": "array", + "items": { "$ref": "#/types/BCP47" } + } + } + } + ] + } + }, + "seeAlso": { + "$ref": "#/types/external" + }, + "partOf": { + "type": "array", + "items": { + "$ref": "#/types/class" + } + }, + "choice": { + "type": "object", + "properties":{ + "type": { + "type": "string", + "const": "Choice" + }, + "items":{ + "type": "array" + } + }, + "required": ["type", "items"] + }, + "resource": { + "oneOf": [ + { + "title": "Annotation bodies MUST have an id and type property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "language": { "type": "string"}, + "rendering": { "$ref": "#/types/external" }, + "service": { "$ref": "#/classes/service" }, + "format": { "$ref": "#/types/format" }, + "label": {"$ref": "#/types/lngString" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + } + }, + "required": ["id", "type"] + }, + { + "title": "Annotation bodies which are TextualBody MUST have an type and value property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^TextualBody$", + "default": "TextualBody" + }, + "value": { "type": "string" }, + "format": { "$ref": "#/types/format" }, + "language": { "type": "string"} + }, + "required": ["value", "type"] + } + ] + }, + "imgSvr": { + "allOf": [ + { "$ref": "#/classes/service" }, + { + "properties": { + "profile": { "type": "string" }, + "@id": { "$ref": "#/types/id" }, + "@type": { "type": "string" } + } + } + ] + }, + "service": { + "type": "array", + "items": { + "oneOf": [ + { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "profile": { "type": "string" }, + "service": { "$ref": "#/classes/service" } + } + } + ] + }, + { + "type": "object", + "properties": { + "@id": { "$ref": "#/types/id" }, + "@type": { "type": "string" }, + "profile": { "type": "string" }, + "service": { "$ref": "#/classes/service" } + }, + "required": ["@id", "@type"] + } + ] + } + }, + "rights": { + "title": "Rights URI isn't from either Creative Commons or RightsStatements.org. Both require http links.", + "oneOf": [ + { + "type": "string", + "format": "uri", + "pattern": "http://creativecommons.org/licenses/.*" + }, + { + "type": "string", + "format": "uri", + "pattern": "http://creativecommons.org/publicdomain/.*" + }, + { + "type": "string", + "format": "uri", + "pattern": "http://rightsstatements.org/vocab/.*" + } + ] + }, + "navDate": { + "type": "string", + "format": "date-time" + }, + "navPlace": { + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "default": "FeatureCollection" + }, + "features": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": ["type"] + }, + "viewingDirection": { + "anyOf": [ + { + "type": "string", + "pattern": "^left-to-right$" + }, + { + "type": "string", + "pattern": "^right-to-left$" + }, + { + "type": "string", + "pattern": "^top-to-bottom$" + }, + { + "type": "string", + "pattern": "^bottom-to-top$" + } + ] + }, + "behavior": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "pattern": "^auto-advance$" + }, + { + "type": "string", + "pattern": "^no-auto-advance$" + }, + { + "type": "string", + "pattern": "^repeat$" + }, + { + "type": "string", + "pattern": "^no-repeat$" + }, + { + "type": "string", + "pattern": "^unordered$" + }, + { + "type": "string", + "pattern": "^individuals$" + }, + { + "type": "string", + "pattern": "^continuous$" + }, + { + "type": "string", + "pattern": "^paged$" + }, + { + "type": "string", + "pattern": "^facing-pages$" + }, + { + "type": "string", + "pattern": "^non-paged$" + }, + { + "type": "string", + "pattern": "^multi-part$" + }, + { + "type": "string", + "pattern": "^together$" + }, + { + "type": "string", + "pattern": "^sequence$" + }, + { + "type": "string", + "pattern": "^thumbnail-nav$" + }, + { + "type": "string", + "pattern": "^no-nav$" + }, + { + "type": "string", + "pattern": "^hidden$" + } + ] + } + }, + "provider": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Agent$", + "default": "Agent" + }, + "homepage": { "$ref": "#/classes/homepage" }, + "logo": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "seeAlso": { "$ref": "#/classes/seeAlso" } + } + } + ] + } + }, + "collection": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Collection", + "default": "Collection", + "title": "Are you validating a collection?", + "description":"If you are validating a manifest, you may get this error if there are errors in the manifest. The validator first validates it as a manifest and if that fails it will try and validate it using the other types." + }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "services": { "$ref": "#/classes/service" }, + "service": { "$ref": "#/classes/service" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/classes/manifestRef" }, + { "$ref": "#/classes/collectionRef" }, + { "$ref": "#/classes/collection" } + ] + } + }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + } + }, + "required": ["id", "type", "label"] + } + ] + }, + "manifest": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + } + }, + { + "type": "string", + "const": "http://iiif.io/api/presentation/3/context.json" + } + ] + }, + "id": { "$ref": "#/types/id" }, + "label": {"$ref": "#/types/lngString" }, + "type": { + "type": "string", + "pattern": "^Manifest", + "default": "Manifest" + }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "service": { "$ref": "#/classes/service" }, + "services": { "$ref": "#/classes/service" }, + "viewingDirection": { "$ref": "#/classes/viewingDirection" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, + "rights": { "$ref": "#/classes/rights" }, + "start": {}, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/canvas" + } + }, + "structures": { + "type": "array", + "items": { + "$ref": "#/classes/range" + } + }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + } + }, + "required": ["id", "type", "label"] + } + ] + }, + "manifestRef": { + "allOf": [ + { "$ref": "#/types/reference" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Manifest$", + "default": "Manifest" + }, + "label": {"$ref": "#/types/lngString" } + }, + "required":["label"] + } + ] + }, + "collectionRef": { + "allOf": [ + { "$ref": "#/types/reference" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Collection$" + }, + "label": {"$ref": "#/types/lngString" } + }, + "required":["label"] + } + ] + }, + "rangeRef": { + "allOf": [ + { "$ref": "#/types/reference" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Range$" + } + } + } + ] + }, + "canvasRef": { + "allOf": [ + { "$ref": "#/types/reference" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$" + } + } + } + ] + }, + "annotationPageRef": { + "allOf": [ + { "$ref": "#/types/reference" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^AnnotationPage$" + } + } + } + ] + }, + "canvas": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$", + "default": "Canvas" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "service": { "$ref": "#/classes/service" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + } + }, + "required": ["items"], + "anyOf":[ + { "required": ["width"] }, + { "required": ["height"] }, + { "required": ["duration"] } + ], + "dependencies": { + "width": ["height"], + "height": ["width"] + } + } + ] + }, + "placeholderCanvas": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$", + "default": "Canvas" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + } + }, + "required": ["items"], + "anyOf":[ + { "required": ["width"] }, + { "required": ["height"] }, + { "required": ["duration"] } + ], + "dependencies": { + "width": ["height"], + "height": ["width"] + }, + "not": { "required": [ "placeholderCanvas", "accompanyingCanvas" ] } + } + ] + }, + "accompanyingCanvas": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$", + "default": "Canvas" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + } + }, + "required": ["items"], + "anyOf":[ + { "required": ["width"] }, + { "required": ["height"] }, + { "required": ["duration"] } + ], + "dependencies": { + "width": ["height"], + "height": ["width"] + }, + "not": { "required": [ "placeholderCanvas", "accompanyingCanvas" ] } + } + ] + }, + "annotationCollection": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^AnnotationCollection$", + "default": "AnnotationCollection" + }, + "rendering": { "$ref": "#/types/external" }, + "partOf": { "$ref": "#/classes/partOf" }, + "next": { "$ref": "#/classes/annotationPageRef" }, + "first": { "$ref": "#/classes/annotationPageRef" }, + "last": { "$ref": "#/classes/annotationPageRef" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotation" + } + } + } + } + ] + }, + "annotationPage": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "title": "AnnotationPage", + "description":"id, type and items required", + "properties": { + "@context": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + } + }, + { + "type": "string", + "const": "http://iiif.io/api/presentation/3/context.json" + } + ] + }, + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^AnnotationPage$", + "default": "AnnotationPage" + }, + "rendering": { "$ref": "#/types/external" }, + "label": {"$ref": "#/types/lngString" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotation" + } + } + }, + "required": ["id","type","items"], + "additionalProperties": false + } + ] + }, + "annotation": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Annotation$", + "default": "Annotation" + }, + "service": { "$ref": "#/classes/service" }, + "rendering": { "$ref": "#/types/external" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "motivation": { + "oneOf": [ + { "type": "string" }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object", + "$ref": "#/classes/resource" + }, + { + "type": "object", + "allOf":[ + { "$ref": "#/classes/choice" }, + { + "properties": { + "items": { + "type": "array", + "items": {"$ref": "#/classes/resource"} + } + }, + "required": ["items"] + } + ] + }, + { + "type": "array", + "items": { + "type": "object" + } + } + + ] + }, + "target": { + "anyOf": [ + { "$ref": "#/classes/annoTarget" }, + { + "type": "array", + "items": { + "$ref": "#/classes/annoTarget" + } + } + ] + } + }, + "required": ["id", "target", "type"] + } + ] + }, + "annoTarget": { + "oneOf": [ + { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + }, + { + "$ref": "#/classes/specificResource" + }, + { + "title": "Annotation target can also contain just scope and source", + "type": "object", + "properties": { + "source": { "$ref": "#/types/id" }, + "scope": { "$ref": "#/types/id" } + }, + "required": ["source", "scope"] + } + ] + }, + "specificResource": { + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^SpecificResource$", + "default": "SpecificResource" + }, + "format": { "$ref": "#/types/format" }, + "accessibility": { "type": "string"}, + "source": { + "oneOf": [ + { "$ref": "#/types/id" }, + { "$ref": "#/types/class" } + ] + }, + "selector": { + "oneOf": [ + { "$ref": "#/classes/selector" }, + { + "type": "array", + "items": { + "$ref": "#/classes/selector" + } + } + ] + } + }, + "required": ["source", "selector"] + }, + "selector": { + "oneOf": [ + { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^PointSelector$", + "default": "PointSelector" + }, + "t": { "$ref": "#/types/duration" }, + "x": { "$ref": "#/types/dimension" }, + "y": { "$ref": "#/types/dimension" } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^FragmentSelector$", + "default": "FragmentSelector" + }, + "conformsTo": { + "type": "string", + "format": "uri", + "pattern": "^http.*$", + "default": "http://www.w3.org/TR/media-frags/" + }, + "value": { + "type:": "string" + } + }, + "required": ["type","value"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^SvgSelector$", + "default": "SvgSelector" + }, + "value": { + "type:": "string" + } + }, + "required": ["type","value"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^ImageApiSelector$", + "default": "ImageApiSelector" + }, + "region": { "type:": "string" }, + "size": { "type:": "string" }, + "rotation": { "type:": "string" }, + "quality": { "type:": "string" }, + "format": { "type:": "string" } + }, + "required": ["type"] + } + ] + }, + "range": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Range$", + "default": "Range" + }, + "rendering": { "$ref": "#/types/external" }, + "supplementary": { "$ref": "#/classes/annotationCollection" }, + "service": { "$ref": "#/classes/service" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, + "annotations": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] + } + }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/classes/specificResource" }, + { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$", + "default": "Canvas" + }, + "items": { + "type": "array" + } + }, + "required": ["items"] + } + ] + }, + { "$ref": "#/classes/range" }, + { "$ref": "#/classes/canvasRef" } + ] + } + } + }, + "required":["items"] + } + ] + } + }, + "$id": "http://iiif.io/api/presentation/3/schema.json" , + "oneOf": [ + { "$ref": "#/classes/manifest" }, + { "$ref": "#/classes/collection" }, + { "$ref": "#/classes/annotationPage" } + ] +} diff --git a/scripts/lib/pb_core_ingester.rb b/scripts/lib/pb_core_ingester.rb index 4973490794..a7e6e358e7 100644 --- a/scripts/lib/pb_core_ingester.rb +++ b/scripts/lib/pb_core_ingester.rb @@ -31,6 +31,7 @@ def self.load_fixtures(*globs) globs << 'spec/fixtures/pbcore/clean-*.xml' if globs.empty? # Get a list of all file paths from all the globs. all_paths = globs.map { |glob| Dir[glob] }.flatten.uniq + all_paths.each do |path| ingester.ingest(path: path) end diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb index fa016e22b1..cc61d40416 100644 --- a/spec/controllers/catalog_controller_spec.rb +++ b/spec/controllers/catalog_controller_spec.rb @@ -101,6 +101,19 @@ end end end + + context 'when adding .iiif as a response format' do + before { get :show, id: id, format: 'iiif' } + let(:id) { 'cpb-aacip-114-90dv49m9' } + let(:response_hash) { JSON.parse(response.body) } + + it 'returns a IIIF manifest for a given record' do + expect(response.status).to eq 200 + expect(response.content_type).to eq 'application/json' + expect(response_hash['@context']).to eq "http://iiif.io/api/presentation/3/context.json" + expect(response_hash['id']).to match(/#{id}\.iiif\Z/) + end + end end end end