n4s
Version:
typed schema validation version of enforce
98 lines (86 loc) • 3.01 kB
text/typescript
import { hasOwnProperty, isObject } from 'vest-utils';
import { ctx } from '../../enforceContext';
import type { RuleInstance } from '../../utils/RuleInstance';
import { RuleRunReturn } from '../../utils/RuleRunReturn';
import {
findDangerousOwnKey,
ownKeys,
safeShallowCopy,
} from './schemaObjectUtils';
import type { Prettify } from './schemaRulesTypes';
import type { ShapeInputType, ShapeType } from './shape';
/**
* Validates that an object matches a schema loosely - all schema keys required, extra keys allowed.
* Like shape() but permits additional properties not defined in the schema.
*
* @template T - The object type to validate
* @param value - The object to validate
* @param schema - Schema mapping keys to validation rules
* @returns RuleRunReturn indicating success or failure
*
* @example
* ```typescript
* // Eager API
* enforce({ name: 'John', age: 30, extra: 'allowed' })
* .loose({
* name: enforce.isString(),
* age: enforce.isNumber()
* }); // passes (extra key is ok)
*
* // Lazy API
* const partialUserSchema = enforce.loose({
* name: enforce.isString(),
* email: enforce.isString()
* });
*
* // All schema keys must be present and valid
* partialUserSchema.test({ name: 'Jane', email: 'jane@example.com' }); // true
* partialUserSchema.test({ name: 'Jane', email: 'jane@example.com', age: 30 }); // true (extra ok)
* partialUserSchema.test({ name: 'Jane' }); // false (missing email)
* ```
*/
// eslint-disable-next-line complexity
export function loose<T extends Record<string, any>>(
value: T,
schema: Record<string, any>,
): RuleRunReturn<T> {
if (!isObject(value)) {
return RuleRunReturn.Failing(value);
}
const dangerousSchemaKey = findDangerousOwnKey(schema);
if (dangerousSchemaKey) {
return {
...RuleRunReturn.Failing(value),
path: [dangerousSchemaKey],
};
}
const dangerousValueKey = findDangerousOwnKey(value);
if (dangerousValueKey) {
return {
...RuleRunReturn.Failing(value),
path: [dangerousValueKey],
};
}
const parsedValue: Record<string, any> = safeShallowCopy(value);
for (const key of ownKeys(schema)) {
const fieldValue = hasOwnProperty(value, key) ? value[key] : undefined;
const res = ctx.run({ value: fieldValue, set: true, meta: { key } }, () =>
schema[key].run(fieldValue),
);
if (!res.pass) {
const currentPath = res.path || [];
const newRes = { ...res, path: [key, ...currentPath] };
return newRes as RuleRunReturn<T>;
}
parsedValue[key] = res.type;
}
return RuleRunReturn.Passing(parsedValue as T);
}
// Types colocated with loose rule
export type LooseRuleInstance<S extends Record<string, RuleInstance<any>>> =
RuleInstance<
Prettify<ShapeType<S> & Record<string, unknown>>,
[Prettify<ShapeInputType<S> & Record<string, unknown>>]
>;
export type LooseShapeValue<S extends Record<string, RuleInstance<any>>> =
Prettify<ShapeType<S> & Record<string, unknown>>;