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