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