Skip to content

Commit 0e47ab4

Browse files
zekthepoberezkin
andauthoredFeb 4, 2022
feat: add uriresolver option (#1862)
* 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>
1 parent 8b993dc commit 0e47ab4

File tree

14 files changed

+428
-388
lines changed

14 files changed

+428
-388
lines changed
 

‎lib/compile/index.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
112112
// TODO refactor - remove compilations
113113
const _sch = getCompilingSchema.call(this, sch)
114114
if (_sch) return _sch
115-
const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails
115+
const rootId = getFullPath(this.opts.uriResolver, sch.root.baseId) // TODO if getFullPath removed 1 tests fails
116116
const {es5, lines} = this.opts.code
117117
const {ownProperties} = this.opts
118118
const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
@@ -208,7 +208,7 @@ export function resolveRef(
208208
baseId: string,
209209
ref: string
210210
): AnySchema | SchemaEnv | undefined {
211-
ref = resolveUrl(baseId, ref)
211+
ref = resolveUrl(this.opts.uriResolver, baseId, ref)
212212
const schOrFunc = root.refs[ref]
213213
if (schOrFunc) return schOrFunc
214214

@@ -257,9 +257,9 @@ export function resolveSchema(
257257
root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
258258
ref: string // reference to resolve
259259
): SchemaEnv | undefined {
260-
const p = URI.parse(ref)
261-
const refPath = _getFullPath(p)
262-
let baseId = getFullPath(root.baseId)
260+
const p = this.opts.uriResolver.parse(ref)
261+
const refPath = _getFullPath(this.opts.uriResolver, p)
262+
let baseId = getFullPath(this.opts.uriResolver, root.baseId, undefined)
263263
// TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
264264
if (Object.keys(root.schema).length > 0 && refPath === baseId) {
265265
return getJsonPointer.call(this, p, root)
@@ -279,7 +279,7 @@ export function resolveSchema(
279279
const {schema} = schOrRef
280280
const {schemaId} = this.opts
281281
const schId = schema[schemaId]
282-
if (schId) baseId = resolveUrl(baseId, schId)
282+
if (schId) baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
283283
return new SchemaEnv({schema, schemaId, root, baseId})
284284
}
285285
return getJsonPointer.call(this, p, schOrRef)
@@ -307,12 +307,12 @@ function getJsonPointer(
307307
// TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
308308
const schId = typeof schema === "object" && schema[this.opts.schemaId]
309309
if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
310-
baseId = resolveUrl(baseId, schId)
310+
baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
311311
}
312312
}
313313
let env: SchemaEnv | undefined
314314
if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
315-
const $ref = resolveUrl(baseId, schema.$ref)
315+
const $ref = resolveUrl(this.opts.uriResolver, baseId, schema.$ref)
316316
env = resolveSchema.call(this, root, $ref)
317317
}
318318
// even though resolution failed we need to return SchemaEnv to throw exception

‎lib/compile/jtd/parse.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ function parseRef(cxt: ParseCxt): void {
342342
const {gen, self, definitions, schema, schemaEnv} = cxt
343343
const {ref} = schema
344344
const refSchema = definitions[ref]
345-
if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
345+
if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
346346
if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema})
347347
const {root} = schemaEnv
348348
const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions)

