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.
		
		
		
		
		
			
		
			
				
					261 lines
				
				7.9 KiB
			
		
		
			
		
	
	
					261 lines
				
				7.9 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, getProperty, CodeGen, Code, Name} from "../codegen" | ||
|  | import MissingRefError from "../ref_error" | ||
|  | import N from "../names" | ||
|  | import {isOwnProperty} from "../../vocabularies/code" | ||
|  | import {hasRef} from "../../vocabularies/jtd/ref" | ||
|  | import {useFunc} from "../util" | ||
|  | import quote from "../../runtime/quote" | ||
|  | 
 | ||
|  | const genSerialize: {[F in JTDForm]: (cxt: SerializeCxt) => void} = { | ||
|  |   elements: serializeElements, | ||
|  |   values: serializeValues, | ||
|  |   discriminator: serializeDiscriminator, | ||
|  |   properties: serializeProperties, | ||
|  |   optionalProperties: serializeProperties, | ||
|  |   enum: serializeString, | ||
|  |   type: serializeType, | ||
|  |   ref: serializeRef, | ||
|  | } | ||
|  | 
 | ||
|  | interface SerializeCxt { | ||
|  |   readonly gen: CodeGen | ||
|  |   readonly self: Ajv // current Ajv instance
 | ||
|  |   readonly schemaEnv: SchemaEnv | ||
|  |   readonly definitions: SchemaObjectMap | ||
|  |   schema: SchemaObject | ||
|  |   data: Code | ||
|  | } | ||
|  | 
 | ||
|  | export default function compileSerializer( | ||
|  |   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 serializeName = gen.scopeName("serialize") | ||
|  |   const cxt: SerializeCxt = { | ||
|  |     self: this, | ||
|  |     gen, | ||
|  |     schema: sch.schema as SchemaObject, | ||
|  |     schemaEnv: sch, | ||
|  |     definitions, | ||
|  |     data: N.data, | ||
|  |   } | ||
|  | 
 | ||
|  |   let sourceCode: string | undefined | ||
|  |   try { | ||
|  |     this._compilations.add(sch) | ||
|  |     sch.serializeName = serializeName | ||
|  |     gen.func(serializeName, N.data, false, () => { | ||
|  |       gen.let(N.json, str``) | ||
|  |       serializeCode(cxt) | ||
|  |       gen.return(N.json) | ||
|  |     }) | ||
|  |     gen.optimize(this.opts.code.optimize) | ||
|  |     const serializeFuncCode = gen.toString() | ||
|  |     sourceCode = `${gen.scopeRefs(N.scope)}return ${serializeFuncCode}` | ||
|  |     const makeSerialize = new Function(`${N.scope}`, sourceCode) | ||
|  |     const serialize: (data: unknown) => string = makeSerialize(this.scope.get()) | ||
|  |     this.scope.value(serializeName, {ref: serialize}) | ||
|  |     sch.serialize = serialize | ||
|  |   } catch (e) { | ||
|  |     if (sourceCode) this.logger.error("Error compiling serializer, function code:", sourceCode) | ||
|  |     delete sch.serialize | ||
|  |     delete sch.serializeName | ||
|  |     throw e | ||
|  |   } finally { | ||
|  |     this._compilations.delete(sch) | ||
|  |   } | ||
|  |   return sch | ||
|  | } | ||
|  | 
 | ||
