diff --git a/src/definitions/_types.ts b/src/definitions/_types.ts index c3f5424..b6efeae 100644 --- a/src/definitions/_types.ts +++ b/src/definitions/_types.ts @@ -1,6 +1,36 @@ import type {KeywordDefinition} from "ajv" +import type {Transformation} from "./transform" -export interface DefinitionOptions { +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" + * + * transform: { + * 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 + */ + [key: string]: { + transformation: Transformation + modulePath: string + } + } +} +export type KeywordsWithCustomization = keyof KeywordOptions + +export interface DefinitionOptions extends KeywordOptions { defaultMeta?: string | boolean } diff --git a/src/definitions/transform.ts b/src/definitions/transform.ts index af4ae29..808d766 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 {DefinitionOptions} 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?: DefinitionOptions): CodeKeywordDefinition { + const customTransformations = opts?.transform || {} + 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..ca70720 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +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 plugins from "./keywords" +import type {DefinitionOptions} from "./definitions/_types" +import keywords from "./keywords" export {AjvKeywordsError} from "./definitions" -const ajvKeywords: Plugin = (ajv: Ajv, keyword?: string | string[]): Ajv => { - if (Array.isArray(keyword)) { - for (const k of keyword) get(k)(ajv) +/** + * @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, + specificKeywords?: string | string[], + options: DefinitionOptions = {} +): Ajv => { + if (Array.isArray(specificKeywords)) { + for (const k of specificKeywords) addKeyword(ajv, k, options) return ajv } - if (keyword) { - get(keyword)(ajv) + if (specificKeywords) { + addKeyword(ajv, specificKeywords, options) return ajv } - for (keyword in plugins) get(keyword)(ajv) + + 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 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