transform.ts 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import type {CodeKeywordDefinition, AnySchemaObject, KeywordCxt, Code, Name} from "ajv"
  2. import {_, stringify, getProperty} from "ajv/dist/compile/codegen"
  3. type TransformName =
  4. | "trimStart"
  5. | "trimEnd"
  6. | "trimLeft"
  7. | "trimRight"
  8. | "trim"
  9. | "toLowerCase"
  10. | "toUpperCase"
  11. | "toEnumCase"
  12. interface TransformConfig {
  13. hash: Record<string, string | undefined>
  14. }
  15. type Transform = (s: string, cfg?: TransformConfig) => string
  16. const transform: {[key in TransformName]: Transform} = {
  17. trimStart: (s) => s.trimStart(),
  18. trimEnd: (s) => s.trimEnd(),
  19. trimLeft: (s) => s.trimStart(),
  20. trimRight: (s) => s.trimEnd(),
  21. trim: (s) => s.trim(),
  22. toLowerCase: (s) => s.toLowerCase(),
  23. toUpperCase: (s) => s.toUpperCase(),
  24. toEnumCase: (s, cfg) => cfg?.hash[configKey(s)] || s,
  25. }
  26. const getDef: (() => CodeKeywordDefinition) & {
  27. transform: typeof transform
  28. } = Object.assign(_getDef, {transform})
  29. function _getDef(): CodeKeywordDefinition {
  30. return {
  31. keyword: "transform",
  32. schemaType: "array",
  33. before: "enum",
  34. code(cxt: KeywordCxt) {
  35. const {gen, data, schema, parentSchema, it} = cxt
  36. const {parentData, parentDataProperty} = it
  37. const tNames: string[] = schema
  38. if (!tNames.length) return
  39. let cfg: Name | undefined
  40. if (tNames.includes("toEnumCase")) {
  41. const config = getEnumCaseCfg(parentSchema)
  42. cfg = gen.scopeValue("obj", {ref: config, code: stringify(config)})
  43. }
  44. gen.if(_`typeof ${data} == "string" && ${parentData} !== undefined`, () => {
  45. gen.assign(data, transformExpr(tNames.slice()))
  46. gen.assign(_`${parentData}[${parentDataProperty}]`, data)
  47. })
  48. function transformExpr(ts: string[]): Code {
  49. if (!ts.length) return data
  50. const t = ts.pop() as string
  51. if (!(t in transform)) throw new Error(`transform: unknown transformation ${t}`)
  52. const func = gen.scopeValue("func", {
  53. ref: transform[t as TransformName],
  54. code: _`require("ajv-keywords/dist/definitions/transform").transform${getProperty(t)}`,
  55. })
  56. const arg = transformExpr(ts)
  57. return cfg && t === "toEnumCase" ? _`${func}(${arg}, ${cfg})` : _`${func}(${arg})`
  58. }
  59. },
  60. metaSchema: {
  61. type: "array",
  62. items: {type: "string", enum: Object.keys(transform)},
  63. },
  64. }
  65. }
  66. function getEnumCaseCfg(parentSchema: AnySchemaObject): TransformConfig {
  67. // build hash table to enum values
  68. const cfg: TransformConfig = {hash: {}}
  69. // requires `enum` in the same schema as transform
  70. if (!parentSchema.enum) throw new Error('transform: "toEnumCase" requires "enum"')
  71. for (const v of parentSchema.enum) {
  72. if (typeof v !== "string") continue
  73. const k = configKey(v)
  74. // requires all `enum` values have unique keys
  75. if (cfg.hash[k]) {
  76. throw new Error('transform: "toEnumCase" requires all lowercased "enum" values to be unique')
  77. }
  78. cfg.hash[k] = v
  79. }
  80. return cfg
  81. }
  82. function configKey(s: string): string {
  83. return s.toLowerCase()
  84. }
  85. export default getDef
  86. module.exports = getDef