diff --git a/lib/compile/index.ts b/lib/compile/index.ts index ebdbe3c78..3dac2699b 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -112,7 +112,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { // TODO refactor - remove compilations const _sch = getCompilingSchema.call(this, sch) if (_sch) return _sch - const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails + const rootId = getFullPath(this.opts.uriResolver, sch.root.baseId) // TODO if getFullPath removed 1 tests fails const {es5, lines} = this.opts.code const {ownProperties} = this.opts const gen = new CodeGen(this.scope, {es5, lines, ownProperties}) @@ -208,7 +208,7 @@ export function resolveRef( baseId: string, ref: string ): AnySchema | SchemaEnv | undefined { - ref = resolveUrl(baseId, ref) + ref = resolveUrl(this.opts.uriResolver, baseId, ref) const schOrFunc = root.refs[ref] if (schOrFunc) return schOrFunc @@ -257,9 +257,9 @@ export function resolveSchema( root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it ref: string // reference to resolve ): SchemaEnv | undefined { - const p = URI.parse(ref) - const refPath = _getFullPath(p) - let baseId = getFullPath(root.baseId) + const p = this.opts.uriResolver.parse(ref) + const refPath = _getFullPath(this.opts.uriResolver, p) + let baseId = getFullPath(this.opts.uriResolver, root.baseId, undefined) // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests if (Object.keys(root.schema).length > 0 && refPath === baseId) { return getJsonPointer.call(this, p, root) @@ -279,7 +279,7 @@ export function resolveSchema( const {schema} = schOrRef const {schemaId} = this.opts const schId = schema[schemaId] - if (schId) baseId = resolveUrl(baseId, schId) + if (schId) baseId = resolveUrl(this.opts.uriResolver, baseId, schId) return new SchemaEnv({schema, schemaId, root, baseId}) } return getJsonPointer.call(this, p, schOrRef) @@ -307,12 +307,12 @@ function getJsonPointer( // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def? const schId = typeof schema === "object" && schema[this.opts.schemaId] if (!PREVENT_SCOPE_CHANGE.has(part) && schId) { - baseId = resolveUrl(baseId, schId) + baseId = resolveUrl(this.opts.uriResolver, baseId, schId) } } let env: SchemaEnv | undefined if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { - const $ref = resolveUrl(baseId, schema.$ref) + const $ref = resolveUrl(this.opts.uriResolver, baseId, schema.$ref) env = resolveSchema.call(this, root, $ref) } // even though resolution failed we need to return SchemaEnv to throw exception diff --git a/lib/compile/jtd/parse.ts b/lib/compile/jtd/parse.ts index cd2611e70..a0141c770 100644 --- a/lib/compile/jtd/parse.ts +++ b/lib/compile/jtd/parse.ts @@ -342,7 +342,7 @@ function parseRef(cxt: ParseCxt): void { const {gen, self, definitions, schema, schemaEnv} = cxt const {ref} = schema const refSchema = definitions[ref] - if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`) + if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`) if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema}) const {root} = schemaEnv const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions) diff --git a/lib/compile/jtd/serialize.ts b/lib/compile/jtd/serialize.ts index a8b36bd88..7ebd26acb 100644 --- a/lib/compile/jtd/serialize.ts +++ b/lib/compile/jtd/serialize.ts @@ -234,7 +234,7 @@ function serializeRef(cxt: SerializeCxt): void { const {gen, self, data, definitions, schema, schemaEnv} = cxt const {ref} = schema const refSchema = definitions[ref] - if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`) + if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`) if (!hasRef(refSchema)) return serializeCode({...cxt, schema: refSchema}) const {root} = schemaEnv const sch = compileSerializer.call(self, new SchemaEnv({schema: refSchema, root}), definitions) diff --git a/lib/compile/ref_error.ts b/lib/compile/ref_error.ts index 22b70b992..386bf0499 100644 --- a/lib/compile/ref_error.ts +++ b/lib/compile/ref_error.ts @@ -1,12 +1,13 @@ import {resolveUrl, normalizeId, getFullPath} from "./resolve" +import type {UriResolver} from "../types" export default class MissingRefError extends Error { readonly missingRef: string readonly missingSchema: string - constructor(baseId: string, ref: string, msg?: string) { + constructor(resolver: UriResolver, baseId: string, ref: string, msg?: string) { super(msg || `can't resolve reference ${ref} from id ${baseId}`) - this.missingRef = resolveUrl(baseId, ref) - this.missingSchema = normalizeId(getFullPath(this.missingRef)) + this.missingRef = resolveUrl(resolver, baseId, ref) + this.missingSchema = normalizeId(getFullPath(resolver, this.missingRef)) } } diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 9031e0390..4360eab06 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,9 +1,9 @@ -import type {AnySchema, AnySchemaObject} from "../types" +import type {AnySchema, AnySchemaObject, UriResolver} from "../types" import type Ajv from "../ajv" +import type {URIComponents} from "uri-js" import {eachItem} from "./util" import * as equal from "fast-deep-equal" import * as traverse from "json-schema-traverse" -import * as URI from "uri-js" // the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution export type LocalRefs = {[Ref in string]?: AnySchemaObject} @@ -67,14 +67,15 @@ function countKeys(schema: AnySchemaObject): number { return count } -export function getFullPath(id = "", normalize?: boolean): string { +export function getFullPath(resolver: UriResolver, id = "", normalize?: boolean): string { if (normalize !== false) id = normalizeId(id) - const p = URI.parse(id) - return _getFullPath(p) + const p = resolver.parse(id) + return _getFullPath(resolver, p) } -export function _getFullPath(p: URI.URIComponents): string { - return URI.serialize(p).split("#")[0] + "#" +export function _getFullPath(resolver: UriResolver, p: URIComponents): string { + const serialized = resolver.serialize(p) + return serialized.split("#")[0] + "#" } const TRAILING_SLASH_HASH = /#\/?$/ @@ -82,19 +83,19 @@ export function normalizeId(id: string | undefined): string { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } -export function resolveUrl(baseId: string, id: string): string { +export function resolveUrl(resolver: UriResolver, baseId: string, id: string): string { id = normalizeId(id) - return URI.resolve(baseId, id) + return resolver.resolve(baseId, id) } const ANCHOR = /^[a-z_][-a-z0-9._]*$/i export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): LocalRefs { if (typeof schema == "boolean") return {} - const {schemaId} = this.opts + const {schemaId, uriResolver} = this.opts const schId = normalizeId(schema[schemaId] || baseId) const baseIds: {[JsonPtr in string]?: string} = {"": schId} - const pathPrefix = getFullPath(schId, false) + const pathPrefix = getFullPath(uriResolver, schId, false) const localRefs: LocalRefs = {} const schemaRefs: Set = new Set() @@ -108,7 +109,9 @@ export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): Loc baseIds[jsonPtr] = baseId function addRef(this: Ajv, ref: string): string { - ref = normalizeId(baseId ? URI.resolve(baseId, ref) : ref) + // eslint-disable-next-line @typescript-eslint/unbound-method + const _resolve = this.opts.uriResolver.resolve + ref = normalizeId(baseId ? _resolve(baseId, ref) : ref) if (schemaRefs.has(ref)) throw ambiguos(ref) schemaRefs.add(ref) let schOrRef = this.refs[ref] diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 0f40584e0..f5910c3a3 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -177,7 +177,7 @@ function checkNoDefault(it: SchemaObjCxt): void { function updateContext(it: SchemaObjCxt): void { const schId = it.schema[it.opts.schemaId] - if (schId) it.baseId = resolveUrl(it.baseId, schId) + if (schId) it.baseId = resolveUrl(it.opts.uriResolver, it.baseId, schId) } function checkAsyncSchema(it: SchemaObjCxt): void { diff --git a/lib/core.ts b/lib/core.ts index 9e0bb326c..3686ffe76 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -49,6 +49,7 @@ import type { Format, AddedFormat, RegExpEngine, + UriResolver, } from "./types" import type {JSONSchemaType} from "./types/json-schema" import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema" @@ -60,9 +61,10 @@ import {Code, ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import {getJSONTypes} from "./compile/validate/dataType" import {eachItem} from "./compile/util" - import * as $dataRefSchema from "./refs/data.json" +import DefaultUriResolver from "./runtime/uri" + const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags) defaultRegExp.code = "new RegExp" @@ -136,6 +138,7 @@ export interface CurrentOptions { int32range?: boolean // JTD only messages?: boolean code?: CodeOptions // NEW + uriResolver?: UriResolver } export interface CodeOptions { @@ -226,7 +229,8 @@ type RequiredInstanceOptions = { | "validateSchema" | "validateFormats" | "int32range" - | "unicodeRegExp"]: NonNullable + | "unicodeRegExp" + | "uriResolver"]: NonNullable } & {code: InstanceCodeOptions} export type InstanceOptions = Options & RequiredInstanceOptions @@ -239,6 +243,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions { const _optz = o.code?.optimize const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0 const regExp = o.code?.regExp ?? defaultRegExp + const uriResolver = o.uriResolver ?? DefaultUriResolver return { strictSchema: o.strictSchema ?? s ?? true, strictNumbers: o.strictNumbers ?? s ?? true, @@ -257,6 +262,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions { validateFormats: o.validateFormats ?? true, unicodeRegExp: o.unicodeRegExp ?? true, int32range: o.int32range ?? true, + uriResolver: uriResolver, } } diff --git a/lib/runtime/uri.ts b/lib/runtime/uri.ts new file mode 100644 index 000000000..7dd35f9d1 --- /dev/null +++ b/lib/runtime/uri.ts @@ -0,0 +1,6 @@ +import * as uri from "uri-js" + +type URI = typeof uri & {code: string} +;(uri as URI).code = 'require("ajv/dist/runtime/uri").default' + +export default uri as URI diff --git a/lib/types/index.ts b/lib/types/index.ts index 03a06e642..123d9df16 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,3 +1,4 @@ +import * as URI from "uri-js" import type {CodeGen, Code, Name, ScopeValueSets, ValueScopeName} from "../compile/codegen" import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile" import type {JSONType} from "../compile/rules" @@ -231,3 +232,9 @@ export interface RegExpEngine { export interface RegExpLike { test: (s: string) => boolean } + +export interface UriResolver { + parse(uri: string): URI.URIComponents + resolve(base: string, path: string): string + serialize(component: URI.URIComponents): string +} diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 079cb9b4b..5d59fbcb2 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -16,7 +16,7 @@ const def: CodeKeywordDefinition = { const {root} = env if (($ref === "#" || $ref === "#/") && baseId === root.baseId) return callRootRef() const schOrEnv = resolveRef.call(self, root, baseId, $ref) - if (schOrEnv === undefined) throw new MissingRefError(baseId, $ref) + if (schOrEnv === undefined) throw new MissingRefError(it.opts.uriResolver, baseId, $ref) if (schOrEnv instanceof SchemaEnv) return callValidate(schOrEnv) return inlineRefSchema(schOrEnv) diff --git a/lib/vocabularies/jtd/ref.ts b/lib/vocabularies/jtd/ref.ts index 0731b1f69..97646ee1b 100644 --- a/lib/vocabularies/jtd/ref.ts +++ b/lib/vocabularies/jtd/ref.ts @@ -28,7 +28,9 @@ const def: CodeKeywordDefinition = { function validateJtdRef(): void { const refSchema = (root.schema as AnySchemaObject).definitions?.[ref] - if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`) + if (!refSchema) { + throw new MissingRefError(it.opts.uriResolver, "", ref, `No definition ${ref}`) + } if (hasRef(refSchema) || !it.opts.inlineRefs) callValidate(refSchema) else inlineRefSchema(refSchema) } diff --git a/package.json b/package.json index 6fa07f6c8..db603fcb9 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "dayjs-plugin-utc": "^0.1.2", "eslint": "^7.8.1", "eslint-config-prettier": "^7.0.0", + "fast-uri": "^1.0.0", "glob": "^7.0.0", "husky": "^7.0.1", "if-node-version": "^1.0.0", diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index 43612ab07..e8930b0d5 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -282,7 +282,7 @@ describe("User-defined keywords", () => { it.baseId.should.equal("#") const ref = schema.$ref const validate = _ajv.getSchema(ref) - if (!validate) throw new _Ajv.MissingRefError(it.baseId, ref) + if (!validate) throw new _Ajv.MissingRefError(_ajv.opts.uriResolver, it.baseId, ref) return validate.schema }, metaSchema: { diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index 0f601b666..2fe5b1041 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -4,406 +4,420 @@ import _Ajv from "./ajv" import type {AnyValidateFunction} from "../dist/types" import type MissingRefError from "../dist/compile/ref_error" import chai from "./chai" +import * as fastUri from "fast-uri" const should = chai.should() -describe("resolve", () => { - let instances: AjvCore[] - - beforeEach(() => { - instances = getAjvInstances(_Ajv, { - allErrors: true, - verbose: true, - inlineRefs: false, - allowUnionTypes: true, - }) - }) +const uriResolvers = [undefined, fastUri] + +uriResolvers.forEach((resolver) => { + let describeTitle: string + if (resolver !== undefined) { + describeTitle = "fast-uri resolver" + } else { + describeTitle = "uri-js resolver" + } + describe(describeTitle, () => { + describe("resolve", () => { + let instances: AjvCore[] + + beforeEach(() => { + instances = getAjvInstances(_Ajv, { + allErrors: true, + verbose: true, + inlineRefs: false, + allowUnionTypes: true, + uriResolver: resolver, + }) + }) - describe("resolve.ids method", () => { - it("should resolve ids in schema", () => { - // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 - const schema = { - $id: "http://x.y.z/rootschema.json#", - $defs: { - schema1: { - $id: "#foo", - description: "schema1", - type: "integer", - }, - schema2: { - $id: "otherschema.json", - description: "schema2", + describe("resolve.ids method", () => { + it("should resolve ids in schema", () => { + // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 + const schema = { + $id: "http://x.y.z/rootschema.json#", $defs: { - nested: { - $id: "#bar", - description: "nested", - type: "string", + schema1: { + $id: "#foo", + description: "schema1", + type: "integer", }, - alsonested: { - $id: "t/inner.json#a", - description: "alsonested", - type: "boolean", + schema2: { + $id: "otherschema.json", + description: "schema2", + $defs: { + nested: { + $id: "#bar", + description: "nested", + type: "string", + }, + alsonested: { + $id: "t/inner.json#a", + description: "alsonested", + type: "boolean", + }, + }, + }, + schema3: { + $id: "some://where.else/completely#", + description: "schema3", + type: "null", }, }, - }, - schema3: { - $id: "some://where.else/completely#", - description: "schema3", - type: "null", - }, - }, - type: "object", - properties: { - foo: {$ref: "#foo"}, - bar: {$ref: "otherschema.json#bar"}, - baz: {$ref: "t/inner.json#a"}, - bax: {$ref: "some://where.else/completely#"}, - }, - required: ["foo", "bar", "baz", "bax"], - } - - instances.forEach((ajv) => { - const validate = ajv.compile(schema) - const data = {foo: 1, bar: "abc", baz: true, bax: null} - validate(data).should.equal(true) - }) - }) - - it("should resolve fragment $id in schema refs when root $id not present", () => { - const schema = { - $schema: "http://json-schema.org/draft-07/schema#", - definitions: { - SeeAlso: {$id: "#SeeAlso", type: "number"}, - Engine: { - $id: "#Engine", type: "object", properties: { - see_also: {$ref: "#SeeAlso"}, + foo: {$ref: "#foo"}, + bar: {$ref: "otherschema.json#bar"}, + baz: {$ref: "t/inner.json#a"}, + bax: {$ref: "some://where.else/completely#"}, }, - }, - }, - } - - instances.forEach((ajv) => { - ajv.addSchema(schema, "yaml.json") - const data = {see_also: 1} - const validate = ajv.validate("yaml.json#/definitions/Engine", data) - validate.should.equal(true) - }) - }) + required: ["foo", "bar", "baz", "bax"], + } - it("should throw if the same id resolves to two different schemas", () => { - instances.forEach((ajv) => { - ajv.compile({ - $id: "http://example.com/1.json", - type: "integer", + instances.forEach((ajv) => { + const validate = ajv.compile(schema) + const data = {foo: 1, bar: "abc", baz: true, bax: null} + validate(data).should.equal(true) + }) }) - should.throw(() => { - ajv.compile({ - type: "object", - additionalProperties: { - $id: "http://example.com/1.json", - type: "string", + + it("should resolve fragment $id in schema refs when root $id not present", () => { + const schema = { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { + SeeAlso: {$id: "#SeeAlso", type: "number"}, + Engine: { + $id: "#Engine", + type: "object", + properties: { + see_also: {$ref: "#SeeAlso"}, + }, + }, }, + } + + instances.forEach((ajv) => { + ajv.addSchema(schema, "yaml.json") + const data = {see_also: 1} + const validate = ajv.validate("yaml.json#/definitions/Engine", data) + validate.should.equal(true) }) - }, /resolves to more than one schema/) + }) - should.throw(() => { - ajv.compile({ - type: ["object", "array"], - items: { - $id: "#int", + it("should throw if the same id resolves to two different schemas", () => { + instances.forEach((ajv) => { + ajv.compile({ + $id: "http://example.com/1.json", type: "integer", - }, - additionalProperties: { - $id: "#int", - type: "string", - }, + }) + should.throw(() => { + ajv.compile({ + type: "object", + additionalProperties: { + $id: "http://example.com/1.json", + type: "string", + }, + }) + }, /resolves to more than one schema/) + + should.throw(() => { + ajv.compile({ + type: ["object", "array"], + items: { + $id: "#int", + type: "integer", + }, + additionalProperties: { + $id: "#int", + type: "string", + }, + }) + }, /resolves to more than one schema/) }) - }, /resolves to more than one schema/) - }) - }) + }) - it("should resolve ids defined as urn's (issue #423)", () => { - const schema = { - type: "object", - properties: { - ip1: { - $id: "urn:some:ip:prop", - type: "string", - pattern: "^(\\d+\\.){3}\\d+$", - }, - ip2: { - $ref: "urn:some:ip:prop", - }, - }, - required: ["ip1", "ip2"], - } - - const data = { - ip1: "0.0.0.0", - ip2: "0.0.0.0", - } - instances.forEach((ajv) => { - const validate = ajv.compile(schema) - validate(data).should.equal(true) + it("should resolve ids defined as urn's (issue #423)", () => { + const schema = { + type: "object", + properties: { + ip1: { + $id: "urn:some:ip:prop", + type: "string", + pattern: "^(\\d+\\.){3}\\d+$", + }, + ip2: { + $ref: "urn:some:ip:prop", + }, + }, + required: ["ip1", "ip2"], + } + + const data = { + ip1: "0.0.0.0", + ip2: "0.0.0.0", + } + instances.forEach((ajv) => { + const validate = ajv.compile(schema) + validate(data).should.equal(true) + }) + }) }) - }) - }) - describe("protocol-relative URIs", () => { - it("should resolve fragment", () => { - instances.forEach((ajv) => { - const schema = { - $id: "//e.com/types", - definitions: { - int: {type: "integer"}, - }, - } + describe("protocol-relative URIs", () => { + it("should resolve fragment", () => { + instances.forEach((ajv) => { + const schema = { + $id: "//e.com/types", + definitions: { + int: {type: "integer"}, + }, + } - ajv.addSchema(schema) - const validate = ajv.compile({$ref: "//e.com/types#/definitions/int"}) - validate(1).should.equal(true) - validate("foo").should.equal(false) + ajv.addSchema(schema) + const validate = ajv.compile({$ref: "//e.com/types#/definitions/int"}) + validate(1).should.equal(true) + validate("foo").should.equal(false) + }) + }) }) - }) - }) - describe("missing schema error", function () { - this.timeout(4000) + describe("missing schema error", function () { + this.timeout(4000) - it("should contain missingRef and missingSchema", () => { - testMissingSchemaError({ - baseId: "http://example.com/1.json", - ref: "http://another.com/int.json", - expectedMissingRef: "http://another.com/int.json", - expectedMissingSchema: "http://another.com/int.json", - }) - }) + it("should contain missingRef and missingSchema", () => { + testMissingSchemaError({ + baseId: "http://example.com/1.json", + ref: "http://another.com/int.json", + expectedMissingRef: "http://another.com/int.json", + expectedMissingSchema: "http://another.com/int.json", + }) + }) - it("should resolve missingRef and missingSchema relative to base id", () => { - testMissingSchemaError({ - baseId: "http://example.com/folder/1.json", - ref: "int.json", - expectedMissingRef: "http://example.com/folder/int.json", - expectedMissingSchema: "http://example.com/folder/int.json", - }) - }) + it("should resolve missingRef and missingSchema relative to base id", () => { + testMissingSchemaError({ + baseId: "http://example.com/folder/1.json", + ref: "int.json", + expectedMissingRef: "http://example.com/folder/int.json", + expectedMissingSchema: "http://example.com/folder/int.json", + }) + }) - it("should resolve missingRef and missingSchema relative to base id from root", () => { - testMissingSchemaError({ - baseId: "http://example.com/folder/1.json", - ref: "/int.json", - expectedMissingRef: "http://example.com/int.json", - expectedMissingSchema: "http://example.com/int.json", - }) - }) + it("should resolve missingRef and missingSchema relative to base id from root", () => { + testMissingSchemaError({ + baseId: "http://example.com/folder/1.json", + ref: "/int.json", + expectedMissingRef: "http://example.com/int.json", + expectedMissingSchema: "http://example.com/int.json", + }) + }) - it("missingRef should and missingSchema should NOT include JSON path (hash fragment)", () => { - testMissingSchemaError({ - baseId: "http://example.com/1.json", - ref: "int.json#/definitions/positive", - expectedMissingRef: "http://example.com/int.json#/definitions/positive", - expectedMissingSchema: "http://example.com/int.json", - }) - }) + it("missingRef should and missingSchema should NOT include JSON path (hash fragment)", () => { + testMissingSchemaError({ + baseId: "http://example.com/1.json", + ref: "int.json#/definitions/positive", + expectedMissingRef: "http://example.com/int.json#/definitions/positive", + expectedMissingSchema: "http://example.com/int.json", + }) + }) - it("should throw missing schema error if same path exist in the current schema but id is different (issue #220)", () => { - testMissingSchemaError({ - baseId: "http://example.com/parent.json", - ref: "object.json#/properties/a", - expectedMissingRef: "http://example.com/object.json#/properties/a", - expectedMissingSchema: "http://example.com/object.json", - }) - }) + it("should throw missing schema error if same path exist in the current schema but id is different (issue #220)", () => { + testMissingSchemaError({ + baseId: "http://example.com/parent.json", + ref: "object.json#/properties/a", + expectedMissingRef: "http://example.com/object.json#/properties/a", + expectedMissingSchema: "http://example.com/object.json", + }) + }) - function testMissingSchemaError(opts) { - instances.forEach((ajv) => { - try { - ajv.compile({ - $id: opts.baseId, - type: "object", - properties: {a: {$ref: opts.ref}}, + function testMissingSchemaError(opts) { + instances.forEach((ajv) => { + try { + ajv.compile({ + $id: opts.baseId, + type: "object", + properties: {a: {$ref: opts.ref}}, + }) + } catch (err) { + const e = err as MissingRefError + e.missingRef.should.equal(opts.expectedMissingRef) + e.missingSchema.should.equal(opts.expectedMissingSchema) + } }) - } catch (err) { - const e = err as MissingRefError - e.missingRef.should.equal(opts.expectedMissingRef) - e.missingSchema.should.equal(opts.expectedMissingSchema) } }) - } - }) - describe("inline referenced schemas without refs in them", () => { - const schemas = [ - {$id: "http://e.com/obj.json#", type: "object", properties: {a: {$ref: "int.json#"}}}, - {$id: "http://e.com/int.json#", type: "integer", minimum: 2, maximum: 4}, - { - $id: "http://e.com/obj1.json#", - type: "object", - definitions: {int: {type: "integer", minimum: 2, maximum: 4}}, - properties: {a: {$ref: "#/definitions/int"}}, - }, - {$id: "http://e.com/list.json#", type: "array", items: {$ref: "obj.json#"}}, - ] - - it("by default should inline schema if it doesn't contain refs", () => { - const ajv = new _Ajv({schemas, code: {source: true}}) - testSchemas(ajv, true) - }) + describe("inline referenced schemas without refs in them", () => { + const schemas = [ + {$id: "http://e.com/obj.json#", type: "object", properties: {a: {$ref: "int.json#"}}}, + {$id: "http://e.com/int.json#", type: "integer", minimum: 2, maximum: 4}, + { + $id: "http://e.com/obj1.json#", + type: "object", + definitions: {int: {type: "integer", minimum: 2, maximum: 4}}, + properties: {a: {$ref: "#/definitions/int"}}, + }, + {$id: "http://e.com/list.json#", type: "array", items: {$ref: "obj.json#"}}, + ] - it("should NOT inline schema if option inlineRefs == false", () => { - const ajv = new _Ajv({schemas, inlineRefs: false, code: {source: true}}) - testSchemas(ajv, false) - }) + it("by default should inline schema if it doesn't contain refs", () => { + const ajv = new _Ajv({schemas, code: {source: true}}) + testSchemas(ajv, true) + }) - it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { - const ajv = new _Ajv({schemas, inlineRefs: 4, code: {source: true}}) - testSchemas(ajv, true) - }) + it("should NOT inline schema if option inlineRefs == false", () => { + const ajv = new _Ajv({schemas, inlineRefs: false, code: {source: true}}) + testSchemas(ajv, false) + }) - it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { - const ajv = new _Ajv({schemas, inlineRefs: 2, code: {source: true}}) - testSchemas(ajv, false) - }) + it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { + const ajv = new _Ajv({schemas, inlineRefs: 4, code: {source: true}}) + testSchemas(ajv, true) + }) - it("should avoid schema substitution when refs are inlined (issue #77)", () => { - const ajv = new _Ajv({verbose: true}) + it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { + const ajv = new _Ajv({schemas, inlineRefs: 2, code: {source: true}}) + testSchemas(ajv, false) + }) + + it("should avoid schema substitution when refs are inlined (issue #77)", () => { + const ajv = new _Ajv({verbose: true}) - const schemaMessage = { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "http://e.com/message.json#", - type: "object", - required: ["header"], - properties: { - header: { + const schemaMessage = { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "http://e.com/message.json#", type: "object", - allOf: [{$ref: "header.json"}, {properties: {msgType: {enum: [0]}}}], - }, - }, - } - - // header schema - const schemaHeader = { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "http://e.com/header.json#", - type: "object", - properties: { - version: { - type: "integer", - maximum: 5, - }, - msgType: {type: "integer"}, - }, - required: ["version", "msgType"], - } - - // a good message - const validMessage = { - header: { - version: 4, - msgType: 0, - }, - } - - // a bad message - const invalidMessage = { - header: { - version: 6, - msgType: 0, - }, - } - - // add schemas and get validator function - ajv.addSchema(schemaHeader) - ajv.addSchema(schemaMessage) - const v: any = ajv.getSchema("http://e.com/message.json#") - - v(validMessage).should.equal(true) - v.schema.$id.should.equal("http://e.com/message.json#") - - v(invalidMessage).should.equal(false) - v.errors.should.have.length(1) - v.schema.$id.should.equal("http://e.com/message.json#") - - v(validMessage).should.equal(true) - v.schema.$id.should.equal("http://e.com/message.json#") - }) + required: ["header"], + properties: { + header: { + type: "object", + allOf: [{$ref: "header.json"}, {properties: {msgType: {enum: [0]}}}], + }, + }, + } - function testSchemas(ajv, expectedInlined) { - const v1 = ajv.getSchema("http://e.com/obj.json"), - v2 = ajv.getSchema("http://e.com/obj1.json"), - v3 = ajv.getSchema("http://e.com/list.json") - testObjSchema(v1) - testObjSchema(v2) - testListSchema(v3) - testInlined(v1, expectedInlined) - testInlined(v2, expectedInlined) - testInlined(v3, false) - } - - function testObjSchema(validate) { - validate({a: 3}).should.equal(true) - validate({a: 1}).should.equal(false) - validate({a: 5}).should.equal(false) - } - - function testListSchema(validate) { - validate([{a: 3}]).should.equal(true) - validate([{a: 1}]).should.equal(false) - validate([{a: 5}]).should.equal(false) - } - - function testInlined(validate: AnyValidateFunction, expectedInlined) { - const inlined: any = !validate.source?.scopeValues.validate - inlined.should.equal(expectedInlined) - } - }) + // header schema + const schemaHeader = { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "http://e.com/header.json#", + type: "object", + properties: { + version: { + type: "integer", + maximum: 5, + }, + msgType: {type: "integer"}, + }, + required: ["version", "msgType"], + } + + // a good message + const validMessage = { + header: { + version: 4, + msgType: 0, + }, + } - describe("duplicate internal $id", () => { - it("should throw error with duplicate IDs in definitions", () => { - const schema = { - $id: "http://example.com/example.json", - $defs: { - foo: { - $id: "#nope", - type: "integer", - }, - bar: { - $id: "#nope", - type: "string", - }, - }, - type: "object", - properties: { - foo: {$ref: "#/$defs/foo"}, - bar: {$ref: "#/$defs/bar"}, - }, - } - - instances.forEach((ajv) => - should.throw(() => ajv.compile(schema), /nope.*resolves to more than one schema/) - ) - }) + // a bad message + const invalidMessage = { + header: { + version: 6, + msgType: 0, + }, + } - it("should throw error with duplicate IDs in properties", () => { - const schema = { - $id: "http://example.com/example.json", - type: "object", - properties: { - foo: { - $id: "#nope", - type: "integer", - }, - bar: { - $id: "#nope", - type: "string", - }, - }, - } + // add schemas and get validator function + ajv.addSchema(schemaHeader) + ajv.addSchema(schemaMessage) + const v: any = ajv.getSchema("http://e.com/message.json#") - instances.forEach((ajv) => - should.throw(() => ajv.compile(schema), /nope.*resolves to more than one schema/) - ) + v(validMessage).should.equal(true) + v.schema.$id.should.equal("http://e.com/message.json#") + + v(invalidMessage).should.equal(false) + v.errors.should.have.length(1) + v.schema.$id.should.equal("http://e.com/message.json#") + + v(validMessage).should.equal(true) + v.schema.$id.should.equal("http://e.com/message.json#") + }) + + function testSchemas(ajv, expectedInlined) { + const v1 = ajv.getSchema("http://e.com/obj.json"), + v2 = ajv.getSchema("http://e.com/obj1.json"), + v3 = ajv.getSchema("http://e.com/list.json") + testObjSchema(v1) + testObjSchema(v2) + testListSchema(v3) + testInlined(v1, expectedInlined) + testInlined(v2, expectedInlined) + testInlined(v3, false) + } + + function testObjSchema(validate) { + validate({a: 3}).should.equal(true) + validate({a: 1}).should.equal(false) + validate({a: 5}).should.equal(false) + } + + function testListSchema(validate) { + validate([{a: 3}]).should.equal(true) + validate([{a: 1}]).should.equal(false) + validate([{a: 5}]).should.equal(false) + } + + function testInlined(validate: AnyValidateFunction, expectedInlined) { + const inlined: any = !validate.source?.scopeValues.validate + inlined.should.equal(expectedInlined) + } + }) + + describe("duplicate internal $id", () => { + it("should throw error with duplicate IDs in definitions", () => { + const schema = { + $id: "http://example.com/example.json", + $defs: { + foo: { + $id: "#nope", + type: "integer", + }, + bar: { + $id: "#nope", + type: "string", + }, + }, + type: "object", + properties: { + foo: {$ref: "#/$defs/foo"}, + bar: {$ref: "#/$defs/bar"}, + }, + } + + instances.forEach((ajv) => + should.throw(() => ajv.compile(schema), /nope.*resolves to more than one schema/) + ) + }) + + it("should throw error with duplicate IDs in properties", () => { + const schema = { + $id: "http://example.com/example.json", + type: "object", + properties: { + foo: { + $id: "#nope", + type: "integer", + }, + bar: { + $id: "#nope", + type: "string", + }, + }, + } + + instances.forEach((ajv) => + should.throw(() => ajv.compile(schema), /nope.*resolves to more than one schema/) + ) + }) + }) }) }) })