Skip to content

Commit

Permalink
feat: add uriresolver option (#1862)
Browse files Browse the repository at this point in the history
* feat: add uriresolver option

* fix: review

* fix: prettier

* fix: lint

* fix: param order

* fix: test

* fix: test

* fix: test

* fix: order

* feat: bump version

* feat: bump version

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
  • Loading branch information
zekth and epoberezkin committed Feb 4, 2022
1 parent 8b993dc commit 0e47ab4
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 388 deletions.
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

0 comments on commit 0e47ab4

Please sign in to comment.