Skip to content

Commit

Permalink
feat: allow custom expression functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Marsup committed Sep 27, 2023
1 parent 01bff41 commit 6a835c1
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 4 deletions.
3 changes: 2 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ const schema = custom.object(); // Returns Joi.object().min(1)
Generates a dynamic expression using a template string where:
- `template` - the template string using the [template syntax](#template-syntax).
- `options` - optional settings used when creating internal references. Supports the same options
as [`ref()`](#refkey-options).
as [`ref()`](#refkey-options), in addition to those options:
- `functions` - an object with keys being function names and values being their implementation that will be executed when used in the expression. Using the same name as a built-in function will result in a local override. Note: carefully check your arguments depending on the situation where the expression is used.

#### Template syntax

Expand Down
24 changes: 21 additions & 3 deletions lib/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,20 @@ module.exports = exports = internals.Template = class {
this.rendered = source;

this._template = null;
this._settings = Clone(options);

if (options) {
const { functions, ...opts } = options;
this._settings = Object.keys(opts).length ? Clone(opts) : undefined;
this._functions = functions;
if (this._functions) {
Assert(Object.keys(this._functions).every((key) => typeof key === 'string'), 'Functions keys must be strings');
Assert(Object.values(this._functions).every((key) => typeof key === 'function'), 'Functions values must be functions');
}
}
else {
this._settings = undefined;
this._functions = undefined;
}

this._parse();
}
Expand Down Expand Up @@ -122,12 +135,16 @@ module.exports = exports = internals.Template = class {
desc.options = this._settings;
}

if (this._functions) {
desc.functions = this._functions;
}

return desc;
}

static build(desc) {

return new internals.Template(desc.template, desc.options);
return new internals.Template(desc.template, desc.options || desc.functions ? { ...desc.options, functions: desc.functions } : undefined);
}

isDynamic() {
Expand Down Expand Up @@ -215,7 +232,8 @@ module.exports = exports = internals.Template = class {
};

try {
var formula = new Formula.Parser(content, { reference, functions: internals.functions, constants: internals.constants });
const functions = this._functions ? { ...internals.functions, ...this._functions } : internals.functions;
var formula = new Formula.Parser(content, { reference, functions, constants: internals.constants });
}
catch (err) {
err.message = `Invalid template variable "${content}" fails due to: ${err.message}`;
Expand Down
43 changes: 43 additions & 0 deletions test/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,49 @@ describe('Template', () => {

describe('functions', () => {

describe('extensions', () => {

it('allow new functions', () => {

const schema = Joi.object().rename(/.*/, Joi.x('{ uppercase(#0) }', {
functions: {
uppercase(value) {

if (typeof value === 'string') {
return value.toUpperCase();
}

return value;
}
}
}));
Helper.validate(schema, {}, [
[{ a: 1, b: true }, true, { A: 1, B: true }],
[{ a: 1, [Symbol.for('b')]: true }, true, { A: 1, [Symbol.for('b')]: true }]
]);
});

it('overrides built-in functions', () => {

const schema = Joi.object({
a: Joi.array().length(Joi.x('{length(b)}', {
functions: {
length(value) {

return value.length - 1;
}
}
})),
b: Joi.string()
});

Helper.validate(schema, [
[{ a: [1], b: 'xx' }, true],
[{ a: [1], b: 'x' }, false, '"a" must contain {length(b)} items']
]);
});
});

describe('msg()', () => {

it('ignores missing options', () => {
Expand Down

0 comments on commit 6a835c1

Please sign in to comment.