java-bean-validation.js
Version:
Java Bean Validation implementation for JavaScript
643 lines (608 loc) • 28.9 kB
text/typescript
/**
* Desriptor of a type (or class in Java), which have the information about a type, it's properties,
* validation contraints, etc.
*/
export interface TypeValidationsDescriptor {
/**
* Name of the type
*
* @see {@link TypeName} for more information about how the type names are represented.
*/
typeName: TypeName
/**
* Validation contraints that apply over the type. It contains all constraints applied ovew a class in Java.
*
* @see {@link ConstraintValidationsDescriptor} for more information about constraints
*/
constraints?: ConstraintValidationsDescriptor[]
/**
* Map with the properties of the type, the key of the map is the property name, and the value is the
* property descriptor.
*
* @see {@link PropertyValidationsDescriptor} for more information about property descriptors
* @see {@link TypeValidationsPropertyDescriptorMap} for more information about the map with the property descriptors
*/
properties?: TypeValidationsPropertyDescriptorMap
/**
* Indicate if the type is a fundamentar data type. A basic or fundamental type is one thar have no properties,
* the contained value must be considered as one indivisible unit, like an int, or a date.
*
* The basic type constraints must be validated in all situations, without care about groups rules or cascade.
*
* Note:
* This property is only used by the fundamental preregisted types.
*/
isBasicType?: boolean
/**
* Indicate if the type is a collection. A collection is a List or a Map.
*
* Notes:
* - Types with generic arguments must have a factory which transform it to a concrete type.
* - This property is used by the preregisted Java collections types factories.
*
* @see {@link TypeName} for more information about how the type names and generics are represented
* @see {@link TypeValidationsDescriptorFactory} for more information about type descriptor factories
*/
isCollection?: boolean
/**
* In a collection type, indicate the validations that apply over each value.
*
* Note: This property is used by the preregisted Java collections types factories.
*/
valueDescriptor?: PropertyValidationsDescriptor
}
/**
* A type descriptor factory is a function with the responsability of transform a a type with generic arguments
* in a concrete type. In order to do it, this function recibes the type name with generic arguments, and returns
* the type definition according to the generic arguments.
*
* Note:
* Every generic type must have its own type descriptor factory registered in the type descriptor map.
*
* @see {@link TypeName} for more information about how the type names and generics are represented
* @see {@link TYPE_DESCRIPTORS} for mor information about the type descriptor map
*/
export type TypeValidationsDescriptorFactory = (typeName: TypeName) => TypeValidationsDescriptor;
/**
* Descriptor of a type's property, which have the onformation about a property, it's type, validation constraints,
* and validation cascade rules.
*/
export interface PropertyValidationsDescriptor {
/**
* Name of the data type of the porperty.
*
* @see {@link TypeName} for more information about how the type names are represented
*/
porpertyTypeName: TypeName
/**
* Validation contraints that apply over the property. It contains all constraints applied ovew a property in Java.
*
* @see {@link ConstraintValidationsDescriptor} for more information about constraints
*/
constraints?: ConstraintValidationsDescriptor[]
/**
* Indicate if the property type must be validated with the value of this property. It corresponds to the
* @Valid annotation in Java.
*
* Note:
* All types marked as a basic type are validated ignoring the value of this property.
*
* see {@link TypeValidationsDescriptor} for more information about type descriptors
*/
cascade?: boolean
/**
* In case of apply a casade validation on this property, this constains the group conversion rules.
* This constains a map where the key is the original group name (from) and the value is the transformed group
* name (to), and in Java corresponds to @ConvertGroup annotation usage.
*
* @see {@link GroupConversions} for more information about the map with the group conversion correpondence.
*/
groupConversions?: GroupConversions
}
/**
* Map with the properties of a type, this map contains all properties of a type, where the key is the property's name
* and the value is the property descriptor, which contains the validations rules of a property.
*
* @see {@link PropertyValidationsDescriptor} for more information about property descriptors
* @see {@link TypeValidationsDescriptor} for more information about type descriptors
*/
export interface TypeValidationsPropertyDescriptorMap { [propertyName: string]: PropertyValidationsDescriptor }
/**
* Map with the groups converson equivalence, this map contains as key the originl group name, where it is going to be
* converted from, ad as a value the name name of the group where the key is going to be converted to. Each rule in the
* map corresponds to a one entry of the @ConvertGroup annotation in Java.
*
* @see {@link PropertyValidationsDescriptor.groupConversions} for more information about group convertions in a property
*/
export interface GroupConversions { [groupNameFrom: string]: string | undefined /*groupNameTo*/ }
/**
* Descriptor of a constraint, it represent a validation rule that could be apply over a type or property.
*
* @see {@link PropertyValidationsDescriptor} for more information about property descriptors
* @see {@link TypeValidationsDescriptor} for more information about type descriptors
*/
export interface ConstraintValidationsDescriptor {
/**
* Name of the constraint rule to apply, it must correspond with the validator funtion that implemnet
* the validation logic for this constraint.
*
* @see {@link Validator} for more informaton about the validator function of a constraint
*/
constraintName: string
/**
* Map with the attribute's values required to validate the value.
*
* It is a map, where the key is the name of the attibute (name of the attibute property of the constraint
* annotation in Java), and the value is the attribute value.
*
* Notable attibutes:
* - groups: List with the name of the groups requered to validate the value, if one of the groups name are
* present in the list of allowed groups during the validaton process, this constraint is going to
* apply during the validation process
* - message: Error message in case to be included in the violation of this constraint
*
* @see {@link groupAllowed} to see how the groups are managed
* @see {@link getMessage} to see how the error messages are created
*/
attributes?: any & { groups?: string[], message?: string }
/**
* List with the composing constraint of this constraint. In order to consider a constraint as passed all
* of its composing constraint must pass too.
*/
composingConstraints?: ConstraintValidationsDescriptor[]
/**
* Indicate if the violation of a compossed constraint must be reported as a violation of this constraint,
* otherwise, the composed constraint violations have its own violation report.
*/
reportAsSingle?: boolean
/**
* If it is know this constraint have no validator associated.
*
* @see {@link Validator} for more informaton about the validator function of a constraint
*/
hasNoValidator?: boolean
/**
* Indicate if the default value of this constraint (indicated in property defaultValues of the corresponding
* validator) was already loaded in this constraint descriptor.
*
* Note:
* This porperty is setted to true during execution time, avoiding load multiple times the default values
*
* @private
* @see {@link Validator.defaultValues} for more information about the default value of a constraint
* @see {@link loadDefaultConstraintValues} for more information about the process of load the default value into
* a constraint definition
*/
defaultValuesLoaded?: boolean
}
/**
* Represent a constraint violation report
*/
export interface Violation {
/**
* Violated constraint descriptor.
*
* @see {@link ConstraintValidationsDescriptor} for more information about constraints
*/
constraintDescriptor: ConstraintValidationsDescriptor
/**
* Value caused of the violation.
*/
invalidValue: any
/**
* Error message template.
*/
messageTemplate: string
/**
* Localized error message.
*/
message: string
/**
* Invalid value path respect to the root object
*/
propertyPath: string
}
/**
* Validator function in charged of evalutate if a value is valid according to a constraint restriction.
*
* Notes:
* - This is a function that can have extra properties
* - This function implements the logic associate to a constraint
* - If the constraint fails no violation is requeried to be appended to the violations list
*
* @param value Value to be validated according to the constraint rules
* @param attributes Constraint attributes values used to test the validity of the tested value
* @param constraint Constraint definition of this validation
* @param path Value's path respect to the root object
* @param globalViolations List where is going to be appended the extras violations found independent of the
* current reportAsSingle constraint rule
* @param violations List where is going to be appended the extras violations found
* @returns True if the value is valid, otherwise returns false
*
* @see {@link VALIDATORS} for more information about register a constraint's implementation
* @see {@link ConstraintValidationsDescriptor.attributes} for more information about constraint's attibutes
*/
export interface Validator {
/**
* Validator function in charged of evalutate if a value is valid according to a constraint restriction.
*
* Notes:
* - This is a function that can have extra properties
* - This function implements the logic associate to a constraint
* - If the constraint fails no violation is requeried to be appended to the violations list
*
* @param value Value to be validated according to the constraint rules
* @param attributes Constraint attributes values used to test the validity of the tested value
* @param constraint Constraint definition of this validation
* @param path Value's path respect to the root object
* @param globalViolations List where is going to be appended the extras violations found independent of the
* current reportAsSingle constraint rule
* @param violations List where is going to be appended the extras violations found
* @returns True if the value is valid, otherwise returns false
*
* @see {@link VALIDATORS} for more information about register a constraint's implementation
* @see {@link ConstraintValidationsDescriptor.attributes} for more information about constraint's attibutes
*/
(value: any, attributes: any, constraint: ConstraintValidationsDescriptor, path: string, globalViolations: Violation[], violations: Violation[]): boolean
/**
* Indicate if all violations of this constraint must be considered as a global one, independtly if it is part of
* a composed constraint
*/
isGlobalValidator?: boolean
/**
* Indicate if this constraint must be interpretated a a required rules in HTML Validations
*/
isHtmlRequiredValidator?: boolean
/**
* Default value of the attributes of a constraint. It is a map, where the key is the name of the attibute
* (name of the attibute property of the constraint annotation in Java), and the value is the default attribute
* value.
*
* Note:
* If a constraint annotation in Java has default value, the default value must be also registered there
*
* @see {@link ConstraintValidationsDescriptor.attributes} for more information about constraint attributes
* @see {@link ConstraintValidationsDescriptor.defaultValuesLoaded} for more information about when the default
* values are loaded into a constraint definition
* @see {@link loadDefaultConstraintValues} for more information about the process of load the default value into
* a constraint definition
*/
defaultValues?: { [propertyName: string]: any | undefined }
}
/**
* Type with the name of a class, it must be a string whith the name of a class,
* but, if tha class is have generaric arguments must be an array with the name
* of the class and the type of each generic argument.
*
* Note:
* The name of the class don't include the name of the package.
*
* Example:
* - The class String in Java must be translated to 'String'
* - Tha class List<String> in Java must be translated to ['List', 'String']
* - The class Map<String, Integer> in Java must be traslated to ['Map', 'String', 'Integer']
* - The class Map<String, List<Integer> in Java must be translated to ['Map', 'String', ['List', 'Integer']]
* - The class String[] in Java must be translated to ['Array', 'String']
*/
export type TypeName = string | string[];
/**
* Functions in charged to translate the error messages.
*
* @param template Template of the error message to be translated, if the constraint have no message the default
* message is '{constraintName}' where constraintName is the name of the constraint's type
* @param attributes Object with the constraint's attributes used to validate the value
* @param invalidValue Value that failed the constraint's validation
* @returns The error message translated in the user's language
*/
export type MessageFormatter = (template: string, attributes: any, invalidValue: any) => string
/* ********************************************************************************************
* Constants & Values
*/
/**
* Map with the type descriptor, where the key is a string with the name of the class (no generic class allowed here)
* and the the value is the type descriptor or a type descriptor factory that create it.
*
* Note:
* The generic classes require here a type factory, a function that create the concrete type descriptor from the array
* representation of the generic type.
*
* Note: The name of the class don't include the name of the package.
*
* @see {@link TypeValidationsDescriptor} for more information about type descriptors
* @see {@link TypeValidationsDescriptorFactory} for more information about type descriptor factories
*/
export const TYPE_DESCRIPTORS: { [typeName: string]: TypeValidationsDescriptor | TypeValidationsDescriptorFactory | undefined } = {};
/**
* Map with the group inherintance rules, where the key is the name of the group, an the value is an array with the
* name of the groups inherited by the key.
*
* It map must contains the name of the validation groups defined in Java and the value must contains the name of
* the other gropus extended by this one.
*
* Note:
* The name of the group is the name of the class without include the name of the package.
*/
export const GROUPS_INHERITANCE: { [groupName: string]: string[] | undefined } = {};
/**
* Map with the constraint validators, where the key is a string with the name of the constranint annotation and the
* value is the validator function of the constraint.
*
* Note:
* The name of the class don't include the name of the package.
*
* @see {@link Validator} for more informaton about the validator function of a constraint
* @see {@link ConstraintValidationsDescriptor} for more information about constraints
*/
export const VALIDATORS: { [constraintName: string]: Validator | undefined } = {};
/**
* Name of the default group for validation propouses
*/
export const DEFAULT_GROUP = 'Default';
/**
* List with the default groups used duting the validation process.
*/
export const DEFAULT_GROUPS = [DEFAULT_GROUP];
/* ********************************************************************************************
* Get / Set messageFormatter
*/
/**
* Variable with the current message formatter to be used to translate the error messages.
*
* @private
* @see {@link MessageFormatter} for more information about message formatter
* @see {@link getMessageFormatter} for get the current message formatter
* @see {@link setMessageFormatter} for set the current message formatter
*/
var formatMessage: MessageFormatter = function (template: string, attributes: any, invalidValue: any): string {
// Default error message generator
return JSON.stringify({ template, attributes, invalidValue });
}
/**
* Get the current message formatter to be used to translate the error messages.
*
* @return The current message formatter to be used to translate the error messages
*
* @see {@link setMessageFormatter} for set the current message formatter
* @see {@link MessageFormatter} for more information about message formatter
*/
export function getMessageFormatter(): MessageFormatter {
return formatMessage;
}
/**
* Set the current message formatter to be used to translate the error messages.
*
* @param formatter The message formatter to be used to translate the error messages
*
* @see {@link getMessageFormatter} for get the current message formatter
* @see {@link MessageFormatter} for more information about message formatter
*/
export function setMessageFormatter(formatter: MessageFormatter) {
formatMessage = formatter;
}
/* ********************************************************************************************
* Get descriptors
*/
/**
* Get the type descriptor associated to a type name passed by argument. If the type is not found, add a
* constraint violation of type 'TypeNotFound' and returns null.
*
* @param typeName Type name
* @param value Value to be used in the violation message as the current value to be validated
* @param path Path of the value
* @param globalViolations List where is going to be appended a violation if the type descriptor was not found
* @returns The type descriptor, if it was found, otherwise null
*
* @see {@link TypeName} for more information about how the type names are represented
* @see {@link TypeValidationsDescriptor} for more information about type descriptors
* @see {@link TYPE_DESCRIPTORS} for mor information about the type descriptor map
* @see {@link Violation} for more information about violations
*/
export function getTypeDescriptor(typeName: TypeName, value: any, path: string, globalViolations: Violation[]): TypeValidationsDescriptor | null {
const name = getSimpleNameWithoutGenericArguments(typeName);
let typeDescriptor = TYPE_DESCRIPTORS[name];
if (typeof typeDescriptor === 'function') {
typeDescriptor = typeDescriptor(typeName);
}
if (!typeDescriptor) {
const violatedConstraint: ConstraintValidationsDescriptor = { constraintName: 'TypeNotFound', attributes: { typeName: typeName } };
addViolation(violatedConstraint, value, path, globalViolations);
return null;
}
return typeDescriptor;
}
/**
* Get the property descriptor associated to a property of a type which descriptor is passed by argument. If the
* property is not found, add a constraint violation of type 'PropertyNotFound' and returns null.
*
* @param type Class name with the property which descriptor is required
* @param propertyName Name of the property which descriptor is required
* @param objectWithProperty Value with the property to be used in the violation messages as the current value to
* be validated
* @param path Path of the value with the property
* @param globalViolations List where is going to be appended a violation if the porperty descriptor was not found
* @returns The property descriptor, if it was found, otherwise null
*
* @see {@link PropertyValidationsDescriptor} for more information about property descriptors
* @see {@link TypeValidationsDescriptor} for more information about type descriptors
* @see {@link Violation} for more information about violations
*/
export function getPropertyDescriptor(type: TypeValidationsDescriptor, objectWithProperty: any, propertyName: string | number, path: string, globalViolations: Violation[]): PropertyValidationsDescriptor | null {
if (type.isCollection) {
const valueDescriptor = type.valueDescriptor;
if (valueDescriptor) {
return valueDescriptor;
}
}
const properties = type.properties;
if (properties) {
const property = properties[propertyName];
if (property) {
return property;
}
}
const violatedConstraint: ConstraintValidationsDescriptor = { constraintName: 'PropertyNotFound', attributes: { typeName: type.typeName, propertyName } };
addViolation(violatedConstraint, objectWithProperty, path, globalViolations);
return null;
}
/**
* Returns the simple name of a class, if the class is generic, returns the name of the class without the generic arguments.
*
* @private
* @param typeName Class name
* @returns Simple name of the class without generic arguments
*
* @see {@link TypeName} for more information about how the type names are represented
*/
function getSimpleNameWithoutGenericArguments(typeName: TypeName): string {
if (typeof typeName === 'string') {
return typeName;
} else {
return typeName[0];
}
}
/* ********************************************************************************************
* Group functions
*/
/**
* Verify if some of the candidate groups, or the inherited groups of it, are present en the list of allowed groups.
*
* Verifiy if there are one group in list of candidate groups present in the list of allowed groups. Also is
* verified if the each group in the list of candiadte groups inherited form other groups present in the list of
* posible groups, if that happens the group is considered as allowed.
*
* @param candidateGroups List of groups candidate to be allowed
* @param allowedGroups List of allowed groups
* @returns True if there is at least one group present in the list of allowed groups,
* if there are no one returns false
*/
export function groupAllowed(candidateGroups: string[], allowedGroups: string[] = DEFAULT_GROUPS): boolean {
for (let i = 0, length = candidateGroups.length; i < length; i++) {
const group = candidateGroups[i];
if (allowedGroups.indexOf(group) >= 0) {
return true;
}
}
for (let j = 0, l = candidateGroups.length; j < l; j++) {
const group = candidateGroups[j];
const inherited = GROUPS_INHERITANCE[group];
if (inherited && groupAllowed(inherited, allowedGroups)) {
return true;
}
}
return false;
}
/**
* Apply the rules of group convertions, defined in Java using @ConvertGroup.
*
* This method take the current groups and the group convertion rules and return a list with all group present
* in the the current group list, changing the name of it, according to the group conversion rules if it is
* requeried.
*
* @param currentGroups List of groups with the current names
* @param groupConversions Group conversion rules in order to change the name of a group
* @returns List of groups with theirs names changed if it was required
*/
export function convertGroups(currentGroups: string[], groupConversions?: GroupConversions): string[] {
if (!groupConversions) {
return currentGroups;
}
const cascadeGroups = [];
for (let i = 0, length = currentGroups.length; i < length; i++) {
const group = currentGroups[i];
const mapped = groupConversions[group];
if (mapped) {
cascadeGroups.push(mapped);
} else {
cascadeGroups.push(group);
}
}
return cascadeGroups;
}
/* ********************************************************************************************
* Register a constraint violation
*/
/**
* Create and register a constraint violation in the the violation list passed by argument.
*
* @param constraint Descriptor of the violated constraint
* @param invalidValue Value that failed the constraint's validation
* @param path Path of the value
* @param violations List where is going to be appended the violation
*
* @see {@link Violation} for more information about violations
*/
export function addViolation(constraint: ConstraintValidationsDescriptor, invalidValue: any, path: string, violations: Violation[]): void {
const messageTemplate = getMessageTemplate(constraint);
const violation: Violation = {
constraintDescriptor: constraint,
invalidValue: invalidValue,
messageTemplate,
message: formatMessage(messageTemplate, constraint.attributes, invalidValue),
propertyPath: path
};
violations.push(violation);
}
/**
* Get the template error message passed as the message attribute of a constraint, if it have no one, the
* default template message is created, it look like '{constraintName}' where constraintName is the name of
* the constraint's type.
*
* @private
* @param constraint Descriptor of the constraint
* @returns Template of the error message
*
* @see {@link ConstraintValidationsDescriptor} for more information about constraints
* @see {@link ConstraintValidationsDescriptor.attributes} for more information about the attribute with the
* constraint message
*/
function getMessageTemplate(constraint: ConstraintValidationsDescriptor): string {
let template = null;
if (constraint.attributes) {
template = constraint.attributes.message;
}
if (!template) {
template = '{' + constraint.constraintName + '}';
}
return template;
}
/* ********************************************************************************************
* Default values management in constraint
*/
/**
* Load the default values of a constraint.
*
* The default values of a constraint is stored in the 'defaultValues' property of the repective validator.
* All properties defined in the defaultValues object are loaded into the constraint descriptor attributes if
* there are absent in the defition.
*
* At the end the flag 'defaultValuesLoaded' is added to the constraint descriptor to indicate that the default
* values was already loaded in this constraint.
*
* @param constraint Constraint definition where the default values ar going to be loaded
* @param validator Validator where the default values are going to be getted
*
* @see {@link ConstraintValidationsDescriptor} for more information about constraints
* @see {@link Validator} for more informaton about the validator function of a constraint
* @see {@link Validator.defaultValues} for more information about the default value of a constraint
* @see {@link ConstraintValidationsDescriptor.attributes} for more information about constraint attributes
* @see {@link ConstraintValidationsDescriptor.defaultValuesLoaded} for more information about when the default
* values are loaded into a constraint definition
*/
export function loadDefaultConstraintValues(constraint: ConstraintValidationsDescriptor, validator: Validator) {
let defaultValues = validator.defaultValues;
if (!defaultValues) {
constraint.defaultValuesLoaded = true;
return;
}
let attributes = constraint.attributes;
if (!attributes) {
attributes = {};
constraint.attributes = attributes;
}
for (const key in defaultValues) {
if (attributes[key] === undefined) {
attributes[key] = defaultValues[key];
}
}
constraint.defaultValuesLoaded = true;
}