From f68fea25e4830562fe2f23b6d26284a496ac5fed Mon Sep 17 00:00:00 2001 From: Vincent LE GOFF Date: Mon, 20 Dec 2021 09:26:41 +0100 Subject: [PATCH] feat: add uriresolver option --- lib/compile/index.ts | 11 +- lib/compile/resolve.ts | 28 +- lib/core.ts | 3 +- lib/types/index.ts | 7 + package.json | 1 + spec/resolve.spec.ts | 728 +++++++++++++++++++++-------------------- 6 files changed, 410 insertions(+), 368 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index ebdbe3c78..b730de5f2 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -257,9 +257,14 @@ 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) + let p: URI.URIComponents + if (this.opts.uriResolver) { + p = this.opts.uriResolver.parse(ref) + } else { + p = URI.parse(ref) + } + const refPath = _getFullPath(p, this.opts.uriResolver) + let baseId = getFullPath(root.baseId, undefined, this.opts.uriResolver) // 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) diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 9031e0390..f57ced05d 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,4 +1,4 @@ -import type {AnySchema, AnySchemaObject} from "../types" +import type {AnySchema, AnySchemaObject, UriResolver} from "../types" import type Ajv from "../ajv" import {eachItem} from "./util" import * as equal from "fast-deep-equal" @@ -67,14 +67,25 @@ function countKeys(schema: AnySchemaObject): number { return count } -export function getFullPath(id = "", normalize?: boolean): string { +export function getFullPath(id = "", normalize?: boolean, resolver?: UriResolver): string { if (normalize !== false) id = normalizeId(id) - const p = URI.parse(id) - return _getFullPath(p) + let p: URI.URIComponents + if (resolver !== undefined) { + p = resolver.parse(id) + } else { + p = URI.parse(id) + } + return _getFullPath(p, resolver) } -export function _getFullPath(p: URI.URIComponents): string { - return URI.serialize(p).split("#")[0] + "#" +export function _getFullPath(p: URI.URIComponents, resolver?: UriResolver): string { + let serialized: string + if (resolver !== undefined) { + serialized = resolver.serialize(p) + } else { + serialized = URI.serialize(p) + } + return serialized.split("#")[0] + "#" } const TRAILING_SLASH_HASH = /#\/?$/ @@ -108,7 +119,10 @@ 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) + const _resolve = + // eslint-disable-next-line @typescript-eslint/unbound-method + this.opts.uriResolver !== undefined ? this.opts.uriResolver?.resolve : URI.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/core.ts b/lib/core.ts index 2cf93c62c..34220d8bc 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,7 +61,6 @@ 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" const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags) @@ -136,6 +136,7 @@ export interface CurrentOptions { int32range?: boolean // JTD only messages?: boolean code?: CodeOptions // NEW + uriResolver?: UriResolver } export interface CodeOptions { 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/package.json b/package.json index a1588cec2..16f23e608 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": "^0.0.1", "glob": "^7.0.0", "husky": "^7.0.1", "if-node-version": "^1.0.0", 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/) + ) + }) + }) }) }) })