resolve.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import type {AnySchema, AnySchemaObject} from "../types"
  2. import type Ajv from "../ajv"
  3. import {eachItem} from "./util"
  4. import equal = require("fast-deep-equal")
  5. import traverse = require("json-schema-traverse")
  6. import URI = require("uri-js")
  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(id = "", normalize?: boolean): string {
  64. if (normalize !== false) id = normalizeId(id)
  65. const p = URI.parse(id)
  66. return _getFullPath(p)
  67. }
  68. export function _getFullPath(p: URI.URIComponents): string {
  69. return URI.serialize(p).split("#")[0] + "#"
  70. }
  71. const TRAILING_SLASH_HASH = /#\/?$/
  72. export function normalizeId(id: string | undefined): string {
  73. return id ? id.replace(TRAILING_SLASH_HASH, "") : ""
  74. }
  75. export function resolveUrl(baseId: string, id: string): string {
  76. id = normalizeId(id)
  77. return URI.resolve(baseId, id)
  78. }
  79. const ANCHOR = /^[a-z_][-a-z0-9._]*$/i
  80. export function getSchemaRefs(this: Ajv, schema: AnySchema): LocalRefs {
  81. if (typeof schema == "boolean") return {}
  82. const schemaId = normalizeId(schema.$id)
  83. const baseIds: {[JsonPtr in string]?: string} = {"": schemaId}
  84. const pathPrefix = getFullPath(schemaId, false)
  85. const localRefs: LocalRefs = {}
  86. const schemaRefs: Set<string> = new Set()
  87. traverse(schema, {allKeys: true}, (sch, jsonPtr, _, parentJsonPtr) => {
  88. if (parentJsonPtr === undefined) return
  89. const fullPath = pathPrefix + jsonPtr
  90. let baseId = baseIds[parentJsonPtr]
  91. if (typeof sch.$id == "string") baseId = addRef.call(this, sch.$id)
  92. addAnchor.call(this, sch.$anchor)
  93. addAnchor.call(this, sch.$dynamicAnchor)
  94. baseIds[jsonPtr] = baseId
  95. function addRef(this: Ajv, ref: string): string {
  96. ref = normalizeId(baseId ? URI.resolve(baseId, ref) : ref)
  97. if (schemaRefs.has(ref)) throw ambiguos(ref)
  98. schemaRefs.add(ref)
  99. let schOrRef = this.refs[ref]
  100. if (typeof schOrRef == "string") schOrRef = this.refs[schOrRef]
  101. if (typeof schOrRef == "object") {
  102. checkAmbiguosRef(sch, schOrRef.schema, ref)
  103. } else if (ref !== normalizeId(fullPath)) {
  104. if (ref[0] === "#") {
  105. checkAmbiguosRef(sch, localRefs[ref], ref)
  106. localRefs[ref] = sch
  107. } else {
  108. this.refs[ref] = fullPath
  109. }
  110. }
  111. return ref
  112. }
  113. function addAnchor(this: Ajv, anchor: unknown): void {
  114. if (typeof anchor == "string") {
  115. if (!ANCHOR.test(anchor)) throw new Error(`invalid anchor "${anchor}"`)
  116. addRef.call(this, `#${anchor}`)
  117. }
  118. }
  119. })
  120. return localRefs
  121. function checkAmbiguosRef(sch1: AnySchema, sch2: AnySchema | undefined, ref: string): void {
  122. if (sch2 !== undefined && !equal(sch1, sch2)) throw ambiguos(ref)
  123. }
  124. function ambiguos(ref: string): Error {
  125. return new Error(`reference "${ref}" resolves to more than one schema`)
  126. }
  127. }