From c7d2325bf9c0f4c8c1b4d0cd97729344e9567bdf Mon Sep 17 00:00:00 2001 From: Joe Ma Date: Wed, 14 Feb 2024 03:21:11 +0800 Subject: [PATCH] Add validator Signed-off-by: Joe Ma --- src/types.d.ts | 9 +++ src/validator.ts | 180 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/validator.ts diff --git a/src/types.d.ts b/src/types.d.ts index 0031cae..5d2186b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -34,4 +34,13 @@ export interface Env { ENDPOINT: string; LARGE_ENDPOINT?: string; LARGE_DOWNLOAD_ENDPOINT?: string; +} + +export interface PasteInit { + content: File | string; // field u + title?: string; // field title + mime?: string; // field mime + paste_type?: PASTE_TYPES; // field paste_type (Default: paste) + pass?: string; // field pass + read_limit?: number; // field number } \ No newline at end of file diff --git a/src/validator.ts b/src/validator.ts new file mode 100644 index 0000000..6f4bbf3 --- /dev/null +++ b/src/validator.ts @@ -0,0 +1,180 @@ +type AnyStr = string & {}; + +export interface ValidateOpts { + fields: Record< + string, + | string + | { + type: 'string' | 'boolean' | 'number' | 'object' | 'array' | AnyStr; + required?: boolean; // default: true + convertible?: boolean; // default: true (only support number and boolean type) + min?: number; + max?: number; + fail_message?: string; + validate_func?: (value: any) => boolean; + } + >; +} + +export class ValidateResult { + #status: boolean; + #err?: string | Record; + #result?: any; + + constructor(result?: any, err?: string | Record) { + this.#status = err === undefined; + this.#err = err; + this.#result = result; + if ((this.#status && this.#result === undefined) || (!this.#status && this.#result !== undefined)) { + throw Error('Cannot specify the error and the result at the same time.'); + } + } + + static err(err?: string | Record) { + return new ValidateResult(undefined, err); + } + + static ok(result?: any) { + return new ValidateResult(result, undefined); + } + + toString() { + return JSON.stringify({ + status: this.#status ? 'pass' : 'failed', + err: this.#err, + }); + } + + valueOf() { + return this.#status ? 1 : 0; + } + + is_ok() { + return this.#status === true; + } + + get status() { + return this.#status; + } + + get err() { + return this.#err; + } + + get result() { + return this.#result; + } +} + +export function validate(object: Record, opts: ValidateOpts): ValidateResult { + const result = Object.assign({}, object); + const fields = Object.entries(opts.fields).map(([key, value]) => { + if (typeof value === 'string') { + return { + field: key, + type: value, + required: true, + convertible: true, + }; + } else if (typeof value === 'object') + return { + field: key, + ...value, + }; + else { + throw new Error('Invalid ValidateOpts.'); + } + }); + + for (const field of fields) { + const key = field.field; + + if (!Object.hasOwn(object, key)) { + if (field.required === false) continue; + else return ValidateResult.err(field.fail_message ?? `Missing required field \`${key}\`.`); + } + + const target = object[key]; + if (field.validate_func) { + if (!field.validate_func(target)) + return ValidateResult.err(field.fail_message ?? `Validation failed on field \`${key}\`.`); + continue; + } + + switch (field.type) { + case 'number': { + if (typeof target !== 'number' && !(typeof target === 'string' && field.convertible !== false)) { + return ValidateResult.err(field.fail_message ?? `The \`${key}\` field should be of type \`${field.type}\`.`); + } + let val: number; + // Conversion + if (typeof target === 'string') { + const converted = parseInt(target); + if (isNaN(converted)) + return ValidateResult.err(field.fail_message ?? `Unable to convert field \`${key}\` to a number.`); + val = converted; + } else if (typeof target === 'number') val = target; + // Check range + if (field.min !== undefined) { + if (val! < field.min) + return ValidateResult.err(field.fail_message ?? `The \`${key}\` field should be larger than ${field.min}.`); + } + if (field.max !== undefined) { + if (val! > field.max) + return ValidateResult.err( + field.fail_message ?? `The \`${key}\` field should be smaller than ${field.max}.` + ); + } + result[key] = val!; + break; + } + + case 'boolean': { + if ( + typeof target !== 'boolean' && + !(typeof target === 'string' && field.convertible !== false) && + !(typeof target === 'number' && field.convertible !== false) + ) { + return ValidateResult.err(field.fail_message ?? `The \`${key}\` field should be of type \`${field.type}\`.`); + } + // Conversion + let val: boolean | undefined; + if (typeof target === 'string') { + if (['true', 'yes', 'y', '1'].includes(target.toLowerCase())) val = true; + else if (['false', 'no', 'n', '0'].includes(target.toLowerCase())) val = false; + } else if (typeof target === 'number') { + if (target === 1) val = true; + else if (target === 0) val = false; + } else val = target; + + if (val === undefined) + return ValidateResult.err(field.fail_message ?? `Unable to convert field \`${key}\` to a boolean.`); + result[key] = val; + break; + } + + case 'string': + if (typeof target !== 'string') + return ValidateResult.err(field.fail_message ?? `The \`${key}\` field should be of type \`${field.type}\`.`); + // Check range + if (field.min !== undefined) { + if (target.length < field.min) + return ValidateResult.err( + field.fail_message ?? `The \`${key}\` field should have at least ${field.min} characters.` + ); + } + if (field.max !== undefined) { + if (target.length > field.max) + return ValidateResult.err( + field.fail_message ?? `The \`${key}\` field should have no more than ${field.max} characters.` + ); + } + break; + + default: + if (typeof target !== field.type) + return ValidateResult.err(field.fail_message ?? `The \`${key}\` field should be of type \`${field.type}\`.`); + } + } + return ValidateResult.ok(result); +}