diff --git a/src/Runtime.ts b/src/Runtime.ts index 66d84e8..7445dff 100644 --- a/src/Runtime.ts +++ b/src/Runtime.ts @@ -20,6 +20,7 @@ export enum InputArgument { export interface InputSignature { types: InputArgument[]; variadic?: boolean; + optional?: boolean; } export type RuntimeFunction = (resolvedArgs: T) => U; @@ -71,31 +72,44 @@ export class Runtime { return functionEntry._func.call(this, resolvedArgs); } + private validateInputSignatures(name: string, signature: InputSignature[]): void { + for (let i = 0; i < signature.length; i += 1) { + if ('variadic' in signature[i] && i !== signature.length - 1) { + throw new Error(`ArgumentError: ${name}() 'variadic' argument ${i + 1} must occur last`); + } + if ('variadic' in signature[i] && 'optional' in signature[i]) { + throw new Error(`ArgumentError: ${name}() 'variadic' argument ${i + 1} cannot also be 'optional'`); + } + } + } + private validateArgs(name: string, args: any[], signature: InputSignature[]): void { let pluralized: boolean; - if (signature[signature.length - 1].variadic) { - if (args.length < signature.length) { - pluralized = signature.length > 1; - throw new Error( - `ArgumentError: ${name}() takes at least ${signature.length} argument${ - (pluralized && 's') || '' - } but received ${args.length}`, - ); - } - } else if (args.length !== signature.length) { + this.validateInputSignatures(name, signature); + const numberOfRequiredArgs = signature.filter(argSignature => !argSignature.optional ?? false).length; + const lastArgIsVariadic = signature[signature.length - 1]?.variadic ?? false; + const tooFewArgs = args.length < numberOfRequiredArgs; + const tooManyArgs = args.length > signature.length; + const tooFewModifier = + tooFewArgs && ((!lastArgIsVariadic && numberOfRequiredArgs > 1) || lastArgIsVariadic) ? 'at least ' : ''; + + if ((lastArgIsVariadic && tooFewArgs) || (!lastArgIsVariadic && (tooFewArgs || tooManyArgs))) { pluralized = signature.length > 1; throw new Error( - `ArgumentError: ${name}() takes ${signature.length} argument${(pluralized && 's') || ''} but received ${ - args.length - }`, + `ArgumentError: ${name}() takes ${tooFewModifier}${numberOfRequiredArgs} argument${ + (pluralized && 's') || '' + } but received ${args.length}`, ); } + let currentSpec: InputArgument[]; let actualType: InputArgument; let typeMatched: boolean; + let isOptional: boolean; for (let i = 0; i < signature.length; i += 1) { typeMatched = false; currentSpec = signature[i].types; + isOptional = signature[i].optional ?? false; actualType = this.getTypeName(args[i]) as InputArgument; let j: number; for (j = 0; j < currentSpec.length; j += 1) { @@ -104,7 +118,7 @@ export class Runtime { break; } } - if (!typeMatched) { + if (!typeMatched && !isOptional) { const expected = currentSpec .map((typeIdentifier): string => { return this.TYPE_NAME_TABLE[typeIdentifier]; diff --git a/test/jmespath.spec.js b/test/jmespath.spec.js index 24c2d57..e3310ef 100644 --- a/test/jmespath.spec.js +++ b/test/jmespath.spec.js @@ -234,4 +234,118 @@ describe('registerFunction', () => { ), ).toThrow('Function already defined: sum()'); }); + it('alerts too few arguments', () => { + registerFunction( + 'tooFewArgs', + () => { + /* EMPTY FUNCTION */ + }, + [{ types: [jmespath.TYPE_ANY] }], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'tooFewArgs()', + ), + ).toThrow('ArgumentError: tooFewArgs() takes 1 argument but received 0'); + }); + it('alerts too many arguments', () => { + registerFunction( + 'tooManyArgs', + () => { + /* EMPTY FUNCTION */ + }, + [], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'tooManyArgs(foo)', + ), + ).toThrow('ArgumentError: tooManyArgs() takes 0 argument but received 1'); + }); + + it('alerts optional variadic arguments', () => { + registerFunction( + 'optionalVariadic', + () => { + /* EMPTY FUNCTION */ + }, + [{ types: [jmespath.TYPE_ANY], optional: true, variadic: true }], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'optionalVariadic(foo)', + ), + ).toThrow("ArgumentError: optionalVariadic() 'variadic' argument 1 cannot also be 'optional'"); + }); + + it('alerts variadic is always last argument', () => { + registerFunction( + 'variadicAlwaysLast', + () => { + /* EMPTY FUNCTION */ + }, + [ + { types: [jmespath.TYPE_ANY], variadic: true }, + { types: [jmespath.TYPE_ANY], optional: true }, + ], + ); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'variadicAlwaysLast(foo)', + ), + ).toThrow("ArgumentError: variadicAlwaysLast() 'variadic' argument 1 must occur last"); + }); + + it('accounts for optional arguments', () => { + registerFunction( + 'optionalArgs', + ([first, second, third]) => { + return { first, second: second ?? 'default[2]', third: third ?? 'default[3]' }; + }, + [{ types: [jmespath.TYPE_ANY] }, { types: [jmespath.TYPE_ANY], optional: true }], + ); + expect( + search( + { + foo: 60, + bar: 10, + }, + 'optionalArgs(foo)', + ), + ).toEqual({ first: 60, second: 'default[2]', third: 'default[3]' }); + expect( + search( + { + foo: 60, + bar: 10, + }, + 'optionalArgs(foo, bar)', + ), + ).toEqual({ first: 60, second: 10, third: 'default[3]' }); + expect(() => + search( + { + foo: 60, + bar: 10, + }, + 'optionalArgs(foo, bar, [foo, bar])', + ), + ).toThrow('ArgumentError: optionalArgs() takes 1 arguments but received 3'); + }); });