parse.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. import type Ajv from "../../core"
  2. import type {SchemaObject} from "../../types"
  3. import {jtdForms, JTDForm, SchemaObjectMap} from "./types"
  4. import {SchemaEnv, getCompilingSchema} from ".."
  5. import {_, str, and, or, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen"
  6. import MissingRefError from "../ref_error"
  7. import N from "../names"
  8. import {hasPropFunc} from "../../vocabularies/code"
  9. import {hasRef} from "../../vocabularies/jtd/ref"
  10. import {intRange, IntType} from "../../vocabularies/jtd/type"
  11. import {parseJson, parseJsonNumber, parseJsonString} from "../../runtime/parseJson"
  12. import {useFunc} from "../util"
  13. import validTimestamp from "../../runtime/timestamp"
  14. type GenParse = (cxt: ParseCxt) => void
  15. const genParse: {[F in JTDForm]: GenParse} = {
  16. elements: parseElements,
  17. values: parseValues,
  18. discriminator: parseDiscriminator,
  19. properties: parseProperties,
  20. optionalProperties: parseProperties,
  21. enum: parseEnum,
  22. type: parseType,
  23. ref: parseRef,
  24. }
  25. interface ParseCxt {
  26. readonly gen: CodeGen
  27. readonly self: Ajv // current Ajv instance
  28. readonly schemaEnv: SchemaEnv
  29. readonly definitions: SchemaObjectMap
  30. schema: SchemaObject
  31. data: Code
  32. parseName: Name
  33. char: Name
  34. }
  35. export default function compileParser(
  36. this: Ajv,
  37. sch: SchemaEnv,
  38. definitions: SchemaObjectMap
  39. ): SchemaEnv {
  40. const _sch = getCompilingSchema.call(this, sch)
  41. if (_sch) return _sch
  42. const {es5, lines} = this.opts.code
  43. const {ownProperties} = this.opts
  44. const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
  45. const parseName = gen.scopeName("parse")
  46. const cxt: ParseCxt = {
  47. self: this,
  48. gen,
  49. schema: sch.schema as SchemaObject,
  50. schemaEnv: sch,
  51. definitions,
  52. data: N.data,
  53. parseName,
  54. char: gen.name("c"),
  55. }
  56. let sourceCode: string | undefined
  57. try {
  58. this._compilations.add(sch)
  59. sch.parseName = parseName
  60. parserFunction(cxt)
  61. gen.optimize(this.opts.code.optimize)
  62. const parseFuncCode = gen.toString()
  63. sourceCode = `${gen.scopeRefs(N.scope)}return ${parseFuncCode}`
  64. const makeParse = new Function(`${N.scope}`, sourceCode)
  65. const parse: (json: string) => unknown = makeParse(this.scope.get())
  66. this.scope.value(parseName, {ref: parse})
  67. sch.parse = parse
  68. } catch (e) {
  69. if (sourceCode) this.logger.error("Error compiling parser, function code:", sourceCode)
  70. delete sch.parse
  71. delete sch.parseName
  72. throw e
  73. } finally {
  74. this._compilations.delete(sch)
  75. }
  76. return sch
  77. }
  78. const undef = _`undefined`
  79. function parserFunction(cxt: ParseCxt): void {
  80. const {gen, parseName, char} = cxt
  81. gen.func(parseName, _`${N.json}, ${N.jsonPos}, ${N.jsonPart}`, false, () => {
  82. gen.let(N.data)
  83. gen.let(char)
  84. gen.assign(_`${parseName}.message`, undef)
  85. gen.assign(_`${parseName}.position`, undef)
  86. gen.assign(N.jsonPos, _`${N.jsonPos} || 0`)
  87. gen.const(N.jsonLen, _`${N.json}.length`)
  88. parseCode(cxt)
  89. skipWhitespace(cxt)
  90. gen.if(N.jsonPart, () => {
  91. gen.assign(_`${parseName}.position`, N.jsonPos)
  92. gen.return(N.data)
  93. })
  94. gen.if(_`${N.jsonPos} === ${N.jsonLen}`, () => gen.return(N.data))
  95. jsonSyntaxError(cxt)
  96. })
  97. }
  98. function parseCode(cxt: ParseCxt): void {
  99. let form: JTDForm | undefined
  100. for (const key of jtdForms) {
  101. if (key in cxt.schema) {
  102. form = key
  103. break
  104. }
  105. }
  106. if (form) parseNullable(cxt, genParse[form])
  107. else parseEmpty(cxt)
  108. }
  109. const parseBoolean = parseBooleanToken(true, parseBooleanToken(false, jsonSyntaxError))
  110. function parseNullable(cxt: ParseCxt, parseForm: GenParse): void {
  111. const {gen, schema, data} = cxt
  112. if (!schema.nullable) return parseForm(cxt)
  113. tryParseToken(cxt, "null", parseForm, () => gen.assign(data, null))
  114. }
  115. function parseElements(cxt: ParseCxt): void {
  116. const {gen, schema, data} = cxt
  117. parseToken(cxt, "[")
  118. const ix = gen.let("i", 0)
  119. gen.assign(data, _`[]`)
  120. parseItems(cxt, "]", () => {
  121. const el = gen.let("el")
  122. parseCode({...cxt, schema: schema.elements, data: el})
  123. gen.assign(_`${data}[${ix}++]`, el)
  124. })
  125. }
  126. function parseValues(cxt: ParseCxt): void {
  127. const {gen, schema, data} = cxt
  128. parseToken(cxt, "{")
  129. gen.assign(data, _`{}`)
  130. parseItems(cxt, "}", () => parseKeyValue(cxt, schema.values))
  131. }
  132. function parseItems(cxt: ParseCxt, endToken: string, block: () => void): void {
  133. tryParseItems(cxt, endToken, block)
  134. parseToken(cxt, endToken)
  135. }
  136. function tryParseItems(cxt: ParseCxt, endToken: string, block: () => void): void {
  137. const {gen} = cxt
  138. gen.for(_`;${N.jsonPos}<${N.jsonLen} && ${jsonSlice(1)}!==${endToken};`, () => {
  139. block()
  140. tryParseToken(cxt, ",", () => gen.break(), hasItem)
  141. })
  142. function hasItem(): void {
  143. tryParseToken(cxt, endToken, () => {}, jsonSyntaxError)
  144. }
  145. }
  146. function parseKeyValue(cxt: ParseCxt, schema: SchemaObject): void {
  147. const {gen} = cxt
  148. const key = gen.let("key")
  149. parseString({...cxt, data: key})
  150. parseToken(cxt, ":")
  151. parsePropertyValue(cxt, key, schema)
  152. }
  153. function parseDiscriminator(cxt: ParseCxt): void {
  154. const {gen, data, schema} = cxt
  155. const {discriminator, mapping} = schema
  156. parseToken(cxt, "{")
  157. gen.assign(data, _`{}`)
  158. const startPos = gen.const("pos", N.jsonPos)
  159. const value = gen.let("value")
  160. const tag = gen.let("tag")
  161. tryParseItems(cxt, "}", () => {
  162. const key = gen.let("key")
  163. parseString({...cxt, data: key})
  164. parseToken(cxt, ":")
  165. gen.if(
  166. _`${key} === ${discriminator}`,
  167. () => {
  168. parseString({...cxt, data: tag})
  169. gen.assign(_`${data}[${key}]`, tag)
  170. gen.break()
  171. },
  172. () => parseEmpty({...cxt, data: value}) // can be discarded/skipped
  173. )
  174. })
  175. gen.assign(N.jsonPos, startPos)
  176. gen.if(_`${tag} === undefined`)
  177. parsingError(cxt, str`discriminator tag not found`)
  178. for (const tagValue in mapping) {
  179. gen.elseIf(_`${tag} === ${tagValue}`)
  180. parseSchemaProperties({...cxt, schema: mapping[tagValue]}, discriminator)
  181. }
  182. gen.else()
  183. parsingError(cxt, str`discriminator value not in schema`)
  184. gen.endIf()
  185. }
  186. function parseProperties(cxt: ParseCxt): void {
  187. const {gen, data} = cxt
  188. parseToken(cxt, "{")
  189. gen.assign(data, _`{}`)
  190. parseSchemaProperties(cxt)
  191. }
  192. function parseSchemaProperties(cxt: ParseCxt, discriminator?: string): void {
  193. const {gen, schema, data} = cxt
  194. const {properties, optionalProperties, additionalProperties} = schema
  195. parseItems(cxt, "}", () => {
  196. const key = gen.let("key")
  197. parseString({...cxt, data: key})
  198. parseToken(cxt, ":")
  199. gen.if(false)
  200. parseDefinedProperty(cxt, key, properties)
  201. parseDefinedProperty(cxt, key, optionalProperties)
  202. if (discriminator) {
  203. gen.elseIf(_`${key} === ${discriminator}`)
  204. const tag = gen.let("tag")
  205. parseString({...cxt, data: tag}) // can be discarded, it is already assigned
  206. }
  207. gen.else()
  208. if (additionalProperties) {
  209. parseEmpty({...cxt, data: _`${data}[${key}]`})
  210. } else {
  211. parsingError(cxt, str`property ${key} not allowed`)
  212. }
  213. gen.endIf()
  214. })
  215. if (properties) {
  216. const hasProp = hasPropFunc(gen)
  217. const allProps: Code = and(
  218. ...Object.keys(properties).map((p): Code => _`${hasProp}.call(${data}, ${p})`)
  219. )
  220. gen.if(not(allProps), () => parsingError(cxt, str`missing required properties`))
  221. }
  222. }
  223. function parseDefinedProperty(cxt: ParseCxt, key: Name, schemas: SchemaObjectMap = {}): void {
  224. const {gen} = cxt
  225. for (const prop in schemas) {
  226. gen.elseIf(_`${key} === ${prop}`)
  227. parsePropertyValue(cxt, key, schemas[prop] as SchemaObject)
  228. }
  229. }
  230. function parsePropertyValue(cxt: ParseCxt, key: Name, schema: SchemaObject): void {
  231. parseCode({...cxt, schema, data: _`${cxt.data}[${key}]`})
  232. }
  233. function parseType(cxt: ParseCxt): void {
  234. const {gen, schema, data, self} = cxt
  235. switch (schema.type) {
  236. case "boolean":
  237. parseBoolean(cxt)
  238. break
  239. case "string":
  240. parseString(cxt)
  241. break
  242. case "timestamp": {
  243. parseString(cxt)
  244. const vts = useFunc(gen, validTimestamp)
  245. const {allowDate, parseDate} = self.opts
  246. const notValid = allowDate ? _`!${vts}(${data}, true)` : _`!${vts}(${data})`
  247. const fail: Code = parseDate
  248. ? or(notValid, _`(${data} = new Date(${data}), false)`, _`isNaN(${data}.valueOf())`)
  249. : notValid
  250. gen.if(fail, () => parsingError(cxt, str`invalid timestamp`))
  251. break
  252. }
  253. case "float32":
  254. case "float64":
  255. parseNumber(cxt)
  256. break
  257. default: {
  258. const t = schema.type as IntType
  259. if (!self.opts.int32range && (t === "int32" || t === "uint32")) {
  260. parseNumber(cxt, 16) // 2 ** 53 - max safe integer
  261. if (t === "uint32") {
  262. gen.if(_`${data} < 0`, () => parsingError(cxt, str`integer out of range`))
  263. }
  264. } else {
  265. const [min, max, maxDigits] = intRange[t]
  266. parseNumber(cxt, maxDigits)
  267. gen.if(_`${data} < ${min} || ${data} > ${max}`, () =>
  268. parsingError(cxt, str`integer out of range`)
  269. )
  270. }
  271. }
  272. }
  273. }
  274. function parseString(cxt: ParseCxt): void {
  275. parseToken(cxt, '"')
  276. parseWith(cxt, parseJsonString)
  277. }
  278. function parseEnum(cxt: ParseCxt): void {
  279. const {gen, data, schema} = cxt
  280. const enumSch = schema.enum
  281. parseToken(cxt, '"')
  282. // TODO loopEnum
  283. gen.if(false)
  284. for (const value of enumSch) {
  285. const valueStr = JSON.stringify(value).slice(1) // remove starting quote
  286. gen.elseIf(_`${jsonSlice(valueStr.length)} === ${valueStr}`)
  287. gen.assign(data, str`${value}`)
  288. gen.add(N.jsonPos, valueStr.length)
  289. }
  290. gen.else()
  291. jsonSyntaxError(cxt)
  292. gen.endIf()
  293. }
  294. function parseNumber(cxt: ParseCxt, maxDigits?: number): void {
  295. const {gen} = cxt
  296. skipWhitespace(cxt)
  297. gen.if(
  298. _`"-0123456789".indexOf(${jsonSlice(1)}) < 0`,
  299. () => jsonSyntaxError(cxt),
  300. () => parseWith(cxt, parseJsonNumber, maxDigits)
  301. )
  302. }
  303. function parseBooleanToken(bool: boolean, fail: GenParse): GenParse {
  304. return (cxt) => {
  305. const {gen, data} = cxt
  306. tryParseToken(
  307. cxt,
  308. `${bool}`,
  309. () => fail(cxt),
  310. () => gen.assign(data, bool)
  311. )
  312. }
  313. }
  314. function parseRef(cxt: ParseCxt): void {
  315. const {gen, self, definitions, schema, schemaEnv} = cxt
  316. const {ref} = schema
  317. const refSchema = definitions[ref]
  318. if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
  319. if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema})
  320. const {root} = schemaEnv
  321. const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
  322. partialParse(cxt, getParser(gen, sch), true)
  323. }
  324. function getParser(gen: CodeGen, sch: SchemaEnv): Code {
  325. return sch.parse
  326. ? gen.scopeValue("parse", {ref: sch.parse})
  327. : _`${gen.scopeValue("wrapper", {ref: sch})}.parse`
  328. }
  329. function parseEmpty(cxt: ParseCxt): void {
  330. parseWith(cxt, parseJson)
  331. }
  332. function parseWith(cxt: ParseCxt, parseFunc: {code: string}, args?: SafeExpr): void {
  333. partialParse(cxt, useFunc(cxt.gen, parseFunc), args)
  334. }
  335. function partialParse(cxt: ParseCxt, parseFunc: Name, args?: SafeExpr): void {
  336. const {gen, data} = cxt
  337. gen.assign(data, _`${parseFunc}(${N.json}, ${N.jsonPos}${args ? _`, ${args}` : nil})`)
  338. gen.assign(N.jsonPos, _`${parseFunc}.position`)
  339. gen.if(_`${data} === undefined`, () => parsingError(cxt, _`${parseFunc}.message`))
  340. }
  341. function parseToken(cxt: ParseCxt, tok: string): void {
  342. tryParseToken(cxt, tok, jsonSyntaxError)
  343. }
  344. function tryParseToken(cxt: ParseCxt, tok: string, fail: GenParse, success?: GenParse): void {
  345. const {gen} = cxt
  346. const n = tok.length
  347. skipWhitespace(cxt)
  348. gen.if(
  349. _`${jsonSlice(n)} === ${tok}`,
  350. () => {
  351. gen.add(N.jsonPos, n)
  352. success?.(cxt)
  353. },
  354. () => fail(cxt)
  355. )
  356. }
  357. function skipWhitespace({gen, char: c}: ParseCxt): void {
  358. gen.code(
  359. _`while((${c}=${N.json}[${N.jsonPos}],${c}===" "||${c}==="\\n"||${c}==="\\r"||${c}==="\\t"))${N.jsonPos}++;`
  360. )
  361. }
  362. function jsonSlice(len: number | Name): Code {
  363. return len === 1
  364. ? _`${N.json}[${N.jsonPos}]`
  365. : _`${N.json}.slice(${N.jsonPos}, ${N.jsonPos}+${len})`
  366. }
  367. function jsonSyntaxError(cxt: ParseCxt): void {
  368. parsingError(cxt, _`"unexpected token " + ${N.json}[${N.jsonPos}]`)
  369. }
  370. function parsingError({gen, parseName}: ParseCxt, msg: Code): void {
  371. gen.assign(_`${parseName}.message`, msg)
  372. gen.assign(_`${parseName}.position`, N.jsonPos)
  373. gen.return(undef)
  374. }