schemaorg-jsd
Version:
JSON Schema validation for JSON-LD files using Schema.org vocabulary.
140 lines (130 loc) • 5.33 kB
text/typescript
import * as fs from 'fs'
import * as https from 'https'
import * as path from 'path'
import Ajv, * as AJV from 'ajv';
import type {JSONSchema7, JSONSchema4} from 'json-schema'
import type {NodeObject} from 'jsonld';
import {requireJSON} from '@chharvey/requirejson';
import type {SDODatatypeSchema, SDOClassSchema, SDOPropertySchema} from './meta-schemata.d'
/**
* An array of meta-schemata against which the content schemata validate.
*
* This is for internal use only. Users should not be expected to use these meta-schemata.
*/
export const META_SCHEMATA: Promise<JSONSchema7[]> =
fs.promises.readdir(path.resolve(__dirname, '../meta/')).then((filenames) =>
Promise.all(filenames.map((filename) =>
requireJSON(path.join(__dirname, '../meta/', filename)) as Promise<JSONSchema7>
))
)
/**
* A single JSON Schema, which validates JSON-LD objects.
* @see https://json-ld.org/schemas/jsonld-schema.json
*/
export const JSONLD_SCHEMA: Promise<JSONSchema7> = new Promise((resolve, reject) => {
https.get('https://cdn.jsdelivr.net/gh/json-ld/json-ld.org@1.0/schemas/jsonld-schema.json', (res) => {
// if failing to load page, reject
if (!res.statusCode || res.statusCode < 200 || 300 <= res.statusCode) {
reject(new Error(`
Failed to load.
Status Code: ${res.statusCode || 'no status code found'}
`.replace(/\n\t\t\t\t/g, '\n')))
res.resume()
return;
}
res.setEncoding('utf8')
const body: string[] = []
res.on('data', (chunk) => { body.push(chunk) })
res.on('end', () => {
let data: JSONSchema4;
// if failing to parse contents as json, reject
try {
data = JSON.parse(body.join(''))
} catch (err) {
reject(err)
return;
}
data.$schema = 'http://json-schema.org/draft-07/schema#'
data.$id = 'https://json-ld.org/schemas/jsonld-schema.json'
resolve(data as JSONSchema7)
})
}).on('error', (err) => { reject(err) }) // if failing to get url, reject
})
/**
* An array of all JSON Schemata validating Schema.org vocabulary.
*
* This array contains all Schema.org schemata in this project.
* That is, schemata against which your JSON-LD documents should validate.
*/
export const SCHEMATA: Promise<(SDODatatypeSchema | SDOClassSchema | SDOPropertySchema)[]> =
fs.promises.readdir(path.resolve(__dirname, '../schema/')).then((filenames) =>
Promise.all(filenames.map((filename) =>
requireJSON(path.join(__dirname, '../schema/', filename)) as Promise<JSONSchema7> as Promise<SDODatatypeSchema | SDOClassSchema | SDOPropertySchema>
))
)
/**
* Validate a JSON-LD object against a Schema.org JSON schema.
*
* ```js
* const { sdoValidate } = require('schemaorg-jsd')
* async function compile(ldobj) {
* let is_valid;
* try {
* is_valid = await sdoValidate(ldobj)
* } catch (err) {
* is_valid = false
* }
* console.log(is_valid)
* }
* // or you could use its Promise (if `async`/`await` is not supported):
* function compilePromise(ldobj) {
* sdoValidate(ldobj)
* .catch((err) => false)
* .then((result) => { console.log(result) })
* }
* ```
*
* @param obj the JSON or JSON-LD object to test, or its path pointing to a `.json` or `.jsonld` file
* @param type the name of the Type to test against; should be a Class in http://schema.org/
* - see the API for supported Types
* - if omitted, will test against the JSON document’s `'@type'` property (if it has one)
* - if `'@type'` is an array, each value of that array is tested
* - if the `'@type'` is not supported or cannot be found, defaults to `'Thing'`
* @param opts options object to pass to the `new Ajv()` constructor
* @returns does the object pass validation?
* @throws {TypeError} if the object fails validation; has a `.details` property for validation details
*/
export async function sdoValidate(obj: NodeObject | string, type: string | null = null, opts: AJV.Options = {}): Promise<true> {
let filename: string = ''
if (typeof obj === 'string') {
filename = obj
obj = await requireJSON(obj) as NodeObject;
}
if (type === null) {
const objtype: string[]|string|null = obj['@type'] || null
if (objtype instanceof Array && objtype.length) {
return (await Promise.all(objtype.map((tp) => sdoValidate(obj, tp)))).every((a) => !!a) as true
} else if (typeof objtype === 'string') {
type = ((await SCHEMATA).find((jsd) => jsd.title === `http://schema.org/${objtype}`)) ? objtype :
(console.warn(`Class \`${objtype}\` is not yet supported. Validating against \`Thing.jsd\` instead…`), 'Thing')
} else {
console.warn(`JSON-LD \`@type\` property was not found. Validating against \`Thing.jsd\`…`)
type = 'Thing'
}
}
const ajv: Ajv = new Ajv(opts)
.addMetaSchema(await META_SCHEMATA)
.addSchema(await JSONLD_SCHEMA)
.addSchema(await SCHEMATA)
const is_data_valid: boolean = await ajv.validate(`https://chharvey.github.io/schemaorg-jsd/schema/${type}.jsd`, obj) as boolean
if (!is_data_valid) {
const err: TypeError&{
filename? : string;
details? : AJV.ErrorObject[];
} = new TypeError(`Object ${obj['@id'] || obj.identifier || obj.name || JSON.stringify(obj)} does not valiate against schema ${type}.jsd!`)
if (filename.length) err.filename = filename
err.details = ajv.errors !
throw err
}
return true
}