Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add uriresolver option #1862

Merged
merged 15 commits into from Feb 4, 2022
16 changes: 8 additions & 8 deletions lib/compile/index.ts
Expand Up @@ -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})
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/compile/jtd/parse.ts
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/compile/jtd/serialize.ts
Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions 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))
}
}
27 changes: 15 additions & 12 deletions 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}
Expand Down Expand Up @@ -67,34 +67,35 @@ 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 = /#\/?$/
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<string> = new Set()

Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion lib/compile/validate/index.ts
Expand Up @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions lib/core.ts
Expand Up @@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -136,6 +138,7 @@ export interface CurrentOptions {
int32range?: boolean // JTD only
messages?: boolean
code?: CodeOptions // NEW
uriResolver?: UriResolver
}

export interface CodeOptions {
Expand Down Expand Up @@ -226,7 +229,8 @@ type RequiredInstanceOptions = {
| "validateSchema"
| "validateFormats"
| "int32range"
| "unicodeRegExp"]: NonNullable<Options[K]>
| "unicodeRegExp"
| "uriResolver"]: NonNullable<Options[K]>
} & {code: InstanceCodeOptions}

export type InstanceOptions = Options & RequiredInstanceOptions
Expand All @@ -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,
Expand All @@ -257,6 +262,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions {
validateFormats: o.validateFormats ?? true,
unicodeRegExp: o.unicodeRegExp ?? true,
int32range: o.int32range ?? true,
uriResolver: uriResolver,
}
}

Expand Down
6 changes: 6 additions & 0 deletions 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
7 changes: 7 additions & 0 deletions 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"
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion lib/vocabularies/core/ref.ts
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion lib/vocabularies/jtd/ref.ts
Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion spec/keyword.spec.ts
Expand Up @@ -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: {
Expand Down