| 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("", 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`,`) |
| ) |
| } |