/**
* Base utility class for tool managers with enhanced type safety
*/
import { getErrorMessage } from "@/utils/error.js";
import { WordPressId, createWordPressId, DeepReadonly, Result, createSuccess, createError } from "@/types/enhanced.js";
interface EnhancedError extends Error {
originalError?: unknown;
operation?: string;
context?: Record<string, unknown> | undefined;
timestamp?: Date;
}
export interface ParameterValidationRule<T = unknown> {
readonly key: string;
readonly required: boolean;
readonly type?: string;
readonly validator?: (value: unknown) => value is T;
readonly transformer?: (value: unknown) => T;
readonly errorMessage?: string;
}
export interface CacheKeyOptions {
readonly namespace?: string;
readonly includeTimestamp?: boolean;
readonly customHasher?: (params: Record<string, unknown>) => string;
}
export class BaseToolUtils {
/**
* Validate required parameters with enhanced type safety
*/
static validateParams<T extends Record<string, unknown>>(
params: unknown,
rules: readonly ParameterValidationRule[],
): Result<T, Error> {
if (!params || typeof params !== "object" || Array.isArray(params)) {
return createError(new ValidationError("Parameters must be a non-null object", "params", params));
}
const typedParams = params as Record<string, unknown>;
const errors: Error[] = [];
for (const rule of rules) {
const value = typedParams[rule.key];
const exists = rule.key in typedParams;
// Check required fields
if (rule.required && (!exists || value === undefined || value === null)) {
errors.push(
new ValidationError(rule.errorMessage || `Missing required parameter: ${rule.key}`, rule.key, value),
);
continue;
}
// Skip optional fields that don't exist
if (!rule.required && !exists) {
continue;
}
// Type validation
if (rule.type && typeof value !== rule.type) {
errors.push(
new ValidationError(
`Parameter ${rule.key} must be of type ${rule.type}, got ${typeof value}`,
rule.key,
value,
),
);
continue;
}
// Custom validation
if (rule.validator && !rule.validator(value)) {
errors.push(
new ValidationError(rule.errorMessage || `Parameter ${rule.key} failed validation`, rule.key, value),
);
continue;
}
// Transform value if transformer provided
if (rule.transformer) {
try {
typedParams[rule.key] = rule.transformer(value);
} catch (_error) {
errors.push(
new ValidationError(
`Failed to transform parameter ${rule.key}: ${getErrorMessage(_error)}`,
rule.key,
value,
),
);
}
}
}
if (errors.length > 0) {
return createError(errors[0]); // Return first error for simplicity
}
return createSuccess(typedParams as T);
}
/**
* Validate ID parameter with WordPress ID branding
*/
static validateId(id: unknown, name = "id"): Result<WordPressId, Error> {
try {
const numId = Number(id);
if (!Number.isInteger(numId) || numId <= 0) {
return createError(new ValidationError(`Invalid ${name}: must be a positive integer`, name, id));
}
return createSuccess(createWordPressId(numId));
} catch (_error) {
return createError(new ValidationError(`Invalid ${name}: ${getErrorMessage(_error)}`, name, id));
}
}
/**
* Validate multiple IDs at once
*/
static validateIds(ids: unknown[], name = "ids"): Result<readonly WordPressId[], Error> {
const validatedIds: WordPressId[] = [];
for (let i = 0; i < ids.length; i++) {
const result = this.validateId(ids[i], `${name}[${i}]`);
if (!result.success) {
return result;
}
validatedIds.push(result.data);
}
return createSuccess(validatedIds as readonly WordPressId[]);
}
/**
* Handle errors consistently with enhanced error context
*/
static handleError(error: unknown, operation: string, context?: Record<string, unknown> | undefined): Error {
const errorMessage = getErrorMessage(error);
const enhancedMessage = `Error in ${operation}: ${errorMessage}`;
const enhancedError = new Error(enhancedMessage) as EnhancedError;
enhancedError.originalError = error;
enhancedError.operation = operation;
enhancedError.context = context;
enhancedError.timestamp = new Date();
return enhancedError;
}
/**
* Generate cache keys with enhanced options
*/
static generateCacheKey(
operation: string,
params: DeepReadonly<Record<string, unknown>>,
options: CacheKeyOptions = {},
): string {
const { namespace = "wp", includeTimestamp = false, customHasher } = options;
const site = params.site || "default";
let paramStr: string;
if (customHasher) {
paramStr = customHasher(params as Record<string, unknown>);
} else {
paramStr = Object.entries(params)
.filter(([key]) => key !== "site")
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}:${JSON.stringify(value)}`)
.join("|");
}
let cacheKey = `${namespace}:${site}:${operation}:${paramStr}`;
if (includeTimestamp) {
const timestamp = Math.floor(Date.now() / 1000);
cacheKey += `:${timestamp}`;
}
return cacheKey;
}
/**
* Format consistent success response messages
*/
static formatSuccessMessage(operation: string, details?: string, count?: number): string {
let message = operation;
if (count !== undefined) {
message += ` (${count} ${count === 1 ? "item" : "items"})`;
}
if (details) {
message += `: ${details}`;
} else {
message += " completed successfully";
}
return message;
}
/**
* Validate string parameters with enhanced checks
*/
static validateString(
value: unknown,
name: string,
options: {
readonly required?: boolean;
readonly minLength?: number;
readonly maxLength?: number;
readonly pattern?: RegExp;
readonly allowEmpty?: boolean;
} = {},
): Result<string, Error> {
const { required = true, minLength, maxLength, pattern, allowEmpty = false } = options;
if (value === undefined || value === null) {
if (required) {
return createError(new ValidationError(`${name} is required`, name, value));
}
return createSuccess("");
}
if (typeof value !== "string") {
return createError(new ValidationError(`${name} must be a string`, name, value));
}
if (!allowEmpty && value.trim().length === 0) {
return createError(new ValidationError(`${name} cannot be empty`, name, value));
}
if (minLength !== undefined && value.length < minLength) {
return createError(new ValidationError(`${name} must be at least ${minLength} characters long`, name, value));
}
if (maxLength !== undefined && value.length > maxLength) {
return createError(new ValidationError(`${name} must be no more than ${maxLength} characters long`, name, value));
}
if (pattern && !pattern.test(value)) {
return createError(new ValidationError(`${name} does not match required pattern`, name, value));
}
return createSuccess(value);
}
/**
* Type-safe parameter extraction
*/
static extractParam<T>(params: DeepReadonly<Record<string, unknown>>, key: string, defaultValue?: T): T | undefined {
const value = params[key];
return value !== undefined ? (value as T) : defaultValue;
}
/**
* Safe array extraction with type validation
*/
static extractArray<T>(
params: DeepReadonly<Record<string, unknown>>,
key: string,
itemValidator?: (item: unknown) => item is T,
): readonly T[] {
const value = params[key];
if (!Array.isArray(value)) {
return [];
}
if (itemValidator) {
return value.filter(itemValidator);
}
return value as readonly T[];
}
}
// Custom ValidationError class for enhanced error handling
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown,
) {
super(message);
this.name = "ValidationError";
}
}