From b9d47bf90669a4b26133072c8afc67c7504e6957 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 21 Apr 2025 13:06:38 -0400 Subject: [PATCH 1/3] Removed validation. --- src/validation/__tests__/validator.spec.ts | 296 ------------------ src/validation/format.ts | 33 -- src/validation/index.ts | 271 ---------------- .../rules/__tests__/allowedCharacters.spec.ts | 32 -- .../rules/__tests__/confirm.spec.ts | 46 --- .../rules/__tests__/containsDigits.spec.ts | 39 --- .../rules/__tests__/containsLowercase.spec.ts | 39 --- .../__tests__/containsNonAlphanumeric.spec.ts | 39 --- .../rules/__tests__/containsUppercase.spec.ts | 39 --- src/validation/rules/__tests__/email.spec.ts | 36 --- .../rules/__tests__/identifier.spec.ts | 36 --- .../rules/__tests__/maximumLength.spec.ts | 42 --- .../rules/__tests__/maximumValue.spec.ts | 54 ---- .../rules/__tests__/minimumLength.spec.ts | 42 --- .../rules/__tests__/minimumValue.spec.ts | 54 ---- .../rules/__tests__/pattern.spec.ts | 32 -- .../rules/__tests__/required.spec.ts | 54 ---- src/validation/rules/__tests__/slug.spec.ts | 30 -- .../rules/__tests__/uniqueCharacters.spec.ts | 30 -- src/validation/rules/__tests__/url.spec.ts | 52 --- src/validation/rules/allowedCharacters.ts | 27 -- src/validation/rules/confirm.ts | 17 - src/validation/rules/containsDigits.ts | 28 -- src/validation/rules/containsLowercase.ts | 28 -- .../rules/containsNonAlphanumeric.ts | 28 -- src/validation/rules/containsUppercase.ts | 28 -- src/validation/rules/email.ts | 37 --- src/validation/rules/identifier.ts | 22 -- src/validation/rules/maximumLength.ts | 30 -- src/validation/rules/maximumValue.ts | 20 -- src/validation/rules/minimumLength.ts | 30 -- src/validation/rules/minimumValue.ts | 20 -- src/validation/rules/pattern.ts | 20 -- src/validation/rules/required.ts | 34 -- src/validation/rules/slug.ts | 18 -- src/validation/rules/uniqueCharacters.ts | 27 -- src/validation/rules/url.ts | 64 ---- src/validation/types.ts | 193 ------------ 38 files changed, 1967 deletions(-) delete mode 100644 src/validation/__tests__/validator.spec.ts delete mode 100644 src/validation/format.ts delete mode 100644 src/validation/index.ts delete mode 100644 src/validation/rules/__tests__/allowedCharacters.spec.ts delete mode 100644 src/validation/rules/__tests__/confirm.spec.ts delete mode 100644 src/validation/rules/__tests__/containsDigits.spec.ts delete mode 100644 src/validation/rules/__tests__/containsLowercase.spec.ts delete mode 100644 src/validation/rules/__tests__/containsNonAlphanumeric.spec.ts delete mode 100644 src/validation/rules/__tests__/containsUppercase.spec.ts delete mode 100644 src/validation/rules/__tests__/email.spec.ts delete mode 100644 src/validation/rules/__tests__/identifier.spec.ts delete mode 100644 src/validation/rules/__tests__/maximumLength.spec.ts delete mode 100644 src/validation/rules/__tests__/maximumValue.spec.ts delete mode 100644 src/validation/rules/__tests__/minimumLength.spec.ts delete mode 100644 src/validation/rules/__tests__/minimumValue.spec.ts delete mode 100644 src/validation/rules/__tests__/pattern.spec.ts delete mode 100644 src/validation/rules/__tests__/required.spec.ts delete mode 100644 src/validation/rules/__tests__/slug.spec.ts delete mode 100644 src/validation/rules/__tests__/uniqueCharacters.spec.ts delete mode 100644 src/validation/rules/__tests__/url.spec.ts delete mode 100644 src/validation/rules/allowedCharacters.ts delete mode 100644 src/validation/rules/confirm.ts delete mode 100644 src/validation/rules/containsDigits.ts delete mode 100644 src/validation/rules/containsLowercase.ts delete mode 100644 src/validation/rules/containsNonAlphanumeric.ts delete mode 100644 src/validation/rules/containsUppercase.ts delete mode 100644 src/validation/rules/email.ts delete mode 100644 src/validation/rules/identifier.ts delete mode 100644 src/validation/rules/maximumLength.ts delete mode 100644 src/validation/rules/maximumValue.ts delete mode 100644 src/validation/rules/minimumLength.ts delete mode 100644 src/validation/rules/minimumValue.ts delete mode 100644 src/validation/rules/pattern.ts delete mode 100644 src/validation/rules/required.ts delete mode 100644 src/validation/rules/slug.ts delete mode 100644 src/validation/rules/uniqueCharacters.ts delete mode 100644 src/validation/rules/url.ts delete mode 100644 src/validation/types.ts diff --git a/src/validation/__tests__/validator.spec.ts b/src/validation/__tests__/validator.spec.ts deleted file mode 100644 index eb6f99d..0000000 --- a/src/validation/__tests__/validator.spec.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import Validator from ".."; -import containsNonAlphanumeric from "../rules/containsNonAlphanumeric"; -import email from "../rules/email"; -import required from "../rules/required"; -import type { - RuleConfiguration, - RuleExecutionOutcome, - RuleOptions, - ValidationContext, - ValidationResult, - ValidationRule, - ValidationRuleKey, - ValidationSeverity, -} from "../types"; - -const required_alt: ValidationRule = (value: unknown): boolean => { - return Boolean(value); -}; - -const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; -const email_alt: ValidationRule = (value: unknown): ValidationSeverity => { - const isValid: boolean = typeof value === "string" && emailRegex.test(value); - return isValid ? "information" : "error"; -}; - -const not_empty: ValidationRule = (value: unknown, _, context: ValidationContext | undefined): RuleExecutionOutcome => { - const trimmed = typeof value === "string" ? value.trim() : ""; - let name: string = ""; - if (context && context.name) { - name = context.name as string; - } - return { - severity: trimmed.length > 0 ? "information" : "error", - key: "NotEmptyValidator", - message: "{{name}} ({{original}} → {{value}}) cannot be an empty string.", - placeholders: { original: value, value: trimmed }, - name: "'" + name + "'", - value: trimmed, - custom: { value, trimmed }, - }; -}; - -describe("Validator", () => { - it.concurrent("should clear all registered rules", () => { - const validator = new Validator(); - validator.setRule("email", email); - validator.setRule("required", required); - validator.clearRules(); - expect(validator.listRules().length).toBe(0); - }); - - it.concurrent("should get a specific rule", () => { - const validator = new Validator(); - validator.setRule("email", email); - validator.setRule("required", required); - const configuration: RuleConfiguration | undefined = validator.getRule("email"); - expect(configuration).toBeDefined(); - expect(configuration?.rule).toBe(email); - expect(configuration?.options).toEqual({}); - }); - - it.concurrent("should return undefined when a rule has not been registered", () => { - const validator = new Validator(); - validator.setRule("required", required); - const configuration: RuleConfiguration | undefined = validator.getRule("email"); - expect(configuration).toBeUndefined(); - }); - - it.concurrent("should check if a rule has been registered", () => { - const validator = new Validator(); - expect(validator.hasRule("required")).toBe(false); - validator.setRule("required", required); - expect(validator.hasRule("required")).toBe(true); - }); - - it.concurrent("should list all registered rules", () => { - const validator = new Validator(); - let rules: [ValidationRuleKey, RuleConfiguration][] = validator.listRules(); - expect(rules.length).toBe(0); - validator.setRule("email", email); - validator.setRule("required", required); - rules = validator.listRules(); - expect(rules.length).toBe(2); - expect(rules[0][0]).toBe("email"); - expect(rules[1][0]).toBe("required"); - }); - - it.concurrent("should remove a rule", () => { - const validator = new Validator(); - validator.setRule("email", email); - validator.setRule("required", required); - validator.removeRule("email"); - expect(validator.hasRule("email")).toBe(false); - expect(validator.hasRule("required")).toBe(true); - }); - - it.concurrent("should register a rule without options", () => { - const validator = new Validator(); - validator.setRule("email", email); - const configuration: RuleConfiguration | undefined = validator.getRule("email"); - expect(configuration).toBeDefined(); - expect(configuration?.rule).toBe(email); - expect(configuration?.options).toEqual({}); - }); - - it.concurrent("should register a rule with options", () => { - const validator = new Validator(); - const options: RuleOptions = { - key: "EmailAddressValidator", - message: "{{name}} n’est pas une adresse courriel valide.", - placeholders: { locale: "fr" }, - }; - validator.setRule("email", email, options); - const configuration: RuleConfiguration | undefined = validator.getRule("email"); - expect(configuration).toBeDefined(); - expect(configuration?.rule).toBe(email); - expect(JSON.stringify(configuration?.options)).toBe(JSON.stringify(options)); - }); - - it.concurrent("should not execute validation rules when args are falsy", () => { - const validator = new Validator(); - validator.setRule("email", email); - validator.setRule("required", required); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: false }); - expect(result.isValid).toBe(true); - expect(Object.keys(result.rules).length).toBe(1); - expect(result.rules.required.severity).toBe("information"); - expect(result.context).toEqual({}); - }); - - it.concurrent("should throw an error when a rule has not been registered", () => { - const validator = new Validator(); - validator.setRule("required", required); - expect(() => validator.validate("email", "test@example.com", { required: true, email: false })).toThrowError(); - }); - - it.concurrent("should succeed when all validation rules are satisfied", () => { - const validator = new Validator(); - validator.setRule("required", required); - validator.setRule("email", email); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: true }); - expect(result.isValid).toBe(true); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.email.severity).toBe("information"); - }); - - it.concurrent("should succeed when warnings are not treated as errors (ctor)", () => { - const validator = new Validator({ treatWarningsAsErrors: false }); - validator.setRule("required", required); - validator.setRule("email", email); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: 1 }); - expect(result.isValid).toBe(true); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.email.severity).toBe("warning"); - }); - - it.concurrent("should succeed when warnings are not treated as errors (validate)", () => { - const validator = new Validator({ treatWarningsAsErrors: false }); - validator.setRule("required", required); - validator.setRule("email", email); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: 1 }, { treatWarningsAsErrors: false }); - expect(result.isValid).toBe(true); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.email.severity).toBe("warning"); - }); - - it.concurrent("should throw an error when throwing on failure (ctor)", () => { - const validator = new Validator({ throwOnFailure: true }); - validator.setRule("required", required); - expect(() => validator.validate("email", undefined, { required: true }, { throwOnFailure: undefined })).toThrowError(); - }); - - it.concurrent("should throw an error when throwing on failure (validator)", () => { - const validator = new Validator({ throwOnFailure: false }); - validator.setRule("required", required); - expect(() => validator.validate("email", undefined, { required: true }, { throwOnFailure: true })).toThrowError(); - }); - - it.concurrent("should fail when some validation rules fail", () => { - const validator = new Validator(); - validator.setRule("required", required); - validator.setRule("email", email); - validator.setRule("containsNonAlphanumeric", containsNonAlphanumeric); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: true, containsNonAlphanumeric: 3 }); - expect(result.isValid).toBe(false); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.email.severity).toBe("information"); - expect(result.rules.containsNonAlphanumeric.severity).toBe("error"); - expect(result.rules.containsNonAlphanumeric.message).toBe("email must contain at least 3 non-alphanumeric character(s)."); - }); - - it.concurrent("should fail when warnings are treated as errors (ctor)", () => { - const validator = new Validator({ treatWarningsAsErrors: true }); - validator.setRule("required", required); - validator.setRule("email", email); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: 1 }, { treatWarningsAsErrors: undefined }); - expect(result.isValid).toBe(false); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.email.severity).toBe("warning"); - expect(result.rules.email.message).toBe("The arguments must be undefined, or a valid email address validation regular expression."); - }); - - it.concurrent("should fail when warnings are treated as errors (validate)", () => { - const validator = new Validator({ treatWarningsAsErrors: false }); - validator.setRule("required", required); - validator.setRule("email", email); - const result = validator.validate("email", "test@example.com", { required: true, email: 1 }, { treatWarningsAsErrors: true }); - expect(result.isValid).toBe(false); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.email.severity).toBe("warning"); - expect(result.rules.email.message).toBe("The arguments must be undefined, or a valid email address validation regular expression."); - }); - - it.concurrent("should use key and message rule override", () => { - const validator = new Validator(); - validator.setRule("required", required); - validator.setRule("email", email, { key: "EmailAddressValidator", message: "{{name}} doit être une adresse courriel valide." }); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true, email: true }); - expect(result.isValid).toBe(true); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.required.key).toBe("required"); - expect(result.rules.required.message).toBeUndefined(); - expect(result.rules.email.severity).toBe("information"); - expect(result.rules.email.key).toBe("EmailAddressValidator"); - expect(result.rules.email.message).toBe("email doit être une adresse courriel valide."); - }); - - it.concurrent("should use placeholders provided in the rule options", () => { - const validator = new Validator(); - validator.setRule("required", required, { placeholders: { name: "This field" } }); - const result: ValidationResult = validator.validate("email", " ", { required: true }); - expect(result.isValid).toBe(false); - expect(result.rules.required.message).toBe("This field cannot be an empty string."); - expect(result.rules.required.placeholders.name).toBe("This field"); - }); - - it.concurrent("should use placeholders provided in the validation options", () => { - const validator = new Validator(); - validator.setRule("required", required); - const result: ValidationResult = validator.validate("email", " ", { required: true }, { placeholders: { name: "This field" } }); - expect(result.isValid).toBe(false); - expect(result.rules.required.message).toBe("This field cannot be an empty string."); - expect(result.rules.required.placeholders.name).toBe("This field"); - }); - - it.concurrent("should handle rules returning a boolean value (invalid)", () => { - const validator = new Validator(); - validator.setRule("required", required_alt); - const result: ValidationResult = validator.validate("email", "", { required: true }); - expect(result.isValid).toBe(false); - expect(result.rules.required.severity).toBe("error"); - expect(result.rules.required.message).toBeUndefined(); - }); - - it.concurrent("should handle rules returning a boolean value (valid)", () => { - const validator = new Validator(); - validator.setRule("required", required_alt); - const result: ValidationResult = validator.validate("email", "test@example.com", { required: true }); - expect(result.isValid).toBe(true); - expect(result.rules.required.severity).toBe("information"); - expect(result.rules.required.message).toBeUndefined(); - }); - - it.concurrent("should handle rules returning a validation severity (invalid)", () => { - const validator = new Validator(); - validator.setRule("email", email_alt); - const result: ValidationResult = validator.validate("email", "", { email: true }); - expect(result.isValid).toBe(false); - expect(result.rules.email.severity).toBe("error"); - expect(result.rules.email.message).toBeUndefined(); - }); - - it.concurrent("should handle rules returning a validation severity (valid)", () => { - const validator = new Validator(); - validator.setRule("email", email_alt); - const result: ValidationResult = validator.validate("email", "test@example.com", { email: true }); - expect(result.isValid).toBe(true); - expect(result.rules.email.severity).toBe("information"); - expect(result.rules.email.message).toBeUndefined(); - }); - - it.concurrent("should handle rule execution outcome values", () => { - const validator = new Validator(); - validator.setRule("notEmpty", not_empty); - const result: ValidationResult = validator.validate("email", " ", { notEmpty: true }, { context: { name: "email" } }); - expect(result.isValid).toBe(false); - expect(result.rules.notEmpty.severity).toBe("error"); - expect(result.rules.notEmpty.key).toBe("NotEmptyValidator"); - expect(result.rules.notEmpty.message).toBe("'email' ( → ) cannot be an empty string."); - expect(result.rules.notEmpty.name).toBe("'email'"); - expect(result.rules.notEmpty.value).toBe(""); - expect(JSON.stringify(result.rules.notEmpty.custom)).toBe(JSON.stringify({ value: " ", trimmed: "" })); - }); -}); diff --git a/src/validation/format.ts b/src/validation/format.ts deleted file mode 100644 index 39292e7..0000000 --- a/src/validation/format.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Defines a message formatter. - */ -export interface MessageFormatter { - /** - * Formats a message with the given placeholders. - * @param message The message to format. - * @param placeholders The placeholders to replace in the message. - * @returns The formatted message. - */ - format(message: string, placeholders: Record): string; -} - -/** - * The default message formatter. This could use [mustache.js](https://github.com/janl/mustache.js), but we don't want to add a dependency for this. We simply replace occurrences of placeholder keys with their values, no other computation. - */ -export default class DefaultMessageFormatter implements MessageFormatter { - /** - * Formats a message with the given placeholders. - * @param message The message to format. - * @param placeholders The placeholders to replace in the message. - * @returns The formatted message. - */ - format(message: string, placeholders: Record): string { - let formatted: string = message; - for (const key in placeholders) { - const pattern = `{{${key}}}`; - const replacement = String(placeholders[key]); - formatted = formatted.split(pattern).join(replacement); - } - return formatted; - } -} diff --git a/src/validation/index.ts b/src/validation/index.ts deleted file mode 100644 index f8b6178..0000000 --- a/src/validation/index.ts +++ /dev/null @@ -1,271 +0,0 @@ -import DefaultMessageFormatter, { MessageFormatter } from "./format"; -import { isNullOrWhiteSpace } from "../helpers/stringUtils"; -import type { - RuleConfiguration, - RuleExecutionOutcome, - RuleExecutionResult, - RuleOptions, - ValidationContext, - ValidationOptions, - ValidationResult, - ValidationRule, - ValidationRuleKey, - ValidationRuleSet, - ValidationSeverity, - ValidatorOptions, -} from "./types"; - -/** - * Applies the execution outcome of a validation rule to a result. - * @param result The result to apply the execution outcome to. - * @param outcome The execution outcome of the validation rule execution. - * @param options The options of the validation rule execution. - */ -function apply(result: RuleExecutionResult, outcome: RuleExecutionOutcome, options: RuleOptions): void { - // severity - result.severity = outcome.severity; - // key - if (!isNullOrWhiteSpace(options.key)) { - result.key = options.key; - } else if (!isNullOrWhiteSpace(outcome.key)) { - result.key = outcome.key; - } - // message - if (!isNullOrWhiteSpace(options.message)) { - result.message = options.message; - } else if (!isNullOrWhiteSpace(outcome.message)) { - result.message = outcome.message; - } - // name - if (!isNullOrWhiteSpace(outcome.name)) { - result.name = outcome.name; - } - // value - if (typeof outcome.value !== "undefined") { - result.value = outcome.value; - } - // custom - result.custom = outcome.custom; -} - -/** - * Fills the placeholders of a validation rule execution. - * @param result The result to fill the placeholders of. - * @param outcome The execution outcome of the validation rule. - * @param rule The options of the validation rule execution. - * @param validation The options of the validation operation. - */ -function fillPlaceholders(result: RuleExecutionResult, outcome?: RuleExecutionOutcome, rule?: RuleOptions, validation?: ValidationOptions): void { - result.placeholders.key = result.key; - result.placeholders.name = result.name; - result.placeholders.value = result.value; - result.placeholders.severity = result.severity; - - if (outcome && outcome.placeholders) { - result.placeholders = { ...result.placeholders, ...outcome.placeholders }; - } - if (rule && rule.placeholders) { - result.placeholders = { ...result.placeholders, ...rule.placeholders }; - } - if (validation && validation.placeholders) { - result.placeholders = { ...result.placeholders, ...validation.placeholders }; - } -} - -/** - * A validator is a collection of validation rules that can be executed on a value. - */ -class Validator { - /** - * The message formatter to use. - */ - private readonly messageFormatter: MessageFormatter; - /** - * The rules registered to this validator. - */ - private readonly rules: Map; - /** - * A value indicating whether the validator should throw an error if the validation fails. - */ - private readonly throwOnFailure: boolean; - /** - * A value indicating whether warnings should be treated as errors. - */ - private readonly treatWarningsAsErrors: boolean; - - /** - * Initializes a new instance of the Validator class. - * @param options The options of the validator. - */ - constructor(options?: ValidatorOptions) { - options ??= {}; - this.messageFormatter = options.messageFormatter ?? new DefaultMessageFormatter(); - this.rules = new Map(); - this.throwOnFailure = options.throwOnFailure ?? false; - this.treatWarningsAsErrors = options.treatWarningsAsErrors ?? false; - } - - /** - * Clears all the rules registered to this validator. - */ - clearRules(): void { - this.rules.clear(); - } - - /** - * Gets a rule from the validator. - * @param key The key of the rule to get. - * @returns The rule configuration. - */ - getRule(key: ValidationRuleKey): RuleConfiguration | undefined { - return this.rules.get(key); - } - - /** - * Checks if a rule is registered to this validator. - * @param key The key of the rule to check. - * @returns A value indicating whether the rule is registered to this validator. - */ - hasRule(key: ValidationRuleKey): boolean { - return this.rules.has(key); - } - - /** - * Lists all the rules registered to this validator. - * @returns The rules registered to this validator. - */ - listRules(): [ValidationRuleKey, RuleConfiguration][] { - return [...this.rules.entries()]; - } - - /** - * Removes a rule from the validator. - * @param key The key of the rule to remove. - * @returns A value indicating whether the rule was removed from the validator. - */ - removeRule(key: ValidationRuleKey): boolean { - return this.rules.delete(key); - } - - /** - * Registers a rule to the validator. - * @param key The key of the rule to register. - * @param rule The rule to register. - * @param options The options of the rule. - */ - setRule(key: ValidationRuleKey, rule: ValidationRule, options?: RuleOptions): void { - options ??= {}; - const configuration: RuleConfiguration = { rule, options }; - this.rules.set(key, configuration); - } - - /** - * Validates a field/property against a set of rules. - * @param name The name of the field/property to validate. - * @param value The value of the field/property to validate. - * @param rules The rule set to validate the value against. - * @param options The options of the validation operation. - * @returns The result of the validation operation. - */ - validate(name: string, value: unknown, rules: ValidationRuleSet, options?: ValidationOptions): ValidationResult { - options ??= {}; - const context: ValidationContext = options.context ?? {}; - - let errors: number = 0; - const results: Record = {}; - - const missingRules: string[] = []; - for (const key in rules) { - const configuration: RuleConfiguration | undefined = this.rules.get(key); - if (!configuration) { - missingRules.push(key); - continue; - } - - const args: unknown = rules[key]; - if (!args) { - continue; - } - - const result: RuleExecutionResult = { - key, - severity: "error", - placeholders: { [key]: args }, - name, - value, - }; - - const outcome: boolean | ValidationSeverity | RuleExecutionOutcome = configuration.rule(value, args, context); - switch (typeof outcome) { - case "boolean": - result.severity = Boolean(outcome) ? "information" : "error"; - break; - case "string": - result.severity = outcome; - break; - default: - apply(result, outcome, configuration.options); - break; - } - - fillPlaceholders(result, typeof outcome === "object" ? outcome : undefined, configuration.options, options); - - this.formatMessage(result, options); - - if (this.isError(result.severity, options)) { - errors++; - } - - results[key] = result; - } - - if (missingRules.length > 0) { - throw new Error(`The following rules are not registered: ${missingRules.join(", ")}`); - } - - const result: ValidationResult = { - isValid: errors === 0, - rules: results, - context, - }; - if (!result.isValid && (options.throwOnFailure ?? this.throwOnFailure)) { - throw result; - } - return result; - } - - /** - * Formats a validation rule execution message. - * @param result The result to format the message of. - * @param options The options of the validation operation. - */ - private formatMessage(result: RuleExecutionResult, options?: ValidationOptions): void { - options ??= {}; - const messageFormatter: MessageFormatter = options.messageFormatter ?? this.messageFormatter; - if (typeof result.message === "string") { - result.message = messageFormatter.format(result.message, result.placeholders); - } - } - - /** - * Checks if a severity is an error. - * @param severity The severity to check. - * @param options The options of the validation operation. - * @returns A value indicating whether the severity is an error. - */ - private isError(severity: ValidationSeverity, options?: ValidationOptions): boolean { - options ??= {}; - switch (severity) { - case "error": - case "critical": - return true; - case "warning": - if (options.treatWarningsAsErrors ?? this.treatWarningsAsErrors) { - return true; - } - break; - } - return false; - } -} -export default Validator; diff --git a/src/validation/rules/__tests__/allowedCharacters.spec.ts b/src/validation/rules/__tests__/allowedCharacters.spec.ts deleted file mode 100644 index 52f25c4..0000000 --- a/src/validation/rules/__tests__/allowedCharacters.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import rule from "../allowedCharacters"; -import { RuleExecutionOutcome } from "../../types"; - -const allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - -describe("allowedCharacters", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = rule(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], true, 0, 0n])("should return warning when the args are not a string", (args) => { - const outcome = rule("valid", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments must be a string containing the allowed characters."); - }); - - it.concurrent("should return invalid when the value contains prohibited characters", () => { - const outcome = rule("invalid!", allowedCharacters) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} contains the following prohibited characters: !. Only the following characters are allowed: {{allowedCharacters}}"); - }); - - it.concurrent("should return valid when the value only contains allowed characters", () => { - const outcome = rule("valid", allowedCharacters) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/confirm.spec.ts b/src/validation/rules/__tests__/confirm.spec.ts deleted file mode 100644 index 19178b4..0000000 --- a/src/validation/rules/__tests__/confirm.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import confirm from "../confirm"; -import { RuleExecutionOutcome } from "../../types"; - -describe("confirm", () => { - test.each([ - [undefined, "undefined"], - [null, {}], - [{ age: 20 }, { name: "John" }], - [ - [1, 2], - [3, 4], - ], - [false, true], - [-1, 1], - [-1n, 1n], - ["hello", "world"], - ])("return invalid when the value does not equal the args", (value, args) => { - const outcome = confirm(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must equal {{confirm}}."); - }); - - test.each([ - [undefined, undefined], - [null, null], - [ - { age: 20, name: "John" }, - { age: 20, name: "John" }, - ], - [ - [1, 2, 3], - [1, 2, 3], - ], - [false, false], - [true, true], - [0, 0], - [0n, 0n], - ["hello world", "hello world"], - ])("return valid when the value equals the args", (value, args) => { - const outcome = confirm(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/containsDigits.spec.ts b/src/validation/rules/__tests__/containsDigits.spec.ts deleted file mode 100644 index c76e036..0000000 --- a/src/validation/rules/__tests__/containsDigits.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import containsDigits from "../containsDigits"; -import { RuleExecutionOutcome } from "../../types"; - -describe("containsDigits", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = containsDigits(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], ["1", "b"], false, -1.23, 0, -1n, 0n, "invalid", "-10", "0"])( - "should return warning when the args is not a positive number", - (args) => { - const outcome = containsDigits("AAaa!!11", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }, - ); - - it.concurrent("should return invalid when the value does not contain enough digits", () => { - const outcome = containsDigits("AAaa!!11", 3) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at least {{containsDigits}} digit(s)."); - }); - - test.each([ - ["AAaa!!11", true], - ["AAaa!!11", ["2"]], - ["AAaa!!11", 2n], - ["AAaa!!11", 2], - ["AAaa!!11", "2"], - ])("should return valid when the value contains enough digits", (value, args) => { - const outcome = containsDigits(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/containsLowercase.spec.ts b/src/validation/rules/__tests__/containsLowercase.spec.ts deleted file mode 100644 index b430b67..0000000 --- a/src/validation/rules/__tests__/containsLowercase.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import containsLowercase from "../containsLowercase"; -import { RuleExecutionOutcome } from "../../types"; - -describe("containsLowercase", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = containsLowercase(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], ["1", "b"], false, -1.23, 0, -1n, 0n, "invalid", "-10", "0"])( - "should return warning when the args is not a positive number", - (args) => { - const outcome = containsLowercase("AAaa!!11", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }, - ); - - it.concurrent("should return invalid when the value does not contain enough lowercase letters", () => { - const outcome = containsLowercase("AAaa!!11", 3) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at least {{containsLowercase}} lowercase letter(s)."); - }); - - test.each([ - ["AAaa!!11", true], - ["AAaa!!11", ["2"]], - ["AAaa!!11", 2n], - ["AAaa!!11", 2], - ["AAaa!!11", "2"], - ])("should return valid when the value contains enough lowercase letters", (value, args) => { - const outcome = containsLowercase(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/containsNonAlphanumeric.spec.ts b/src/validation/rules/__tests__/containsNonAlphanumeric.spec.ts deleted file mode 100644 index 8ee34ef..0000000 --- a/src/validation/rules/__tests__/containsNonAlphanumeric.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import containsNonAlphanumeric from "../containsNonAlphanumeric"; -import { RuleExecutionOutcome } from "../../types"; - -describe("containsNonAlphanumerics", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = containsNonAlphanumeric(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], ["1", "b"], false, -1.23, 0, -1n, 0n, "invalid", "-10", "0"])( - "should return warning when the args is not a positive number", - (args) => { - const outcome = containsNonAlphanumeric("AAaa!!11", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }, - ); - - it.concurrent("should return invalid when the value does not contain enough nonalphanumerics", () => { - const outcome = containsNonAlphanumeric("AAaa!!11", 3) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at least {{containsNonAlphanumeric}} non-alphanumeric character(s)."); - }); - - test.each([ - ["AAaa!!11", true], - ["AAaa!!11", ["2"]], - ["AAaa!!11", 2n], - ["AAaa!!11", 2], - ["AAaa!!11", "2"], - ])("should return valid when the value contains enough nonalphanumerics", (value, args) => { - const outcome = containsNonAlphanumeric(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/containsUppercase.spec.ts b/src/validation/rules/__tests__/containsUppercase.spec.ts deleted file mode 100644 index caf1434..0000000 --- a/src/validation/rules/__tests__/containsUppercase.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import containsUppercase from "../containsUppercase"; -import { RuleExecutionOutcome } from "../../types"; - -describe("containsUppercase", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = containsUppercase(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], ["1", "b"], false, -1.23, 0, -1n, 0n, "invalid", "-10", "0"])( - "should return warning when the args is not a positive number", - (args) => { - const outcome = containsUppercase("AAaa!!11", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }, - ); - - it.concurrent("should return invalid when the value does not contain enough uppercase letters", () => { - const outcome = containsUppercase("AAaa!!11", 3) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at least {{containsUppercase}} uppercase letter(s)."); - }); - - test.each([ - ["AAaa!!11", true], - ["AAaa!!11", ["2"]], - ["AAaa!!11", 2n], - ["AAaa!!11", 2], - ["AAaa!!11", "2"], - ])("should return valid when the value contains enough uppercase letters", (value, args) => { - const outcome = containsUppercase(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/email.spec.ts b/src/validation/rules/__tests__/email.spec.ts deleted file mode 100644 index d0f5663..0000000 --- a/src/validation/rules/__tests__/email.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import email from "../email"; -import { RuleExecutionOutcome } from "../../types"; - -describe("email", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = email(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([null, {}, [], 0, 0n])("should return warning when the args are not valid", (args) => { - const outcome = email("test@example.com", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments must be undefined, or a valid email address validation regular expression."); - }); - - it.concurrent("should return invalid when the value is not a valid email address", () => { - const outcome = email("aa@@bb..cc") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a valid email address."); - }); - - it.concurrent("should return valid when the value is a valid email address", () => { - const outcome = email("test@example.com") as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return valid when the value matches the arguments pattern", () => { - const outcome = email("test@example.com", /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/identifier.spec.ts b/src/validation/rules/__tests__/identifier.spec.ts deleted file mode 100644 index fb6a763..0000000 --- a/src/validation/rules/__tests__/identifier.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import identifier from "../identifier"; -import { RuleExecutionOutcome } from "../../types"; - -describe("identifier", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = identifier(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - it.concurrent("should return invalid when the value is an empty string", () => { - const outcome = identifier("") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} cannot be an empty string."); - }); - - it.concurrent("should return invalid when the value starts with a digit", () => { - const outcome = identifier("123") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} cannot start with a digit."); - }); - - it.concurrent("should return invalid when the value contains non-alphanumeric characters", () => { - const outcome = identifier("invalid_123!") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} may only contain letters, digits and underscores (_)."); - }); - - test.each(["_valid", "valid_123", "valid"])("should return valid when the value is a valid identifier", (value) => { - const outcome = identifier(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/maximumLength.spec.ts b/src/validation/rules/__tests__/maximumLength.spec.ts deleted file mode 100644 index c4e22bd..0000000 --- a/src/validation/rules/__tests__/maximumLength.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import maximumLength from "../maximumLength"; -import { RuleExecutionOutcome } from "../../types"; - -describe("maximumLength", () => { - test.each([undefined, null, {}, [], false, 0, 0n])("should return warning when the args is not a positive number", (args) => { - const outcome = maximumLength("test", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }); - - it.concurrent("should return invalid when the value is a string that is too long", () => { - const outcome = maximumLength("test", true) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be at most {{maximumLength}} character(s) long."); - }); - - it.concurrent("should return invalid when the value is an array that is too long", () => { - const outcome = maximumLength([1, 2, 3], 2) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at most {{maximumLength}} element(s)."); - }); - - test.each([undefined, null, {}, false, 0, 0n])("should return invalid when the value is not a string or an array", (value) => { - const outcome = maximumLength(value, 1) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string or an array."); - }); - - it.concurrent("should return valid when the value is a string that is not too long", () => { - const outcome = maximumLength("test", 5) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return valid when the value is an array that is not too long", () => { - const outcome = maximumLength([1, 2, 3], 3) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/maximumValue.spec.ts b/src/validation/rules/__tests__/maximumValue.spec.ts deleted file mode 100644 index 1fcccde..0000000 --- a/src/validation/rules/__tests__/maximumValue.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import maximumValue from "../maximumValue"; -import { RuleExecutionOutcome } from "../../types"; - -describe("maximumValue", () => { - test.each([ - [true, false], - [1, -1], - [10n, -10n], - ["def", "abc"], - ["456", 123], - [new Date("2010-01-01"), new Date("2000-01-01")], - [new Date(), null], - [Infinity, -Infinity], - ])("should return invalid when the value is greater than args", (value, args) => { - const outcome = maximumValue(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be at most {{maximumValue}}."); - }); - - test.each([ - [false, true], - [false, false], - [-1, 1], - [0, 0], - [-10n, 10n], - [100n, 100n], - ["456", 789], - ["abc", "def"], - ["ghi", "ghi"], - [new Date("2010-01-01"), new Date("2020-01-01")], - [new Date("2000-01-01"), new Date("2000-01-01")], - [null, new Date()], - [-Infinity, NaN], - [-Infinity, undefined], - ])("should return valid when the value is lower than or equal to args", (value, args) => { - const outcome = maximumValue(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return warning when the values could not be compared", () => { - const a = { - valueOf: function () { - throw new Error("Error during valueOf"); - }, - }; - const b = 5; - const outcome = maximumValue(a, b) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("Could not compare {{name}} ({{value}} | object) with args ({{maximumValue}} | number)."); - }); -}); diff --git a/src/validation/rules/__tests__/minimumLength.spec.ts b/src/validation/rules/__tests__/minimumLength.spec.ts deleted file mode 100644 index ad540db..0000000 --- a/src/validation/rules/__tests__/minimumLength.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import minimumLength from "../minimumLength"; -import { RuleExecutionOutcome } from "../../types"; - -describe("minimumLength", () => { - test.each([undefined, null, {}, [], false, 0, 0n])("should return warning when the args is not a positive number", (args) => { - const outcome = minimumLength("test", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }); - - it.concurrent("should return invalid when the value is a string that is too short", () => { - const outcome = minimumLength("", true) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be at least {{minimumLength}} character(s) long."); - }); - - it.concurrent("should return invalid when the value is an array that is too short", () => { - const outcome = minimumLength([1], 2) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at least {{minimumLength}} element(s)."); - }); - - test.each([undefined, null, {}, false, 0, 0n])("should return invalid when the value is not a string or an array", (value) => { - const outcome = minimumLength(value, 1) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string or an array."); - }); - - it.concurrent("should return valid when the value is a string that is not too short", () => { - const outcome = minimumLength("AAaa!!11", 5) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return valid when the value is an array that is not too short", () => { - const outcome = minimumLength([1, 2, 3], 2) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/minimumValue.spec.ts b/src/validation/rules/__tests__/minimumValue.spec.ts deleted file mode 100644 index 5e739f9..0000000 --- a/src/validation/rules/__tests__/minimumValue.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import minimumValue from "../minimumValue"; -import { RuleExecutionOutcome } from "../../types"; - -describe("minimumValue", () => { - test.each([ - [false, true], - [-1, 1], - [-10n, 10n], - ["abc", "def"], - [123, "456"], - [new Date("2000-01-01"), new Date("2010-01-01")], - [null, new Date()], - [-Infinity, Infinity], - ])("should return invalid when the value is lower than args", (value, args) => { - const outcome = minimumValue(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be at least {{minimumValue}}."); - }); - - test.each([ - [true, false], - [true, true], - [1, -1], - [0, 0], - [10n, -10n], - [100n, 100n], - [789, "456"], - ["def", "abc"], - ["ghi", "ghi"], - [new Date("2020-01-01"), new Date("2010-01-01")], - [new Date("2000-01-01"), new Date("2000-01-01")], - [new Date(), null], - [NaN, -Infinity], - [undefined, -Infinity], - ])("should return valid when the value is greater than or equal to args", (value, args) => { - const outcome = minimumValue(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return warning when the values could not be compared", () => { - const a = { - valueOf: function () { - throw new Error("Error during valueOf"); - }, - }; - const b = 5; - const outcome = minimumValue(a, b) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("Could not compare {{name}} ({{value}} | object) with args ({{minimumValue}} | number)."); - }); -}); diff --git a/src/validation/rules/__tests__/pattern.spec.ts b/src/validation/rules/__tests__/pattern.spec.ts deleted file mode 100644 index 50a6b4b..0000000 --- a/src/validation/rules/__tests__/pattern.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import rule from "../pattern"; -import { RuleExecutionOutcome } from "../../types"; - -const pattern = /^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z][ -]?\d[ABCEGHJ-NPRSTV-Z]\d$/; - -describe("pattern", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = rule(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], true, 0, 0n])("should return warning when the args is not a regular expression", () => { - const outcome = rule("") as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a regular expression."); - }); - - test.each(["h2x3y2", "H2X -3Y2", "H2U 3Y2"])("should return invalid when the value does not match the pattern", (value) => { - const outcome = rule(value, pattern) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must match the pattern {{pattern}}."); - }); - - test.each(["H2X3Y2", "H2X 3Y2", "H2X-3Y2"])("should return valid when the value matches the pattern", (value) => { - const outcome = rule(value, pattern) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/required.spec.ts b/src/validation/rules/__tests__/required.spec.ts deleted file mode 100644 index 40673e2..0000000 --- a/src/validation/rules/__tests__/required.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import required from "../required"; -import { RuleExecutionOutcome } from "../../types"; - -describe("required", () => { - test.each([NaN, 0])("should return invalid when the number is NaN or 0", (value) => { - const outcome = required(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a number different from 0."); - }); - - test.each(["", " "])("should return invalid when the string is empty or white-space", (value) => { - const outcome = required(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} cannot be an empty string."); - }); - - it.concurrent("should return invalid when the array is empty", () => { - const outcome = required([]) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} cannot be an empty array."); - }); - - test.each([undefined, null, false, 0n])("should return invalid when the value is falsy", (value) => { - const outcome = required(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} is required."); - }); - - test.each([1, 123, 123.456])("should return valid when the number is not NaN or 0", (value) => { - const outcome = required(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return valid when the string is not empty", () => { - const outcome = required("hello world") as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - it.concurrent("should return valid when the array is not empty", () => { - const outcome = required([1, 2, 3]) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - test.each([{}, true, 1n])("should return valid when the value is not falsy", (value) => { - const outcome = required(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/slug.spec.ts b/src/validation/rules/__tests__/slug.spec.ts deleted file mode 100644 index 4e31407..0000000 --- a/src/validation/rules/__tests__/slug.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import slug from "../slug"; -import { RuleExecutionOutcome } from "../../types"; - -describe("slug", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = slug(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - it.concurrent("should return invalid when the value contains an empty word", () => { - const outcome = slug("aa--bb") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be composed of non-empty alphanumeric words separated by hyphens (-)."); - }); - - it.concurrent("should return invalid when the value contains non-alphanumeric characters", () => { - const outcome = slug("invalid!") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be composed of non-empty alphanumeric words separated by hyphens (-)."); - }); - - test.each(["valid", "valid-123"])("should return valid when the value is a valid slug", (value) => { - const outcome = slug(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/uniqueCharacters.spec.ts b/src/validation/rules/__tests__/uniqueCharacters.spec.ts deleted file mode 100644 index 927c2c1..0000000 --- a/src/validation/rules/__tests__/uniqueCharacters.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import uniqueCharacters from "../uniqueCharacters"; -import { RuleExecutionOutcome } from "../../types"; - -describe("uniqueCharacters", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = uniqueCharacters(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each([undefined, null, {}, [], false, 0, 0n])("should return warning when the args is not a positive number", (args) => { - const outcome = uniqueCharacters("test", args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe("The arguments should be a positive number."); - }); - - it.concurrent("should return invalid when the value does not have enough unique characters", () => { - const outcome = uniqueCharacters("AAaa!!11", 5) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must contain at least {{uniqueCharacters}} unique character(s)."); - }); - - it.concurrent("should return valid when the value has enough unique characters", () => { - const outcome = uniqueCharacters("AAaa!!11", 4) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); -}); diff --git a/src/validation/rules/__tests__/url.spec.ts b/src/validation/rules/__tests__/url.spec.ts deleted file mode 100644 index 7966918..0000000 --- a/src/validation/rules/__tests__/url.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it, expect, test } from "vitest"; - -import url from "../url"; -import { RuleExecutionOutcome } from "../../types"; - -describe("url", () => { - test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => { - const outcome = url(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a string."); - }); - - test.each(["", " "])("should return invalid when the value is empty or white-space", (value) => { - const outcome = url(value) as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} cannot be an empty string."); - }); - - it.concurrent("should return invalid when the value is not a valid URL", () => { - const outcome = url("invalid-url") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be a valid URL."); - }); - - it.concurrent("should return invalid when the value is not an URL with a valid protocol", () => { - const outcome = url("ftp://example.com") as RuleExecutionOutcome; - expect(outcome.severity).toBe("error"); - expect(outcome.message).toBe("{{name}} must be an URL with one of the following protocols: http, https."); - }); - - it.concurrent("should return warning when the args are not valid", () => { - const outcome = url("http://example.com", 123) as RuleExecutionOutcome; - expect(outcome.severity).toBe("warning"); - expect(outcome.message).toBe( - "The arguments must be undefined, a string containing the allowed protocols separated by commas, semicolons or pipes, or an array of allowed protocols.", - ); - }); - - test.each([ - ["http://example.com", undefined], - ["http://example.com", "http,https"], - ["http://example.com", "http;https"], - ["https://example.com", "http|https"], - ["ftp://example.com", [" FTP: ", " HTTP: "]], - ])("should return valid when the value is a valid URL with an allowed protocol", (value, args) => { - const outcome = url(value, args) as RuleExecutionOutcome; - expect(outcome.severity).toBe("information"); - expect(outcome.message).toBeUndefined(); - }); - - test.each([])("should return valid when the value is a valid URL with an allowed protocol", (value) => {}); -}); diff --git a/src/validation/rules/allowedCharacters.ts b/src/validation/rules/allowedCharacters.ts deleted file mode 100644 index abd610c..0000000 --- a/src/validation/rules/allowedCharacters.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a string only contains allowed characters. - * @param value The value to validate. - * @param args The allowed characters. - * @returns The result of the validation rule execution. - */ -const allowedCharacters: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } else if (typeof args !== "string") { - return { severity: "warning", message: "The arguments must be a string containing the allowed characters." }; - } - - const prohibitedCharacters = new Set([...value].filter((c) => !args.includes(c))); - if (prohibitedCharacters.size > 0) { - return { - severity: "error", - message: `{{name}} contains the following prohibited characters: ${[...prohibitedCharacters].join("")}. Only the following characters are allowed: {{allowedCharacters}}`, - }; - } - - return { severity: "information" }; -}; - -export default allowedCharacters; diff --git a/src/validation/rules/confirm.ts b/src/validation/rules/confirm.ts deleted file mode 100644 index 1b08c3e..0000000 --- a/src/validation/rules/confirm.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a value is equal to another value. - * @param value The value to validate. - * @param args The value to compare the value to. - * @returns The result of the validation rule execution. - */ -const confirm: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - const isValid: boolean = typeof value === "object" || typeof args === "object" ? JSON.stringify(value) === JSON.stringify(args) : value === args; - if (!isValid) { - return { severity: "error", message: "{{name}} must equal {{confirm}}." }; - } - return { severity: "information" }; -}; - -export default confirm; diff --git a/src/validation/rules/containsDigits.ts b/src/validation/rules/containsDigits.ts deleted file mode 100644 index ba029b1..0000000 --- a/src/validation/rules/containsDigits.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isDigit } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a string contains a minimum number of digits. - * @param value The value to validate. - * @param args The minimum number of digits. - * @returns The result of the validation rule execution. - */ -const containsDigits: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } - - const requiredDigits: number = Number(args); - if (isNaN(requiredDigits) || requiredDigits <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - const digits: number = [...value].filter(isDigit).length; - if (digits < requiredDigits) { - return { severity: "error", message: "{{name}} must contain at least {{containsDigits}} digit(s)." }; - } - - return { severity: "information" }; -}; - -export default containsDigits; diff --git a/src/validation/rules/containsLowercase.ts b/src/validation/rules/containsLowercase.ts deleted file mode 100644 index 5bc9c1f..0000000 --- a/src/validation/rules/containsLowercase.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isLetter } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a string contains a minimum number of lowercase letters. - * @param value The value to validate. - * @param args The minimum number of lowercase letters. - * @returns The result of the validation rule execution. - */ -const containsLowercase: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } - - const requiredLowercase: number = Number(args); - if (isNaN(requiredLowercase) || requiredLowercase <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - const lowercase: number = [...value].filter((c) => isLetter(c) && c.toLowerCase() === c).length; - if (lowercase < requiredLowercase) { - return { severity: "error", message: "{{name}} must contain at least {{containsLowercase}} lowercase letter(s)." }; - } - - return { severity: "information" }; -}; - -export default containsLowercase; diff --git a/src/validation/rules/containsNonAlphanumeric.ts b/src/validation/rules/containsNonAlphanumeric.ts deleted file mode 100644 index 43eee83..0000000 --- a/src/validation/rules/containsNonAlphanumeric.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isLetterOrDigit } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a string contains a minimum number of non-alphanumeric characters. - * @param value The value to validate. - * @param args The minimum number of non-alphanumeric characters. - * @returns The result of the validation rule execution. - */ -const containsNonAlphanumeric: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } - - const requiredNonAlphanumeric: number = Number(args); - if (isNaN(requiredNonAlphanumeric) || requiredNonAlphanumeric <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - const nonAlphanumeric: number = [...value].filter((c) => !isLetterOrDigit(c)).length; - if (nonAlphanumeric < requiredNonAlphanumeric) { - return { severity: "error", message: "{{name}} must contain at least {{containsNonAlphanumeric}} non-alphanumeric character(s)." }; - } - - return { severity: "information" }; -}; - -export default containsNonAlphanumeric; diff --git a/src/validation/rules/containsUppercase.ts b/src/validation/rules/containsUppercase.ts deleted file mode 100644 index 7144896..0000000 --- a/src/validation/rules/containsUppercase.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isLetter } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a string contains a minimum number of uppercase letters. - * @param value The value to validate. - * @param args The minimum number of uppercase letters. - * @returns The result of the validation rule execution. - */ -const containsUppercase: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } - - const requiredUppercase: number = Number(args); - if (isNaN(requiredUppercase) || requiredUppercase <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - const uppercase: number = [...value].filter((c) => isLetter(c) && c.toUpperCase() === c).length; - if (uppercase < requiredUppercase) { - return { severity: "error", message: "{{name}} must contain at least {{containsUppercase}} uppercase letter(s)." }; - } - - return { severity: "information" }; -}; - -export default containsUppercase; diff --git a/src/validation/rules/email.ts b/src/validation/rules/email.ts deleted file mode 100644 index bede65a..0000000 --- a/src/validation/rules/email.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -// https://github.com/colinhacks/zod/blob/40e72f9eaf576985f876d1afc2dbc22f73abc1ba/src/types.ts#L595 -const defaultRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; - -/** - * A validation rule that checks if a string is a valid email address. - * @param value The value to validate. - * @param args The regular expression to validate the email address against. - * @returns The result of the validation rule execution. - */ -const email: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } - - let isArgsValid: boolean = true; - let regex: RegExp; - if (typeof args === "string" || args instanceof RegExp) { - regex = new RegExp(args); - } else { - regex = new RegExp(defaultRegex); - if (typeof args !== "undefined" && typeof args !== "boolean") { - isArgsValid = false; - } - } - - if (!regex.test(value)) { - return { severity: "error", message: "{{name}} must be a valid email address." }; - } else if (!isArgsValid) { - return { severity: "warning", message: "The arguments must be undefined, or a valid email address validation regular expression." }; - } - - return { severity: "information" }; -}; - -export default email; diff --git a/src/validation/rules/identifier.ts b/src/validation/rules/identifier.ts deleted file mode 100644 index a740702..0000000 --- a/src/validation/rules/identifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isDigit, isLetterOrDigit, isNullOrEmpty } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a string is a valid identifier. - * @param value The value to validate. - * @returns The result of the validation rule execution. - */ -const identifier: ValidationRule = (value: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } else if (isNullOrEmpty(value)) { - return { severity: "error", message: "{{name}} cannot be an empty string." }; - } else if (isDigit(value[0])) { - return { severity: "error", message: "{{name}} cannot start with a digit." }; - } else if ([...value].some((c) => !isLetterOrDigit(c) && c !== "_")) { - return { severity: "error", message: "{{name}} may only contain letters, digits and underscores (_)." }; - } - return { severity: "information" }; -}; - -export default identifier; diff --git a/src/validation/rules/maximumLength.ts b/src/validation/rules/maximumLength.ts deleted file mode 100644 index 9db6cbe..0000000 --- a/src/validation/rules/maximumLength.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a string or an array is shorter than a maximum length. - * @param value The value to validate. - * @param args The maximum length. - * @returns The result of the validation rule execution. - */ -const maximumLength: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - const maximumLength: number = Number(args); - if (isNaN(maximumLength) || maximumLength <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - if (typeof value === "string") { - if (value.length > maximumLength) { - return { severity: "error", message: "{{name}} must be at most {{maximumLength}} character(s) long." }; - } - } else if (Array.isArray(value)) { - if (value.length > maximumLength) { - return { severity: "error", message: "{{name}} must contain at most {{maximumLength}} element(s)." }; - } - } else { - return { severity: "error", message: "{{name}} must be a string or an array." }; - } - - return { severity: "information" }; -}; - -export default maximumLength; diff --git a/src/validation/rules/maximumValue.ts b/src/validation/rules/maximumValue.ts deleted file mode 100644 index 042f2d0..0000000 --- a/src/validation/rules/maximumValue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a value is less than or equal to a maximum value. - * @param value The value to validate. - * @param args The maximum value. - * @returns The result of the validation rule execution. - */ -const maximumValue: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - try { - if (value > args) { - return { severity: "error", message: "{{name}} must be at most {{maximumValue}}." }; - } - } catch (_) { - return { severity: "warning", message: `Could not compare {{name}} ({{value}} | ${typeof value}) with args ({{maximumValue}} | ${typeof args}).` }; - } - return { severity: "information" }; -}; - -export default maximumValue; diff --git a/src/validation/rules/minimumLength.ts b/src/validation/rules/minimumLength.ts deleted file mode 100644 index a356b88..0000000 --- a/src/validation/rules/minimumLength.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a string or an array is longer than a minimum length. - * @param value The value to validate. - * @param args The minimum length. - * @returns The result of the validation rule execution. - */ -const minimumLength: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - const minimumLength: number = Number(args); - if (isNaN(minimumLength) || minimumLength <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - if (typeof value === "string") { - if (value.length < minimumLength) { - return { severity: "error", message: "{{name}} must be at least {{minimumLength}} character(s) long." }; - } - } else if (Array.isArray(value)) { - if (value.length < minimumLength) { - return { severity: "error", message: "{{name}} must contain at least {{minimumLength}} element(s)." }; - } - } else { - return { severity: "error", message: "{{name}} must be a string or an array." }; - } - - return { severity: "information" }; -}; - -export default minimumLength; diff --git a/src/validation/rules/minimumValue.ts b/src/validation/rules/minimumValue.ts deleted file mode 100644 index 06492df..0000000 --- a/src/validation/rules/minimumValue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a value is greater than or equal to a minimum value. - * @param value The value to validate. - * @param args The minimum value. - * @returns The result of the validation rule execution. - */ -const minimumValue: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - try { - if (value < args) { - return { severity: "error", message: "{{name}} must be at least {{minimumValue}}." }; - } - } catch (_) { - return { severity: "warning", message: `Could not compare {{name}} ({{value}} | ${typeof value}) with args ({{minimumValue}} | ${typeof args}).` }; - } - return { severity: "information" }; -}; - -export default minimumValue; diff --git a/src/validation/rules/pattern.ts b/src/validation/rules/pattern.ts deleted file mode 100644 index 6390b5f..0000000 --- a/src/validation/rules/pattern.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a string matches a regular expression. - * @param value The value to validate. - * @param args The regular expression to validate the string against. - * @returns The result of the validation rule execution. - */ -const pattern: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } else if (typeof args !== "string" && !(args instanceof RegExp)) { - return { severity: "warning", message: "The arguments should be a regular expression." }; - } else if (!new RegExp(args).test(value)) { - return { severity: "error", message: "{{name}} must match the pattern {{pattern}}." }; - } - return { severity: "information" }; -}; - -export default pattern; diff --git a/src/validation/rules/required.ts b/src/validation/rules/required.ts deleted file mode 100644 index 0637c7c..0000000 --- a/src/validation/rules/required.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isNullOrWhiteSpace } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a required value is provided. - * @param value The value to validate. - * @returns The result of the validation rule execution. - */ -const required: ValidationRule = (value: unknown): RuleExecutionOutcome => { - switch (typeof value) { - case "number": - if (isNaN(value) || value === 0) { - return { severity: "error", message: "{{name}} must be a number different from 0." }; - } - break; - case "string": - if (isNullOrWhiteSpace(value)) { - return { severity: "error", message: "{{name}} cannot be an empty string." }; - } - break; - default: - if (Array.isArray(value)) { - if (value.length === 0) { - return { severity: "error", message: "{{name}} cannot be an empty array." }; - } - } else if (!Boolean(value)) { - return { severity: "error", message: "{{name}} is required." }; - } - break; - } - return { severity: "information" }; -}; - -export default required; diff --git a/src/validation/rules/slug.ts b/src/validation/rules/slug.ts deleted file mode 100644 index ce676ea..0000000 --- a/src/validation/rules/slug.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isLetterOrDigit, isNullOrEmpty } from "../../helpers/stringUtils"; - -/** - * A validation rule that checks if a string is a valid slug. - * @param value The value to validate. - * @returns The result of the validation rule execution. - */ -const slug: ValidationRule = (value: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } else if (value.split("-").some((word) => isNullOrEmpty(word) || [...word].some((c) => !isLetterOrDigit(c)))) { - return { severity: "error", message: "{{name}} must be composed of non-empty alphanumeric words separated by hyphens (-)." }; - } - return { severity: "information" }; -}; - -export default slug; diff --git a/src/validation/rules/uniqueCharacters.ts b/src/validation/rules/uniqueCharacters.ts deleted file mode 100644 index bc1cff9..0000000 --- a/src/validation/rules/uniqueCharacters.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; - -/** - * A validation rule that checks if a string contains a minimum number of unique characters. - * @param value The value to validate. - * @param args The minimum number of unique characters. - * @returns The result of the validation rule execution. - */ -const uniqueCharacters: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } - - const uniqueCharacters: number = Number(args); - if (isNaN(uniqueCharacters) || uniqueCharacters <= 0) { - return { severity: "warning", message: "The arguments should be a positive number." }; - } - - const count: number = [...new Set(value)].length; - if (count < uniqueCharacters) { - return { severity: "error", message: "{{name}} must contain at least {{uniqueCharacters}} unique character(s)." }; - } - - return { severity: "information" }; -}; - -export default uniqueCharacters; diff --git a/src/validation/rules/url.ts b/src/validation/rules/url.ts deleted file mode 100644 index f75c055..0000000 --- a/src/validation/rules/url.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { RuleExecutionOutcome, ValidationRule } from "../types"; -import { isNullOrWhiteSpace, trimEnd } from "../../helpers/stringUtils"; - -/** - * Format a protocol string to be used in a set. - * @param protocol The protocol to format. - * @returns The formatted protocol. - */ -function format(protocol: string): string { - return trimEnd(protocol.trim().toLowerCase(), ":"); -} - -/** - * A validation rule that checks if a string is a valid URL. - * @param value The value to validate. - * @param args The allowed protocols. - * @returns The result of the validation rule execution. - */ -const url: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => { - if (typeof value !== "string") { - return { severity: "error", message: "{{name}} must be a string." }; - } else if (isNullOrWhiteSpace(value)) { - return { severity: "error", message: "{{name}} cannot be an empty string." }; - } - - let isArgsValid: boolean = true; - const protocols: Set = new Set(["http", "https"]); - if (typeof args !== "undefined") { - let values: string[] = []; - if (typeof args === "string") { - values = args.split(/[,;\|]/); - } else if (Array.isArray(args)) { - values = args; - } - if (values.length === 0) { - isArgsValid = false; - } else { - values.forEach((value) => protocols.add(format(value))); - } - } - - let url: URL; - try { - url = new URL(value.trim()); - } catch (_) { - return { severity: "error", message: "{{name}} must be a valid URL." }; - } - - if (!protocols.has(format(url.protocol))) { - return { severity: "error", message: `{{name}} must be an URL with one of the following protocols: ${[...protocols].join(", ")}.` }; - } - - if (!isArgsValid) { - return { - severity: "warning", - message: - "The arguments must be undefined, a string containing the allowed protocols separated by commas, semicolons or pipes, or an array of allowed protocols.", - }; - } - - return { severity: "information" }; -}; - -export default url; diff --git a/src/validation/types.ts b/src/validation/types.ts deleted file mode 100644 index 37e365b..0000000 --- a/src/validation/types.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { MessageFormatter } from "./format"; - -/** - * Defines the outcome of the execution of a validation rule. - */ -export type RuleExecutionOutcome = { - /** - * The severity of the outcome. - */ - severity: ValidationSeverity; - /** - * The key of the rule that was executed. - */ - key?: string; - /** - * The message of the outcome. - */ - message?: string; - /** - * The message placeholders of the outcome. - */ - placeholders?: Record; - /** - * The name of the field/property that was validated. - */ - name?: string; - /** - * The value of the field/property that was validated. - */ - value?: unknown; - /** - * Custom state that was provided by the validation rule. - */ - custom?: unknown; -}; - -/** - * Defines the result of the execution of a validation rule. - */ -export type RuleExecutionResult = { - /** - * The key of the rule that was executed. - */ - key: string; - /** - * The severity of the result. - */ - severity: ValidationSeverity; - /** - * The message of the result. - */ - message?: string; - /** - * The message placeholders of the result. - */ - placeholders: Record; - /** - * The name of the field/property that was validated. - */ - name: string; - /** - * The value of the field/property that was validated. - */ - value: unknown; - /** - * Custom state that was provided by the validation rule. - */ - custom?: unknown; -}; - -/** - * Defines the configuration of a validation rule. - */ -export type RuleConfiguration = { - /** - * The validation rule to execute. - */ - rule: ValidationRule; - /** - * The options of the rule execution. - */ - options: RuleOptions; -}; - -/** - * Defines the options of a validation rule execution. - */ -export type RuleOptions = { - /** - * Overrides the key of the rule. - */ - key?: string; - /** - * Overrides the message of the rule. - */ - message?: string; - /** - * Provides additional placeholders for the rule message. - */ - placeholders?: Record; -}; - -/** - * Defines a validation context. The context is shared between validation rules and returned in the result. - */ -export type ValidationContext = Record; - -/** - * Defines the options of a validation operation. - */ -export type ValidationOptions = { - /** - * The context of the validation opteration. - */ - context?: ValidationContext; - /** - * The message formatter to use. - */ - messageFormatter?: MessageFormatter; - /** - * Provides additional placeholders for the validation messages. - */ - placeholders?: Record; - /** - * A value indicating whether the validation should throw an error if the validation fails. - */ - throwOnFailure?: boolean; - /** - * A value indicating whether warnings should be treated as errors. - */ - treatWarningsAsErrors?: boolean; -}; - -/** - * Defines the result of a validation operation. - */ -export type ValidationResult = { - /** - * A value indicating whether the validation was successful. - */ - isValid: boolean; - /** - * The results of the executed validation rules. - */ - rules: Record; - /** - * The context of the validation operation. - */ - context: ValidationContext; -}; - -/** - * Defines a validation rule. - * @param value The value to validate. - * @param args Additional arguments to pass to the validation rule. - * @param context The context of the validation operation. - * @returns A value indicating whether the validation was successful. - */ -export type ValidationRule = (value: unknown, args?: unknown, context?: ValidationContext) => boolean | ValidationSeverity | RuleExecutionOutcome; - -/** - * Defines the key of a validation rule. - */ -export type ValidationRuleKey = string; - -/** - * Defines a set of validation rules. - */ -export type ValidationRuleSet = Record; - -/** - * Defines the severity of a validation execution outcome. - * Reference: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel - */ -export type ValidationSeverity = "trace" | "debug" | "information" | "warning" | "error" | "critical"; - -/** - * Defines the options of a validator. - */ -export type ValidatorOptions = { - /** - * The message formatter to use. - */ - messageFormatter?: MessageFormatter; - /** - * A value indicating whether the validator should throw an error if the validation fails. - */ - throwOnFailure?: boolean; - /** - * A value indicating whether warnings should be treated as errors. - */ - treatWarningsAsErrors?: boolean; -}; From 072817e539b3b98b145d5a9a11829a0c16dff879 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 21 Apr 2025 13:09:16 -0400 Subject: [PATCH 2/3] Release 2.0.0 --- CHANGELOG.md | 10 +++++----- LICENSE | 2 +- package-lock.json | 8 ++++---- package.json | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e8e33..fe69fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Nothing yet. -## [1.1.0] - 2025-04-21 +## [2.0.0] - 2025-04-21 -### Added +### Changed -- Implemented validation. +- Package ID from `logitar-js` to `@logitar/js`. ## [1.0.1] - 2025-04-20 @@ -69,8 +69,8 @@ Official release. - Helper functions for arrays, Dates, objects, parsing, strings and URLs. -[unreleased]: https://github.com/Logitar/js/compare/v1.1.0...HEAD -[1.1.0]: https://github.com/Logitar/js/compare/v1.0.1...v1.1.0 +[unreleased]: https://github.com/Logitar/js/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/Logitar/js/compare/v1.0.1...v2.0.0 [1.0.1]: https://github.com/Logitar/js/compare/v1.0.1...v1.0.1 [1.0.0]: https://github.com/Logitar/js/compare/v0.5.0...v1.0.0 [0.5.0]: https://github.com/Logitar/js/compare/v0.4.0...v0.5.0 diff --git a/LICENSE b/LICENSE index 0487f9b..70fdfae 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Logitar +Copyright (c) 2025 Logitar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package-lock.json b/package-lock.json index 0384067..4545238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "logitar-js", - "version": "1.1.0", + "name": "@logitar/js", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "logitar-js", - "version": "1.1.0", + "name": "@logitar/js", + "version": "2.0.0", "license": "MIT", "devDependencies": { "@vitest/coverage-v8": "^1.5.0", diff --git a/package.json b/package.json index 682eb61..50bcfe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "logitar-js", - "version": "1.1.0", + "name": "@logitar/js", + "version": "2.0.0", "description": "Helper functions distributed by Logitar.", "keywords": [ "logitar", From 608fda2ab285b0e7ec36216e6c2097607dbc4efa Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 21 Apr 2025 13:17:53 -0400 Subject: [PATCH 3/3] publish --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 198cf1c..d8651ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,6 +26,6 @@ jobs: run: npm run build - name: Publish to NPM registry - run: npm publish + run: npm publish --access=public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}