‎lib/compile/jtd/serialize.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ function serializeRef(cxt: SerializeCxt): void {
234234
const {gen, self, data, definitions, schema, schemaEnv} = cxt
235235
const {ref} = schema
236236
const refSchema = definitions[ref]
237-
if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
237+
if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
238238
if (!hasRef(refSchema)) return serializeCode({...cxt, schema: refSchema})
239239
const {root} = schemaEnv
240240
const sch = compileSerializer.call(self, new SchemaEnv({schema: refSchema, root}), definitions)

‎lib/compile/ref_error.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {resolveUrl, normalizeId, getFullPath} from "./resolve"
2+
import type {UriResolver} from "../types"
23

34
export default class MissingRefError extends Error {
45
readonly missingRef: string
56
readonly missingSchema: string
67

7-
constructor(baseId: string, ref: string, msg?: string) {
8+
constructor(resolver: UriResolver, baseId: string, ref: string, msg?: string) {
89
super(msg || `can't resolve reference ${ref} from id ${baseId}`)
9-
this.missingRef = resolveUrl(baseId, ref)
10-
this.missingSchema = normalizeId(getFullPath(this.missingRef))
10+
this.missingRef = resolveUrl(resolver, baseId, ref)
11+
this.missingSchema = normalizeId(getFullPath(resolver, this.missingRef))
1112
}
1213
}

‎lib/compile/resolve.ts

+15-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type {AnySchema, AnySchemaObject} from "../types"
1+
import type {AnySchema, AnySchemaObject, UriResolver} from "../types"
22
import type Ajv from "../ajv"
3+
import type {URIComponents} from "uri-js"
34
import {eachItem} from "./util"
45
import * as equal from "fast-deep-equal"
56
import * as traverse from "json-schema-traverse"
6-
import * as URI from "uri-js"
77

88
// the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution
99
export type LocalRefs = {[Ref in string]?: AnySchemaObject}
@@ -67,34 +67,35 @@ function countKeys(schema: AnySchemaObject): number {
6767
return count
6868
}
6969

70-
export function getFullPath(id = "", normalize?: boolean): string {
70+
export function getFullPath(resolver: UriResolver, id = "", normalize?: boolean): string {
7171
if (normalize !== false) id = normalizeId(id)
72-
const p = URI.parse(id)
73-
return _getFullPath(p)
72+
const p = resolver.parse(id)
73+
return _getFullPath(resolver, p)
7474
}
7575

76-
export function _getFullPath(p: URI.URIComponents): string {
77-
return URI.serialize(p).split("#")[0] + "#"
76+
export function _getFullPath(resolver: UriResolver, p: URIComponents): string {
77+
const serialized = resolver.serialize(p)
78+
return serialized.split("#")[0] + "#"
7879
}
7980

8081
const TRAILING_SLASH_HASH = /#\/?$/
8182
export function normalizeId(id: string | undefined): string {
8283
return id ? id.replace(TRAILING_SLASH_HASH, "") : ""
8384
}
8485

85-
export function resolveUrl(baseId: string, id: string): string {
86+
export function resolveUrl(resolver: UriResolver, baseId: string, id: string): string {
8687
id = normalizeId(id)
87-
return URI.resolve(baseId, id)
88+
return resolver.resolve(baseId, id)
8889
}
8990

9091
const ANCHOR = /^[a-z_][-a-z0-9._]*$/i
9192

9293
export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): LocalRefs {
9394
if (typeof schema == "boolean") return {}
94-
const {schemaId} = this.opts
95+
const {schemaId, uriResolver} = this.opts
9596
const schId = normalizeId(schema[schemaId] || baseId)
9697
const baseIds: {[JsonPtr in string]?: string} = {"": schId}
97-
const pathPrefix = getFullPath(schId, false)
98+
const pathPrefix = getFullPath(uriResolver, schId, false)
9899
const localRefs: LocalRefs = {}
99100
const schemaRefs: Set<string> = new Set()
100101

@@ -108,7 +109,9 @@ export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): Loc
108109
baseIds[jsonPtr] = baseId
109110

110111
function addRef(this: Ajv, ref: string): string {
111-
ref = normalizeId(baseId ? URI.resolve(baseId, ref) : ref)
112+
// eslint-disable-next-line @typescript-eslint/unbound-method
113+
const _resolve = this.opts.uriResolver.resolve
114+
ref = normalizeId(baseId ? _resolve(baseId, ref) : ref)
112115
if (schemaRefs.has(ref)) throw ambiguos(ref)
113116
schemaRefs.add(ref)
114117
let schOrRef = this.refs[ref]

‎lib/compile/validate/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ function checkNoDefault(it: SchemaObjCxt): void {
177177

178178
function updateContext(it: SchemaObjCxt): void {
179179
const schId = it.schema[it.opts.schemaId]
180-
if (schId) it.baseId = resolveUrl(it.baseId, schId)
180+
if (schId) it.baseId = resolveUrl(it.opts.uriResolver, it.baseId, schId)
181181
}
182182

183183
function checkAsyncSchema(it: SchemaObjCxt): void {

‎lib/core.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import type {
4949
Format,
5050
AddedFormat,
5151
RegExpEngine,
52+
UriResolver,
5253
} from "./types"
5354
import type {JSONSchemaType} from "./types/json-schema"
5455
import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
@@ -60,9 +61,10 @@ import {Code, ValueScope} from "./compile/codegen"
6061
import {normalizeId, getSchemaRefs} from "./compile/resolve"
6162
import {getJSONTypes} from "./compile/validate/dataType"
6263
import {eachItem} from "./compile/util"
63-
6464
import * as $dataRefSchema from "./refs/data.json"
6565

66+
import DefaultUriResolver from "./runtime/uri"
67+
6668
const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags)
6769
defaultRegExp.code = "new RegExp"
6870

@@ -136,6 +138,7 @@ export interface CurrentOptions {
136138
int32range?: boolean // JTD only
137139
messages?: boolean
138140
code?: CodeOptions // NEW
141+
uriResolver?: UriResolver
139142
}
140143

141144
export interface CodeOptions {
@@ -226,7 +229,8 @@ type RequiredInstanceOptions = {
226229
| "validateSchema"
227230
| "validateFormats"
228231
| "int32range"
229-
| "unicodeRegExp"]: NonNullable<Options[K]>
232+
| "unicodeRegExp"
233+
| "uriResolver"]: NonNullable<Options[K]>
230234
} & {code: InstanceCodeOptions}
231235

232236
export type InstanceOptions = Options & RequiredInstanceOptions
@@ -239,6 +243,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions {
239243
const _optz = o.code?.optimize
240244
const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
241245
const regExp = o.code?.regExp ?? defaultRegExp
246+
const uriResolver = o.uriResolver ?? DefaultUriResolver
242247
return {
243248
strictSchema: o.strictSchema ?? s ?? true,
244249
strictNumbers: o.strictNumbers ?? s ?? true,
@@ -257,6 +262,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions {
257262
validateFormats: o.validateFormats ?? true,
258263
unicodeRegExp: o.unicodeRegExp ?? true,
259264
int32range: o.int32range ?? true,
265+
uriResolver: uriResolver,
260266
}
261267
}
262268

‎lib/runtime/uri.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as uri from "uri-js"
2+
3+
type URI = typeof uri & {code: string}
4+
;(uri as URI).code = 'require("ajv/dist/runtime/uri").default'
5+
6+
export default uri as URI

‎lib/types/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as URI from "uri-js"
12
import type {CodeGen, Code, Name, ScopeValueSets, ValueScopeName} from "../compile/codegen"
23
import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile"
34
import type {JSONType} from "../compile/rules"
@@ -231,3 +232,9 @@ export interface RegExpEngine {
231232
export interface RegExpLike {
232233
test: (s: string) => boolean
233234
}
235+
236+
export interface UriResolver {
237+
parse(uri: string): URI.URIComponents
238+
resolve(base: string, path: string): string
239+
serialize(component: URI.URIComponents): string
240+
}

‎lib/vocabularies/core/ref.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const def: CodeKeywordDefinition = {
1616
const {root} = env
1717
if (($ref === "#" || $ref === "#/") && baseId === root.baseId) return callRootRef()
1818
const schOrEnv = resolveRef.call(self, root, baseId, $ref)
19-
if (schOrEnv === undefined) throw new MissingRefError(baseId, $ref)
19+
if (schOrEnv === undefined) throw new MissingRefError(it.opts.uriResolver, baseId, $ref)
2020
if (schOrEnv instanceof SchemaEnv) return callValidate(schOrEnv)
2121
return inlineRefSchema(schOrEnv)
2222

‎lib/vocabularies/jtd/ref.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ const def: CodeKeywordDefinition = {
2828

2929
function validateJtdRef(): void {
3030
const refSchema = (root.schema as AnySchemaObject).definitions?.[ref]
31-
if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
31+
if (!refSchema) {
32+
throw new MissingRefError(it.opts.uriResolver, "", ref, `No definition ${ref}`)
33+
}
3234
if (hasRef(refSchema) || !it.opts.inlineRefs) callValidate(refSchema)
3335
else inlineRefSchema(refSchema)
3436
}

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"dayjs-plugin-utc": "^0.1.2",
8484
"eslint": "^7.8.1",
8585
"eslint-config-prettier": "^7.0.0",
86+
"fast-uri": "^1.0.0",
8687
"glob": "^7.0.0",
8788
"husky": "^7.0.1",
8889
"if-node-version": "^1.0.0",

‎spec/keyword.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ describe("User-defined keywords", () => {
282282
it.baseId.should.equal("#")
283283
const ref = schema.$ref
284284
const validate = _ajv.getSchema(ref)
285-
if (!validate) throw new _Ajv.MissingRefError(it.baseId, ref)
285+
if (!validate) throw new _Ajv.MissingRefError(_ajv.opts.uriResolver, it.baseId, ref)
286286
return validate.schema
287287
},
288288
metaSchema: {

‎spec/resolve.spec.ts

+371-357
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.