Skip to content

Commit 36aae73

Browse files
authored
feat: enhanced custom function registration (#60)
Adds enhanced function registration API with type safety and error handling. Maintain the existing `registerFunction` API for backward compatibility. ```typescript import { register, search, TYPE_NUMBER } from "@jmespath-community/jmespath"; // TypeScript prevents registering built-in functions at compile time // register('sum', myFunc, signature); // TypeScript error! // Enhanced registration with better error handling const result = register('multiply', ([a, b]) => a * b, [ { types: [TYPE_NUMBER] }, { types: [TYPE_NUMBER] } ]); if (result.success) { console.log(result.message); // "Function multiply() registered successfully" } else { console.error(result.message); // Detailed error information } ``` ```javascript import { registerFunction, register } from "@jmespath-community/jmespath"; // Option 1: Using registerFunction with options registerFunction('myFunc', () => 'first', []); registerFunction('myFunc', () => 'second', [], { override: true, warn: true }); // Console: "Warning: Overriding existing function: myFunc()" // Option 2: Using enhanced register API const result = register('myFunc', () => 'third', [], { override: true }); console.log(result.message); // "Function myFunc() overridden successfully" ``` ```javascript import { isRegistered, getRegisteredFunctions, getCustomFunctions, unregisterFunction, clearCustomFunctions } from "@jmespath-community/jmespath"; // Check if function exists console.log(isRegistered('sum')); // true (built-in) console.log(isRegistered('myFunc')); // true (if registered) // Get all registered functions const allFunctions = getRegisteredFunctions(); console.log(allFunctions); // ['abs', 'avg', 'ceil', ..., 'myFunc'] // Get only custom functions const customFunctions = getCustomFunctions(); console.log(customFunctions); // ['myFunc', 'divide', ...] // Unregister custom function (built-ins cannot be unregistered) const removed = unregisterFunction('myFunc'); console.log(removed); // true if successful // Clear all custom functions clearCustomFunctions(); console.log(getCustomFunctions()); // [] ``` <!-- ps-id: 6c0db344-9d2c-4393-8899-8d6e92a911fc -->
1 parent 05955bc commit 36aae73

File tree

4 files changed

+603
-30
lines changed

4 files changed

+603
-30
lines changed

README.md

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,13 @@ TreeInterpreter.search(ast, { foo: { bar: "BAZ" } });
9191

9292
## EXTENSIONS TO ORIGINAL SPEC
9393

94-
1. ### Register you own custom functions
94+
1. ### Register your own custom functions
9595

96-
#### `registerFunction(functionName: string, customFunction: RuntimeFunction, signature: InputSignature[]): void`
96+
#### Enhanced Function Registry API
9797

98-
Extend the list of built in JMESpath expressions with your own functions.
98+
The library provides both backward-compatible and enhanced APIs for registering custom functions with improved developer experience, type safety, and flexible override behavior.
99+
100+
##### Basic Usage (Backward Compatible)
99101

100102
```javascript
101103
import { search, registerFunction, TYPE_NUMBER } from "@jmespath-community/jmespath";
@@ -117,14 +119,84 @@ TreeInterpreter.search(ast, { foo: { bar: "BAZ" } });
117119
// OUTPUTS: 6
118120
```
119121

122+
##### Enhanced Registry API with Type Safety
123+
124+
```typescript
125+
import { register, search, TYPE_NUMBER } from "@jmespath-community/jmespath";
126+
127+
// TypeScript prevents registering built-in functions at compile time
128+
// register('sum', myFunc, signature); // TypeScript error!
129+
130+
// Enhanced registration with better error handling
131+
const result = register('multiply', ([a, b]) => a * b, [
132+
{ types: [TYPE_NUMBER] },
133+
{ types: [TYPE_NUMBER] }
134+
]);
135+
136+
if (result.success) {
137+
console.log(result.message); // "Function multiply() registered successfully"
138+
} else {
139+
console.error(result.message); // Detailed error information
140+
}
141+
```
142+
143+
##### Override Existing Functions
144+
145+
```javascript
146+
import { registerFunction, register } from "@jmespath-community/jmespath";
147+
148+
// Option 1: Using registerFunction with options
149+
registerFunction('myFunc', () => 'first', []);
150+
registerFunction('myFunc', () => 'second', [], { override: true, warn: true });
151+
// Console: "Warning: Overriding existing function: myFunc()"
152+
153+
// Option 2: Using enhanced register API
154+
const result = register('myFunc', () => 'third', [], { override: true });
155+
console.log(result.message); // "Function myFunc() overridden successfully"
156+
```
157+
158+
##### Registry Management
159+
160+
```javascript
161+
import {
162+
isRegistered,
163+
getRegisteredFunctions,
164+
getCustomFunctions,
165+
unregisterFunction,
166+
clearCustomFunctions
167+
} from "@jmespath-community/jmespath";
168+
169+
// Check if function exists
170+
console.log(isRegistered('sum')); // true (built-in)
171+
console.log(isRegistered('myFunc')); // true (if registered)
172+
173+
// Get all registered functions
174+
const allFunctions = getRegisteredFunctions();
175+
console.log(allFunctions); // ['abs', 'avg', 'ceil', ..., 'myFunc']
176+
177+
// Get only custom functions
178+
const customFunctions = getCustomFunctions();
179+
console.log(customFunctions); // ['myFunc', 'divide', ...]
180+
181+
// Unregister custom function (built-ins cannot be unregistered)
182+
const removed = unregisterFunction('myFunc');
183+
console.log(removed); // true if successful
184+
185+
// Clear all custom functions
186+
clearCustomFunctions();
187+
console.log(getCustomFunctions()); // []
188+
```
189+
190+
##### Optional Arguments
191+
120192
Optional arguments are supported by setting `{..., optional: true}` in argument signatures
121193

122194
```javascript
123195
registerFunction(
124196
"divide",
125197
(resolvedArgs) => {
126198
const [dividend, divisor] = resolvedArgs;
127-
return dividend / divisor ?? 1; //OPTIONAL DIVISOR THAT DEFAULTS TO 1
199+
return dividend / (divisor ?? 1); //OPTIONAL DIVISOR THAT DEFAULTS TO 1
128200
},
129201
[{ types: [TYPE_NUMBER] }, { types: [TYPE_NUMBER], optional: true }], //SIGNATURE
130202
);

src/Runtime.ts

Lines changed: 224 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,109 @@ export interface FunctionTable {
5858
[functionName: string]: FunctionSignature;
5959
}
6060

61-
export class Runtime {
61+
// Built-in function names for TypeScript 5.x template literal type checking
62+
export type BuiltInFunctionNames =
63+
| 'abs'
64+
| 'avg'
65+
| 'ceil'
66+
| 'contains'
67+
| 'ends_with'
68+
| 'find_first'
69+
| 'find_last'
70+
| 'floor'
71+
| 'from_items'
72+
| 'group_by'
73+
| 'items'
74+
| 'join'
75+
| 'keys'
76+
| 'length'
77+
| 'lower'
78+
| 'map'
79+
| 'max'
80+
| 'max_by'
81+
| 'merge'
82+
| 'min'
83+
| 'min_by'
84+
| 'not_null'
85+
| 'pad_left'
86+
| 'pad_right'
87+
| 'replace'
88+
| 'reverse'
89+
| 'sort'
90+
| 'sort_by'
91+
| 'split'
92+
| 'starts_with'
93+
| 'sum'
94+
| 'to_array'
95+
| 'to_number'
96+
| 'to_string'
97+
| 'type'
98+
| 'upper'
99+
| 'values'
100+
| 'zip';
101+
102+
// Registration options for enhanced registerFunction behavior
103+
export interface RegisterOptions {
104+
/**
105+
* Allow overriding existing functions. Default: false
106+
* When true, replaces existing function without error
107+
* When false, throws error if function already exists (backward compatible)
108+
*/
109+
override?: boolean;
110+
/**
111+
* Emit warning when overriding existing functions. Default: false
112+
* Only applies when override is true
113+
*/
114+
warn?: boolean;
115+
}
116+
117+
// Registration result for better error handling and introspection
118+
export type RegistrationResult =
119+
| { success: true; message?: string }
120+
| { success: false; reason: 'already-exists' | 'invalid-signature' | 'invalid-name'; message: string };
121+
122+
// Enhanced function registry interface for state management
123+
export interface FunctionRegistry {
124+
/**
125+
* Register a new function with optional override behavior
126+
*/
127+
register<T extends string>(
128+
name: T extends BuiltInFunctionNames ? never : T,
129+
func: RuntimeFunction<(JSONValue | ExpressionNode)[], JSONValue>,
130+
signature: InputSignature[],
131+
options?: RegisterOptions,
132+
): RegistrationResult;
133+
134+
/**
135+
* Unregister a custom function (built-in functions cannot be unregistered)
136+
*/
137+
unregister<T extends string>(name: T extends BuiltInFunctionNames ? never : T): boolean;
138+
139+
/**
140+
* Check if a function is registered
141+
*/
142+
isRegistered(name: string): boolean;
143+
144+
/**
145+
* Get list of all registered function names
146+
*/
147+
getRegistered(): string[];
148+
149+
/**
150+
* Get list of custom (non-built-in) function names
151+
*/
152+
getCustomFunctions(): string[];
153+
154+
/**
155+
* Clear all custom functions (built-in functions remain)
156+
*/
157+
clearCustomFunctions(): void;
158+
}
159+
160+
export class Runtime implements FunctionRegistry {
62161
_interpreter: TreeInterpreter;
63162
_functionTable: FunctionTable;
163+
private _customFunctions: Set<string> = new Set();
64164
TYPE_NAME_TABLE: { [InputArgument: number]: string } = {
65165
[InputArgument.TYPE_NUMBER]: 'number',
66166
[InputArgument.TYPE_ANY]: 'any',
@@ -81,18 +181,139 @@ export class Runtime {
81181
this._functionTable = this.functionTable;
82182
}
83183

184+
/**
185+
* Enhanced registerFunction with backward compatibility and new options
186+
* @deprecated Use register() method for enhanced functionality
187+
*/
84188
registerFunction(
85189
name: string,
86190
customFunction: RuntimeFunction<(JSONValue | ExpressionNode)[], JSONValue>,
87191
signature: InputSignature[],
192+
options?: RegisterOptions,
88193
): void {
89-
if (name in this._functionTable) {
90-
throw new Error(`Function already defined: ${name}()`);
194+
// For backward compatibility, we bypass the type checking here
195+
// The register method will still validate the function name at runtime
196+
const result = this._registerInternal(name, customFunction, signature, options);
197+
if (!result.success) {
198+
throw new Error(result.message);
91199
}
200+
}
201+
202+
/**
203+
* Internal registration method that bypasses TypeScript type checking
204+
*/
205+
private _registerInternal(
206+
name: string,
207+
customFunction: RuntimeFunction<(JSONValue | ExpressionNode)[], JSONValue>,
208+
signature: InputSignature[],
209+
options: RegisterOptions = {},
210+
): RegistrationResult {
211+
// Validate function name
212+
if (!name || typeof name !== 'string' || name.trim() === '') {
213+
return {
214+
success: false,
215+
reason: 'invalid-name',
216+
message: 'Function name must be a non-empty string',
217+
};
218+
}
219+
220+
// Validate signature
221+
try {
222+
this.validateInputSignatures(name, signature);
223+
} catch (error) {
224+
return {
225+
success: false,
226+
reason: 'invalid-signature',
227+
message: error instanceof Error ? error.message : 'Invalid function signature',
228+
};
229+
}
230+
231+
const { override = false, warn = false } = options;
232+
const exists = name in this._functionTable;
233+
234+
// Handle existing function
235+
if (exists && !override) {
236+
return {
237+
success: false,
238+
reason: 'already-exists',
239+
message: `Function already defined: ${name}(). Use { override: true } to replace it.`,
240+
};
241+
}
242+
243+
// Emit warning if requested
244+
if (exists && override && warn) {
245+
console.warn(`Warning: Overriding existing function: ${name}()`);
246+
}
247+
248+
// Register the function
92249
this._functionTable[name] = {
93250
_func: customFunction.bind(this),
94251
_signature: signature,
95252
};
253+
254+
// Track custom functions (exclude built-ins)
255+
this._customFunctions.add(name);
256+
257+
const message = exists
258+
? `Function ${name}() overridden successfully`
259+
: `Function ${name}() registered successfully`;
260+
return { success: true, message };
261+
}
262+
263+
/**
264+
* Register a new function with enhanced options and type safety
265+
*/
266+
register<T extends string>(
267+
name: T extends BuiltInFunctionNames ? never : T,
268+
customFunction: RuntimeFunction<(JSONValue | ExpressionNode)[], JSONValue>,
269+
signature: InputSignature[],
270+
options: RegisterOptions = {},
271+
): RegistrationResult {
272+
return this._registerInternal(name, customFunction, signature, options);
273+
}
274+
275+
/**
276+
* Unregister a custom function (built-in functions cannot be unregistered)
277+
*/
278+
unregister<T extends string>(name: T extends BuiltInFunctionNames ? never : T): boolean {
279+
if (!this._customFunctions.has(name)) {
280+
return false; // Function doesn't exist or is built-in
281+
}
282+
283+
delete this._functionTable[name];
284+
this._customFunctions.delete(name);
285+
return true;
286+
}
287+
288+
/**
289+
* Check if a function is registered
290+
*/
291+
isRegistered(name: string): boolean {
292+
return name in this._functionTable;
293+
}
294+
295+
/**
296+
* Get list of all registered function names
297+
*/
298+
getRegistered(): string[] {
299+
return Object.keys(this._functionTable);
300+
}
301+
302+
/**
303+
* Get list of custom (non-built-in) function names
304+
*/
305+
getCustomFunctions(): string[] {
306+
return Array.from(this._customFunctions);
307+
}
308+
309+
/**
310+
* Clear all custom functions (built-in functions remain)
311+
*/
312+
clearCustomFunctions(): void {
313+
for (const name of this._customFunctions) {
314+
delete this._functionTable[name];
315+
}
316+
this._customFunctions.clear();
96317
}
97318

98319
callFunction(name: string, resolvedArgs: (JSONValue | ExpressionNode)[]): JSONValue {

0 commit comments

Comments
 (0)