serialize.ts 8.0 KB

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