From b1fba03420c5e3bf741e726885372d2289a1305f Mon Sep 17 00:00:00 2001 From: Olzhas Alexandrov Date: Fri, 22 Apr 2022 10:08:36 -0500 Subject: [PATCH 1/2] Add ability to override or add new `transform`'s transformations --- src/definitions/_types.ts | 32 ++++++++++++++++++++++++++ src/definitions/transform.ts | 44 ++++++++++++++++++++++++------------ src/index.ts | 13 +++++++---- src/keywords/transform.ts | 3 ++- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/definitions/_types.ts b/src/definitions/_types.ts index c3f5424..e1f8ae2 100644 --- a/src/definitions/_types.ts +++ b/src/definitions/_types.ts @@ -1,7 +1,39 @@ import type {KeywordDefinition} from "ajv" +import type {Transformation} from "./transform" export interface DefinitionOptions { defaultMeta?: string | boolean } +export interface KeywordOptions { + /** + * Modifications to the "transform" keyword + */ + transform?: { + /** + * Override existing transformations or add additional (new) + * @example // overrides existing "trim" transformation, if "trim" didn't exist, it would have been added + * import { trim } from "@example-org/pkgName/example-trim" + * + * customTransformations: { + * trim: { + * transformation: trim, + * modulePath: "@example-org/pkgName/example-trim", + * } + * } + * + * // module "example.trim.ts" ("@example-org/pkgName/example-trim") exports a function "trim" + * export const trim = (value: string) => value.trim() + * @end + */ + customTransformations?: { + [key: string]: { + transformation: Transformation + modulePath: string + } + } + } +} +export type KeywordsWithCustomization = keyof KeywordOptions + export type GetDefinition = (opts?: DefinitionOptions) => T diff --git a/src/definitions/transform.ts b/src/definitions/transform.ts index af4ae29..334bb9a 100644 --- a/src/definitions/transform.ts +++ b/src/definitions/transform.ts @@ -1,7 +1,8 @@ import type {CodeKeywordDefinition, AnySchemaObject, KeywordCxt, Code, Name} from "ajv" +import type {KeywordOptions} from "./_types" import {_, stringify, getProperty} from "ajv/dist/compile/codegen" -type TransformName = +type TransformationName = | "trimStart" | "trimEnd" | "trimLeft" @@ -15,9 +16,9 @@ interface TransformConfig { hash: Record } -type Transform = (s: string, cfg?: TransformConfig) => string +export type Transformation = (s: string, cfg?: TransformConfig) => string -const transform: {[key in TransformName]: Transform} = { +const builtInTransformations: {[key in TransformationName]: Transformation} = { trimStart: (s) => s.trimStart(), trimEnd: (s) => s.trimEnd(), trimLeft: (s) => s.trimStart(), @@ -28,11 +29,13 @@ const transform: {[key in TransformName]: Transform} = { toEnumCase: (s, cfg) => cfg?.hash[configKey(s)] || s, } -const getDef: (() => CodeKeywordDefinition) & { - transform: typeof transform -} = Object.assign(_getDef, {transform}) +function getDef(opts?: KeywordOptions["transform"]): CodeKeywordDefinition { + const customTransformations = opts?.customTransformations || {} + const availableTransformations = [ + ...Object.keys(builtInTransformations), + ...Object.keys(customTransformations), + ] -function _getDef(): CodeKeywordDefinition { return { keyword: "transform", schemaType: "array", @@ -54,19 +57,32 @@ function _getDef(): CodeKeywordDefinition { function transformExpr(ts: string[]): Code { if (!ts.length) return data - const t = ts.pop() as string - if (!(t in transform)) throw new Error(`transform: unknown transformation ${t}`) - const func = gen.scopeValue("func", { - ref: transform[t as TransformName], - code: _`require("ajv-keywords/dist/definitions/transform").transform${getProperty(t)}`, - }) + const t = ts.pop() as TransformationName | string + if (!availableTransformations.includes(t)) { + throw new Error(`transform: unknown transformation ${t}`) + } + + const func = gen.scopeValue( + "func", + customTransformations[t] // eslint-disable-line @typescript-eslint/no-unnecessary-condition + ? { + ref: customTransformations[t].transformation, + code: _`require("${customTransformations[t].modulePath}")${getProperty(t)}`, + } + : { + ref: builtInTransformations[t as TransformationName], + code: _`require("ajv-keywords/dist/definitions/transform").transform${getProperty( + t + )}`, + } + ) const arg = transformExpr(ts) return cfg && t === "toEnumCase" ? _`${func}(${arg}, ${cfg})` : _`${func}(${arg})` } }, metaSchema: { type: "array", - items: {type: "string", enum: Object.keys(transform)}, + items: {type: "string", enum: availableTransformations}, }, } } diff --git a/src/index.ts b/src/index.ts index bb580fb..1070a38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,24 @@ import type Ajv from "ajv" import type {Plugin} from "ajv" +import type {KeywordOptions, KeywordsWithCustomization} from "./definitions/_types" import plugins from "./keywords" export {AjvKeywordsError} from "./definitions" -const ajvKeywords: Plugin = (ajv: Ajv, keyword?: string | string[]): Ajv => { +const ajvKeywords: Plugin = ( + ajv: Ajv, + keyword?: string | string[], + keywordOptions: KeywordOptions = {} +): Ajv => { if (Array.isArray(keyword)) { - for (const k of keyword) get(k)(ajv) + for (const k of keyword) get(k)(ajv, keywordOptions[k as KeywordsWithCustomization]) return ajv } if (keyword) { - get(keyword)(ajv) + get(keyword)(ajv, keywordOptions[keyword as KeywordsWithCustomization]) return ajv } - for (keyword in plugins) get(keyword)(ajv) + for (keyword in plugins) get(keyword)(ajv, keywordOptions[keyword as KeywordsWithCustomization]) return ajv } diff --git a/src/keywords/transform.ts b/src/keywords/transform.ts index d6335ec..5f5c1af 100644 --- a/src/keywords/transform.ts +++ b/src/keywords/transform.ts @@ -1,7 +1,8 @@ import type {Plugin} from "ajv" +import type {KeywordOptions} from "../definitions/_types" import getDef from "../definitions/transform" -const transform: Plugin = (ajv) => ajv.addKeyword(getDef()) +const transform: Plugin = (ajv, opts) => ajv.addKeyword(getDef(opts)) export default transform module.exports = transform From 8969651eb3e24935202b1df082dd453db8e3f75f Mon Sep 17 00:00:00 2001 From: Olzhas Alexandrov Date: Fri, 22 Apr 2022 12:25:08 -0500 Subject: [PATCH 2/2] Fix types & allow customization of all keywords --- src/definitions/_types.ts | 18 ++++++++---------- src/definitions/transform.ts | 6 +++--- src/index.ts | 33 +++++++++++++++++++++++---------- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/definitions/_types.ts b/src/definitions/_types.ts index e1f8ae2..b6efeae 100644 --- a/src/definitions/_types.ts +++ b/src/definitions/_types.ts @@ -1,10 +1,6 @@ import type {KeywordDefinition} from "ajv" import type {Transformation} from "./transform" -export interface DefinitionOptions { - defaultMeta?: string | boolean -} - export interface KeywordOptions { /** * Modifications to the "transform" keyword @@ -15,7 +11,7 @@ export interface KeywordOptions { * @example // overrides existing "trim" transformation, if "trim" didn't exist, it would have been added * import { trim } from "@example-org/pkgName/example-trim" * - * customTransformations: { + * transform: { * trim: { * transformation: trim, * modulePath: "@example-org/pkgName/example-trim", @@ -26,14 +22,16 @@ export interface KeywordOptions { * export const trim = (value: string) => value.trim() * @end */ - customTransformations?: { - [key: string]: { - transformation: Transformation - modulePath: string - } + [key: string]: { + transformation: Transformation + modulePath: string } } } export type KeywordsWithCustomization = keyof KeywordOptions +export interface DefinitionOptions extends KeywordOptions { + defaultMeta?: string | boolean +} + export type GetDefinition = (opts?: DefinitionOptions) => T diff --git a/src/definitions/transform.ts b/src/definitions/transform.ts index 334bb9a..808d766 100644 --- a/src/definitions/transform.ts +++ b/src/definitions/transform.ts @@ -1,5 +1,5 @@ import type {CodeKeywordDefinition, AnySchemaObject, KeywordCxt, Code, Name} from "ajv" -import type {KeywordOptions} from "./_types" +import type {DefinitionOptions} from "./_types" import {_, stringify, getProperty} from "ajv/dist/compile/codegen" type TransformationName = @@ -29,8 +29,8 @@ const builtInTransformations: {[key in TransformationName]: Transformation} = { toEnumCase: (s, cfg) => cfg?.hash[configKey(s)] || s, } -function getDef(opts?: KeywordOptions["transform"]): CodeKeywordDefinition { - const customTransformations = opts?.customTransformations || {} +function getDef(opts?: DefinitionOptions): CodeKeywordDefinition { + const customTransformations = opts?.transform || {} const availableTransformations = [ ...Object.keys(builtInTransformations), ...Object.keys(customTransformations), diff --git a/src/index.ts b/src/index.ts index 1070a38..ca70720 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,48 @@ +/* eslint-disable valid-jsdoc */ // this rule is deprecated and insists on adding type annotations that already exist in TypeScript import type Ajv from "ajv" import type {Plugin} from "ajv" -import type {KeywordOptions, KeywordsWithCustomization} from "./definitions/_types" -import plugins from "./keywords" +import type {DefinitionOptions} from "./definitions/_types" +import keywords from "./keywords" export {AjvKeywordsError} from "./definitions" +/** + * @param ajv instance + * @param specificKeywords only these keywords to-be-added + * @param options modifications passed to keywords + * @returns ajv instance + */ const ajvKeywords: Plugin = ( ajv: Ajv, - keyword?: string | string[], - keywordOptions: KeywordOptions = {} + specificKeywords?: string | string[], + options: DefinitionOptions = {} ): Ajv => { - if (Array.isArray(keyword)) { - for (const k of keyword) get(k)(ajv, keywordOptions[k as KeywordsWithCustomization]) + if (Array.isArray(specificKeywords)) { + for (const k of specificKeywords) addKeyword(ajv, k, options) return ajv } - if (keyword) { - get(keyword)(ajv, keywordOptions[keyword as KeywordsWithCustomization]) + if (specificKeywords) { + addKeyword(ajv, specificKeywords, options) return ajv } - for (keyword in plugins) get(keyword)(ajv, keywordOptions[keyword as KeywordsWithCustomization]) + + for (const keyword in keywords) addKeyword(ajv, keyword, options) return ajv } ajvKeywords.get = get function get(keyword: string): Plugin { - const defFunc = plugins[keyword] + const defFunc = keywords[keyword] if (!defFunc) throw new Error("Unknown keyword " + keyword) return defFunc } +function addKeyword(ajv: Ajv, keyword: string, options: DefinitionOptions): void { + const defFunc = get(keyword) + defFunc(ajv, options) +} + export default ajvKeywords module.exports = ajvKeywords