fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
149 lines (129 loc) • 4.45 kB
text/typescript
import { BaseSchema } from "./BaseSchema";
import { SchemaValidationResult, ObjectSchemaOptions } from "../../../types/types";
/**
* Object schema with property validation
*/
export class ObjectSchema<T extends Record<string, any>> extends BaseSchema<T> {
private _strict = false;
private _allowUnknown = true;
constructor(private shape: { [K in keyof T]: BaseSchema<T[K]> }) {
super();
}
/**
* Enable strict mode (no extra properties allowed)
*/
strict(): this {
const cloned = this.clone();
cloned._strict = true;
cloned._allowUnknown = false;
return cloned;
}
/**
* Allow unknown properties
*/
allowUnknown(allow: boolean = true): this {
const cloned = this.clone();
cloned._allowUnknown = allow;
return cloned;
}
/**
* Validate object value
*/
validate(value: any): SchemaValidationResult<T> {
// Handle common validation (undefined/null)
const commonResult = this.handleCommonValidation(value);
if (commonResult) {
return commonResult;
}
const result: SchemaValidationResult<T> = {
success: true,
errors: [],
warnings: [],
data: undefined,
};
// Type check
if (typeof value !== "object" || Array.isArray(value)) {
result.success = false;
result.errors.push("Expected object");
return result;
}
const validatedObject: any = {};
const inputKeys = Object.keys(value);
const shapeKeys = Object.keys(this.shape);
// Validate each property in shape
for (const [key, schema] of Object.entries(this.shape)) {
const propResult = (schema as BaseSchema).validate(value[key]);
if (!propResult.success) {
result.success = false;
result.errors.push(
`Property '${key}': ${propResult.errors.join(", ")}`
);
} else if (propResult.data !== undefined) {
validatedObject[key] = propResult.data;
}
// Collect warnings from properties
if (propResult.warnings.length > 0) {
result.warnings.push(
...propResult.warnings.map((w) => `Property '${key}': ${w}`)
);
}
}
// Handle extra properties
const extraKeys = inputKeys.filter((key) => !shapeKeys.includes(key));
if (extraKeys.length > 0) {
if (this._strict) {
result.success = false;
result.errors.push(
`Unexpected properties: ${extraKeys.join(", ")}`
);
} else if (this._allowUnknown) {
// Include unknown properties in result
for (const key of extraKeys) {
validatedObject[key] = value[key];
}
if (extraKeys.length > 0) {
result.warnings.push(
`Unknown properties included: ${extraKeys.join(", ")}`
);
}
} else {
result.warnings.push(
`Unknown properties ignored: ${extraKeys.join(", ")}`
);
}
}
if (result.success) {
result.data = validatedObject as T;
}
return result;
}
/**
* Clone the object schema with shape
*/
protected clone(): this {
const cloned = new ObjectSchema(this.shape) as this;
cloned._strict = this._strict;
cloned._allowUnknown = this._allowUnknown;
cloned._optional = this._optional;
cloned._nullable = this._nullable;
cloned._default = this._default;
return cloned;
}
/**
* Get schema configuration
*/
getConfig(): ObjectSchemaOptions & { properties: any } {
const properties: any = {};
for (const [key, schema] of Object.entries(this.shape)) {
properties[key] = (schema as BaseSchema).getConfig();
}
return {
strict: this._strict,
allowUnknown: this._allowUnknown,
optional: this._optional,
nullable: this._nullable,
default: this._default,
properties,
};
}
}