resolve.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import type {AnySchema, AnySchemaObject, UriResolver} from "../types"
  2. import type Ajv from "../ajv"
  3. import type {URIComponent} from "fast-uri"
  4. import {eachItem} from "./util"
  5. import * as equal from "fast-deep-equal"
  6. import * as traverse from "json-schema-traverse"
  7. // the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution
  8. export type LocalRefs = {[Ref in string]?: AnySchemaObject}
  9. // TODO refactor to use keyword definitions
  10. const SIMPLE_INLINED = new Set([
  11. "type",
  12. "format",
  13. "pattern",
  14. "maxLength",
  15. "minLength",
  16. "maxProperties",
  17. "minProperties",
  18. "maxItems",
  19. "minItems",
  20. "maximum",
  21. "minimum",
  22. "uniqueItems",
  23. "multipleOf",
  24. "required",
  25. "enum",
  26. "const",
  27. ])
  28. export function inlineRef(schema: AnySchema, limit: boolean | number = true): boolean {
  29. if (typeof schema == "boolean") return true
  30. if (limit === true) return !hasRef(schema)
  31. if (!limit) return false
  32. return countKeys(schema) <= limit
  33. }
  34. const REF_KEYWORDS = new Set([
  35. "$ref",
  36. "$recursiveRef",
  37. "$recursiveAnchor",
  38. "$dynamicRef",
  39. "$dynamicAnchor",
  40. ])
  41. function hasRef(schema: AnySchemaObject): boolean {
  42. for (const key in schema) {
  43. if (REF_KEYWORDS.has(key)) return true
  44. const sch = schema[key]
  45. if (Array.isArray(sch) && sch.some(hasRef)) return true
  46. if (typeof sch == "object" && hasRef(sch)) return true
  47. }
  48. return false
  49. }
  50. function countKeys(schema: AnySchemaObject): number {
  51. let count = 0
  52. for (const key in schema) {
  53. if (key === "$ref") return Infinity
  54. count++
  55. if (SIMPLE_INLINED.has(key)) continue
  56. if (typeof schema[key] == "object") {
  57. eachItem(schema[key], (sch) => (count += countKeys(sch)))
  58. }
  59. if (count === Infinity) return Infinity
  60. }
  61. return count
  62. }
  63. export function getFullPath(resolver: UriResolver, id = "", normalize?: boolean): string {
  64. if (normalize !== false) id = normalizeId(id)
  65. const p = resolver.parse(id)
  66. return _getFullPath(resolver, p)
  67. }
  68. export function _getFullPath(resolver: UriResolver, p: URIComponent): string {
  69. const serialized = resolver.serialize(p)
  70. return serialized.split("#")[0] + "#"
  71. }
  72. const TRAILING_SLASH_HASH = /#\/?$/
  73. export function normalizeId(id: string | undefined): string {
  74. return id ? id.replace(TRAILING_SLASH_HASH, "") : ""
  75. }
  76. export function resolveUrl(resolver: UriResolver, baseId: string, id: string): string {
  77. id = normalizeId(id)
  78. return resolver.resolve(baseId, id)
  79. }
  80. const ANCHOR = /^[a-z_][-a-z0-9._]*$/i
  81. export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): LocalRefs {
  82. if (typeof schema == "boolean") return {}
  83. const {schemaId, uriResolver} = this.opts
  84. const schId = normalizeId(schema[schemaId] || baseId)
  85. const baseIds: {[JsonPtr in string]?: string} = {"": schId}
  86. const pathPrefix = getFullPath(uriResolver, schId, false)
  87. const localRefs: LocalRefs = {}
  88. const schemaRefs: Set<string> = new Set()
  89. traverse(schema, {allKeys: true}, (sch, jsonPtr, _, parentJsonPtr) => {
  90. if (parentJsonPtr === undefined) return
  91. const fullPath = pathPrefix + jsonPtr
  92. let innerBaseId = baseIds[parentJsonPtr]
  93. if (typeof sch[schemaId] == "string") innerBaseId = addRef.call(this, sch[schemaId])
  94. addAnchor.call(this, sch.$anchor)
  95. addAnchor.call(this, sch.$dynamicAnchor)
  96. baseIds[jsonPtr] = innerBaseId
  97. function addRef(this: Ajv, ref: string): string {
  98. // eslint-disable-next-line @typescript-eslint/unbound-method
  99. const _resolve = this.opts.uriResolver.resolve
  100. ref = normalizeId(innerBaseId ? _resolve(innerBaseId, ref) : ref)
  101. if (schemaRefs.has(ref)) throw ambiguos(ref)
  102. schemaRefs.add(ref)
  103. let schOrRef = this.refs[ref]
  104. if (typeof schOrRef == "string") schOrRef = this.refs[schOrRef]
  105. if (typeof schOrRef == "object") {
  106. checkAmbiguosRef(sch, schOrRef.schema, ref)
  107. } else if (ref !== normalizeId(fullPath)) {
  108. if (ref[0] === "#") {
  109. checkAmbiguosRef(sch, localRefs[ref], ref)
  110. localRefs[ref] = sch
  111. } else {
  112. this.refs[ref] = fullPath
  113. }
  114. }
  115. return ref
  116. }
  117. function addAnchor(this: Ajv, anchor: unknown): void {
  118. if (typeof anchor == "string") {
  119. if (!ANCHOR.test(anchor)) throw new Error(`invalid anchor "${anchor}"`)
  120. addRef.call(this, `#${anchor}`)
  121. }
  122. }
  123. })
  124. return localRefs
  125. function checkAmbiguosRef(sch1: AnySchema, sch2: AnySchema | undefined, ref: string): void {
  126. if (sch2 !== undefined && !equal(sch1, sch2)) throw ambiguos(ref)
  127. }
  128. function ambiguos(ref: string): Error {
  129. return new Error(`reference "${ref}" resolves to more than one schema`)
  130. }
  131. }