Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
- [`string.truncate([enabled])`](#stringtruncateenabled)
- [`string.creditCard()`](#stringcreditcard)
- [`string.length(limit, [encoding])`](#stringlengthlimit-encoding)
- [`string.regex(pattern, [name])`](#stringregexpattern-name)
- [`string.regex(pattern, [name | options])`](#stringregexpattern-name--options)
- [`string.replace(pattern, replacement)`](#stringreplacepattern-replacement)
- [`string.alphanum()`](#stringalphanum)
- [`string.token()`](#stringtoken)
Expand Down Expand Up @@ -1542,14 +1542,29 @@ const schema = Joi.object({
});
```

#### `string.regex(pattern, [name])`
#### `string.regex(pattern, [name | options])`

Defines a regular expression rule where:
- `pattern` - a regular expression object the string value must match against.
- `name` - optional name for patterns (useful with multiple patterns). Defaults to 'required'.
- `name` - optional name for patterns (useful with multiple patterns).
- `options` - an optional configuration object with the following supported properties:
- `name` - optional pattern name.
- `invert` - optional boolean flag. Defaults to `false` behavior. If specified as `true`, the provided pattern will be disallowed instead of required.

```js
const schema = Joi.string().regex(/^[abc]+$/);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the simpler inlined string.

const inlineNamedSchema = Joi.string().regex(/[0-9]/, 'numbers');
inlineNamedSchema.validate('alpha'); // ValidationError: "value" with value "alpha" fails to match the numbers pattern

const namedSchema = Joi.string().regex(/[0-9]/, { name: 'numbers'});
namedSchema.validate('alpha'); // ValidationError: "value" with value "alpha" fails to match the numbers pattern

const invertedSchema = Joi.string().regex(/[a-z]/, { invert: true });
invertedSchema.validate('lowercase'); // ValidationError: "value" with value "lowercase" matches the inverted pattern: [a-z]

const invertedNamedSchema = Joi.string().regex(/[a-z]/, { name: 'alpha', invert: true });
invertedNamedSchema.validate('lowercase'); // ValidationError: "value" with value "lowercase" matches the inverted alpha pattern
```

#### `string.replace(pattern, replacement)`
Expand Down
4 changes: 3 additions & 1 deletion lib/language.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ exports.errors = {
token: 'must only contain alpha-numeric and underscore characters',
regex: {
base: 'with value "{{!value}}" fails to match the required pattern: {{pattern}}',
name: 'with value "{{!value}}" fails to match the {{name}} pattern'
inverted: 'with value "{{!value}}" matches the inverted pattern: {{pattern}}',
name: 'with value "{{!value}}" fails to match the {{name}} pattern',
invertedName: 'with value "{{!value}}" matches the inverted {{name}} pattern'
},
email: 'must be a valid email',
uri: 'must be a valid uri',
Expand Down
30 changes: 25 additions & 5 deletions lib/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,39 @@ internals.String = class extends Any {
});
}

regex(pattern, name) {
regex(pattern, patternOptions) {

Hoek.assert(pattern instanceof RegExp, 'pattern must be a RegExp');

pattern = new RegExp(pattern.source, pattern.ignoreCase ? 'i' : undefined); // Future version should break this and forbid unsupported regex flags
const patternObject = {
pattern: new RegExp(pattern.source, pattern.ignoreCase ? 'i' : undefined) // Future version should break this and forbid unsupported regex flags
};
let patternIsInverted = false;

if (typeof patternOptions === 'string') {
patternObject.name = patternOptions;
}
else if (typeof patternOptions === 'object') {
patternIsInverted = !!patternOptions.invert;
patternObject.invert = patternIsInverted;

if (patternOptions.name) {
patternObject.name = patternOptions.name;
}
}

const baseRegex = patternIsInverted ? 'string.regex.inverted' : 'string.regex.base';
const nameRegex = patternIsInverted ? 'string.regex.invertedName' : 'string.regex.name';

return this._test('regex', patternObject, function (value, state, options) {

return this._test('regex', pattern, function (value, state, options) {
const patternMatch = patternObject.pattern.test(value);

if (pattern.test(value)) {
if (patternMatch ^ patternIsInverted) {
return value;
}

return this.createError((name ? 'string.regex.name' : 'string.regex.base'), { name, pattern, value }, state, options);
return this.createError((patternObject.name ? nameRegex : baseRegex), { name: patternObject.name, pattern, value }, state, options);
});
}

Expand Down
59 changes: 56 additions & 3 deletions test/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('string', () => {

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

it('invalidates case sensitive values', (done) => {
it('inverts case sensitive values', (done) => {

Helper.validate(Joi.string().invalid('a', 'b'), [
['a', false, null, '"value" contains an invalid value'],
Expand All @@ -91,7 +91,7 @@ describe('string', () => {
], done);
});

it('invalidates case insensitive values', (done) => {
it('inverts case insensitive values', (done) => {

Helper.validate(Joi.string().invalid('a', 'b').insensitive(), [
['a', false, null, '"value" contains an invalid value'],
Expand Down Expand Up @@ -875,6 +875,37 @@ describe('string', () => {
done();
});
});

it('should include a pattern name in options object', (done) => {

const schema = Joi.string().regex(/[a-z]+/, { name: 'letters' }).regex(/[0-9]+/, { name: 'numbers' });
schema.validate('abcd', (err, value) => {

expect(err.message).to.contain('numbers pattern');
done();
});
});

it('should "invert" regex pattern if specified in options object', (done) => {

const schema = Joi.string().regex(/[a-z]/, { invert: true });
Helper.validate(schema, [
['0123456789', true],
['abcdefg', false, null, '"value" with value "abcdefg" matches the inverted pattern: /[a-z]/']
], done);
});

it('should include inverted pattern name if specified', (done) => {

const schema = Joi.string().regex(/[a-z]/, {
name : 'lowercase',
invert: true
});
Helper.validate(schema, [
['0123456789', true],
['abcdefg', false, null, '"value" with value "abcdefg" matches the inverted lowercase pattern']
], done);
});
});

describe('ip()', () => {
Expand Down Expand Up @@ -2085,7 +2116,7 @@ describe('string', () => {
], done);
});

it('should invalidate invalid values', (done) => {
it('should invert invalid values', (done) => {

const schema = Joi.string().valid('a', 'b', 'c');
Helper.validate(schema, [
Expand Down Expand Up @@ -3628,5 +3659,27 @@ describe('string', () => {
});
done();
});

it('describes invert regex pattern', (done) => {

const schema = Joi.string().regex(/[a-z]/, {
invert: true
});
const description = schema.describe();
expect(description).to.equal({
type: 'string',
invalids: [''],
rules: [
{
name: 'regex',
arg: {
pattern: /[a-z]/,
invert: true
}
}
]
});
done();
});
});
});