zappyzep/backend/src/validatorUtils.ts
Dragory ba2873a29a Run a loose pre-check before preprocessStaticConfig
This loose pre-check checks the config schema by treating every object
as partial. This means that if a property exists, it's guaranteed to be
the correct type (e.g. object). However, there's no guarantee that all
or any properties exist.

This allows preprocessStaticConfig implementations to be much less
defensive and thus reduce boilerplate.
2019-11-28 02:34:41 +02:00

119 lines
3.6 KiB
TypeScript

import * as t from "io-ts";
import { pipe } from "fp-ts/lib/pipeable";
import { fold, either } from "fp-ts/lib/Either";
import { noop } from "./utils";
import deepDiff from "deep-diff";
import safeRegex from "safe-regex";
const regexWithFlags = /^\/(.*?)\/([i]*)$/;
/**
* The TSafeRegex type supports two syntaxes for regexes: /<regex>/<flags> and just <regex>
* The value is then checked for "catastrophic exponential-time regular expressions" by
* https://www.npmjs.com/package/safe-regex
*/
const safeRegexAllowedFlags = ["i"];
export const TSafeRegex = new t.Type<RegExp, string>(
"TSafeRegex",
(s): s is RegExp => s instanceof RegExp,
(from, to) =>
either.chain(t.string.validate(from, to), s => {
const advancedSyntaxMatch = s.match(regexWithFlags);
const [regexStr, flags] = advancedSyntaxMatch ? [advancedSyntaxMatch[1], advancedSyntaxMatch[2]] : [s, ""];
const finalFlags = flags
.split("")
.filter(flag => safeRegexAllowedFlags.includes(flag))
.join("");
return safeRegex(regexStr) ? t.success(new RegExp(regexStr, finalFlags)) : t.failure(from, to, "Unsafe regex");
}),
s => `/${s.source}/${s.flags}`,
);
// From io-ts/lib/PathReporter
function stringify(v) {
if (typeof v === "function") {
return t.getFunctionName(v);
}
if (typeof v === "number" && !isFinite(v)) {
if (isNaN(v)) {
return "NaN";
}
return v > 0 ? "Infinity" : "-Infinity";
}
return JSON.stringify(v);
}
// From io-ts/lib/PathReporter
// tslint:disable
function getContextPath(context) {
return context
.map(function(_a) {
var key = _a.key,
type = _a.type;
return key + ": " + type.name;
})
.join("/");
}
// tslint:enable
export class StrictValidationError extends Error {
private readonly errors;
constructor(errors: string[]) {
errors = Array.from(new Set(errors));
super(errors.join("\n"));
this.errors = errors;
}
getErrors() {
return this.errors;
}
}
const report = fold((errors: any): StrictValidationError | void => {
const errorStrings = errors.map(err => {
const context = err.context.map(c => c.key).filter(k => k && !k.startsWith("{"));
while (context.length > 0 && !isNaN(context[context.length - 1])) context.splice(-1);
const value = stringify(err.value);
return value === undefined
? `<${context.join("/")}> is required`
: `Invalid value supplied to <${context.join("/")}>${err.message ? `: ${err.message}` : ""}`;
});
return new StrictValidationError(errorStrings);
}, noop);
export function validate(schema: t.Type<any>, value: any): StrictValidationError | null {
const validationResult = schema.decode(value);
return pipe(
validationResult,
fold(
err => report(validationResult),
result => null,
),
);
}
/**
* Decodes and validates the given value against the given schema while also disallowing extra properties
* See: https://github.com/gcanti/io-ts/issues/322
*/
export function decodeAndValidateStrict(schema: t.HasProps, value: any): StrictValidationError | any {
const validationResult = t.exact(schema).decode(value);
return pipe(
validationResult,
fold(
err => report(validationResult),
result => {
// Make sure there are no extra properties
if (JSON.stringify(value) !== JSON.stringify(result)) {
const diff = deepDiff(result, value);
const errors = diff.filter(d => d.kind === "N").map(d => `Unknown property <${d.path.join(".")}>`);
if (errors.length) return new StrictValidationError(errors);
}
return result;
},
),
);
}