UNPKG

4.36 kBPlain TextView Raw
1import type {AnySchema, AnySchemaObject} from "../types"
2import type Ajv from "../ajv"
3import {eachItem} from "./util"
4import * as equal from "fast-deep-equal"
5import * as traverse from "json-schema-traverse"
6import * as URI from "uri-js"
7
8// the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution
9export type LocalRefs = {[Ref in string]?: AnySchemaObject}
10
11// TODO refactor to use keyword definitions
12const SIMPLE_INLINED = new Set([
13 "type",
14 "format",
15 "pattern",
16 "maxLength",
17 "minLength",
18 "maxProperties",
19 "minProperties",
20 "maxItems",
21 "minItems",
22 "maximum",
23 "minimum",
24 "uniqueItems",
25 "multipleOf",
26 "required",
27 "enum",
28 "const",
29])
30
31export 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
36}
37
38const REF_KEYWORDS = new Set([
39 "$ref",
40 "$recursiveRef",
41 "$recursiveAnchor",
42 "$dynamicRef",
43 "$dynamicAnchor",
44])
45
46function 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
52 }
53 return false
54}
55
56function countKeys(schema: AnySchemaObject): number {
57 let count = 0
58 for (const key in schema) {
59 if (key === "$ref") return Infinity
60 count++
61 if (SIMPLE_INLINED.has(key)) continue
62 if (typeof schema[key] == "object") {
63 eachItem(schema[key], (sch) => (count += countKeys(sch)))
64 }
65 if (count === Infinity) return Infinity
66 }
67 return count
68}
69
70export function getFullPath(id = "", normalize?: boolean): string {
71 if (normalize !== false) id = normalizeId(id)
72 const p = URI.parse(id)
73 return _getFullPath(p)
74}
75
76export function _getFullPath(p: URI.URIComponents): string {
77 return URI.serialize(p).split("#")[0] + "#"
78}
79
80const TRAILING_SLASH_HASH = /#\/?$/
81export function normalizeId(id: string | undefined): string {
82 return id ? id.replace(TRAILING_SLASH_HASH, "") : ""
83}
84
85export function resolveUrl(baseId: string, id: string): string {
86 id = normalizeId(id)
87 return URI.resolve(baseId, id)
88}
89
90const ANCHOR = /^[a-z_][-a-z0-9._]*$/i
91
92export 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()
100
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
109
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)
113 schemaRefs.add(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)
121 localRefs[ref] = sch
122 } else {
123 this.refs[ref] = fullPath
124 }
125 }
126 return ref
127 }
128
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}`)
133 }
134 }
135 })
136
137 return localRefs
138
139 function checkAmbiguosRef(sch1: AnySchema, sch2: AnySchema | undefined, ref: string): void {
140 if (sch2 !== undefined && !equal(sch1, sch2)) throw ambiguos(ref)
141 }
142
143 function ambiguos(ref: string): Error {
144 return new Error(`reference "${ref}" resolves to more than one schema`)
145 }
146}