Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,113 @@ describe('date helper', () => {
);
});
});

describe('formatNumber helper', () => {
test('formats string numbers', () => {
const url = 'https://elastic.co/{{formatNumber value "0.0"}}';
expect(compile(url, { value: '32.9999' })).toMatchInlineSnapshot(`"https://elastic.co/33.0"`);
expect(compile(url, { value: '32.555' })).toMatchInlineSnapshot(`"https://elastic.co/32.6"`);
});

test('formats numbers', () => {
const url = 'https://elastic.co/{{formatNumber value "0.0"}}';
expect(compile(url, { value: 32.9999 })).toMatchInlineSnapshot(`"https://elastic.co/33.0"`);
expect(compile(url, { value: 32.555 })).toMatchInlineSnapshot(`"https://elastic.co/32.6"`);
});

test("doesn't fail on Nan", () => {
const url = 'https://elastic.co/{{formatNumber value "0.0"}}';
expect(compile(url, { value: null })).toMatchInlineSnapshot(`"https://elastic.co/"`);
expect(compile(url, { value: undefined })).toMatchInlineSnapshot(`"https://elastic.co/"`);
expect(compile(url, { value: 'not a number' })).toMatchInlineSnapshot(
`"https://elastic.co/not%20a%20number"`
);
});

test('fails on missing format string', () => {
const url = 'https://elastic.co/{{formatNumber value}}';
expect(() => compile(url, { value: 12 })).toThrowError();
});

// this doesn't work and doesn't seem
// possible to validate with our version of numeral
test.skip('fails on malformed format string', () => {
const url = 'https://elastic.co/{{formatNumber value "not a real format string"}}';
expect(() => compile(url, { value: 12 })).toThrowError();
});
});

describe('match helper', () => {
test('matches RegExp and uses capture group', () => {
const url = 'https://elastic.co/{{lookup (lookup (match value "Label:(.*)") 0) 1}}';

expect(compile(url, { value: 'Label:Feature:Something' })).toMatchInlineSnapshot(
`"https://elastic.co/Feature:Something"`
);
});

test('no matches', () => {
const url = 'https://elastic.co/{{lookup (lookup (match value "Label:(.*)") 0) 1}}';

expect(compile(url, { value: 'No matches' })).toMatchInlineSnapshot(`"https://elastic.co/"`);
});
});

