Skip to content

Instantly share code, notes, and snippets.

@CMCDragonkai
Last active February 1, 2022 01:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CMCDragonkai/d1739230b49fd9871fd81157957d7480 to your computer and use it in GitHub Desktop.
Save CMCDragonkai/d1739230b49fd9871fd81157957d7480 to your computer and use it in GitHub Desktop.
General Data Validation #typescript #javascript
import { CustomError } from 'ts-custom-error';
class ErrorParse extends CustomError {
public readonly errors: Array<ErrorRule>;
constructor(errors: Array<ErrorRule>) {
const message = errors.map((e) => e.message).join('; ');
super(message);
this.errors = errors;
}
}
class ErrorRule extends CustomError {
public keyPath: Array<string>;
public value: any;
public context: object;
constructor(message?: string) {
super(message);
}
}
/**
* Functional data-validation, inspired by recursion schemes and `JSON.parse` reviver
* Post-fix depth first traversal on the data structure tree
* Capable of modifying the data structure during traversal
* Works like `JSON.parse` but on POJOs rather than JSON strings
* Compose validation rules into the `rules` function
*/
async function parse(
rules: (keyPath: Array<string>, value: any) => Promise<any>,
data: any,
options: { mode: 'greedy' | 'lazy' } = { mode: 'lazy' }
): Promise<any> {
const errors: Array<ErrorRule> = [];
const parse_ = async (keyPath: Array<string>, value: any, context: object) => {
if (typeof value === 'object' && value != null) {
for (const key in value) {
value[key] = await parse_([...keyPath, key], value[key], value);
}
}
try {
value = await rules.bind(context)(keyPath, value);
} catch (e) {
if (e instanceof ErrorRule) {
e.keyPath = keyPath;
e.value = value;
e.context = context;
errors.push(e);
// If lazy mode, short circuit evaluation
// And throw the error up
if (options.mode === 'lazy') {
throw e;
}
} else {
throw e;
}
}
return value;
};
try {
// The root context is an object containing the root data but keyed with undefined
data = await parse_([], data, { undefined: data });
} catch (e) {
if (e instanceof ErrorRule) {
throw new ErrorParse(errors);
} else {
throw e;
}
}
if (errors.length > 0) {
throw new ErrorParse(errors);
}
return data;
}
export { parse };
@CMCDragonkai
Copy link
Author

Use it like:

async function main () {
  try {
    const output = await parse(
      function (keyPath, value) {
        switch (keyPath.join('.')) {
          case 'name':
            if (typeof value !== 'string' || value.length < 3) {
              throw new ErrorRule('name must be 3 characters or longer');
            }
            break;
          case 'age':
            if (typeof value !== 'number') {
              throw new ErrorRule('age must be a number');
            }
            break;
        }
        return value;
      },
      {
        name: 'bc',
        age: 'a',
      },
      { mode: 'greedy' }
    );
    console.log(output);
  } catch (e) {
    console.log(e);
  }
}

main();

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