Skip to content

Commit

Permalink
Add support for function aliases, case-insensitive matching (elastic#748
Browse files Browse the repository at this point in the history
)

* feat: add support for function aliases

* fix: rename to getByAlias

* feat: case-insensitive matching

* fix: remove isAlias in arg def since it is no longer used
  • Loading branch information
lukasolson authored and Rashid Khan committed Jul 6, 2018
1 parent b293545 commit df733c6
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 21 deletions.
2 changes: 1 addition & 1 deletion common/functions/switch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const switchFn = () => ({
args: {
_: {
types: ['case'],
aliases: ['cases'],
aliases: ['case'],
resolve: false,
multi: true,
help: 'The list of conditions to check',
Expand Down
19 changes: 10 additions & 9 deletions common/interpreter/interpret.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { each, keys, last, mapValues, reduce, zipObject } from 'lodash';
import clone from 'lodash.clone';
import { getType } from '../lib/get_type';
import { fromExpression } from '../lib/ast';
import { getByAlias } from '../lib/get_by_alias';
import { typesRegistry } from '../lib/types_registry';
import { castProvider } from './cast';

Expand Down Expand Up @@ -45,7 +46,7 @@ export function interpretProvider(config) {
const chain = clone(chainArr);
const link = chain.shift(); // Every thing in the chain will always be a function right?
const { function: fnName, arguments: fnArgs } = link;
const fnDef = functions[fnName];
const fnDef = getByAlias(functions, fnName);

// if the function is not found, pass the expression chain to the not found handler
// in this case, it will try to execute the function in another context
Expand All @@ -59,7 +60,7 @@ export function interpretProvider(config) {
// resolveArgs returns an object because the arguments themselves might
// actually have a 'then' function which would be treated as a promise
const { resolvedArgs } = await resolveArgs(fnDef, context, fnArgs);
const newContext = await invokeFunction(fnName, context, resolvedArgs);
const newContext = await invokeFunction(fnDef, context, resolvedArgs);

// if something failed, just return the failure
if (getType(newContext) === 'error') {
Expand All @@ -75,9 +76,8 @@ export function interpretProvider(config) {
}
}

async function invokeFunction(name, context, args) {
async function invokeFunction(fnDef, context, args) {
// Check function input.
const fnDef = functions[name];
const acceptableContext = cast(context, fnDef.context.types);
const fnOutput = await fnDef.fn(acceptableContext, args, handlers);

Expand All @@ -87,7 +87,8 @@ export function interpretProvider(config) {
const expectedType = fnDef.type;
if (expectedType && returnType !== expectedType) {
throw new Error(
`Function '${name}' should return '${expectedType}',` + ` actually returned '${returnType}'`
`Function '${fnDef.name}' should return '${expectedType}',` +
` actually returned '${returnType}'`
);
}

Expand All @@ -97,7 +98,7 @@ export function interpretProvider(config) {
try {
type.validate(fnOutput);
} catch (e) {
throw new Error(`Output of '${name}' is not a valid type '${fnDef.type}': ${e}`);
throw new Error(`Output of '${fnDef.name}' is not a valid type '${fnDef.type}': ${e}`);
}
}

Expand All @@ -112,12 +113,12 @@ export function interpretProvider(config) {
const dealiasedArgAsts = reduce(
argAsts,
(argAsts, argAst, argName) => {
const argDef = getByAlias(argDefs, argName);
// TODO: Implement a system to allow for undeclared arguments
if (!argDefs[argName]) {
if (!argDef) {
throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`);
}
const { name } = argDefs[argName];
argAsts[name] = (argAsts[name] || []).concat(argAst);
argAsts[argDef.name] = (argAsts[argDef.name] || []).concat(argAst);
return argAsts;
},
{}
Expand Down
4 changes: 2 additions & 2 deletions common/interpreter/socket_interpret.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { has } from 'lodash';
import uuid from 'uuid/v4';
import { getByAlias } from '../lib/get_by_alias';
import { serializeProvider } from '../lib/serialize';
import { interpretProvider } from './interpret';

Expand Down Expand Up @@ -34,7 +34,7 @@ export function socketInterpreterProvider({
// Get the list of functions that are known elsewhere
return Promise.resolve(referableFunctions).then(referableFunctionMap => {
// Check if the not-found function is in the list of alternatives, if not, throw
if (!has(referableFunctionMap, functionName)) {
if (!getByAlias(referableFunctionMap, functionName)) {
throw new Error(`Function not found: ${functionName}`);
}

Expand Down
47 changes: 47 additions & 0 deletions common/lib/__tests__/get_by_alias.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import expect from 'expect.js';
import { getByAlias } from '../get_by_alias';

describe('getByAlias', () => {
const fns = {
foo: { aliases: ['f'] },
bar: { aliases: ['b'] },
};

it('returns the function by name', () => {
expect(getByAlias(fns, 'foo')).to.be(fns.foo);
expect(getByAlias(fns, 'bar')).to.be(fns.bar);
});

it('returns the function by alias', () => {
expect(getByAlias(fns, 'f')).to.be(fns.foo);
expect(getByAlias(fns, 'b')).to.be(fns.bar);
});

it('returns the function by case-insensitive name', () => {
expect(getByAlias(fns, 'FOO')).to.be(fns.foo);
expect(getByAlias(fns, 'BAR')).to.be(fns.bar);
});

it('returns the function by case-insensitive alias', () => {
expect(getByAlias(fns, 'F')).to.be(fns.foo);
expect(getByAlias(fns, 'B')).to.be(fns.bar);
});

it('handles empty strings', () => {
const emptyStringFns = { '': {} };
const emptyStringAliasFns = { foo: { aliases: [''] } };
expect(getByAlias(emptyStringFns, '')).to.be(emptyStringFns['']);
expect(getByAlias(emptyStringAliasFns, '')).to.be(emptyStringAliasFns.foo);
});

it('handles "undefined" strings', () => {
const emptyStringFns = { undefined: {} };
const emptyStringAliasFns = { foo: { aliases: ['undefined'] } };
expect(getByAlias(emptyStringFns, 'undefined')).to.be(emptyStringFns.undefined);
expect(getByAlias(emptyStringAliasFns, 'undefined')).to.be(emptyStringAliasFns.foo);
});

it('returns undefined if not found', () => {
expect(getByAlias(fns, 'baz')).to.be(undefined);
});
});
1 change: 0 additions & 1 deletion common/lib/arg.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export function Arg(config) {
this.types = config.types || [];
this.default = config.default;
this.aliases = config.aliases || [];
this.isAlias = config.isAlias || false;
this.multi = config.multi == null ? false : config.multi;
this.resolve = config.resolve == null ? true : config.resolve;
this.accepts = type => {
Expand Down
10 changes: 2 additions & 8 deletions common/lib/fn.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { each, includes } from 'lodash';
import { mapValues, includes } from 'lodash';
import { Arg } from './arg';

export function Fn(config) {
Expand All @@ -16,13 +16,7 @@ export function Fn(config) {

// Optional
this.help = config.help || ''; // A short help text
this.args = {};
each(config.args, (arg, name) => {
this.args[name] = new Arg({ name, ...arg });
each(arg.aliases, alias => {
this.args[alias] = new Arg({ name, ...arg, isAlias: true });
});
});
this.args = mapValues(config.args || {}, (arg, name) => new Arg({ name, ...arg }));

this.context = config.context || {};

Expand Down
15 changes: 15 additions & 0 deletions common/lib/get_by_alias.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* This is used for looking up function/argument definitions. It looks through
* the given object for a case-insensitive match, which could be either the
* name of the key itself, or something under the `aliases` property.
*/
export function getByAlias(specs, name) {
const lowerCaseName = name.toLowerCase();
const key = Object.keys(specs).find(key => {
if (key.toLowerCase() === lowerCaseName) return true;
return (specs[key].aliases || []).some(alias => {
return alias.toLowerCase() === lowerCaseName;
});
});
if (typeof key !== undefined) return specs[key];
}

0 comments on commit df733c6

Please sign in to comment.