|  | function serializeCode(cxt: SerializeCxt): void { | ||
|  |   let form: JTDForm | undefined | ||
|  |   for (const key of jtdForms) { | ||
|  |     if (key in cxt.schema) { | ||
|  |       form = key | ||
|  |       break | ||
|  |     } | ||
|  |   } | ||
|  |   serializeNullable(cxt, form ? genSerialize[form] : serializeEmpty) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeNullable(cxt: SerializeCxt, serializeForm: (_cxt: SerializeCxt) => void): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   if (!schema.nullable) return serializeForm(cxt) | ||
|  |   gen.if( | ||
|  |     _`${data} === undefined || ${data} === null`, | ||
|  |     () => gen.add(N.json, _`"null"`), | ||
|  |     () => serializeForm(cxt) | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeElements(cxt: SerializeCxt): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   gen.add(N.json, str`[`) | ||
|  |   const first = gen.let("first", true) | ||
|  |   gen.forOf("el", data, (el) => { | ||
|  |     addComma(cxt, first) | ||
|  |     serializeCode({...cxt, schema: schema.elements, data: el}) | ||
|  |   }) | ||
|  |   gen.add(N.json, str`]`) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeValues(cxt: SerializeCxt): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   gen.add(N.json, str`{`) | ||
|  |   const first = gen.let("first", true) | ||
|  |   gen.forIn("key", data, (key) => serializeKeyValue(cxt, key, schema.values, first)) | ||
|  |   gen.add(N.json, str`}`) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeKeyValue(cxt: SerializeCxt, key: Name, schema: SchemaObject, first: Name): void { | ||
|  |   const {gen, data} = cxt | ||
|  |   addComma(cxt, first) | ||
|  |   serializeString({...cxt, data: key}) | ||
|  |   gen.add(N.json, str`:`) | ||
|  |   const value = gen.const("value", _`${data}${getProperty(key)}`) | ||
|  |   serializeCode({...cxt, schema, data: value}) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeDiscriminator(cxt: SerializeCxt): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   const {discriminator} = schema | ||
|  |   gen.add(N.json, str`{${JSON.stringify(discriminator)}:`) | ||
|  |   const tag = gen.const("tag", _`${data}${getProperty(discriminator)}`) | ||
|  |   serializeString({...cxt, data: tag}) | ||
|  |   gen.if(false) | ||
|  |   for (const tagValue in schema.mapping) { | ||
|  |     gen.elseIf(_`${tag} === ${tagValue}`) | ||
|  |     const sch = schema.mapping[tagValue] | ||
|  |     serializeSchemaProperties({...cxt, schema: sch}, discriminator) | ||
|  |   } | ||
|  |   gen.endIf() | ||
|  |   gen.add(N.json, str`}`) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeProperties(cxt: SerializeCxt): void { | ||
|  |   const {gen} = cxt | ||
|  |   gen.add(N.json, str`{`) | ||
|  |   serializeSchemaProperties(cxt) | ||
|  |   gen.add(N.json, str`}`) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeSchemaProperties(cxt: SerializeCxt, discriminator?: string): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   const {properties, optionalProperties} = schema | ||
|  |   const props = keys(properties) | ||
|  |   const optProps = keys(optionalProperties) | ||
|  |   const allProps = allProperties(props.concat(optProps)) | ||
|  |   let first = !discriminator | ||
|  |   for (const key of props) { | ||
|  |     serializeProperty(key, properties[key], keyValue(key)) | ||
|  |   } | ||
|  |   for (const key of optProps) { | ||
|  |     const value = keyValue(key) | ||
|  |     gen.if(and(_`${value} !== undefined`, isOwnProperty(gen, data, key)), () => | ||
|  |       serializeProperty(key, optionalProperties[key], value) | ||
|  |     ) | ||
|  |   } | ||
|  |   if (schema.additionalProperties) { | ||
|  |     gen.forIn("key", data, (key) => | ||
|  |       gen.if(isAdditional(key, allProps), () => | ||
|  |         serializeKeyValue(cxt, key, {}, gen.let("first", first)) | ||
|  |       ) | ||
|  |     ) | ||
|  |   } | ||
|  | 
 | ||
|  |   function keys(ps?: SchemaObjectMap): string[] { | ||
|  |     return ps ? Object.keys(ps) : [] | ||
|  |   } | ||
|  | 
 | ||
|  |   function allProperties(ps: string[]): string[] { | ||
|  |     if (discriminator) ps.push(discriminator) | ||
|  |     if (new Set(ps).size !== ps.length) { | ||
|  |       throw new Error("JTD: properties/optionalProperties/disciminator overlap") | ||
|  |     } | ||
|  |     return ps | ||
|  |   } | ||
|  | 
 | ||
|  |   function keyValue(key: string): Name { | ||
|  |     return gen.const("value", _`${data}${getProperty(key)}`) | ||
|  |   } | ||
|  | 
 | ||
|  |   function serializeProperty(key: string, propSchema: SchemaObject, value: Name): void { | ||
|  |     if (first) first = false | ||
|  |     else gen.add(N.json, str`,`) | ||
|  |     gen.add(N.json, str`${JSON.stringify(key)}:`) | ||
|  |     serializeCode({...cxt, schema: propSchema, data: value}) | ||
|  |   } | ||
|  | 
 | ||
|  |   function isAdditional(key: Name, ps: string[]): Code | true { | ||
|  |     return ps.length ? and(...ps.map((p) => _`${key} !== ${p}`)) : true | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function serializeType(cxt: SerializeCxt): void { | ||
|  |   const {gen, schema, data} = cxt | ||
|  |   switch (schema.type) { | ||
|  |     case "boolean": | ||
|  |       gen.add(N.json, _`${data} ? "true" : "false"`) | ||
|  |       break | ||
|  |     case "string": | ||
|  |       serializeString(cxt) | ||
|  |       break | ||
|  |     case "timestamp": | ||
|  |       gen.if( | ||
|  |         _`${data} instanceof Date`, | ||
|  |         () => gen.add(N.json, _`'"' + ${data}.toISOString() + '"'`), | ||
|  |         () => serializeString(cxt) | ||
|  |       ) | ||
|  |       break | ||
|  |     default: | ||
|  |       serializeNumber(cxt) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function serializeString({gen, data}: SerializeCxt): void { | ||
|  |   gen.add(N.json, _`${useFunc(gen, quote)}(${data})`) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeNumber({gen, data}: SerializeCxt): void { | ||
|  |   gen.add(N.json, _`"" + ${data}`) | ||
|  | } | ||
|  | 
 | ||
|  | function serializeRef(cxt: SerializeCxt): void { | ||
|  |   const {gen, self, data, 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 serializeCode({...cxt, schema: refSchema}) | ||
|  |   const {root} = schemaEnv | ||
|  |   const sch = compileSerializer.call(self, new SchemaEnv({schema: refSchema, root}), definitions) | ||
|  |   gen.add(N.json, _`${getSerialize(gen, sch)}(${data})`) | ||
|  | } | ||
|  | 
 | ||
|  | function getSerialize(gen: CodeGen, sch: SchemaEnv): Code { | ||
|  |   return sch.serialize | ||
|  |     ? gen.scopeValue("serialize", {ref: sch.serialize}) | ||
|  |     : _`${gen.scopeValue("wrapper", {ref: sch})}.serialize` | ||
|  | } | ||
|  | 
 | ||
|  | function serializeEmpty({gen, data}: SerializeCxt): void { | ||
|  |   gen.add(N.json, _`JSON.stringify(${data})`) | ||
|  | } | ||
|  | 
 | ||
|  | function addComma({gen}: SerializeCxt, first: Name): void { | ||
|  |   gen.if( | ||
|  |     first, | ||
|  |     () => gen.assign(first, false), | ||
|  |     () => gen.add(N.json, str`,`) | ||
|  |   ) | ||
|  | } |