Skip to content
110 changes: 101 additions & 9 deletions src/core/domains/validator/abstract/AbstractRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,162 @@ import forceString from "@src/core/util/str/forceString";
import { logger } from "../../logger/services/LoggerService";
import { IRuleError } from "../interfaces/IRule";

/**
* Abstract base class for validation rules.
* Provides common functionality for implementing validation rules with customizable error messages and options.
*
* @template TOptions - Type of options object that can be passed to configure the rule
*/
abstract class AbstractRule<TOptions extends object = object> {

/** Name of the validation rule */
protected abstract name: string;

/** Template string for error messages. Use :attribute for the field name and :key for option values */
protected abstract errorTemplate: string;

/** Default error message if error template processing fails */
protected defaultError: string = 'This field is invalid.'


/** Configuration options for the rule */
protected options: TOptions = {} as TOptions

/** The value to validate */
protected data: unknown = undefined

/** All attributes/fields being validated */
protected attributes: unknown = undefined

protected field!: string;

/** Dot notation path to the field being validated (e.g. "users.*.name") */
protected path!: string;

/**
* Tests if the current data value passes the validation rule
* @returns True if validation passes, false if it fails
*/
public abstract test(): Promise<boolean>;

/**
* Gets the validation error details if validation fails
* @returns Object containing error information
*/
public abstract getError(): IRuleError;

/**
* Validates the data against the rule
* If the last part of the path contains a wildcard (*), validates each item in the array
* Otherwise validates the single value
*
* For example:
* - For path "users.*.name", validates name field for each user
* - For path "email", validates single email value
*

* @returns True if validation passes, false if it fails
*/
public async validate(): Promise<boolean> {
return await this.test()
}

/**
* Sets the configuration options for this validation rule
* @param options - Rule-specific options object
* @returns this - For method chaining
*/
public setOptions(options: TOptions): this {
this.options = options
return this
}


/**
* Sets the value to be validated
* @param data - The value to validate
* @returns this - For method chaining
*/
public setData(data: unknown): this {
this.data = data
return this
}

/**
* Gets the current value being validated
* @returns The value being validated
*/
public getData(): unknown {
return this.data
}

public setField(field: string): this {
this.field = field
return this
}

/**
* Sets all attributes/fields being validated
* @param attributes - Object containing all fields being validated
* @returns this - For method chaining
*/
public setAttributes(attributes: unknown): this {
this.attributes = attributes
return this
}

/**
* Gets all attributes/fields being validated
* @returns Object containing all fields being validated
*/
public getAttributes(): unknown {
return this.attributes
}

/**
* Gets a specific option value by key
* @param key - The option key to retrieve
* @returns The option value
*/
public getOption(key: string): unknown {
return this.options[key]
}

/**
* Gets the name of this validation rule
* @returns The rule name
*/
public getName(): string {
return this.name
}

/**
* Gets the error message template
* @returns The error template string
*/
protected getErrorTemplate(): string {
return this.errorTemplate
}

/**
* Sets the dot notation path to the field being validated
* @param path - The field path (e.g. "users.*.name")
* @returns this - For method chaining
*/
public setPath(path: string): this {
this.path = path
return this
}

/**
* Gets the dot notation path to the field being validated
* @returns The field path
*/
public getPath(): string {
return this.path
}

/**
* Builds an error message by replacing placeholders in the error template
* @param replace - Object containing key-value pairs to replace in the template
* @returns The formatted error message
*/
protected buildError(replace?: Record<string, unknown>): string {
try {
let error = this.errorTemplate.replace(':attribute', this.field)

let error = this.errorTemplate.replace(':attribute', this.getPath())

if (!replace) {
return error
Expand Down
6 changes: 4 additions & 2 deletions src/core/domains/validator/interfaces/IRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ export interface IRuleError {


export interface IRule {
setField(field: string): this
setPath(field: string): this
getPath(): string
setData(data: unknown): this
setAttributes(attributes: unknown): this
validate(): boolean
validate(): Promise<boolean>
getError(): IRuleError
getName(): string




}


Expand Down
8 changes: 2 additions & 6 deletions src/core/domains/validator/rules/Accepted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@ import isTruthy from "../utils/isTruthy";

class Accepted extends AbstractRule implements IRule {


protected name: string = 'accepted'

protected errorTemplate: string = 'The :attribute field must be accepted.';


public validate(): boolean {
public async test(): Promise<boolean> {
return isTruthy(this.getData())
}



public getError(): IRuleError {
return {
[this.field]: this.buildError()
[this.getPath()]: this.buildError()
}
}

Expand Down
5 changes: 2 additions & 3 deletions src/core/domains/validator/rules/AcceptedIf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ class AcceptedIf extends AbstractRule<AcceptedIfOptions> implements IRule {

protected errorTemplate: string = 'The :attribute field must be accepted when :another is :value.';


constructor(anotherField: string, value: unknown) {
super()
this.options.anotherField = anotherField
this.options.value = value
}

public validate(): boolean {
public async test(): Promise<boolean> {
const {
anotherField,
value: expectedValue
Expand All @@ -42,7 +41,7 @@ class AcceptedIf extends AbstractRule<AcceptedIfOptions> implements IRule {

getError(): IRuleError {
return {
[this.field]: this.buildError({
[this.getPath()]: this.buildError({
another: this.options.anotherField,
value: this.options.value
})
Expand Down
5 changes: 2 additions & 3 deletions src/core/domains/validator/rules/Equals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ class Equals extends AbstractRule implements IRule {
this.matches = matches;
}

public validate(): boolean {
public test(): boolean {
return this.getData() === this.matches
}


public getError(): IRuleError {
return {
[this.field]: this.buildError({
[this.getPath()]: this.buildError({
matches: this.matches
})
}
Expand Down
6 changes: 4 additions & 2 deletions src/core/domains/validator/rules/IsArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ class IsArray extends AbstractRule implements IRule {

protected errorTemplate: string = 'The :attribute field must be an array.';

public validate(): boolean {
protected testArrayItems = false

public async test(): Promise<boolean> {
return Array.isArray(this.getData())
}

public getError(): IRuleError {
return {
[this.field]: this.buildError()
[this.getPath()]: this.buildError()
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/core/domains/validator/rules/IsObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
import AbstractRule from "../abstract/AbstractRule";
import { IRule, IRuleError } from "../interfaces/IRule";

class IsString extends AbstractRule implements IRule {
class isObject extends AbstractRule implements IRule {

protected name: string = 'object'

protected errorTemplate: string = 'The :attribute field must be an object.';

public validate(): boolean {
public async test(): Promise<boolean> {
return typeof this.getData() === 'object'
}

public getError(): IRuleError {
return {
[this.field]: this.buildError()
[this.getPath()]: this.buildError()
}
}

}


export default IsString;
export default isObject;
14 changes: 9 additions & 5 deletions src/core/domains/validator/rules/IsString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ class IsString extends AbstractRule implements IRule {
protected name: string = 'string'

protected errorTemplate: string = 'The :attribute field must be a string.';

public async test(): Promise<boolean> {

if(Array.isArray(this.getData())) {
return (this.getData() as unknown[]).every(item => typeof item === 'string')
}

return typeof this.getData() === 'string'

public validate(): boolean {
const value = this.getData()
return typeof value === 'string'
}

public getError(): IRuleError {
return {
[this.field]: this.buildError()
[this.getPath()]: this.buildError()
}
}


}


Expand Down
6 changes: 2 additions & 4 deletions src/core/domains/validator/rules/Required.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ class Required extends AbstractRule implements IRule {

protected errorTemplate: string = 'The :attribute field is required.';

public validate(): boolean {
public async test(): Promise<boolean> {
const value = this.getData()
return value !== undefined && value !== null && value !== ''
}



public getError(): IRuleError {
return {
[this.field]: this.buildError()
[this.getPath()]: this.buildError()
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/core/domains/validator/rules/isNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

import AbstractRule from "../abstract/AbstractRule";
import { IRule, IRuleError } from "../interfaces/IRule";

class IsNumber extends AbstractRule implements IRule {

protected name: string = 'number'

protected errorTemplate: string = 'The :attribute field must be a number.';

protected testArrayItems: boolean = true

public async test(): Promise<boolean> {
return typeof this.getData() === 'number'
}


public getError(): IRuleError {
return {
[this.getPath()]: this.buildError()
}
}

}


export default IsNumber;
Loading