1 import type {AnySchema, AnySchemaObject} from "../types"
2 import type Ajv from "../ajv"
3 import {eachItem} from "./util"
4 import * as equal from "fast-deep-equal"
5 import * as traverse from "json-schema-traverse"
6 import * as URI from "uri-js"
8 // the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution
9 export type LocalRefs = {[Ref in string]?: AnySchemaObject}
11 // TODO refactor to use keyword definitions
12 const SIMPLE_INLINED = new Set([
31 export function inlineRef(schema: AnySchema, limit: boolean | number = true): boolean {
32 if (typeof schema == "boolean") return true
33 if (limit === true) return !hasRef(schema)
34 if (!limit) return false
35 return countKeys(schema) <= limit
38 const REF_KEYWORDS = new Set([
46 function hasRef(schema: AnySchemaObject): boolean {
47 for (const key in schema) {
48 if (REF_KEYWORDS.has(key)) return true
49 const sch = schema[key]
50 if (Array.isArray(sch) && sch.some(hasRef)) return true
51 if (typeof sch == "object" && hasRef(sch)) return true
56 function countKeys(schema: AnySchemaObject): number {
58 for (const key in schema) {
59 if (key === "$ref") return Infinity
61 if (SIMPLE_INLINED.has(key)) continue
62 if (typeof schema[key] == "object") {
63 eachItem(schema[key], (sch) => (count += countKeys(sch)))
65 if (count === Infinity) return Infinity
70 export function getFullPath(id = "", normalize?: boolean): string {
71 if (normalize !== false) id = normalizeId(id)
72 const p = URI.parse(id)
73 return _getFullPath(p)
76 export function _getFullPath(p: URI.URIComponents): string {
77 return URI.serialize(p).split("#")[0] + "#"
80 const TRAILING_SLASH_HASH = /#\/?$/
81 export function normalizeId(id: string | undefined): string {
82 return id ? id.replace(TRAILING_SLASH_HASH, "") : ""
85 export function resolveUrl(baseId: string, id: string): string {
87 return URI.resolve(baseId, id)
90 const ANCHOR = /^[a-z_][-a-z0-9._]*$/i
92 export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): LocalRefs {
93 if (typeof schema == "boolean") return {}
94 const {schemaId} = this.opts
95 const schId = normalizeId(schema[schemaId] || baseId)
96 const baseIds: {[JsonPtr in string]?: string} = {"": schId}
97 const pathPrefix = getFullPath(schId, false)
98 const localRefs: LocalRefs = {}
99 const schemaRefs: Set<string> = new Set()
101 traverse(schema, {allKeys: true}, (sch, jsonPtr, _, parentJsonPtr) => {
102 if (parentJsonPtr === undefined) return
103 const fullPath = pathPrefix + jsonPtr
104 let baseId = baseIds[parentJsonPtr]
105 if (typeof sch[schemaId] == "string") baseId = addRef.call(this, sch[schemaId])
106 addAnchor.call(this, sch.$anchor)
107 addAnchor.call(this, sch.$dynamicAnchor)
108 baseIds[jsonPtr] = baseId
110 function addRef(this: Ajv, ref: string): string {
111 ref = normalizeId(baseId ? URI.resolve(baseId, ref) : ref)
112 if (schemaRefs.has(ref)) throw ambiguos(ref)
114 let schOrRef = this.refs[ref]
115 if (typeof schOrRef == "string") schOrRef = this.refs[schOrRef]
116 if (typeof schOrRef == "object") {
117 checkAmbiguosRef(sch, schOrRef.schema, ref)
118 } else if (ref !== normalizeId(fullPath)) {
119 if (ref[0] === "#") {
120 checkAmbiguosRef(sch, localRefs[ref], ref)
123 this.refs[ref] = fullPath
129 function addAnchor(this: Ajv, anchor: unknown): void {
130 if (typeof anchor == "string") {
131 if (!ANCHOR.test(anchor)) throw new Error(`invalid anchor "${anchor}"`)
132 addRef.call(this, `#${anchor}`)
139 function checkAmbiguosRef(sch1: AnySchema, sch2: AnySchema | undefined, ref: string): void {
140 if (sch2 !== undefined && !equal(sch1, sch2)) throw ambiguos(ref)
143 function ambiguos(ref: string): Error {
144 return new Error(`reference "${ref}" resolves to more than one schema`)