Skip to content

Commit

Permalink
var(). Closes #1930
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jun 25, 2019
1 parent 08a1a6f commit 5d8ea13
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 43 deletions.
31 changes: 24 additions & 7 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- [`ref(key, [options])`](#refkey-options)
- [Relative references](#relative-references)
- [`isRef(ref)`](#isrefref)
- [`template(template, [options])`](#templatetemplate-options)
- [`var(template, [options])`](#vartemplate-options)
- [Template syntax](#template-syntax)
- [`isSchema(schema, [options])`](#isschemaschema-options)
- [`reach(schema, path)`](#reachschema-path)
Expand Down Expand Up @@ -564,27 +564,44 @@ const ref = Joi.ref('a');
Joi.isRef(ref); // returns true
```

### `template(template, [options])`
### `var(template, [options])`

Generates a template from a string where:
Generates a dyanmic variable 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).

#### Template syntax

The template syntax uses `{}` and `{{}}` enclosed variables to indicate that these values should be
replace with the actual referenced values at rendering time. Single braces `{}` leave the value
as-is, while double braces `{{}}` HTML-escape the result (unless the template is used for error messages
The template syntax uses `{}` and `{{}}` enclosed formulas to reference values as well as perform
number and string operations. Single braces `{}` leave the formula result as-is, while double
braces `{{}}` HTML-escape the formula result (unless the template is used for error messages
and the `errors.escapeHtml` preference flag is set to `false`).

The variable names can have one of the following prefixes:
The formula uses a simple mathematical syntax such as `a + b * 2` where the named formula variables
are references. Most references can be used as-is but some can create ambiguity with the formula
syntax and must be enclosed in `[]` bracets (e.g. `[.]`).

The formulas can only operate on `null`, booleans, numbers, and strings. If any operation involves
a string, all other numbers will be casted to strings (as the internal implementation uses simple
JavaScript operators). The supported operators are: `^`, `*`, `/`, `%`, `+`, `-`, `<`, `<=`, `>`,
`>=`, `==`, `!=`, `&&`, `||`, and `??` (in this order of precendece).

The reference names can have one of the following prefixes:
- `#` - indicates the variable references a local context value. For example, in errors this is the
error context, while in rename operations, it is the regular expression matching groups.
- `$` - indicates the variable references a global context value from the `context` preference object
provided as an option to the validation function or set using [`any.prefs()`](#anyprefsoptions--aliases-preferences-options).
- any other variable references a key within the current value being validated.

The formula syntax also supports built-in functions:
- `if(condition, then, otherwise)`

And the following constants:
- `null`
- `true`
- `false`

### `isSchema(schema, [options])`

Checks whether or not the provided argument is a **joi** schema where:
Expand Down
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ internals.root = function () {
return Common.callWithDefaults(this, internals.symbol, args);
};

root.template = function (...args) {
root.var = function (...args) {

return new Template(...args);
};

root.isTemplate = function (template) {
root.isVar = function (template) {

return Template.isTemplate(template);
};
Expand Down
19 changes: 18 additions & 1 deletion lib/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ module.exports = exports = internals.Template = class {
};

try {
var formula = new Formula(content, { reference });
var formula = new Formula(content, { reference, functions: internals.functions, constants: internals.constants });
}
catch (err) {
err.message = `Invalid template variable "${content}" fails due to: ${err.message}`;
Expand Down Expand Up @@ -305,3 +305,20 @@ internals.stringify = function (value, prefs, options) {

return options.wrapArrays ? '[' + partial + ']' : partial;
};


internals.constants = {

true: true,
false: false,
null: null
};


internals.functions = {

if: function (condition, then, otherwise) {

return condition ? then : otherwise;
}
};
2 changes: 1 addition & 1 deletion test/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('cast', () => {

const schema = Joi.object({
a: Joi.number(),
b: Joi.template('{a + 1}')
b: Joi.var('{a + 1}')
});

expect(schema.validate({ a: 5, b: 6 }).error).to.not.exist();
Expand Down
6 changes: 3 additions & 3 deletions test/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe('errors', () => {
},
latin: {
root: 'valorem',
'number.min': Joi.template('{@label} angustus', { prefix: { local: '@' } })
'number.min': Joi.var('{@label} angustus', { prefix: { local: '@' } })
},
empty: {}
};
Expand Down Expand Up @@ -203,7 +203,7 @@ describe('errors', () => {
'number.min': '{#label} too small'
},
latin: {
'number.min': Joi.template('{@label} angustus', { prefix: { local: '@' } })
'number.min': Joi.var('{@label} angustus', { prefix: { local: '@' } })
},
empty: {}
};
Expand Down Expand Up @@ -232,7 +232,7 @@ describe('errors', () => {
'number.min': '{#label} too small'
},
latin: {
'number.min': Joi.template('{@label} angustus', { prefix: { local: '@' } })
'number.min': Joi.var('{@label} angustus', { prefix: { local: '@' } })
},
empty: {}
};
Expand Down
36 changes: 18 additions & 18 deletions test/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('Template', () => {
it('skips template without {', () => {

const source = 'text without variables';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.isDynamic()).to.be.false();
Expand All @@ -27,7 +27,7 @@ describe('Template', () => {
it('skips template without variables', () => {

const source = 'text {{{ without }}} any }} variables';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.isDynamic()).to.be.false();
Expand All @@ -37,7 +37,7 @@ describe('Template', () => {
it('skips template without variables (trailing {)', () => {

const source = 'text {{{ without }}} any }} variables {';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.isDynamic()).to.be.false();
Expand All @@ -47,7 +47,7 @@ describe('Template', () => {
it('skips template without variables (trailing {{)', () => {

const source = 'text {{{ without }}} any }} variables {{';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.isDynamic()).to.be.false();
Expand All @@ -57,7 +57,7 @@ describe('Template', () => {
it('skips template without reference variables (trailing {{)', () => {

const source = 'text {"x"} {{{ without }}} any }} variables {{';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.isDynamic()).to.be.false();
Expand All @@ -67,7 +67,7 @@ describe('Template', () => {
it('skips template without variables (escaped)', () => {

const source = 'text {{{ without }}} any }} \\{{escaped}} variables';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.isDynamic()).to.be.false();
Expand All @@ -77,7 +77,7 @@ describe('Template', () => {
it('parses template (escaped)', () => {

const source = 'text {{$x}}{{$y}}{{$z}} \\{{escaped}} xxx abc {{{ignore}} 123 {{x';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.render({}, {}, { context: { x: 'hello', y: '!' } })).to.equal('text hello&#x21; {{escaped}} xxx abc {{{ignore}} 123 {{x');
Expand All @@ -87,7 +87,7 @@ describe('Template', () => {
it('parses template with single variable', () => {

const source = '{$x}';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.render({}, {}, { context: { x: 'hello' } })).to.equal('hello');
Expand All @@ -96,7 +96,7 @@ describe('Template', () => {
it('parses template (raw)', () => {

const source = 'text {$x}{$y}{$z} \\{{escaped}} xxx abc {{{ignore}} 123 {{x';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.render({}, {}, { context: { x: 'hello', y: '!' } })).to.equal('text hello! {{escaped}} xxx abc {{{ignore}} 123 {{x');
Expand All @@ -105,26 +105,26 @@ describe('Template', () => {
it('parses template with odd {{ variables', () => {

const source = 'text {{$\\{{\\}} }} \\{{boom}} {{!\\}}';
const template = Joi.template(source);
const template = Joi.var(source);

expect(template.source).to.equal(source);
expect(template.render({}, {}, { context: { '{{}}': 'and' } })).to.equal('text and {{boom}} {{!}}');
});

it('throws on invalid characters', () => {

expect(() => Joi.template('test\u0000')).to.throw('Template source cannot contain reserved control characters');
expect(() => Joi.template('test\u0001')).to.throw('Template source cannot contain reserved control characters');
expect(() => Joi.var('test\u0000')).to.throw('Template source cannot contain reserved control characters');
expect(() => Joi.var('test\u0001')).to.throw('Template source cannot contain reserved control characters');
});

describe('isTemplate()', () => {
describe('isVar()', () => {

it('checks if item is a joi template', () => {

expect(Joi.isTemplate(null)).to.be.false();
expect(Joi.isTemplate({})).to.be.false();
expect(Joi.isTemplate('test')).to.be.false();
expect(Joi.isTemplate(Joi.template('test'))).to.be.true();
expect(Joi.isVar(null)).to.be.false();
expect(Joi.isVar({})).to.be.false();
expect(Joi.isVar('test')).to.be.false();
expect(Joi.isVar(Joi.var('test'))).to.be.true();
});
});

Expand All @@ -133,7 +133,7 @@ describe('Template', () => {
it('errors on tempalte with invalid formula', () => {

const source = '{x +}';
expect(() => Joi.template(source)).to.throw('Invalid template variable "x +" fails due to: Formula contains invalid trailing operator');
expect(() => Joi.var(source)).to.throw('Invalid template variable "x +" fails due to: Formula contains invalid trailing operator');
});
});
});
24 changes: 18 additions & 6 deletions test/types/any.js
Original file line number Diff line number Diff line change
Expand Up @@ -1865,7 +1865,7 @@ describe('any', () => {
it('overrides message with template', () => {

const schema = Joi.number()
.min(10).message(Joi.template('way too small'));
.min(10).message(Joi.var('way too small'));

expect(schema.validate(1).error).to.be.an.error('way too small');
});
Expand All @@ -1879,7 +1879,7 @@ describe('any', () => {
},
latin: {
root: 'valorem',
'number.min': Joi.template('{@label} angustus', { prefix: { local: '@' } })
'number.min': Joi.var('{@label} angustus', { prefix: { local: '@' } })
}
};

Expand Down Expand Up @@ -1926,7 +1926,7 @@ describe('any', () => {

const messages = {
root: 'valorem',
'number.min': Joi.template('{@label} angustus', { prefix: { local: '@' } })
'number.min': Joi.var('{@label} angustus', { prefix: { local: '@' } })
};

const schema = Joi.object({ a: Joi.number().min(10).message(messages) });
Expand Down Expand Up @@ -2666,7 +2666,7 @@ describe('any', () => {

const schema = Joi.object({
a: Joi.number(),
b: Joi.valid(Joi.template('{a + 1}'))
b: Joi.valid(Joi.var('{a + 1}'))
});

expect(schema.validate({ a: 5, b: 6 }).error).to.not.exist();
Expand All @@ -2677,7 +2677,7 @@ describe('any', () => {

const schema = Joi.object({
a: Joi.number(),
b: Joi.valid(Joi.template('x{a + 1}'))
b: Joi.valid(Joi.var('x{a + 1}'))
});

expect(schema.validate({ a: 5, b: 'x6' }).error).to.not.exist();
Expand All @@ -2688,12 +2688,24 @@ describe('any', () => {

const schema = Joi.object({
a: Joi.number(),
b: Joi.valid(Joi.template('x'))
b: Joi.valid(Joi.var('x'))
});

expect(schema.validate({ a: 5, b: 'x' }).error).to.not.exist();
expect(schema.validate({ a: 5, b: 'y' }).error).to.be.an.error('"b" must be one of [x]');
});

it('supports templates with functions', () => {

const schema = Joi.object({
a: Joi.number(),
b: Joi.boolean(),
c: Joi.valid(Joi.var('{if(a == 5 && b == true, a * 2, null)}'))
});

expect(schema.validate({ a: 5, b: true, c: 10 }).error).to.not.exist();
expect(schema.validate({ a: 5, b: false, c: null }).error).to.not.exist();
});
});

describe('_validate()', () => {
Expand Down
10 changes: 5 additions & 5 deletions test/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ describe('object', () => {
it('uses template', async () => {

const schema = Joi.object()
.rename(/^(\d+)$/, Joi.template('x{#1}x'))
.rename(/^(\d+)$/, Joi.var('x{#1}x'))
.pattern(/^x\d+x$/, Joi.any());

const input = {
Expand Down Expand Up @@ -1474,7 +1474,7 @@ describe('object', () => {
it('uses template with prefix override', async () => {

const schema = Joi.object()
.rename(/^(\d+)$/, Joi.template('x{@1}x', { prefix: { local: '@' } }))
.rename(/^(\d+)$/, Joi.var('x{@1}x', { prefix: { local: '@' } }))
.pattern(/^x\d+x$/, Joi.any());

const input = {
Expand Down Expand Up @@ -1518,7 +1518,7 @@ describe('object', () => {
const schema = Joi.object({
prefix: Joi.string().lowercase().required()
})
.rename(/^(\d+)$/, Joi.template('{.prefix}{#1}'))
.rename(/^(\d+)$/, Joi.var('{.prefix}{#1}'))
.unknown();

const input = {
Expand All @@ -1541,7 +1541,7 @@ describe('object', () => {

const schema = Joi.object({
a: Joi.object()
.rename(/^(\d+)$/, Joi.template('{b.prefix}{#1}'))
.rename(/^(\d+)$/, Joi.var('{b.prefix}{#1}'))
.unknown(),
b: {
prefix: Joi.string().lowercase()
Expand All @@ -1560,7 +1560,7 @@ describe('object', () => {
it('uses template without refs', async () => {

const schema = Joi.object()
.rename(/^(\d+)$/, Joi.template('x'))
.rename(/^(\d+)$/, Joi.var('x'))
.unknown();

const value = await Joi.compile(schema).validate({ 1: 'x' });
Expand Down

0 comments on commit 5d8ea13

Please sign in to comment.