Skip to content

Commit ea6557c

Browse files
authored
Merge pull request #2988 from hapijs/feat/custom-expression-functions
feat: allow custom expression functions
2 parents 01bff41 + 6a835c1 commit ea6557c

File tree

3 files changed

+66
-4
lines changed

3 files changed

+66
-4
lines changed

API.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ const schema = custom.object(); // Returns Joi.object().min(1)
233233
Generates a dynamic expression using a template string where:
234234
- `template` - the template string using the [template syntax](#template-syntax).
235235
- `options` - optional settings used when creating internal references. Supports the same options
236-
as [`ref()`](#refkey-options).
236+
as [`ref()`](#refkey-options), in addition to those options:
237+
- `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.
237238

238239
#### Template syntax
239240

lib/template.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,20 @@ module.exports = exports = internals.Template = class {
3737
this.rendered = source;
3838

3939
this._template = null;
40-
this._settings = Clone(options);
40+
41+
if (options) {
42+
const { functions, ...opts } = options;
43+
this._settings = Object.keys(opts).length ? Clone(opts) : undefined;
44+
this._functions = functions;
45+
if (this._functions) {
46+
Assert(Object.keys(this._functions).every((key) => typeof key === 'string'), 'Functions keys must be strings');
47+
Assert(Object.values(this._functions).every((key) => typeof key === 'function'), 'Functions values must be functions');
48+
}
49+
}
50+
else {
51+
this._settings = undefined;
52+
this._functions = undefined;
53+
}
4154

4255
this._parse();
4356
}
@@ -122,12 +135,16 @@ module.exports = exports = internals.Template = class {
122135
desc.options = this._settings;
123136
}
124137

138+
if (this._functions) {
139+
desc.functions = this._functions;
140+
}
141+
125142
return desc;
126143
}
127144

128145
static build(desc) {
129146

130-
return new internals.Template(desc.template, desc.options);
147+
return new internals.Template(desc.template, desc.options || desc.functions ? { ...desc.options, functions: desc.functions } : undefined);
131148
}
132149

133150
isDynamic() {
@@ -215,7 +232,8 @@ module.exports = exports = internals.Template = class {
215232
};
216233

217234
try {
218-
var formula = new Formula.Parser(content, { reference, functions: internals.functions, constants: internals.constants });
235+
const functions = this._functions ? { ...internals.functions, ...this._functions } : internals.functions;
236+
var formula = new Formula.Parser(content, { reference, functions, constants: internals.constants });
219237
}
220238
catch (err) {
221239
err.message = `Invalid template variable "${content}" fails due to: ${err.message}`;

test/template.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,49 @@ describe('Template', () => {
176176

177177
describe('functions', () => {
178178

179+
describe('extensions', () => {
180+
181+
it('allow new functions', () => {
182+
183+
const schema = Joi.object().rename(/.*/, Joi.x('{ uppercase(#0) }', {
184+
functions: {
185+
uppercase(value) {
186+
187+
if (typeof value === 'string') {
188+
return value.toUpperCase();
189+
}
190+
191+
return value;
192+
}
193+
}
194+
}));
195+
Helper.validate(schema, {}, [
196+
[{ a: 1, b: true }, true, { A: 1, B: true }],
197+
[{ a: 1, [Symbol.for('b')]: true }, true, { A: 1, [Symbol.for('b')]: true }]
198+
]);
199+
});
200+
201+
it('overrides built-in functions', () => {
202+
203+
const schema = Joi.object({
204+
a: Joi.array().length(Joi.x('{length(b)}', {
205+
functions: {
206+
length(value) {
207+
208+
return value.length - 1;
209+
}
210+
}
211+
})),
212+
b: Joi.string()
213+
});
214+
215+
Helper.validate(schema, [
216+
[{ a: [1], b: 'xx' }, true],
217+
[{ a: [1], b: 'x' }, false, '"a" must contain {length(b)} items']
218+
]);
219+
});
220+
});
221+
179222
describe('msg()', () => {
180223

181224
it('ignores missing options', () => {

0 commit comments

Comments
 (0)