parse.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen"
  6. import {MissingRefError} from "../error_classes"
  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 {func} from "../util"
  13. import validTimestamp from "../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} = 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. // TODO parse timestamp?
  244. parseString(cxt)
  245. const vts = func(gen, validTimestamp)
  246. gen.if(_`!${vts}(${data})`, () => parsingError(cxt, str`invalid timestamp`))
  247. break
  248. }
  249. case "float32":
  250. case "float64":
  251. parseNumber(cxt)
  252. break
  253. default: {
  254. const [min, max, maxDigits] = intRange[schema.type as IntType]
  255. parseNumber(cxt, maxDigits)
  256. gen.if(_`${data} < ${min} || ${data} > ${max}`, () =>
  257. parsingError(cxt, str`integer out of range`)
  258. )
  259. }
  260. }
  261. }
  262. function parseString(cxt: ParseCxt): void {
  263. parseToken(cxt, '"')
  264. parseWith(cxt, parseJsonString)
  265. }
  266. function parseEnum(cxt: ParseCxt): void {
  267. const {gen, data, schema} = cxt
  268. const enumSch = schema.enum
  269. parseToken(cxt, '"')
  270. // TODO loopEnum
  271. gen.if(false)
  272. for (const value of enumSch) {
  273. const valueStr = JSON.stringify(value).slice(1) // remove starting quote
  274. gen.elseIf(_`${jsonSlice(valueStr.length)} === ${valueStr}`)
  275. gen.assign(data, str`${value}`)
  276. gen.add(N.jsonPos, valueStr.length)
  277. }
  278. gen.else()
  279. jsonSyntaxError(cxt)
  280. gen.endIf()
  281. }
  282. function parseNumber(cxt: ParseCxt, maxDigits?: number): void {
  283. const {gen} = cxt
  284. skipWhitespace(cxt)
  285. gen.if(
  286. _`"-0123456789".indexOf(${jsonSlice(1)}) < 0`,
  287. () => jsonSyntaxError(cxt),
  288. () => parseWith(cxt, parseJsonNumber, maxDigits)
  289. )
  290. }
  291. function parseBooleanToken(bool: boolean, fail: GenParse): GenParse {
  292. return (cxt) => {
  293. const {gen, data} = cxt
  294. tryParseToken(
  295. cxt,
  296. `${bool}`,
  297. () => fail(cxt),
  298. () => gen.assign(data, bool)
  299. )
  300. }
  301. }
  302. function parseRef(cxt: ParseCxt): void {
  303. const {gen, self, definitions, schema, schemaEnv} = cxt
  304. const {ref} = schema
  305. const refSchema = definitions[ref]
  306. if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
  307. if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema})
  308. const {root} = schemaEnv
  309. const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
  310. partialParse(cxt, getParser(gen, sch), true)
  311. }
  312. function getParser(gen: CodeGen, sch: SchemaEnv): Code {
  313. return sch.parse
  314. ? gen.scopeValue("parse", {ref: sch.parse})
  315. : _`${gen.scopeValue("wrapper", {ref: sch})}.parse`
  316. }
  317. function parseEmpty(cxt: ParseCxt): void {
  318. parseWith(cxt, parseJson)
  319. }
  320. function parseWith(cxt: ParseCxt, parseFunc: {code: Code}, args?: SafeExpr): void {
  321. const f = cxt.gen.scopeValue("func", {
  322. ref: parseFunc,
  323. code: parseFunc.code,
  324. })
  325. partialParse(cxt, f, args)
  326. }
  327. function partialParse(cxt: ParseCxt, parseFunc: Name, args?: SafeExpr): void {
  328. const {gen, data} = cxt
  329. gen.assign(data, _`${parseFunc}(${N.json}, ${N.jsonPos}${args ? _`, ${args}` : nil})`)
  330. gen.assign(N.jsonPos, _`${parseFunc}.position`)
  331. gen.if(_`${data} === undefined`, () => parsingError(cxt, _`${parseFunc}.message`))
  332. }
  333. function parseToken(cxt: ParseCxt, tok: string): void {
  334. tryParseToken(cxt, tok, jsonSyntaxError)
  335. }
  336. function tryParseToken(cxt: ParseCxt, tok: string, fail: GenParse, success?: GenParse): void {
  337. const {gen} = cxt
  338. const n = tok.length
  339. skipWhitespace(cxt)
  340. gen.if(
  341. _`${jsonSlice(n)} === ${tok}`,
  342. () => {
  343. gen.add(N.jsonPos, n)
  344. success?.(cxt)
  345. },
  346. () => fail(cxt)
  347. )
  348. }
  349. function skipWhitespace({gen, char: c}: ParseCxt): void {
  350. gen.code(
  351. _`while((${c}=${N.json}[${N.jsonPos}],${c}===" "||${c}==="\\n"||${c}==="\\r"||${c}==="\\t"))${N.jsonPos}++;`
  352. )
  353. }
  354. function jsonSlice(len: number | Name): Code {
  355. return len === 1
  356. ? _`${N.json}[${N.jsonPos}]`
  357. : _`${N.json}.slice(${N.jsonPos}, ${N.jsonPos}+${len})`
  358. }
  359. function jsonSyntaxError(cxt: ParseCxt): void {
  360. parsingError(cxt, _`"unexpected token " + ${N.json}[${N.jsonPos}]`)
  361. }
  362. function parsingError({gen, parseName}: ParseCxt, msg: Code): void {
  363. gen.assign(_`${parseName}.message`, msg)
  364. gen.assign(_`${parseName}.position`, N.jsonPos)
  365. gen.return(undef)
  366. }