[6a3a178] | 1 | import type Ajv from "../../core"
|
---|
| 2 | import type {SchemaObject} from "../../types"
|
---|
| 3 | import {jtdForms, JTDForm, SchemaObjectMap} from "./types"
|
---|
| 4 | import {SchemaEnv, getCompilingSchema} from ".."
|
---|
| 5 | import {_, str, and, getProperty, CodeGen, Code, Name} from "../codegen"
|
---|
| 6 | import MissingRefError from "../ref_error"
|
---|
| 7 | import N from "../names"
|
---|
| 8 | import {isOwnProperty} from "../../vocabularies/code"
|
---|
| 9 | import {hasRef} from "../../vocabularies/jtd/ref"
|
---|
| 10 | import {useFunc} from "../util"
|
---|
| 11 | import quote from "../../runtime/quote"
|
---|
| 12 |
|
---|
| 13 | const genSerialize: {[F in JTDForm]: (cxt: SerializeCxt) => void} = {
|
---|
| 14 | elements: serializeElements,
|
---|
| 15 | values: serializeValues,
|
---|
| 16 | discriminator: serializeDiscriminator,
|
---|
| 17 | properties: serializeProperties,
|
---|
| 18 | optionalProperties: serializeProperties,
|
---|
| 19 | enum: serializeString,
|
---|
| 20 | type: serializeType,
|
---|
| 21 | ref: serializeRef,
|
---|
| 22 | }
|
---|
| 23 |
|
---|
| 24 | interface SerializeCxt {
|
---|
| 25 | readonly gen: CodeGen
|
---|
| 26 | readonly self: Ajv // current Ajv instance
|
---|
| 27 | readonly schemaEnv: SchemaEnv
|
---|
| 28 | readonly definitions: SchemaObjectMap
|
---|
| 29 | schema: SchemaObject
|
---|
| 30 | data: Code
|
---|
| 31 | }
|
---|
| 32 |
|
---|
| 33 | export default function compileSerializer(
|
---|
| 34 | this: Ajv,
|
---|
| 35 | sch: SchemaEnv,
|
---|
| 36 | definitions: SchemaObjectMap
|
---|
| 37 | ): SchemaEnv {
|
---|
| 38 | const _sch = getCompilingSchema.call(this, sch)
|
---|
| 39 | if (_sch) return _sch
|
---|
| 40 | const {es5, lines} = this.opts.code
|
---|
| 41 | const {ownProperties} = this.opts
|
---|
| 42 | const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
|
---|
| 43 | const serializeName = gen.scopeName("serialize")
|
---|
| 44 | const cxt: SerializeCxt = {
|
---|
| 45 | self: this,
|
---|
| 46 | gen,
|
---|
| 47 | schema: sch.schema as SchemaObject,
|
---|
| 48 | schemaEnv: sch,
|
---|
| 49 | definitions,
|
---|
| 50 | data: N.data,
|
---|
| 51 | }
|
---|
| 52 |
|
---|
| 53 | let sourceCode: string | undefined
|
---|
| 54 | try {
|
---|
| 55 | this._compilations.add(sch)
|
---|
| 56 | sch.serializeName = serializeName
|
---|
| 57 | gen.func(serializeName, N.data, false, () => {
|
---|
| 58 | gen.let(N.json, str``)
|
---|
| 59 | serializeCode(cxt)
|
---|
| 60 | gen.return(N.json)
|
---|
| 61 | })
|
---|
| 62 | gen.optimize(this.opts.code.optimize)
|
---|
| 63 | const serializeFuncCode = gen.toString()
|
---|
| 64 | sourceCode = `${gen.scopeRefs(N.scope)}return ${serializeFuncCode}`
|
---|
| 65 | const makeSerialize = new Function(`${N.scope}`, sourceCode)
|
---|
| 66 | const serialize: (data: unknown) => string = makeSerialize(this.scope.get())
|
---|
| 67 | this.scope.value(serializeName, {ref: serialize})
|
---|
| 68 | sch.serialize = serialize
|
---|
| 69 | } catch (e) {
|
---|
| 70 | if (sourceCode) this.logger.error("Error compiling serializer, function code:", sourceCode)
|
---|
| 71 | delete sch.serialize
|
---|
| 72 | delete sch.serializeName
|
---|
| 73 | throw e
|
---|
| 74 | } finally {
|
---|
| 75 | this._compilations.delete(sch)
|
---|
| 76 | }
|
---|
| 77 | return sch
|
---|
| 78 | }
|
---|
| 79 |
|
---|
| 80 | function serializeCode(cxt: SerializeCxt): void {
|
---|
| 81 | let form: JTDForm | undefined
|
---|
| 82 | for (const key of jtdForms) {
|
---|
| 83 | if (key in cxt.schema) {
|
---|
| 84 | form = key
|
---|
| 85 | break
|
---|
| 86 | }
|
---|
| 87 | }
|
---|
| 88 | serializeNullable(cxt, form ? genSerialize[form] : serializeEmpty)
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | function serializeNullable(cxt: SerializeCxt, serializeForm: (_cxt: SerializeCxt) => void): void {
|
---|
| 92 | const {gen, schema, data} = cxt
|
---|
| 93 | if (!schema.nullable) return serializeForm(cxt)
|
---|
| 94 | gen.if(
|
---|
| 95 | _`${data} === undefined || ${data} === null`,
|
---|
| 96 | () => gen.add(N.json, _`"null"`),
|
---|
| 97 | () => serializeForm(cxt)
|
---|
| 98 | )
|
---|
| 99 | }
|
---|
| 100 |
|
---|
| 101 | function serializeElements(cxt: SerializeCxt): void {
|
---|
| 102 | const {gen, schema, data} = cxt
|
---|
| 103 | gen.add(N.json, str`[`)
|
---|
| 104 | const first = gen.let("first", true)
|
---|
| 105 | gen.forOf("el", data, (el) => {
|
---|
| 106 | addComma(cxt, first)
|
---|
| 107 | serializeCode({...cxt, schema: schema.elements, data: el})
|
---|
| 108 | })
|
---|
| 109 | gen.add(N.json, str`]`)
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 | function serializeValues(cxt: SerializeCxt): void {
|
---|
| 113 | const {gen, schema, data} = cxt
|
---|
| 114 | gen.add(N.json, str`{`)
|
---|
| 115 | const first = gen.let("first", true)
|
---|
| 116 | gen.forIn("key", data, (key) => serializeKeyValue(cxt, key, schema.values, first))
|
---|
| 117 | gen.add(N.json, str`}`)
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | function serializeKeyValue(cxt: SerializeCxt, key: Name, schema: SchemaObject, first: Name): void {
|
---|
| 121 | const {gen, data} = cxt
|
---|
| 122 | addComma(cxt, first)
|
---|
| 123 | serializeString({...cxt, data: key})
|
---|
| 124 | gen.add(N.json, str`:`)
|
---|
| 125 | const value = gen.const("value", _`${data}${getProperty(key)}`)
|
---|
| 126 | serializeCode({...cxt, schema, data: value})
|
---|
| 127 | }
|
---|
| 128 |
|
---|
| 129 | function serializeDiscriminator(cxt: SerializeCxt): void {
|
---|
| 130 | const {gen, schema, data} = cxt
|
---|
| 131 | const {discriminator} = schema
|
---|
| 132 | gen.add(N.json, str`{${JSON.stringify(discriminator)}:`)
|
---|
| 133 | const tag = gen.const("tag", _`${data}${getProperty(discriminator)}`)
|
---|
| 134 | serializeString({...cxt, data: tag})
|
---|
| 135 | gen.if(false)
|
---|
| 136 | for (const tagValue in schema.mapping) {
|
---|
| 137 | gen.elseIf(_`${tag} === ${tagValue}`)
|
---|
| 138 | const sch = schema.mapping[tagValue]
|
---|
| 139 | serializeSchemaProperties({...cxt, schema: sch}, discriminator)
|
---|
| 140 | }
|
---|
| 141 | gen.endIf()
|
---|
| 142 | gen.add(N.json, str`}`)
|
---|
| 143 | }
|
---|
| 144 |
|
---|
| 145 | function serializeProperties(cxt: SerializeCxt): void {
|
---|
| 146 | const {gen} = cxt
|
---|
| 147 | gen.add(N.json, str`{`)
|
---|
| 148 | serializeSchemaProperties(cxt)
|
---|
| 149 | gen.add(N.json, str`}`)
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | function serializeSchemaProperties(cxt: SerializeCxt, discriminator?: string): void {
|
---|
| 153 | const {gen, schema, data} = cxt
|
---|
| 154 | const {properties, optionalProperties} = schema
|
---|
| 155 | const props = keys(properties)
|
---|
| 156 | const optProps = keys(optionalProperties)
|
---|
| 157 | const allProps = allProperties(props.concat(optProps))
|
---|
| 158 | let first = !discriminator
|
---|
| 159 | for (const key of props) {
|
---|
| 160 | serializeProperty(key, properties[key], keyValue(key))
|
---|
| 161 | }
|
---|
| 162 | for (const key of optProps) {
|
---|
| 163 | const value = keyValue(key)
|
---|
| 164 | gen.if(and(_`${value} !== undefined`, isOwnProperty(gen, data, key)), () =>
|
---|
| 165 | serializeProperty(key, optionalProperties[key], value)
|
---|
| 166 | )
|
---|
| 167 | }
|
---|
| 168 | if (schema.additionalProperties) {
|
---|
| 169 | gen.forIn("key", data, (key) =>
|
---|
| 170 | gen.if(isAdditional(key, allProps), () =>
|
---|
| 171 | serializeKeyValue(cxt, key, {}, gen.let("first", first))
|
---|
| 172 | )
|
---|
| 173 | )
|
---|
| 174 | }
|
---|
| 175 |
|
---|
| 176 | function keys(ps?: SchemaObjectMap): string[] {
|
---|
| 177 | return ps ? Object.keys(ps) : []
|
---|
| 178 | }
|
---|
| 179 |
|
---|
| 180 | function allProperties(ps: string[]): string[] {
|
---|
| 181 | if (discriminator) ps.push(discriminator)
|
---|
| 182 | if (new Set(ps).size !== ps.length) {
|
---|
| 183 | throw new Error("JTD: properties/optionalProperties/disciminator overlap")
|
---|
| 184 | }
|
---|
| 185 | return ps
|
---|
| 186 | }
|
---|
| 187 |
|
---|
| 188 | function keyValue(key: string): Name {
|
---|
| 189 | return gen.const("value", _`${data}${getProperty(key)}`)
|
---|
| 190 | }
|
---|
| 191 |
|
---|
| 192 | function serializeProperty(key: string, propSchema: SchemaObject, value: Name): void {
|
---|
| 193 | if (first) first = false
|
---|
| 194 | else gen.add(N.json, str`,`)
|
---|
| 195 | gen.add(N.json, str`${JSON.stringify(key)}:`)
|
---|
| 196 | serializeCode({...cxt, schema: propSchema, data: value})
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | function isAdditional(key: Name, ps: string[]): Code | true {
|
---|
| 200 | return ps.length ? and(...ps.map((p) => _`${key} !== ${p}`)) : true
|
---|
| 201 | }
|
---|
| 202 | }
|
---|
| 203 |
|
---|
| 204 | function serializeType(cxt: SerializeCxt): void {
|
---|
| 205 | const {gen, schema, data} = cxt
|
---|
| 206 | switch (schema.type) {
|
---|
| 207 | case "boolean":
|
---|
| 208 | gen.add(N.json, _`${data} ? "true" : "false"`)
|
---|
| 209 | break
|
---|
| 210 | case "string":
|
---|
| 211 | serializeString(cxt)
|
---|
| 212 | break
|
---|
| 213 | case "timestamp":
|
---|
| 214 | gen.if(
|
---|
| 215 | _`${data} instanceof Date`,
|
---|
| 216 | () => gen.add(N.json, _`'"' + ${data}.toISOString() + '"'`),
|
---|
| 217 | () => serializeString(cxt)
|
---|
| 218 | )
|
---|
| 219 | break
|
---|
| 220 | default:
|
---|
| 221 | serializeNumber(cxt)
|
---|
| 222 | }
|
---|
| 223 | }
|
---|
| 224 |
|
---|
| 225 | function serializeString({gen, data}: SerializeCxt): void {
|
---|
| 226 | gen.add(N.json, _`${useFunc(gen, quote)}(${data})`)
|
---|
| 227 | }
|
---|
| 228 |
|
---|
| 229 | function serializeNumber({gen, data}: SerializeCxt): void {
|
---|
| 230 | gen.add(N.json, _`"" + ${data}`)
|
---|
| 231 | }
|
---|
| 232 |
|
---|
| 233 | function serializeRef(cxt: SerializeCxt): void {
|
---|
| 234 | const {gen, self, data, definitions, schema, schemaEnv} = cxt
|
---|
| 235 | const {ref} = schema
|
---|
| 236 | const refSchema = definitions[ref]
|
---|
| 237 | if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
|
---|
| 238 | if (!hasRef(refSchema)) return serializeCode({...cxt, schema: refSchema})
|
---|
| 239 | const {root} = schemaEnv
|
---|
| 240 | const sch = compileSerializer.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
|
---|
| 241 | gen.add(N.json, _`${getSerialize(gen, sch)}(${data})`)
|
---|
| 242 | }
|
---|
| 243 |
|
---|
| 244 | function getSerialize(gen: CodeGen, sch: SchemaEnv): Code {
|
---|
| 245 | return sch.serialize
|
---|
| 246 | ? gen.scopeValue("serialize", {ref: sch.serialize})
|
---|
| 247 | : _`${gen.scopeValue("wrapper", {ref: sch})}.serialize`
|
---|
| 248 | }
|
---|
| 249 |
|
---|
| 250 | function serializeEmpty({gen, data}: SerializeCxt): void {
|
---|
| 251 | gen.add(N.json, _`JSON.stringify(${data})`)
|
---|
| 252 | }
|
---|
| 253 |
|
---|
| 254 | function addComma({gen}: SerializeCxt, first: Name): void {
|
---|
| 255 | gen.if(
|
---|
| 256 | first,
|
---|
| 257 | () => gen.assign(first, false),
|
---|
| 258 | () => gen.add(N.json, str`,`)
|
---|
| 259 | )
|
---|
| 260 | }
|
---|