261 lines
7.9 KiB
TypeScript
261 lines
7.9 KiB
TypeScript
|
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`,`)
|
||
|
)
|
||
|
}
|