You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					412 lines
				
				12 KiB
			
		
		
			
		
	
	
					412 lines
				
				12 KiB
			| 
											3 years ago
										 | import type Ajv from "../../core" | ||
|  | import type {SchemaObject} from "../../types" | ||
|  | import {jtdForms, JTDForm, SchemaObjectMap} from "./types" | ||
|  | import {SchemaEnv, getCompilingSchema} from ".." | ||
|  | import {_, str, and, or, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen" | ||
|  | import MissingRefError from "../ref_error" | ||
|  | import N from "../names" | ||
|  | import {hasPropFunc} from "../../vocabularies/code" | ||
|  | import {hasRef} from "../../vocabularies/jtd/ref" | ||
|  | import {intRange, IntType} from "../../vocabularies/jtd/type" | ||
|  | import {parseJson, parseJsonNumber, parseJsonString} from "../../runtime/parseJson" | ||
|  | import {useFunc} from "../util" | ||
|  | import validTimestamp from "../../runtime/timestamp" | ||
|  | 
 | ||
|  | type GenParse = (cxt: ParseCxt) => void | ||
|  | 
 | ||
|  | const genParse: {[F in JTDForm]: GenParse} = { | ||
|  |   elements: parseElements, | ||
|  |   values: parseValues, | ||
|  |   discriminator: parseDiscriminator, | ||
|  |   properties: parseProperties, | ||
|  |   optionalProperties: parseProperties, | ||
|  |   enum: parseEnum, | ||
|  |   type: parseType, | ||
|  |   ref: parseRef, | ||
|  | } | ||
|  | 
 | ||
|  | interface ParseCxt { | ||
|  |   readonly gen: CodeGen | ||
|  |   readonly self: Ajv // current Ajv instance
 | ||
|  |   readonly schemaEnv: SchemaEnv | ||
|  |   readonly definitions: SchemaObjectMap | ||
|  |   schema: SchemaObject | ||
|  |   data: Code | ||
|  |   parseName: Name | ||
|  |   char: Name | ||
|  | } | ||
|  | 
 | ||
|  | export default function compileParser( | ||
|  |   this: Ajv, | ||
|  |   sch: SchemaEnv, | ||
|  |   definitions: SchemaObjectMap | ||
|  | ): SchemaEnv { | ||
|  |   const _sch = getCompilingSchema.call(this, sch) | ||
|  |   if (_sch) return _sch | ||
|  |   const {es5, lines} = this.opts.code | ||
|  |   const {ownProperties} = this.opts | ||
|  |   const gen = new CodeGen(this.scope, {es5, lines, ownProperties}) | ||
|  |   const parseName = gen.scopeName("parse") | ||
|  |   const cxt: ParseCxt = { | ||
|  |     self: this, | ||
|  |     gen, | ||
|  |     schema: sch.schema as SchemaObject, | ||
|  |     schemaEnv: sch, | ||
|  |     definitions, | ||
|  |     data: N.data, | ||
|  |     parseName, | ||
|  |     char: gen.name("c"), | ||
|  |   } | ||
|  | 
 | ||
|  |   let sourceCode: string | undefined | ||
|  |   try { | ||
|  |     this._compilations.add(sch) | ||
|  |     sch.parseName = parseName | ||
|  |     parserFunction(cxt) | ||
|  |     gen.optimize(this.opts.code.optimize) | ||
|  |     const parseFuncCode = gen.toString() | ||
|  |     sourceCode = `${gen.scopeRefs(N.scope)}return ${parseFuncCode}` | ||
|  |     const makeParse = new Function(`${N.scope}`, sourceCode) | ||
|  |     const parse: (json: string) => unknown = makeParse(this.scope.get()) | ||
|  |     this.scope.value(parseName, {ref: parse}) | ||
|  |     sch.parse = parse | ||
|  |   } catch (e) { | ||
|  |     if (sourceCode) this.logger.error("Error compiling parser, function code:", sourceCode) | ||
|  |     delete sch.parse | ||
|  |     delete sch.parseName | ||
|  |     throw e | ||
|  |   } finally { | ||
|  |     this._compilations.delete(sch) | ||
|  |   } | ||
|  |   return sch | ||
|  | } | ||
|  | 
 | ||
|  | const undef = _`undefined` | ||
|  | 
 | ||
|  | function parserFunction(cxt: ParseCxt): void { | ||
|  |   const {gen, parseName, char} = cxt | ||
|  |   gen.func(parseName, _`${N.json}, ${N.jsonPos}, ${N.jsonPart}`, false, () => { | ||
|  |     gen.let(N.data) | ||
|  |     gen.let(char) | ||
|  |     gen.assign(_`${parseName}.message`, undef) | ||
|  |     gen.assign(_`${parseName}.position`, undef) | ||
|  |     gen.assign(N.jsonPos, _`${N.jsonPos} || 0`) | ||
|  |     gen.const(N.jsonLen, _`${N.json}.length`) | ||
|  |     parseCode(cxt) | ||
|  |     skipWhitespace(cxt) | ||
|  |     gen.if(N.jsonPart, () => { | ||
|  |       gen.assign(_`${parseName}.position`, N.jsonPos) | ||
|  |       gen.return(N.data) | ||
|  |     }) | ||
|  |     gen.if(_`${N.jsonPos} === ${N.jsonLen}`, () => gen.return(N.data)) | ||
|  |     jsonSyntaxError(cxt) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | function parseCode(cxt: ParseCxt): void { | ||
|  |   let form: JTDForm | undefined | ||
|  |   for (const key of jtdForms) { | ||
|  |     if (key in cxt.schema) { | ||
|  |       form = key | ||
|  |       break | ||
|  |     } | ||
|  |   } | ||
|  |   if (form) parseNullable(cxt, genParse[form]) | ||
|  |   else parseEmpty(cxt) | ||
|  | } | ||
|  | 
 | ||
|  | const parseBoolean = parseBooleanToken(true, parseBooleanToken(false, jsonSyntaxError)) | ||
|  | 
 | ||
|  | function parseNullable(cxt: ParseCxt, parseForm: GenParse): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   if (!schema.nullable) return parseForm(cxt) | ||
|  |   tryParseToken(cxt, "null", parseForm, () => gen.assign(data, null)) | ||
|  | } | ||
|  | 
 | ||
|  | function parseElements(cxt: ParseCxt): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   parseToken(cxt, "[") | ||
|  |   const ix = gen.let("i", 0) | ||
|  |   gen.assign(data, _`[]`) | ||
|  |   parseItems(cxt, "]", () => { | ||
|  |     const el = gen.let("el") | ||
|  |     parseCode({...cxt, schema: schema.elements, data: el}) | ||
|  |     gen.assign(_`${data}[${ix}++]`, el) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | function parseValues(cxt: ParseCxt): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   parseToken(cxt, "{") | ||
|  |   gen.assign(data, _`{}`) | ||
|  |   parseItems(cxt, "}", () => parseKeyValue(cxt, schema.values)) | ||
|  | } | ||
|  | 
 | ||
|  | function parseItems(cxt: ParseCxt, endToken: string, block: () => void): void { | ||
|  |   tryParseItems(cxt, endToken, block) | ||
|  |   parseToken(cxt, endToken) | ||
|  | } | ||
|  | 
 | ||
|  | function tryParseItems(cxt: ParseCxt, endToken: string, block: () => void): void { | ||
|  |   const {gen} = cxt | ||
|  |   gen.for(_`;${N.jsonPos}<${N.jsonLen} && ${jsonSlice(1)}!==${endToken};`, () => { | ||
|  |     block() | ||
|  |     tryParseToken(cxt, ",", () => gen.break(), hasItem) | ||
|  |   }) | ||
|  | 
 | ||
|  |   function hasItem(): void { | ||
|  |     tryParseToken(cxt, endToken, () => {}, jsonSyntaxError) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function parseKeyValue(cxt: ParseCxt, schema: SchemaObject): void { | ||
|  |   const {gen} = cxt | ||
|  |   const key = gen.let("key") | ||
|  |   parseString({...cxt, data: key}) | ||
|  |   parseToken(cxt, ":") | ||
|  |   parsePropertyValue(cxt, key, schema) | ||
|  | } | ||
|  | 
 | ||
|  | function parseDiscriminator(cxt: ParseCxt): void { | ||
|  |   const {gen, data, schema} = cxt | ||
|  |   const {discriminator, mapping} = schema | ||
|  |   parseToken(cxt, "{") | ||
|  |   gen.assign(data, _`{}`) | ||
|  |   const startPos = gen.const("pos", N.jsonPos) | ||
|  |   const value = gen.let("value") | ||
|  |   const tag = gen.let("tag") | ||
|  |   tryParseItems(cxt, "}", () => { | ||
|  |     const key = gen.let("key") | ||
|  |     parseString({...cxt, data: key}) | ||
|  |     parseToken(cxt, ":") | ||
|  |     gen.if( | ||
|  |       _`${key} === ${discriminator}`, | ||
|  |       () => { | ||
|  |         parseString({...cxt, data: tag}) | ||
|  |         gen.assign(_`${data}[${key}]`, tag) | ||
|  |         gen.break() | ||
|  |       }, | ||
|  |       () => parseEmpty({...cxt, data: value}) // can be discarded/skipped
 | ||
|  |     ) | ||
|  |   }) | ||
|  |   gen.assign(N.jsonPos, startPos) | ||
|  |   gen.if(_`${tag} === undefined`) | ||
|  |   parsingError(cxt, str`discriminator tag not found`) | ||
|  |   for (const tagValue in mapping) { | ||
|  |     gen.elseIf(_`${tag} === ${tagValue}`) | ||
|  |     parseSchemaProperties({...cxt, schema: mapping[tagValue]}, discriminator) | ||
|  |   } | ||
|  |   gen.else() | ||
|  |   parsingError(cxt, str`discriminator value not in schema`) | ||
|  |   gen.endIf() | ||
|  | } | ||
|  | 
 | ||
|  | function parseProperties(cxt: ParseCxt): void { | ||
|  |   const {gen, data} = cxt | ||
|  |   parseToken(cxt, "{") | ||
|  |   gen.assign(data, _`{}`) | ||
|  |   parseSchemaProperties(cxt) | ||
|  | } | ||
|  | 
 | ||
|  | function parseSchemaProperties(cxt: ParseCxt, discriminator?: string): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   const {properties, optionalProperties, additionalProperties} = schema | ||
|  |   parseItems(cxt, "}", () => { | ||
|  |     const key = gen.let("key") | ||
|  |     parseString({...cxt, data: key}) | ||
|  |     parseToken(cxt, ":") | ||
|  |     gen.if(false) | ||
|  |     parseDefinedProperty(cxt, key, properties) | ||
|  |     parseDefinedProperty(cxt, key, optionalProperties) | ||
|  |     if (discriminator) { | ||
|  |       gen.elseIf(_`${key} === ${discriminator}`) | ||
|  |       const tag = gen.let("tag") | ||
|  |       parseString({...cxt, data: tag}) // can be discarded, it is already assigned
 | ||
|  |     } | ||
|  |     gen.else() | ||
|  |     if (additionalProperties) { | ||
|  |       parseEmpty({...cxt, data: _`${data}[${key}]`}) | ||
|  |     } else { | ||
|  |       parsingError(cxt, str`property ${key} not allowed`) | ||
|  |     } | ||
|  |     gen.endIf() | ||
|  |   }) | ||
|  |   if (properties) { | ||
|  |     const hasProp = hasPropFunc(gen) | ||
|  |     const allProps: Code = and( | ||
|  |       ...Object.keys(properties).map((p): Code => _`${hasProp}.call(${data}, ${p})`) | ||
|  |     ) | ||
|  |     gen.if(not(allProps), () => parsingError(cxt, str`missing required properties`)) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function parseDefinedProperty(cxt: ParseCxt, key: Name, schemas: SchemaObjectMap = {}): void { | ||
|  |   const {gen} = cxt | ||
|  |   for (const prop in schemas) { | ||
|  |     gen.elseIf(_`${key} === ${prop}`) | ||
|  |     parsePropertyValue(cxt, key, schemas[prop] as SchemaObject) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function parsePropertyValue(cxt: ParseCxt, key: Name, schema: SchemaObject): void { | ||
|  |   parseCode({...cxt, schema, data: _`${cxt.data}[${key}]`}) | ||
|  | } | ||
|  | 
 | ||
|  | function parseType(cxt: ParseCxt): void { | ||
|  |   const {gen, schema, data, self} = cxt | ||
|  |   switch (schema.type) { | ||
|  |     case "boolean": | ||
|  |       parseBoolean(cxt) | ||
|  |       break | ||
|  |     case "string": | ||
|  |       parseString(cxt) | ||
|  |       break | ||
|  |     case "timestamp": { | ||
|  |       parseString(cxt) | ||
|  |       const vts = useFunc(gen, validTimestamp) | ||
|  |       const {allowDate, parseDate} = self.opts | ||
|  |       const notValid = allowDate ? _`!${vts}(${data}, true)` : _`!${vts}(${data})` | ||
|  |       const fail: Code = parseDate | ||
|  |         ? or(notValid, _`(${data} = new Date(${data}), false)`, _`isNaN(${data}.valueOf())`) | ||
|  |         : notValid | ||
|  |       gen.if(fail, () => parsingError(cxt, str`invalid timestamp`)) | ||
|  |       break | ||
|  |     } | ||
|  |     case "float32": | ||
|  |     case "float64": | ||
|  |       parseNumber(cxt) | ||
|  |       break | ||
|  |     default: { | ||
|  |       const t = schema.type as IntType | ||
|  |       if (!self.opts.int32range && (t === "int32" || t === "uint32")) { | ||
|  |         parseNumber(cxt, 16) // 2 ** 53 - max safe integer
 | ||
|  |         if (t === "uint32") { | ||
|  |           gen.if(_`${data} < 0`, () => parsingError(cxt, str`integer out of range`)) | ||
|  |         } | ||
|  |       } else { | ||
|  |         const [min, max, maxDigits] = intRange[t] | ||
|  |         parseNumber(cxt, maxDigits) | ||
|  |         gen.if(_`${data} < ${min} || ${data} > ${max}`, () => | ||
|  |           parsingError(cxt, str`integer out of range`) | ||
|  |         ) | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function parseString(cxt: ParseCxt): void { | ||
|  |   parseToken(cxt, '"') | ||
|  |   parseWith(cxt, parseJsonString) | ||
|  | } | ||
|  | 
 | ||
|  | function parseEnum(cxt: ParseCxt): void { | ||
|  |   const {gen, data, schema} = cxt | ||
|  |   const enumSch = schema.enum | ||
|  |   parseToken(cxt, '"') | ||
|  |   // TODO loopEnum
 | ||
|  |   gen.if(false) | ||
|  |   for (const value of enumSch) { | ||
|  |     const valueStr = JSON.stringify(value).slice(1) // remove starting quote
 | ||
|  |     gen.elseIf(_`${jsonSlice(valueStr.length)} === ${valueStr}`) | ||
|  |     gen.assign(data, str`${value}`) | ||
|  |     gen.add(N.jsonPos, valueStr.length) | ||
|  |   } | ||
|  |   gen.else() | ||
|  |   jsonSyntaxError(cxt) | ||
|  |   gen.endIf() | ||
|  | } | ||
|  | 
 | ||
|  | function parseNumber(cxt: ParseCxt, maxDigits?: number): void { | ||
|  |   const {gen} = cxt | ||
|  |   skipWhitespace(cxt) | ||
|  |   gen.if( | ||
|  |     _`"-0123456789".indexOf(${jsonSlice(1)}) < 0`, | ||
|  |     () => jsonSyntaxError(cxt), | ||
|  |     () => parseWith(cxt, parseJsonNumber, maxDigits) | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function parseBooleanToken(bool: boolean, fail: GenParse): GenParse { | ||
|  |   return (cxt) => { | ||
|  |     const {gen, data} = cxt | ||
|  |     tryParseToken( | ||
|  |       cxt, | ||
|  |       `${bool}`, | ||
|  |       () => fail(cxt), | ||
|  |       () => gen.assign(data, bool) | ||
|  |     ) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function parseRef(cxt: ParseCxt): void { | ||
|  |   const {gen, self, definitions, schema, schemaEnv} = cxt | ||
|  |   const {ref} = schema | ||
|  |   const refSchema = definitions[ref] | ||
|  |   if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`) | ||
|  |   if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema}) | ||
|  |   const {root} = schemaEnv | ||
|  |   const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions) | ||
|  |   partialParse(cxt, getParser(gen, sch), true) | ||
|  | } | ||
|  | 
 | ||
|  | function getParser(gen: CodeGen, sch: SchemaEnv): Code { | ||
|  |   return sch.parse | ||
|  |     ? gen.scopeValue("parse", {ref: sch.parse}) | ||
|  |     : _`${gen.scopeValue("wrapper", {ref: sch})}.parse` | ||
|  | } | ||
|  | 
 | ||
|  | function parseEmpty(cxt: ParseCxt): void { | ||
|  |   parseWith(cxt, parseJson) | ||
|  | } | ||
|  | 
 | ||
|  | function parseWith(cxt: ParseCxt, parseFunc: {code: string}, args?: SafeExpr): void { | ||
|  |   partialParse(cxt, useFunc(cxt.gen, parseFunc), args) | ||
|  | } | ||
|  | 
 | ||
|  | function partialParse(cxt: ParseCxt, parseFunc: Name, args?: SafeExpr): void { | ||
|  |   const {gen, data} = cxt | ||
|  |   gen.assign(data, _`${parseFunc}(${N.json}, ${N.jsonPos}${args ? _`, ${args}` : nil})`) | ||
|  |   gen.assign(N.jsonPos, _`${parseFunc}.position`) | ||
|  |   gen.if(_`${data} === undefined`, () => parsingError(cxt, _`${parseFunc}.message`)) | ||
|  | } | ||
|  | 
 | ||
|  | function parseToken(cxt: ParseCxt, tok: string): void { | ||
|  |   tryParseToken(cxt, tok, jsonSyntaxError) | ||
|  | } | ||
|  | 
 | ||
|  | function tryParseToken(cxt: ParseCxt, tok: string, fail: GenParse, success?: GenParse): void { | ||
|  |   const {gen} = cxt | ||
|  |   const n = tok.length | ||
|  |   skipWhitespace(cxt) | ||
|  |   gen.if( | ||
|  |     _`${jsonSlice(n)} === ${tok}`, | ||
|  |     () => { | ||
|  |       gen.add(N.jsonPos, n) | ||
|  |       success?.(cxt) | ||
|  |     }, | ||
|  |     () => fail(cxt) | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function skipWhitespace({gen, char: c}: ParseCxt): void { | ||
|  |   gen.code( | ||
|  |     _`while((${c}=${N.json}[${N.jsonPos}],${c}===" "||${c}==="\\n"||${c}==="\\r"||${c}==="\\t"))${N.jsonPos}++;` | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function jsonSlice(len: number | Name): Code { | ||
|  |   return len === 1 | ||
|  |     ? _`${N.json}[${N.jsonPos}]` | ||
|  |     : _`${N.json}.slice(${N.jsonPos}, ${N.jsonPos}+${len})` | ||
|  | } | ||
|  | 
 | ||
|  | function jsonSyntaxError(cxt: ParseCxt): void { | ||
|  |   parsingError(cxt, _`"unexpected token " + ${N.json}[${N.jsonPos}]`) | ||
|  | } | ||
|  | 
 | ||
|  | function parsingError({gen, parseName}: ParseCxt, msg: Code): void { | ||
|  |   gen.assign(_`${parseName}.message`, msg) | ||
|  |   gen.assign(_`${parseName}.position`, N.jsonPos) | ||
|  |   gen.return(undef) | ||
|  | } |