serialize.ts 8.0 KB

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