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 let firstProp: Name | undefined for (const key of props) { if (first) first = false else gen.add(N.json, str`,`) serializeProperty(key, properties[key], keyValue(key)) } if (first) firstProp = gen.let("first", true) for (const key of optProps) { const value = keyValue(key) gen.if(and(_`${value} !== undefined`, isOwnProperty(gen, data, key)), () => { addComma(cxt, firstProp) serializeProperty(key, optionalProperties[key], value) }) } if (schema.additionalProperties) { gen.forIn("key", data, (key) => gen.if(isAdditional(key, allProps), () => serializeKeyValue(cxt, key, {}, firstProp)) ) } 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 { 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 { if (first) { gen.if( first, () => gen.assign(first, false), () => gen.add(N.json, str`,`) ) } else { gen.add(N.json, str`,`) } }