From f3d6f0281bd2123d7b8047069312fba9e777a09b Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Sun, 7 Jun 2020 20:05:37 +0100 Subject: [PATCH] Added presentation 3 validator --- packages/validator/package.json | 29 + packages/validator/rollup.config.js | 4 + packages/validator/src/index.ts | 1 + .../validator/src/schemas/presentation-3.json | 736 ++++++++++++++++++ packages/validator/src/validator.ts | 49 ++ packages/validator/tsconfig.json | 48 ++ packages/validator/tsconfig.types.json | 19 + 7 files changed, 886 insertions(+) create mode 100644 packages/validator/package.json create mode 100644 packages/validator/rollup.config.js create mode 100644 packages/validator/src/index.ts create mode 100644 packages/validator/src/schemas/presentation-3.json create mode 100644 packages/validator/src/validator.ts create mode 100644 packages/validator/tsconfig.json create mode 100644 packages/validator/tsconfig.types.json diff --git a/packages/validator/package.json b/packages/validator/package.json new file mode 100644 index 00000000..9d74733e --- /dev/null +++ b/packages/validator/package.json @@ -0,0 +1,29 @@ +{ + "name": "@hyperion-framework/validator", + "version": "1.0.0-alpha.3", + "main": "dist/index.js", + "module": "dist/index.es.js", + "web": "dist/index.umd.js", + "typings": "dist/types/index.d.ts", + "author": "Stephen Fraser ", + "license": "MIT", + "files": [ + "dist" + ], + "scripts": { + "build": "rollup -c && tsc -p ./tsconfig.types.json", + "start": "rollup -c --watch", + "test": "jest", + "build-types": "tsc -p ./tsconfig.types.json", + "prepare": "NODE_ENV=production rollup -c && tsc -p ./tsconfig.types.json" + }, + "dependencies": { + "ajv": "6.12.2" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "typescript": "^3.9.5" + } +} diff --git a/packages/validator/rollup.config.js b/packages/validator/rollup.config.js new file mode 100644 index 00000000..a81760bf --- /dev/null +++ b/packages/validator/rollup.config.js @@ -0,0 +1,4 @@ +import { createRollupConfig } from '../../create-rollup-config'; +import pkg from './package.json'; + +export default createRollupConfig('HyperionParser', pkg); diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts new file mode 100644 index 00000000..80bf090f --- /dev/null +++ b/packages/validator/src/index.ts @@ -0,0 +1 @@ +export * from './validator'; diff --git a/packages/validator/src/schemas/presentation-3.json b/packages/validator/src/schemas/presentation-3.json new file mode 100644 index 00000000..1714ef4d --- /dev/null +++ b/packages/validator/src/schemas/presentation-3.json @@ -0,0 +1,736 @@ +{ + "$schema": "http://json-schema.org/schema#", + "$comment": "IIIF basic types", + "types": { + "id": { + "type": "string", + "format": "uri", + "pattern": "^http.*$", + "title": "Id must be presesnt and must be a URI" + }, + "lngString": { + "$id": "#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 + }, + "keyValueString": { + "$id": "#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", + "minimum": 0 + }, + "external": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "format": { "$ref": "#/types/format" }, + "profile": { + "type": "string" + } + } + } + ] + } + } + }, + + "$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": { + "type": "integer" + }, + "width": { + "type": "integer" + }, + "duration": { + "$ref": "#/types/duration" + }, + "language": { "type": "string"}, + "service": { "$ref": "#/classes/service" }, + "format": { "$ref": "#/types/format" }, + "label": {"$ref": "#/types/lngString" } + }, + "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$" + }, + "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/mark/.*" + }, + { + "type": "string", + "format": "uri", + "pattern": "http://rightsstatements.org/vocab/.*" + } + ] + }, + "navDate": { + "type": "string", + "format": "date-time" + }, + "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$" + }, + "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", + "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" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "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": { + "oneOf": [ + { "$ref": "#/classes/manifest" }, + { "$ref": "#/classes/collection" } + ] + } + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + } + }, + "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": {}, + "label": {}, + "type": { + "type": "string", + "pattern": "^Manifest" + }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "service": { "$ref": "#/classes/service" }, + "viewingDirection": { "$ref": "#/classes/viewingDirection" }, + "rights": { "$ref": "#/classes/rights" }, + "start": {}, + "logo": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "navDate": { "$ref": "#/classes/navDate" }, + "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": { + "$ref": "#/classes/annotationPage" + } + } + } + } + ] + }, + "canvas": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + }, + "duration": { + "$ref": "#/types/duration" + }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "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/annotationPage" + } + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + } + }, + "anyOf":[ + { "required": ["width"] }, + { "required": ["height"] }, + { "required": ["duration"] } + ], + "dependencies": { + "width": ["height"], + "height": ["width"] + } + } + ] + }, + "annotationCollection": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^AnnotationCollection$" + }, + "partOf": { "$ref": "#/classes/partOf" }, + "next": { "$ref": "#/classes/annotationPage" }, + "first": { "$ref": "#/classes/annotationPage" }, + "last": { "$ref": "#/classes/annotationPage" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotation" + } + } + } + } + ] + }, + "annotationPage": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "@context": {}, + "type": { + "type": "string", + "pattern": "^AnnotationPage$" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotation" + } + } + }, + "additionalProperties": false + } + ] + }, + "annotation": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Annotation$" + }, + "motivation": { + "oneOf": [ + { "type": "string" }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "body": { + "oneOf": [ + { + "$ref": "#/classes/resource" + }, + { + "allOf":[ + { "$ref": "#/classes/choice" }, + { + "properties": { + "items": { + "type": "array", + "items": {"$ref": "#/classes/resource"} + } + }, + "required": ["items"] + } + ] + } + ] + }, + "target": { + "oneOf": [ + { "$ref": "#/classes/annoTarget" }, + { + "type": "array", + "items": { + "$ref": "#/classes/annoTarget" + } + } + ] + } + }, + "required": ["target", "type"] + } + ] + }, + "annoTarget": { + "oneOf": [ + { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + }, + { + "$ref": "#/classes/specificResource" + } + ] + }, + "specificResource": { + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^SpecificResource$" + }, + "format": { "$ref": "#/types/format" }, + "accessibility": { "type": "string"}, + "source": { "$ref": "#/types/id" }, + "selector": { "$ref": "#/classes/annoSelector" } + }, + "required": ["source", "selector"] + }, + "annoSelector": { + "oneOf": [ + { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + }, + { + "type": "object", + "properties": { + "type": { "type": "string" }, + "t": { "$ref": "#/types/duration" } + }, + "required": ["type"] + } + ] + }, + "range": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Range$" + }, + "supplementary": { "$ref": "#/classes/annotationCollection" }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/classes/specificResource" + }, + { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$" + } + } + + } + ] + }, + { + "$ref": "#/classes/range" + } + ] + } + } + } + } + ] + } + }, + "$id": "http://iiif.io/api/presentation/3/schema.json" , + "allOf": [ + { "$ref": "#/types/class" }, + { + "oneOf": [ + { "$ref": "#/classes/manifest" }, + { "$ref": "#/classes/collection" }, + { "$ref": "#/classes/annotationPage" } + ] + } + ] +} diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts new file mode 100644 index 00000000..2ef4914c --- /dev/null +++ b/packages/validator/src/validator.ts @@ -0,0 +1,49 @@ +import Ajv, { ValidateFunction } from 'ajv'; +import schema from './schemas/presentation-3.json'; + +const schemaForType = (type: string) => ({ + $id: `http://iiif.io/api/presentation/3/${type}.json`, + $ref: `schema.json#/classes/${type}`, +}); + +export class Validator { + ajv: Ajv.Ajv; + validators: { [key: string]: ValidateFunction }; + constructor(options: Ajv.Options = {}) { + this.ajv = new Ajv({ + logger: false, + schemas: [schema, schemaForType('collection'), schemaForType('manifest'), schemaForType('annotationPage')], + ...options, + }); + this.validators = { + all: this.ajv.getSchema('http://iiif.io/api/presentation/3/schema.json') as ValidateFunction, + manifest: this.ajv.getSchema('http://iiif.io/api/presentation/3/manifest.json') as ValidateFunction, + collection: this.ajv.getSchema('http://iiif.io/api/presentation/3/collection.json') as ValidateFunction, + annotationPage: this.ajv.getSchema('http://iiif.io/api/presentation/3/annotationPage.json') as ValidateFunction, + }; + } + + validate(data: any) { + return this.validators.all(data); + } + + validateManifest(data: any) { + return this.validators.manifest(data); + } + + validateCollection(data: any) { + return this.validators.collection(data); + } + + validateAnnotationPage(data: any) { + return this.validators.annotationPage(data); + } + + validateCustom(type: string, data: any) { + if (!this.validators[type]) { + this.ajv.addSchema(schemaForType(type)); + this.validators[type] = this.ajv.getSchema(`http://iiif.io/api/presentation/3/${type}.json`) as ValidateFunction; + } + return this.validators[type](data); + } +} diff --git a/packages/validator/tsconfig.json b/packages/validator/tsconfig.json new file mode 100644 index 00000000..f06909ee --- /dev/null +++ b/packages/validator/tsconfig.json @@ -0,0 +1,48 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "allowJs": false, + "checkJs": false, + "declaration": false, + "downlevelIteration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": true, + "skipLibCheck": true, + "noEmitHelpers": true, + "lib": [ + "dom", + "esnext" + ], + "jsx": "react", + "target": "es2015", + "module": "es2015", + "moduleResolution": "node", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "traceResolution": false, + "strict": true, + "strictNullChecks": true, + "pretty": false, + "removeComments": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": [ + "./src/**/*", + "./src/*", + "./demos/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "lib" + ] +} diff --git a/packages/validator/tsconfig.types.json b/packages/validator/tsconfig.types.json new file mode 100644 index 00000000..c43ac8fb --- /dev/null +++ b/packages/validator/tsconfig.types.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/types", + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "./src" + }, + "include": [ + "./src/**/*", + "./src/*" + ], + "exclude": [ + "node_modules", + "dist", + "lib", + "src/__tests__" + ] +}