util.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import type {AnySchema, EvaluatedProperties, EvaluatedItems} from "../types"
  2. import type {SchemaCxt, SchemaObjCxt} from "."
  3. import {_, getProperty, Code, Name, CodeGen} from "./codegen"
  4. import {_Code} from "./codegen/code"
  5. import type {Rule, ValidationRules} from "./rules"
  6. // TODO refactor to use Set
  7. export function toHash<T extends string = string>(arr: T[]): {[K in T]?: true} {
  8. const hash: {[K in T]?: true} = {}
  9. for (const item of arr) hash[item] = true
  10. return hash
  11. }
  12. export function alwaysValidSchema(it: SchemaCxt, schema: AnySchema): boolean | void {
  13. if (typeof schema == "boolean") return schema
  14. if (Object.keys(schema).length === 0) return true
  15. checkUnknownRules(it, schema)
  16. return !schemaHasRules(schema, it.self.RULES.all)
  17. }
  18. export function checkUnknownRules(it: SchemaCxt, schema: AnySchema = it.schema): void {
  19. const {opts, self} = it
  20. if (!opts.strictSchema) return
  21. if (typeof schema === "boolean") return
  22. const rules = self.RULES.keywords
  23. for (const key in schema) {
  24. if (!rules[key]) checkStrictMode(it, `unknown keyword: "${key}"`)
  25. }
  26. }
  27. export function schemaHasRules(
  28. schema: AnySchema,
  29. rules: {[Key in string]?: boolean | Rule}
  30. ): boolean {
  31. if (typeof schema == "boolean") return !schema
  32. for (const key in schema) if (rules[key]) return true
  33. return false
  34. }
  35. export function schemaHasRulesButRef(schema: AnySchema, RULES: ValidationRules): boolean {
  36. if (typeof schema == "boolean") return !schema
  37. for (const key in schema) if (key !== "$ref" && RULES.all[key]) return true
  38. return false
  39. }
  40. export function schemaRefOrVal(
  41. {topSchemaRef, schemaPath}: SchemaObjCxt,
  42. schema: unknown,
  43. keyword: string,
  44. $data?: string | false
  45. ): Code | number | boolean {
  46. if (!$data) {
  47. if (typeof schema == "number" || typeof schema == "boolean") return schema
  48. if (typeof schema == "string") return _`${schema}`
  49. }
  50. return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}`
  51. }
  52. export function unescapeFragment(str: string): string {
  53. return unescapeJsonPointer(decodeURIComponent(str))
  54. }
  55. export function escapeFragment(str: string | number): string {
  56. return encodeURIComponent(escapeJsonPointer(str))
  57. }
  58. export function escapeJsonPointer(str: string | number): string {
  59. if (typeof str == "number") return `${str}`
  60. return str.replace(/~/g, "~0").replace(/\//g, "~1")
  61. }
  62. export function unescapeJsonPointer(str: string): string {
  63. return str.replace(/~1/g, "/").replace(/~0/g, "~")
  64. }
  65. export function eachItem<T>(xs: T | T[], f: (x: T) => void): void {
  66. if (Array.isArray(xs)) {
  67. for (const x of xs) f(x)
  68. } else {
  69. f(xs)
  70. }
  71. }
  72. type SomeEvaluated = EvaluatedProperties | EvaluatedItems
  73. type MergeEvaluatedFunc<T extends SomeEvaluated> = (
  74. gen: CodeGen,
  75. from: Name | T,
  76. to: Name | Exclude<T, true> | undefined,
  77. toName?: typeof Name
  78. ) => Name | T
  79. interface MakeMergeFuncArgs<T extends SomeEvaluated> {
  80. mergeNames: (gen: CodeGen, from: Name, to: Name) => void
  81. mergeToName: (gen: CodeGen, from: T, to: Name) => void
  82. mergeValues: (from: T, to: Exclude<T, true>) => T
  83. resultToName: (gen: CodeGen, res?: T) => Name
  84. }
  85. function makeMergeEvaluated<T extends SomeEvaluated>({
  86. mergeNames,
  87. mergeToName,
  88. mergeValues,
  89. resultToName,
  90. }: MakeMergeFuncArgs<T>): MergeEvaluatedFunc<T> {
  91. return (gen, from, to, toName) => {
  92. const res =
  93. to === undefined
  94. ? from
  95. : to instanceof Name
  96. ? (from instanceof Name ? mergeNames(gen, from, to) : mergeToName(gen, from, to), to)
  97. : from instanceof Name
  98. ? (mergeToName(gen, to, from), from)
  99. : mergeValues(from, to)
  100. return toName === Name && !(res instanceof Name) ? resultToName(gen, res) : res
  101. }
  102. }
  103. interface MergeEvaluated {
  104. props: MergeEvaluatedFunc<EvaluatedProperties>
  105. items: MergeEvaluatedFunc<EvaluatedItems>
  106. }
  107. export const mergeEvaluated: MergeEvaluated = {
  108. props: makeMergeEvaluated({
  109. mergeNames: (gen, from, to) =>
  110. gen.if(_`${to} !== true && ${from} !== undefined`, () => {
  111. gen.if(
  112. _`${from} === true`,
  113. () => gen.assign(to, true),
  114. () => gen.assign(to, _`${to} || {}`).code(_`Object.assign(${to}, ${from})`)
  115. )
  116. }),
  117. mergeToName: (gen, from, to) =>
  118. gen.if(_`${to} !== true`, () => {
  119. if (from === true) {
  120. gen.assign(to, true)
  121. } else {
  122. gen.assign(to, _`${to} || {}`)
  123. setEvaluated(gen, to, from)
  124. }
  125. }),
  126. mergeValues: (from, to) => (from === true ? true : {...from, ...to}),
  127. resultToName: evaluatedPropsToName,
  128. }),
  129. items: makeMergeEvaluated({
  130. mergeNames: (gen, from, to) =>
  131. gen.if(_`${to} !== true && ${from} !== undefined`, () =>
  132. gen.assign(to, _`${from} === true ? true : ${to} > ${from} ? ${to} : ${from}`)
  133. ),
  134. mergeToName: (gen, from, to) =>
  135. gen.if(_`${to} !== true`, () =>
  136. gen.assign(to, from === true ? true : _`${to} > ${from} ? ${to} : ${from}`)
  137. ),
  138. mergeValues: (from, to) => (from === true ? true : Math.max(from, to)),
  139. resultToName: (gen, items) => gen.var("items", items),
  140. }),
  141. }
  142. export function evaluatedPropsToName(gen: CodeGen, ps?: EvaluatedProperties): Name {
  143. if (ps === true) return gen.var("props", true)
  144. const props = gen.var("props", _`{}`)
  145. if (ps !== undefined) setEvaluated(gen, props, ps)
  146. return props
  147. }
  148. export function setEvaluated(gen: CodeGen, props: Name, ps: {[K in string]?: true}): void {
  149. Object.keys(ps).forEach((p) => gen.assign(_`${props}${getProperty(p)}`, true))
  150. }
  151. const snippets: {[S in string]?: _Code} = {}
  152. export function useFunc(gen: CodeGen, f: {code: string}): Name {
  153. return gen.scopeValue("func", {
  154. ref: f,
  155. code: snippets[f.code] || (snippets[f.code] = new _Code(f.code)),
  156. })
  157. }
  158. export enum Type {
  159. Num,
  160. Str,
  161. }
  162. export function getErrorPath(
  163. dataProp: Name | string | number,
  164. dataPropType?: Type,
  165. jsPropertySyntax?: boolean
  166. ): Code | string {
  167. // let path
  168. if (dataProp instanceof Name) {
  169. const isNumber = dataPropType === Type.Num
  170. return jsPropertySyntax
  171. ? isNumber
  172. ? _`"[" + ${dataProp} + "]"`
  173. : _`"['" + ${dataProp} + "']"`
  174. : isNumber
  175. ? _`"/" + ${dataProp}`
  176. : _`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")` // TODO maybe use global escapePointer
  177. }
  178. return jsPropertySyntax ? getProperty(dataProp).toString() : "/" + escapeJsonPointer(dataProp)
  179. }
  180. export function checkStrictMode(
  181. it: SchemaCxt,
  182. msg: string,
  183. mode: boolean | "log" = it.opts.strictSchema
  184. ): void {
  185. if (!mode) return
  186. msg = `strict mode: ${msg}`
  187. if (mode === true) throw new Error(msg)
  188. it.self.logger.warn(msg)
  189. }