describe('basic string formatting helpers', () => {
test('lowercase', () => {
const compileUrl = (value: unknown) =>
compile('https://elastic.co/{{lowercase value}}', { value });

expect(compileUrl('Some String Value')).toMatchInlineSnapshot(
`"https://elastic.co/some%20string%20value"`
);
expect(compileUrl(4)).toMatchInlineSnapshot(`"https://elastic.co/4"`);
expect(compileUrl(null)).toMatchInlineSnapshot(`"https://elastic.co/null"`);
});
test('uppercase', () => {
const compileUrl = (value: unknown) =>
compile('https://elastic.co/{{uppercase value}}', { value });

expect(compileUrl('Some String Value')).toMatchInlineSnapshot(
`"https://elastic.co/SOME%20STRING%20VALUE"`
);
expect(compileUrl(4)).toMatchInlineSnapshot(`"https://elastic.co/4"`);
expect(compileUrl(null)).toMatchInlineSnapshot(`"https://elastic.co/NULL"`);
});
test('trim', () => {
const compileUrl = (fn: 'trim' | 'trimLeft' | 'trimRight', value: unknown) =>
compile(`https://elastic.co/{{${fn} value}}`, { value });

expect(compileUrl('trim', ' trim-me ')).toMatchInlineSnapshot(`"https://elastic.co/trim-me"`);
expect(compileUrl('trimRight', ' trim-me ')).toMatchInlineSnapshot(
`"https://elastic.co/%20%20trim-me"`
);
expect(compileUrl('trimLeft', ' trim-me ')).toMatchInlineSnapshot(
`"https://elastic.co/trim-me%20%20"`
);
});
test('left,right,mid', () => {
const compileExpression = (expression: string, value: unknown) =>
compile(`https://elastic.co/${expression}`, { value });

expect(compileExpression('{{left value 3}}', '12345')).toMatchInlineSnapshot(
`"https://elastic.co/123"`
);
expect(compileExpression('{{right value 3}}', '12345')).toMatchInlineSnapshot(
`"https://elastic.co/345"`
);
expect(compileExpression('{{mid value 1 3}}', '12345')).toMatchInlineSnapshot(
`"https://elastic.co/234"`
);
});

test('concat', () => {
expect(
compile(`https://elastic.co/{{concat value1 "," value2}}`, { value1: 'v1', value2: 'v2' })
).toMatchInlineSnapshot(`"https://elastic.co/v1,v2"`);

expect(
compile(`https://elastic.co/{{concat valueArray}}`, { valueArray: ['1', '2', '3'] })
).toMatchInlineSnapshot(`"https://elastic.co/1,2,3"`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { create as createHandlebars, HelperDelegate, HelperOptions } from 'handl
import { encode, RisonValue } from 'rison-node';
import dateMath from '@elastic/datemath';
import moment, { Moment } from 'moment';
import numeral from '@elastic/numeral';

const handlebars = createHandlebars();

Expand Down Expand Up @@ -69,6 +70,63 @@ handlebars.registerHelper('date', (...args) => {
return format ? momentDate.format(format) : momentDate.toISOString();
});

handlebars.registerHelper('formatNumber', (rawValue: unknown, pattern: string) => {
if (!pattern || typeof pattern !== 'string')
throw new Error(`[formatNumber]: pattern string is required`);
const value = Number(rawValue);
if (rawValue == null || Number.isNaN(value)) return rawValue;
return numeral(value).format(pattern);
});

/**
* Allows to match regex patterns and extract capturing groups.
* Result is array of arrays.
*
* @example
*
* Have a string: "Label:Feature:Something"
* and want to extract: "Feature:Something"
*
* expression: `{{match value "Label:(.*)"}}`,
* returns: [["Label:Feature:Something", "Feature:Something"]]
*/
handlebars.registerHelper('match', (rawValue: unknown, regexpString: string) => {
if (!regexpString || typeof regexpString !== 'string')
throw new Error(`[match]: regexp string is required`);
const regexp = new RegExp(regexpString, 'g');
const valueString = String(rawValue);
return Array.from(valueString.matchAll(regexp));
});

function toString(value: unknown): string {
return String(value);
}
handlebars.registerHelper('lowercase', (rawValue: unknown) => toString(rawValue).toLowerCase());
handlebars.registerHelper('uppercase', (rawValue: unknown) => toString(rawValue).toUpperCase());
handlebars.registerHelper('trim', (rawValue: unknown) => toString(rawValue).trim());
handlebars.registerHelper('trimLeft', (rawValue: unknown) => toString(rawValue).trimLeft());
handlebars.registerHelper('trimRight', (rawValue: unknown) => toString(rawValue).trimRight());
handlebars.registerHelper('concat', (...args) => {
const values = args.slice(0, -1) as unknown[];
return values.join('');
});

handlebars.registerHelper('left', (rawValue: unknown, numberOfChars: number) => {
if (typeof numberOfChars !== 'number')
throw new Error('[left]: expected "number of characters to extract" to be a number');
return toString(rawValue).slice(0, numberOfChars);
});
handlebars.registerHelper('right', (rawValue: unknown, numberOfChars: number) => {
if (typeof numberOfChars !== 'number')
throw new Error('[left]: expected "number of characters to extract" to be a number');
return toString(rawValue).slice(-1 * numberOfChars);
});
handlebars.registerHelper('mid', (rawValue: unknown, start: number, length: number) => {
if (typeof start !== 'number') throw new Error('[left]: expected "start" to be a number');
if (typeof length !== 'number') throw new Error('[left]: expected "length" to be a number');
return toString(rawValue).substr(start, length);
});

export function compile(url: string, context: object): string {
const template = handlebars.compile(url, { strict: true, noEscape: true });
return encodeURI(template(context));
Expand Down