fluent-ts-validator
v3.0.3
Published
A fluent validator written in TypeScript
Downloads
7,260
Maintainers
Readme
fluent-ts-validator
A validation library written in TypeScript which uses a fluent API and lambda expressions to build validation rules. It is inspired by the FluentValidation library for .NET written by Jeremy Skinner.
Instead of implementing an awful lot of validation logic within this project again this library makes use of the mature string validation library validator.js and delegates invocations to it where it makes sense.
In this respect, special thanks go to these two projects.
The fluent-ts-validator library is licensed under MIT. For past, current, and maybe upcoming changes take a look at the Change Log.
Installation
npm i fluent-ts-validator --save
or:
yarn add fluent-ts-validator
Content
Usage
Creating a validator for your needs is simply done by extending the AbstractValidator<T>
class for a specific type and defining a set of validation rules within the constructor of that
class. Then create an instance of that validator and invoke the validate()
or validateAsync()
method with an object you want to validate.
Basic Validation Example
import {Superhero} from "../models/superhero";
import {AbstractValidator, Severity} from "fluent-ts-validator";
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(hero => hero.name)
.isAlphanumeric().hasMinLength(3)
.withFailureMessage("C'mon! At least some pronounceable name.");
this.validateIf(hero => hero.superpowers)
.isNotEmpty()
.unless(hero => hero.immortal)
.withFailureCode("FAKE-001")
.withSeverity(Severity.WARNING);
this.validateIfEachString(hero => hero.superpowers.map(power => power.description))
.hasLengthBetween(5, 50)
.when(hero => hero.superpowers != null);
}
}
const hero: Superhero = ...;
const validator = new SuperheroValidator();
const result = validator.validate(hero);
const validationSucceeded = result.isValid();
const validationFailed = result.isInvalid();
const failures = result.getFailures();
const messages = result.getFailureMessages();
Pro Tip: Your custom Validator
classes love to be transpiled to ES6 (at least). Otherwise,
they might throw things like TypeError: Class constructor AbstractValidator cannot be invoked
without 'new'
at you. Not nice! So make sure to set the compiler options in your tsconfig.json
accordingly:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
...
}
}
Rule Building
Validation rules are built in a fluent API style. Entry points are always the
type-based validateIf
or validateIfEach
kind of methods. These variants are available:
validateIf
: for common validation rules (independent of datatype)validateIfAny
: for type-based validation rulesvalidateIfNumber
: for number-based validation rulesvalidateIfDate
: for Date-based validation rulesvalidateIfString
: for String-based validation rulesvalidateIfIterable
: for Iterable-based validation rulesvalidateIfEach
: same asvalidateIf
but forIterables
validateIfEachAny
: same asvalidateIfAny
but forIterables
validateIfEachNumber
: same asvalidateIfNumber
but forIterables
validateIfEachDate
: same asvalidateIfDate
but forIterables
validateIfEachString
: same asvalidateIfString
but forIterables
All of these methods expect a lambda expression as parameter which maps the input to specific
validatable properties of that instance. Depending on the type of the validatable properties
different validation rules are available. The lambda expressions in the validateIfEach
-methods
map to an instance that complies to the Iterable
protocol. Obviously, all elements in an Array
,
Set
, or whatever kind of Iterable
will then be validated.
For example:
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(hero => hero.name).isAlphanumeric();
this.validateIfNumber(hero => hero.epicFightsWon).isGreaterThanOrEqual(3);
this.validateIfDate(hero => hero.lastSighting).isAfter(THEdate);
}
}
Above, AbstractValidator
is typed with the Superhero
class. So
the lamdba expressions expect instances of superheros. The type of the attribute to validate
determines which validateIf
method to use. So, checks on a superhero's name require the
validateIfString()
method, whether ensuring a superhero has won a certain amount of epic
fights cries out for the validateIfNumber()
method. In addition, the type of the attribute to
validate also determines which kind of validation rules are available. While isAlphanumeric()
makes sense for a string
it does not so much for a Date
object. Using isAfter(anotherDate)
is
plausible for a date but not for a number, etc.
Only the types of validation rules that make sense for the attributes you are about to validate will be available. And that is an epic win for auto completion. A detailed overview of all available validation options can be found in the Validation Rules section below.
Building Blocks
Okay, to create your own validator start by subclassing the AbstractValidator<T>
and specify the type T
of objects you want to validate with it.
Within the constructor of your validator define the relevant validation steps:
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(hero => hero.name) // specify which property of which type needs validation
.isAlphanumeric().hasMinLength(3) // validation rules to apply to the property
.whenNotNull() // validation conditions that might prevent validation
.withFailureCode("NAME-01"); // configure parts of the failure object you receive upon failed validation
}
}
The building blocks of a validation step are:
validateIf...()
-method which takes a lamba expression as parameter- the lambda expression obviously maps from an object to a property. The property is eventually the thing you want to validate.
- one or more validation rules (which depend on the type of the property)
- optional validation conditions (to define under which circumstances the validation should be performed or omitted)
- optional failure configurations (e.g. failure messages or codes you want to receive in case validation fails)
Validation conditions and failure configurations can only be added after validation rules have been specified.
Rule Concatenation
Validation rules can also be concatenated.
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(hero => hero.name)
.isAlphanumeric().isUppercase().hasMinLength(3).isNotIn(arrayOfBadGuys);
}
}
Conditional Validation
Sometimes, the necessity of validating a property depends on certain conditions.
Conditional rules allow you to specify under which circumstances a validation should be executed. That's the reason why you can append whenDefined()
, whenNotNull()
, whenNotEmpty()
– or more general – when()
or unless()
methods to your
validation rules. The three former ones come without method parameters. They internally make use of the lambda expression specified in the corresponding validateIf()
method. However, when()
and unless()
expect a lambda expression as parameter that evaluates to a boolean
value. When the lambda expression in a when()
results in true
, the validation is
executed. With unless()
it is the other way round. The validation does not take place when
the corresponding lambda expression evaluates to true
.
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(hero => hero.name).isIn(hallOfFame)
.when(hero => hero.epicFightsWon > 100);
this.validateIf(hero => hero.superpowers).isNotEmpty()
.unless(hero => hero.immortal);
this.validateIfDate(hero => hero.lastSighting).isAfter(THEdate)
.whenNotNull();
}
}
whenDefined()
: validation is performed whenpropertyUnderValidation
is notundefined
whenNotNull()
: validation is performed whenpropertyUnderValidation
is neitherundefined
, nornull
whenNotEmpty()
: validation is performed whenpropertyUnderValidation
is neitherundefined
, nornull
, nor an emptystring
""
; this method is only available if the type ofpropertyUnderValidation
is astring
when(expression: (input: T) => boolean)
: validation is performed when expression evaluates totrue
unless(expression: (input: T) => boolean)
: validation is performed when expression evaluates tofalse
Applying multiple conditions at once
In case more than one condition is defined for a validation step, they are logically ANDed to determine if the validation has to be performed. That is, as soon as one condition fails, the corresponding validation rules are skipped.
For example, the following SuperheroValidator
only validates if a superhero's name is in some sort of Hall of Fame
when the hero has won more than 100 epic fights AND the hero's name is not empty:
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(hero => hero.name).isIn(hallOfFame)
.when(hero => hero.epicFightsWon > 100)
.whenNotEmpty();
}
}
Validation Failure Configuration
Eventually, the appearance of validation failures can be configured on a per-property basis. In
case an invalid object is passed to the validate()
or validateAsync()
method, you might be
interested in details about the failure, possibly react to it, or pass some failure-specific
information along. For details about the various methods see Validation Result & Validation Failures.
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfNumber(hero => hero.epicFightsWon).isNotEqualTo(0)
.withSeverity(Severity.INFO)
.withFailureCode("F_0")
.withFailureMessage("Don't give up!")
.onFailure(failure => console.log(failure));
}
}
Validation Rules
On the one hand this library provides validation rules that are specific to a property's type (String, Number, Date, and Iterable Validation Rules). On the other hand there are rules that can be applied irrespectively of a property's type (Common and Type Validation Rules).
Common Validation Rules
Common validation rules are applicable to properties of all types.
Methods
isDefined()
: Checks if a property is defined.isUndefined()
: Checks if a property is undefined.isNull()
: Checks if a property is null.isNotNull()
: Checks if a property is not null.isEmpty()
: Checks if a property is empty.- Empty in this context means either an empty
string
,null
, orundefined
. Or in case of collections (Array
,Set
,Map
) that they do not contain any element (length === 0
,size === 0
)
- Empty in this context means either an empty
isNotEmpty()
: Checks if a property is not empty.- That is, neither
null
norundefined
and not an emptystring
. If the property in question is a collection (Array
,Set
,Map
) this method checks if the collection contains elements.
- That is, neither
isEqualTo(comparison: TProperty)
: Checks if a property is equal to (===
) thecomparison
parameter.isNotEqualTo(comparison: TProperty)
: Checks if a property is not equal to (!==
) thecomparison
parameter.isIn(obj: Iterable<TProperty> | object)
: In case an iterable is provided, checks if a property is equal to an element of the iterable (===
). For non-iterables, checks if a member's value is equal to the property under validation.isNotIn(obj: Iterable<TProperty> | object)
: In case an iterable is provided, checks if a property is not equal to an element of the iterable (!==
). For non-iterables, checks if no member's value is equal to the property under validation.
String Validation Rules
The largest group of validation rules targets properties of type string
. Most of these rules
delegate the actual validation to the internal validator.js library.
Methods
contains(seed: string)
: Checks if a string contains a substring orseed
.hasLength(length: number)
: Checks if a string has exactly the lengthlength
.hasLengthBetween(min: number, max: number)
: Checks if a string falls in a themin
-max
range.hasMinLength(min: number)
: Checks if a string has at leastmin
length.hasMaxLength(max: number)
: Checks if a string has at mostmax
length.isAlpha(locale?: AlphaLocale)
: Checks if a string contains letters (a-zA-Z) only.- an optional locale can be set, which is one of
AlphaLocale = "ar" | "ar-AE" | "ar-BH" | "ar-DZ" | "ar-EG" | "ar-IQ" | "ar-JO" | "ar-KW" | "ar-LB" | "ar-LY" | "ar-MA" | "ar-QA" | "ar-QM" | "ar-SA" | "ar-SD" | "ar-SY" | "ar-TN" | "ar-YE" | "cs-CZ" | "de-DE" | "en-AU" | "en-GB" | "en-HK" | "en-IN" | "en-NZ" | "en-US" | "en-ZA" | "en-ZM" | "es-ES" | "fr-FR" | "hu-HU" | "nb-NO" | "nl-NL" | "nn-NO" | "pl-PL" | "pt-BR" | "pt-PT" | "ru-RU" | "sr-RS" | "sr-RS@latin" | "tr-TR";
if not set, defaults toen-US
.
- an optional locale can be set, which is one of
isAlphanumeric(locale?: AlphanumericLocale)
: Checks if a string is alphanumeric.- an optional locale can be set. It accepts a locale of
AlphanumericLocale = "ar" | "ar-AE" | "ar-BH" | "ar-DZ" | "ar-EG" | "ar-IQ" | "ar-JO" | "ar-KW" | "ar-LB" | "ar-LY" | "ar-MA" | "ar-QA" | "ar-QM" | "ar-SA" | "ar-SD" | "ar-SY" | "ar-TN" | "ar-YE" | "cs-CZ" | "de-DE" | "en-AU" | "en-GB" | "en-HK" | "en-IN" | "en-NZ" | "en-US" | "en-ZA" | "en-ZM" | "es-ES" | "fr-FR" | "fr-BE" | "hu-HU" | "nl-BE" | "nb-NO" | "nl-NL" | "nn-NO" | "pl-PL" | "pt-BR" | "pt-PT" | "ru-RU" | "sr-RS" | "sr-RS@latin" | "tr-TR";
defaults toen-US
- an optional locale can be set. It accepts a locale of
isAscii()
: Checks if a string contains ASCII chars only.isBase64()
: Checks if a string is Base64 encoded.isBooleanString()
: Checks if a string is a boolean.isCurrency(options?: CurrencyOptions)
: Checks if a string is a valid currency amount.- the optional parameter defaults to
CurrencyOptions: { symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_space_after_digits: false }
- the optional parameter defaults to
isDecimalString()
: Checks if a string represents a decimal number, such as 0.1, .3, 1.1, 1 .00003, 4.0, etc.isEmail(options?: EmailOptions)
: Checks if a string is an email.- the optional parameter defaults to
EmailOptions: { allow_display_name: false, require_display_name: false, allow_utf8_local_part: true, require_tld: true }
. - if
allow_display_name
is set totrue
, the validator will also matchDisplay Name <email-address>
. Ifrequire_display_name
is set to true, the validator will reject strings without the formatDisplay Name <email-address>
. Ifallow_utf8_local_part
is set tofalse
, the validator will not allow any non-English UTF8 character in email address' local part. Ifrequire_tld
is set tofalse
, e-mail addresses without having TLD in their domain will also be matched.
- the optional parameter defaults to
isFqdn(options?: FqdnOptions)
: Checks if a string is a fully qualified domain name (e.g. domain.com).- the optional parameter is an option that defaults to
FqdnOptions: { require_tld: true, allow_underscores: false, allow_trailing_dot: false }
.
- the optional parameter is an option that defaults to
isHexadecimal()
: Checks if a string is a hexadecimal number.isIso8601()
: Checks if a string is a valid ISO 8601 date.isJson()
: Check if a string is valid JSON (note: uses JSON.parse).isLatLong()
: Checks if a string represents valid latitude-longitude coordinates.isLowercase()
: Checks if a string is all lowercase.isMobilePhoneNo(locale: MobilePhoneLocale)
: Checks if a string is a mobile phone number.- the
locale
is one ofMobilePhoneLocale = "ar-DZ" | "ar-SA" | "ar-SY" | "cs-CZ" | "da-DK" | "de-DE" | "el-GR" | "en-AU" | "en-CA" | "en-GB" | "en-HK" | "en-IN" | "en-KE" | "en-NZ" | "en-RW" | "en-TZ" | "en-UG" | "en-US" | "en-ZA" | "en-ZM" | "es-ES" | "fa-IR" | "fi-FI" | "fr-FR" | "hu-HU" | "id-ID" | "it-IT" | "ja-JP" | "lt-LT" | "ms-MY" | "nb-NO" | "nn-NO" | "pl-PL" | "pt-PT" | "ru-RU" | "sr-RS" | "tr-TR" | "vi-VN" | "zh-CN" | "zh-TW";
- the
isNumericString()
: Checks if a string contains only numbers.isPostalCode(locale: PostalCodeLocale)
: Checks if a string is a postal code.- the
locale
is one ofPostalCodeLocale = "AT" | "AU" | "BE" | "CA" | "CH" | "CZ" | "DE" | "DK" | "DZ" | "ES" | "FI" | "FR" | "GB" | "GR" | "IL" | "IN" | "IS" | "IT" | "JP" | "KE" | "LI" | "MX" | "NL" | "NO" | "PL" | "PT" | "RO" | "RU" | "SA" | "SE" | "TW" | "US" | "ZA" | "ZM";
OR"any"
. If"any"
is used, the underlying validator.js library will check if any of the locales match.
- the
isUppercase()
: Checks if a string is all uppercase.isUrl(options?: UrlOptions)
: Checks if a string is a URL.- the optional parameter defaults to
UrlOptions: { protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false }
- the optional parameter defaults to
isUuid(version?: UuidVersion)
: Checks if a string is a UUID.- Optional
version
is one value ofUuidVersion = "3" | "4" | "5" | "all";
- defaults to
all
- Optional
matches(pattern: RegExp, modifiers?: string)
: Checks if a string matches a pattern.- optional
modifiers
are the same as aRegExp
constructor would accept (e.g."i"
for ignore case, or"g"
for global match, etc.)
- optional
Number Validation Rules
Validation rules for properties of type number
.
Methods
isPositive()
: Checks if a number is positive (> 0
).isNegative()
: Checks if a number is negative (< 0
).isGreaterThan(threshold: number)
: Checks if a number is greater thanthreshold
(>
).isGreaterThanOrEqual(threshold: number)
: Checks if a number is greater than or equal tothreshold
(>=
).isLessThan(threshold: number)
: Checks if a number is less thanthreshold
(<
).isLessThanOrEqual(threshold: number)
: Checks if a number is less than or equal tothreshold
(<=
).
Date Validation Rules
Validation rules for properties of type Date
.
Methods
isBefore(date: Date)
: Checks if a date is beforedate
.isSameAs(date: Date)
: Checks if a date is the same asdate
.isAfter(date: Date)
: Checks if a date is afterdate
.isSameOrBefore(date: Date)
: Checks if a date is the same as or beforedate
.isSameOrAfter(date: Date)
: Checks if a date is the same as or afterdate
.isBetween(date1: Date, date2: Date, lowerBoundary?: "(" | "[", upperBoundary?: ")" | "]")
: Checks if a date is betweendate1
anddate2
.- uses the same boundary characters as moment.js
[
and]
indicate inclusion of a date(
and)
indicates exclusion of a date- defaults to exclusion of lower and upper boundary if not specified
Type Validation Rules
Validation rules to check for certain types.
Methods
isArray()
: Checks if a property is of typeArray
.isBoolean()
: Checks if a property is a realboolean
.isDate()
: Checks if a property is of typeDate
.isNumber()
: Checks if a property is a realnumber
.isString()
: Checks if a property is a realstring
.
Iterable Validation Rules
Validation rules for iterable properties.
Methods
isEmpty()
: Checks if anIterable
does not have an element.isNotEmpty()
: Checks if anIterable
has at least one element.hasNumberOfElements(elementCount: number)
: Checks if anIterable
has exactelementCount
elements.hasMinNumberOfElements(min: number)
: Checks if anIterable
has at leastmin
elements.hasMaxNumberOfElements(max: number)
: Checks if anIterable
has at mostmax
elements.hasNumberOfElementsBetween(min: number, max: number)
: Checks if anIterable
has at leastmin
and at mostmax
elements.contains(element: TProperty)
: Checks if anIterable
contains the specifiedelement
.doesNotContain(element: TProperty)
: Checks if anIterable
does not contain the specifiedelement
.
Custom Validation Rules
Sometimes it is useful to reuse one of your validators within a different validator. This is
where the fulfills
method comes in handy:
export class SuperpowerValidator extends AbstractValidator<Superpower> {
constructor() {
super();
this.validateIf(superpower => superpower.type).isNotEmpty();
}
}
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfEach(hero => hero.superpowers).fulfills(new SuperpowerValidator());
}
}
The fulfills
method is actually overloaded which allows us to use it for our own validation
expressions as well. In case the provided validation rules just don't fit your needs, be a
Superhero and formulate your own validation expression:
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIf(hero => hero.name).fulfills(name => {
return !(name.includes('SUPER') || name.includes('BAT'));
});
}
}
Validation Result & Validation Failures
Each validator created with this library returns a ValidationResult
object at the end of the
validation process. It contains some information regarding the validation process you are probably very interested in.
Its methods are:
isValid(): boolean
- returns
true
if noValidationFailure
exists,false
otherwise.
- returns
isInvalid(): boolean
- returns
true
if at least oneValidationFailure
exists,false
otherwise.
- returns
getFailures(): ValidationFailure[]
- returns an array containing
ValidationFailures
for the invalid properties. If no failures exist, meaning the result is valid, an empty array is returned.
- returns an array containing
getFailureMessages(): string[]
- collects all the non-empty failure messages of all validation failures and returns them in one array; if no validation failures exist, an empty array is returned
getFailureCodes(): string[]
- collects all the non-empty failure codes of all validation failures and returns them in one array; if no validation failures exist, an empty array is returned
So, what is a ValidationFailure
? It is an object with the following properties (all of them
being readonly):
target: any
: the object as a whole that was validatedpropertyName: string
: the name of the property that is considered invalidattemptedValue: any
: the actual value of the property that is considered invalidcode: string
: a failure code, if set; otherwiseundefined
message: string
: a failure message; if not explicitly set, it defaults to '<propertyName>
is invalid'severity: string
: the severity of the failure; defaults toERROR
The following methods can be used to influence the appearance of a ValidationFailure
:
withFailureCode(code: string)
: sets a failure codewithFailureMessage(message: string)
: sets a failure messagewithSeverity(severity: Severity)
: sets the severitySeverity
is anenum
with the possible valuesERROR
,WARNING
, orINFO
- defaults to
ERROR
if not set
withPropertyName(name: string)
: sets a name for the property under validation- although fluent-ts-validator tries its best to automatically detect the name of the properties under validation it might sometimes be useful to set the name explicitly. For example, in case an uglifier scrambles the code and throws original property names out of the window.
To make the relationship between a Validator
and a ValidationFailure
clear, take a look at the
following SuperheroValidator
and Superhero
instance:
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfNumber(superhero => superhero.epicFightsWon).isNotEqualTo(0)
.withSeverity(Severity.INFO)
.withFailureCode("F_0")
.withFailureMessage("Don't give up!");
}
}
const superhero = new Superhero();
superhero.name = "SUPER DUDE";
superhero.epicFightsWon = 0;
const validator = new SuperheroValidator();
const result = validator.validate(superhero);
The ValidationResult
will contain a ValidationFailure
object in its array that looks like this:
ValidationFailure {
target: Superhero { name: 'SUPER DUDE', epicFightsWon: 0 },
propertyName: 'epicFightsWon',
attemptedValue: 0,
code: 'F_0',
message: 'Don\'t give up!',
severity: 'INFO'
}
That makes it obvious which object failed validation due to which property and value.
Asynchronous Validation
A validation can also be performed asynchronously. Besides the validate
method every validator
provides a validateAsync
method which returns a Promise
for a ValidationResult
.
const promise: Promise<ValidationResult> = validator.validateAsync(superhero);
promise.then(result => console.log(result.isValid()));
Callbacks
In case you want to respond to failures immediately callbacks can be used. Just use the
onFailure()
method when building your validation rule. It accepts a callback-method as parameter
with the following signature: (failure: ValidationFailure) => void
export class SuperheroValidator extends AbstractValidator<Superhero> {
constructor() {
super();
this.validateIfString(superhero => superhero.name).isEqualTo("The DUDE")
.onFailure(failure => console.log("We have found his Dudeness!"));
}
}