ts-valid8
Version:
A next-generation TypeScript validation library with advanced features
888 lines (878 loc) • 29.1 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
/**
* BaseSchema - The foundation for all validation schemas
* Provides common validation operations and chainable API
*/
class BaseSchema {
constructor() {
this._validators = [];
this._isOptional = false;
this._isNullable = false;
}
/**
* Validate a value against this schema
*/
validate(value, context = { path: [] }) {
// Handle optional and nullable cases
if (value === undefined && this._isOptional) {
if (this._defaultValue !== undefined) {
const defaultValue = typeof this._defaultValue === 'function'
? this._defaultValue()
: this._defaultValue;
return { success: true, value: defaultValue };
}
return { success: true, value: undefined };
}
if (value === null && this._isNullable) {
if (value === null && this._defaultValue !== undefined) {
const defaultValue = typeof this._defaultValue === 'function'
? this._defaultValue()
: this._defaultValue;
return { success: true, value: defaultValue };
}
return { success: true, value: null };
}
// Run type-specific validation
const typeResult = this.validateType(value, context);
if (!typeResult.success) {
return typeResult;
}
// Run all validators in sequence
const errors = [];
for (const validator of this._validators) {
const error = validator(value, context);
if (error) {
errors.push(error);
}
}
if (errors.length > 0) {
return {
success: false,
value: value,
errors,
};
}
return {
success: true,
value: value,
};
}
/**
* Make this schema accept undefined values
*/
optional() {
const schema = this.clone();
schema._isOptional = true;
return schema;
}
/**
* Make this schema accept null values
*/
nullable() {
const schema = this.clone();
schema._isNullable = true;
return schema;
}
/**
* Set a default value for this schema
*/
default(value) {
const schema = this.clone();
schema._defaultValue = value;
return schema;
}
/**
* Mark this field as required (not undefined)
*/
required(message = 'This field is required') {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (value === undefined) {
return this.createError('required', message, context);
}
return null;
});
return schema;
}
/**
* Add a custom validation rule
*/
refine(refinement, message) {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
const result = refinement(value, context);
if (result === false) {
const errorMessage = typeof message === 'function'
? message(value)
: message;
return {
path: context.path,
message: errorMessage,
code: 'custom',
};
}
return null;
});
return schema;
}
/**
* Apply a transformation function to the schema's value
*/
transform(fn) {
const schema = this.clone();
// Base implementation - subclasses may override this with type-specific logic
// Using the parameter to avoid the unused parameter warning
schema._validators.push((_value, _context) => {
return null;
});
return schema;
}
/**
* Create validation error
*/
createError(code, message, context, params) {
return {
path: context.path,
message,
code,
params,
};
}
/**
* Use a plugin with this schema
*/
use(plugin) {
return plugin(this);
}
}
/**
* StringSchema - Advanced string validation with rich pattern matching
* and complex rule composition
*/
class StringSchema extends BaseSchema {
constructor() {
super(...arguments);
this._type = 'string';
this._patterns = new Map();
this._transformers = [];
}
validateType(value, context) {
if (typeof value !== 'string') {
return {
success: false,
value: value,
errors: [
this.createError('type', `Expected string, received ${typeof value}`, context),
],
};
}
return { success: true, value };
}
clone() {
const schema = new StringSchema();
schema._validators = [...this._validators];
schema._isOptional = this._isOptional;
schema._isNullable = this._isNullable;
schema._defaultValue = this._defaultValue;
schema._patterns = new Map(this._patterns);
schema._transformers = [...this._transformers];
return schema;
}
/**
* Require the string to have a minimum length
*/
min(length, message) {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (typeof value === 'string' && value.length < length) {
return this.createError('string.min', message || `String must be at least ${length} characters long`, context, { minLength: length, actualLength: value.length });
}
return null;
});
return schema;
}
/**
* Require the string to have a maximum length
*/
max(length, message) {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (typeof value === 'string' && value.length > length) {
return this.createError('string.max', message || `String must be at most ${length} characters long`, context, { maxLength: length, actualLength: value.length });
}
return null;
});
return schema;
}
/**
* Require the string to have an exact length
*/
length(length, message) {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (typeof value === 'string' && value.length !== length) {
return this.createError('string.length', message || `String must be exactly ${length} characters long`, context, { expectedLength: length, actualLength: value.length });
}
return null;
});
return schema;
}
/**
* Require the string to match a regular expression
*/
pattern(regex, message) {
const schema = this.clone();
const errorMessage = message || `String must match pattern: ${regex}`;
schema._patterns.set(regex, errorMessage);
schema._validators.push((value, context = { path: [] }) => {
if (typeof value === 'string' && !regex.test(value)) {
return this.createError('string.pattern', errorMessage, context, { pattern: regex.toString() });
}
return null;
});
return schema;
}
/**
* Require the string to be a valid email
*/
email(message) {
// RFC 5322 compliant email regex
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return this.pattern(emailRegex, message || 'Must be a valid email address');
}
/**
* Require the string to be a valid URL
*/
url(message) {
const urlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i;
return this.pattern(urlRegex, message || 'Must be a valid URL');
}
/**
* Require the string to be a valid UUID
*/
uuid(message) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return this.pattern(uuidRegex, message || 'Must be a valid UUID');
}
/**
* Complex password validation with multiple rules
* Allows customization of requirements
*/
password(options) {
const opts = {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireNumbers: true,
requireSpecialChars: true,
...options
};
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (typeof value !== 'string')
return null;
const errors = [];
if (opts.minLength && value.length < opts.minLength) {
errors.push(`at least ${opts.minLength} characters`);
}
if (opts.requireLowercase && !/[a-z]/.test(value)) {
errors.push('lowercase letters');
}
if (opts.requireUppercase && !/[A-Z]/.test(value)) {
errors.push('uppercase letters');
}
if (opts.requireNumbers && !/[0-9]/.test(value)) {
errors.push('numbers');
}
if (opts.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
errors.push('special characters');
}
if (errors.length > 0) {
return this.createError('string.password', opts.customMessage || `Password must contain ${errors.join(', ')}`, context, { requirements: opts });
}
return null;
});
return schema;
}
/**
* Transforms the string value to uppercase
*/
toUpperCase() {
const schema = this.clone();
schema._transformers = schema._transformers || [];
schema._transformers.push((value) => value.toUpperCase());
return schema;
}
/**
* Transforms the string value to lowercase
*/
toLowerCase() {
const schema = this.clone();
schema._transformers = schema._transformers || [];
schema._transformers.push((value) => value.toLowerCase());
return schema;
}
/**
* Trim the string value
*/
trim() {
const schema = this.clone();
schema._transformers = schema._transformers || [];
schema._transformers.push((value) => value.trim());
return schema;
}
/**
* Apply a custom transformation to the string
*/
transform(fn) {
const schema = this.clone();
schema._transformers = schema._transformers || [];
schema._transformers.push(fn);
return schema;
}
/**
* Require the string to be one of the allowed values
*/
oneOf(values, message) {
const schema = this.clone();
const allowedValues = Array.isArray(values) ? values : [values];
schema._validators.push((value, context = { path: [] }) => {
if (typeof value === 'string' && !allowedValues.includes(value)) {
return this.createError('string.oneOf', message || `String must be one of: ${allowedValues.join(', ')}`, context, { allowedValues, actual: value });
}
return null;
});
return schema;
}
}
/**
* ObjectSchema - Advanced object validation with cross-field validation support
*/
class ObjectSchema extends BaseSchema {
constructor(shape) {
super();
this._type = 'object';
this._crossFieldValidators = [];
this._strict = false;
this.shape = shape;
}
validateType(value, context) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return {
success: false,
value: value,
errors: [
this.createError('type', `Expected object, received ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`, context),
],
};
}
const input = value;
const result = {};
const errors = [];
// Validate each field
for (const key in this.shape) {
const fieldSchema = this.shape[key];
const fieldValue = input[key];
// Create context for field validation
const fieldContext = {
...context,
path: [...context.path, key],
parent: input,
siblings: input,
root: context.root || input,
};
const fieldResult = fieldSchema.validate(fieldValue, fieldContext);
result[key] = fieldResult.value;
if (!fieldResult.success && fieldResult.errors) {
errors.push(...fieldResult.errors);
}
}
// Check for unknown fields if strict mode is enabled
if (this._strict) {
const knownKeys = Object.keys(this.shape);
const inputKeys = Object.keys(input);
for (const key of inputKeys) {
if (!knownKeys.includes(key)) {
errors.push(this.createError('object.unknown', `Unrecognized key: ${key}`, { ...context, path: [...context.path, key] }));
}
}
}
// Apply cross-field validation
for (const validator of this._crossFieldValidators) {
const error = validator(result, context);
if (error) {
errors.push(error);
}
}
if (errors.length > 0) {
return {
success: false,
value: result,
errors,
};
}
return {
success: true,
value: result,
};
}
clone() {
const schema = new ObjectSchema(this.shape);
schema._validators = [...this._validators];
schema._crossFieldValidators = [...this._crossFieldValidators];
schema._isOptional = this._isOptional;
schema._isNullable = this._isNullable;
schema._defaultValue = this._defaultValue;
schema._strict = this._strict;
return schema;
}
/**
* Add a validation rule that depends on multiple fields
*/
crossField(validator, message, affectedFields) {
const schema = this.clone();
schema._crossFieldValidators.push((values, context) => {
const result = validator(values);
if (result === false) {
const errorMessage = typeof message === 'function'
? message(values)
: message;
const affectedPaths = affectedFields
? affectedFields.map(field => [...context.path, field])
: [context.path];
// Return only the first affected field's error
return {
path: affectedPaths[0],
message: errorMessage,
code: 'object.crossField',
params: { affectedFields }
};
}
return null;
});
return schema;
}
/**
* Add password matching validation between two fields
*/
passwordsMatch(passwordField, confirmField, message = 'Passwords must match') {
return this.crossField((values) => values[passwordField] === values[confirmField], message, [confirmField]);
}
/**
* Enable strict mode (unknown fields will cause validation to fail)
*/
strict(enabled = true) {
const schema = this.clone();
schema._strict = enabled;
return schema;
}
/**
* Allow unknown fields (passthrough mode)
*/
passthrough() {
return this.strict(false);
}
/**
* Define a validation rule that depends on a condition from another field
*/
when(field, condition, then) {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (typeof value !== 'object' || value === null)
return null;
const obj = value;
const fieldValue = obj[field];
if (condition(fieldValue)) {
const thenSchema = then(new ObjectSchema(this.shape));
const result = thenSchema.validate(value, context);
if (!result.success && result.errors) {
// Return only the first error
return result.errors[0];
}
}
return null;
});
return schema;
}
/**
* Pick a subset of fields from this schema to create a new schema
*/
pick(keys) {
const shape = {};
for (const key of keys) {
shape[key] = this.shape[key];
}
return new ObjectSchema(shape);
}
/**
* Omit fields from this schema to create a new schema
*/
omit(keys) {
const shape = { ...this.shape };
for (const key of keys) {
delete shape[key];
}
return new ObjectSchema(shape);
}
/**
* Create a partial version of the schema where all properties are optional
*/
partial() {
const partialShape = {};
for (const key in this.shape) {
if (this.shape.hasOwnProperty(key)) {
partialShape[key] = this.shape[key].optional();
}
}
const schema = new ObjectSchema(partialShape);
schema._strict = this._strict;
return schema;
}
/**
* Extend this schema with additional fields or override existing ones
*/
extend(shape) {
const extendedShape = { ...this.shape };
for (const key in shape) {
if (shape.hasOwnProperty(key)) {
extendedShape[key] = shape[key];
}
}
const schema = new ObjectSchema(extendedShape);
schema._strict = this._strict;
return schema;
}
}
/**
* BooleanSchema - Boolean value validation with extended functionality
*/
class BooleanSchema extends BaseSchema {
constructor() {
super(...arguments);
this._type = 'boolean';
}
validateType(value, context) {
// Strict boolean validation
if (typeof value !== 'boolean') {
return {
success: false,
value: value,
errors: [
this.createError('type', `Expected boolean, received ${typeof value}`, context),
],
};
}
return { success: true, value };
}
clone() {
const schema = new BooleanSchema();
schema._validators = [...this._validators];
schema._isOptional = this._isOptional;
schema._isNullable = this._isNullable;
schema._defaultValue = this._defaultValue;
return schema;
}
/**
* Require the boolean value to be true
*/
true(message = 'Value must be true') {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (value !== true) {
return this.createError('boolean.true', message, context);
}
return null;
});
return schema;
}
/**
* Require the boolean value to be false
*/
false(message = 'Value must be false') {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (value !== false) {
return this.createError('boolean.false', message, context);
}
return null;
});
return schema;
}
/**
* Mark this field as required
*/
required(message = 'This field is required') {
const schema = this.clone();
schema._validators.push((value, context = { path: [] }) => {
if (value === undefined) {
return this.createError('boolean.required', message, context);
}
return null;
});
return schema;
}
/**
* Accept string values and convert them to boolean
* 'true', '1', 'yes' will be converted to true
* 'false', '0', 'no' will be converted to false
*/
coerce() {
const schema = this.clone();
const originalValidate = schema.validateType;
schema.validateType = (value, context) => {
// Convert string values to boolean if applicable
if (typeof value === 'string') {
const lowercaseValue = value.toLowerCase().trim();
if (['true', '1', 'yes', 'y', 'on'].includes(lowercaseValue)) {
return { success: true, value: true };
}
if (['false', '0', 'no', 'n', 'off'].includes(lowercaseValue)) {
return { success: true, value: false };
}
}
// Convert numeric values to boolean
if (value === 1 || value === 0) {
return { success: true, value: value === 1 };
}
// Fall back to standard validation
return originalValidate.call(schema, value, context);
};
return schema;
}
/**
* Require the boolean to be a specific value (true or false)
*/
equals(value, message) {
const schema = this.clone();
schema._validators.push((val, context = { path: [] }) => {
if (typeof val === 'boolean' && val !== value) {
return this.createError('boolean.equals', message || `Boolean must be ${value}`, context, { expected: value, actual: val });
}
return null;
});
return schema;
}
/**
* Apply a custom transformation to the boolean
*/
transform(fn) {
const schema = this.clone();
// Store the original validateType function
const originalValidateType = schema.validateType.bind(schema);
// Override the validateType function to apply the transform
schema.validateType = (value, context) => {
const result = originalValidateType(value, context);
if (result.success) {
try {
result.value = fn(result.value);
}
catch (error) {
return {
success: false,
value: value,
errors: [
this.createError('boolean.transform', 'Error transforming boolean value', context, { error: error.message })
]
};
}
}
return result;
};
return schema;
}
}
/**
* Plugin System for Valid8
*
* This system allows for extending the library's functionality through plugins
* that can add new validator types or enhance existing ones.
*/
/**
* Plugin Manager for registering and managing plugins
*/
class PluginManager {
constructor() {
this.plugins = new Map();
this.extensions = new Map();
this.schemas = new Map();
this.validators = new Map();
}
/**
* Register a plugin with the validation library
*/
use(plugin) {
if (this.plugins.has(plugin.name)) {
console.warn(`Plugin "${plugin.name}" is already registered. Skipping.`);
return;
}
this.plugins.set(plugin.name, plugin);
const api = {
extend: (schemaType, methodName, method) => {
if (!this.extensions.has(schemaType)) {
this.extensions.set(schemaType, new Map());
}
this.extensions.get(schemaType).set(methodName, method);
},
registerSchema: (schemaType, schemaConstructor) => {
this.schemas.set(schemaType, schemaConstructor);
},
registerValidator: (name, validatorFn) => {
this.validators.set(name, validatorFn);
},
};
plugin.install(api);
}
/**
* Get an extension method for a specific schema type
*/
getExtension(schemaType, methodName) {
const schemaExtensions = this.extensions.get(schemaType);
if (!schemaExtensions)
return undefined;
return schemaExtensions.get(methodName);
}
/**
* Get a registered schema constructor
*/
getSchema(schemaType) {
return this.schemas.get(schemaType);
}
/**
* Get a registered validator function
*/
getValidator(name) {
return this.validators.get(name);
}
}
// Create and export a singleton plugin manager
const pluginManager = new PluginManager();
// Helper function to create plugins
function createPlugin(name, version, installFn) {
return {
name,
version,
install: installFn,
};
}
/**
* Validator Builder - A fluent API for building validation schemas
* Simplifies the creation of complex validation logic with a chainable interface
*/
/**
* Safely applies a plugin to a schema, checking if the use method exists
* @param schema The schema to apply the plugin to
* @param plugin The plugin to apply
* @returns The schema with the plugin applied
* @throws If the schema does not support plugins
*/
function applyPlugin(schema, plugin) {
if (!schema.use) {
throw new Error(`The schema of type '${schema._type}' does not support plugins`);
}
return schema.use(plugin);
}
/**
* Valid8 - Next-generation TypeScript validation library
* A powerful, type-safe, and feature-rich validation library with support for
* complex validations and cross-field validation
*/
/**
* Valid8 - The main library API
*/
const Valid8 = {
/**
* Create a string schema
*/
string() {
return new StringSchema();
},
/**
* Create an object schema
*/
object(shape) {
return new ObjectSchema(shape);
},
/**
* Create a boolean schema
*/
boolean() {
return new BooleanSchema();
},
/**
* Create a validation pipeline with multiple schemas
* Tries each schema in order until one succeeds
*/
oneOf(schemas) {
// Implementation details hidden for brevity
// This would try each schema in sequence until one passes
return {
_type: 'union',
_validators: [],
validate(value, context = { path: [] }) {
const errors = [];
for (const schema of schemas) {
const result = schema.validate(value, context);
if (result.success) {
return result;
}
if (result.errors) {
errors.push(...result.errors);
}
}
return {
success: false,
value,
errors,
};
},
optional() { throw new Error('Not implemented'); },
nullable() { throw new Error('Not implemented'); },
default(_value) { throw new Error('Not implemented'); },
refine() { throw new Error('Not implemented'); },
required() { throw new Error('Not implemented'); },
transform() { throw new Error('Not implemented'); },
};
},
/**
* Create a validation context with custom data
* Useful for advanced validation scenarios
*/
context(data) {
return {
withSchema(schema) {
return {
validate(value) {
return schema.validate(value, { path: [], ...data });
},
};
},
};
},
/**
* Plugin system for extending the library
*/
plugins: {
/**
* Register a plugin with the library
*/
use: pluginManager.use.bind(pluginManager),
/**
* Create a custom plugin
*/
create: (name, version, install) => {
return {
name,
version,
install
};
}
}
};
exports.PluginManager = PluginManager;
exports.Valid8 = Valid8;
exports.applyPlugin = applyPlugin;
exports.createPlugin = createPlugin;
exports["default"] = Valid8;
exports.pluginManager = pluginManager;
//# sourceMappingURL=index.js.map