UNPKG

ts-valid8

Version:

A next-generation TypeScript validation library with advanced features

888 lines (878 loc) 29.1 kB
'use strict'; 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