Skip to content

Instantly share code, notes, and snippets.

@honungsburk
Created October 13, 2022 11:14
Show Gist options
  • Save honungsburk/d8cc687f660585d9901d52ad022782db to your computer and use it in GitHub Desktop.
Save honungsburk/d8cc687f660585d9901d52ad022782db to your computer and use it in GitHub Desktop.
A typescript validation object where you can only call methods once
/**
* Entry point for an integer parser
*
* @param error - error to throw if the parsed value is not an integer
* @returns a parser for integers
*/
export function int(error?: (s: string) => any): typeof ValidatorBuilderNumber {
const obj = { ...ValidatorBuilderNumber };
obj[onParseError] = error;
return obj;
}
// By defining a Symbol we can create private functions/variables since no one
// outside this file has access to this symbol!
const onParseError = Symbol();
const onRequiredError = Symbol();
const minV = Symbol();
const onMinError = Symbol();
const maxV = Symbol();
const onMaxError = Symbol();
/**
* The base class contains all variables and the parse function that uses them.
* We must seperate the class into two parts so that we can self-reference
*/
const ValidatorBuilderNumberBase = {
// Problem is that you can see what should be private variables...
[onRequiredError]: undefined as undefined | any,
[onParseError]: undefined as undefined | ((s: string) => any),
[minV]: undefined as undefined | number,
[onMinError]: undefined as undefined | ((n: number) => any),
[maxV]: undefined as undefined | number,
[onMaxError]: undefined as undefined | ((n: number) => any),
/**
* Parse your string into either a number or number | undefined
*
* @param value - the value to parse
* @param onUndefined - if the return value would be undefined, throw this instead
* @returns if onUndefined is defined it will return a number, otherwise number or undefined
*/
parse<A = any>(
value?: any,
onUndefined?: A
): A extends undefined ? number | undefined : number {
// required check
if (value === undefined) {
if (this[onRequiredError]) {
throw this[onRequiredError];
}
return throwIfNotUndefined(onUndefined);
}
if (typeof value !== "string") {
if (this[onParseError]) {
throw this[onParseError](value);
}
return throwIfNotUndefined(onUndefined);
}
let n: number;
// Parse
try {
n = parseInt(value);
} catch (err) {
if (this[onParseError]) {
throw this[onParseError](value);
}
return throwIfNotUndefined(onUndefined);
}
//min check
if (this[minV] !== undefined && this[minV] > n) {
if (this[onMinError]) {
throw this[onMinError](n);
}
return throwIfNotUndefined(onUndefined);
}
//max check
if (this[maxV] !== undefined && this[maxV] < n) {
if (this[onMaxError]) {
throw this[onMaxError](n);
}
return throwIfNotUndefined(onUndefined);
}
return n;
},
};
function throwIfNotUndefined(onUndefined: any): any {
if (onUndefined !== undefined) {
throw onUndefined;
} else {
return undefined as any;
}
}
/**
* This class removes functions after they are called.
*/
const ValidatorBuilderNumber = {
...ValidatorBuilderNumberBase,
/**
*
* @param n the minimal value the parser will allow
* @param error the error to throw if the value is out of bounds
* @returns a NumberParserBuilder
*/
min<T extends typeof ValidatorBuilderNumberBase & { min: unknown }>(
this: T,
n: number,
error?: (n: number) => any
) {
const { min, ..._this } = this;
return { ..._this, [minV]: n, [onMinError]: error };
},
/**
*
* @param n the maximal value the parser will allow
* @param error the error to throw if the value is out of bounds
* @returns a NumberParserBuilder
*/
max<T extends typeof ValidatorBuilderNumberBase & { max: unknown }>(
this: T,
n: number,
error?: (n: number) => any
) {
const { max, ..._this } = this;
return { ..._this, [maxV]: n, [onMaxError]: error };
},
/**
*
* @param error - thow this error if value is required
* @returns a NumberParserBuilder
*/
required<T extends typeof ValidatorBuilderNumberBase & { required: unknown }>(
this: T,
error: { [key: string]: string | number | boolean }
) {
const { required, ..._this } = this;
return { ..._this, [onRequiredError]: error };
},
};
@honungsburk
Copy link
Author

Example usage:

 const parseLimitParam = Validator.int((v: string) =>
    Types.API.Error({
      status: 400,
      type: "InvalidQueryParameters",
      title: "Must be an integer",
      detail: `The 'limit' parameter must be an integer, recieved '${v}'.`,
    })
  )
    .min(0, (v: number) =>
      Types.API.Error({
        status: 400,
        type: "InvalidQueryParameters",
        title: "To small",
        detail: `The 'limit' parameter must be larger then 0 but was '${v}'.`,
      })
    )
    .max(100, (v: number) =>
      Types.API.Error({
        status: 400,
        type: "InvalidQueryParameters",
        title: "To large",
        detail: `The 'limit' parameter must not be larger then 100 but was '${v}'.`,
      })
    )
    .required(
      Types.API.Error({
        status: 400,
        type: "InvalidQueryParameters",
        title: "Missing",
        detail: `The 'limit' parameter is required.`,
      })
    );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment