validata
Version:
Type safe data validation and sanitization
193 lines (168 loc) • 7.72 kB
text/typescript
import { isIssue, Issue, IssueResult, Next, Path, Result, ValueProcessor } from './types';
export interface WithDefault<T> {
default?: T | (() => T);
}
export interface MaybeOptions {
incorrectTypeToUndefined?: boolean;
strictParsing?: boolean;
}
export type UndefinedHandler<T> = () => Result<T> | undefined;
export type Definitely<T> = (next: Next<unknown, T>) => (value: unknown, path?: Path[]) => Result<T>;
export type IsAs<T> = (next: Next<T, T>) => (value: unknown, path: Path[]) => Result<T>;
export type Maybe<T> = (next: Next<unknown, T>) => (value: unknown, path?: Path[]) => Result<T | undefined>;
export type Empty = (value: unknown) => boolean;
export type Check<T> = (value: unknown) => value is T;
export type Convert<T, O = CommonConvertOptions<T>> = (value: unknown, options?: O) => T | undefined;
export type Coerce<T, O> = (options?: O) => (next: Next<T, T>) => (value: T, path: Path[]) => Result<T>;
export type Validate<T, O> = (value: T, path: Path[], options: O) => IssueResult;
export const withDefault = <T>(options?: WithDefault<T>): UndefinedHandler<T> => (): Result<T> | undefined => {
if (options?.default === undefined) return undefined;
return { value: options?.default instanceof Function ? options.default() : options.default };
};
export const definitely = <T>(undefinedHandler?: UndefinedHandler<T>): Definitely<T> => (next) => (value, path = []) => {
if (value === null || value === undefined) {
return undefinedHandler?.() ?? { issues: [Issue.forPath(path, value, 'not-defined')] };
}
return next(value, path);
};
export const nullOrUndefined: Empty = (value) => {
return value === null || value === undefined;
};
export const maybe = <T>(empty: Empty, check: Check<T>, options?: MaybeOptions, undefinedHandler?: UndefinedHandler<T>): Maybe<T> => (next) => (value, path = []) => {
if (empty(value)) {
return undefinedHandler?.() ?? { value: undefined };
}
if (options?.incorrectTypeToUndefined) {
if (!check(value)) {
return { value: undefined };
}
}
const result = next(value, path);
if (isIssue(result) && result.issues.length === 1 && result.issues[0].reason === 'no-conversion') {
if (options?.strictParsing) return result;
return { value: undefined };
}
return result;
};
export const is = <T>(check: Check<T>, typeName: string): IsAs<T> => (next) => (value, path = []) => {
if (!check(value)) {
return { issues: [Issue.forPath(path, value, 'incorrect-type', { expectedType: typeName })] };
}
return next(value, path);
};
export const as = <T, O extends CommonConvertOptions<T>>(check: Check<T>, convert: Convert<T, O>, typeName: string, undefinedHandler?: UndefinedHandler<T>, options?: O): IsAs<T> => (next) => (value, path = []) => {
if (check(value)) return next(value, path);
const converted = options?.converter?.(value, options.convertOptions) ?? convert(value, options);
if (converted === undefined || converted === null) {
return undefinedHandler?.() ?? { issues: [Issue.forPath(path, value, 'no-conversion', { toType: typeName })] };
}
return next(converted, path);
};
const getResultOrValidationIssues = <T, O extends CommonValidationOptions<T>>(validate: Validate<T, O>, value: T, path: Path[], options?: O): Result<T> => {
if (!options) return { value };
const validationResult = validate(value, path, options);
return validationResult.issues.length ? validationResult : { value };
};
export interface CommonValidationOptions<T> {
validator?: (value: T, options?: any, path?: Path[]) => boolean | Issue[];
validatorOptions?: any;
}
export interface CommonConvertOptions<T> {
converter?: (value: unknown, options?: any) => T | undefined;
convertOptions?: any;
}
export const isNullable = <T>(processor: ValueProcessor<T>): ValueProcessor<T | null> => ({
process: (value: unknown, path?: Path[]): Result<T | null> => {
if (value === null) return { value: null };
return processor.process(value, path);
},
});
export const asNullable = <T>(processor: ValueProcessor<T>, options?: WithDefault<Exclude<T, undefined> | null>): ValueProcessor<Exclude<T, undefined> | null> => ({
process: (value: unknown, path?: Path[]): Result<Exclude<T, undefined> | null> => {
if (value === null) return { value: null };
const result = processor.process(value, path);
if (!isIssue(result) && result.value === undefined) {
const defaultValue = options?.default === undefined
? null
: options?.default instanceof Function
? options.default()
: options.default;
return { value: defaultValue };
}
return result as Result<Exclude<T, undefined>>;
},
});
export const createIsCheck = <T, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
typeName: string,
check: Check<T>,
coerce: Coerce<T, TCoerceOptions>,
validate: Validate<T, TValidationOptions>,
) => (options?: TCoerceOptions & TValidationOptions): ValueProcessor<T> => {
return {
process: definitely<T>()(
is(check, typeName)(
coerce(options)(
(value, path) => getResultOrValidationIssues(validate, value, path, options)
)
)
),
};
};
export const createMaybeCheck = <T, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
typeName: string,
check: Check<T>,
coerce: Coerce<T, TCoerceOptions>,
validate: Validate<T, TValidationOptions>,
empty = nullOrUndefined,
) => (options?: MaybeOptions & TCoerceOptions & TValidationOptions): ValueProcessor<T | undefined> => {
return {
process: maybe(empty, check, options)(
is(check, typeName)(
coerce(options)(
(value, path) => getResultOrValidationIssues(validate, value, path, options)
)
)
),
};
};
export const createAsCheck = <T, TConvertOptions extends CommonConvertOptions<T>, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
typeName: string,
check: Check<T>,
convert: Convert<T, TConvertOptions>,
coerce: Coerce<T, TCoerceOptions>,
validate: Validate<T, TValidationOptions>,
) => (options?: WithDefault<T> & TConvertOptions & TCoerceOptions & TValidationOptions): ValueProcessor<T> => {
return {
process: definitely<T>(withDefault(options))(
as(check, convert, typeName, withDefault(options), options)(
coerce(options)(
(value, path) => getResultOrValidationIssues(validate, value, path, options)
)
)
),
};
};
export const createMaybeAsCheck = <T, TConvertOptions extends CommonConvertOptions<T>, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
typeName: string,
check: Check<T>,
convert: Convert<T, TConvertOptions>,
coerce: Coerce<T, TCoerceOptions>,
validate: Validate<T, TValidationOptions>,
empty = nullOrUndefined,
) => (options?: MaybeOptions & WithDefault<T> & TConvertOptions & TCoerceOptions & TValidationOptions): ValueProcessor<T | undefined> => {
return {
process: maybe(empty, check, options, withDefault(options))(
as(check, convert, typeName, withDefault(options), options)(
coerce(options)(
(value, path) => getResultOrValidationIssues(validate, value, path, options)
)
),
),
};
};
export const basicValidation = <T>(value: T, path: Path[], options: CommonValidationOptions<T>): IssueResult => {
const customValidatorResult = options.validator?.(value, options.validatorOptions, path);
if (customValidatorResult === undefined || customValidatorResult === true) return { issues: [] };
if (customValidatorResult === false) return { issues: [Issue.forPath(path, value, 'validator')] };
return { issues: customValidatorResult };
};