validata
Version:
Type safe data validation and sanitization
103 lines (92 loc) • 3.58 kB
text/typescript
import { DateTime, Duration } from 'luxon';
import { basicValidation, Check, Coerce, CommonValidationOptions, Convert, createAsCheck, createIsCheck, createMaybeAsCheck, createMaybeCheck, Validate } from './common';
import { StringFormatCheck } from './string-format';
import { Issue } from './types';
interface StringPadding {
length: number;
padWith: string;
}
type StringTransform = (value: string) => string;
interface CoerceOptions {
limitLength?: number;
padStart?: StringPadding;
padEnd?: StringPadding;
transform?: StringTransform | StringTransform[];
trim?: 'start' | 'end' | 'both' | 'none';
}
interface ValidationOptions extends CommonValidationOptions<string> {
format?: StringFormatCheck;
regex?: RegExp;
maxLength?: number;
minLength?: number;
}
const check: Check<string> = (value): value is string => {
return typeof value === 'string';
};
const convert: Convert<string> = (value) => {
if (value instanceof Date) {
return DateTime.fromJSDate(value).toUTC().toISO() ?? undefined;
}
if (value instanceof DateTime) {
return value.toUTC().toISO() ?? undefined;
}
if (value instanceof Duration) {
return value.toISO() ?? undefined;
}
return String(value);
};
const coerce: Coerce<string, CoerceOptions> = (options) => (next) => (value, path) => {
if (!options) return next(value, path);
let coerced = value;
if (options.limitLength !== undefined && coerced.length > options.limitLength) {
coerced = coerced.slice(0, options.limitLength);
}
switch (options.trim) {
case 'start':
coerced = coerced.trimStart();
break;
case 'end':
coerced = coerced.trimRight();
break;
case 'both':
coerced = coerced.trim();
break;
}
if (options.transform) {
if (Array.isArray(options.transform)) {
coerced = options.transform.reduce((acc, transform) => transform(acc), coerced);
} else {
coerced = options.transform(coerced);
}
}
if (options.padStart && coerced.length < options.padStart.length) {
coerced = coerced.padStart(options.padStart.length, options.padStart.padWith);
}
if (options.padEnd && coerced.length < options.padEnd.length) {
coerced = coerced.padEnd(options.padEnd.length, options.padEnd.padWith);
}
return next(coerced, path);
};
const validate: Validate<string, ValidationOptions> = (value, path, options) => {
const result = basicValidation(value, path, options);
if (options.minLength !== undefined && value.length < options.minLength) {
result.issues.push(Issue.forPath(path, value, 'min-length', { length: value.length, min: options.minLength }));
}
if (options.maxLength !== undefined && value.length > options.maxLength) {
result.issues.push(Issue.forPath(path, value, 'max-length', { length: value.length, max: options.maxLength }));
}
if (options.regex !== undefined && !options.regex.test(value)) {
result.issues.push(Issue.forPath(path, value, 'regex', { regex: options.regex.toString() }));
}
if (options.format !== undefined) {
const formatResult = options.format(value);
if (formatResult !== true) {
result.issues.push(Issue.forPath(path, value, 'incorrect-format', formatResult));
}
}
return result;
};
export const isString = createIsCheck('string', check, coerce, validate);
export const maybeString = createMaybeCheck('string', check, coerce, validate);
export const asString = createAsCheck('string', check, convert, coerce, validate);
export const maybeAsString = createMaybeAsCheck('string', check, convert, coerce, validate);