2019-07-11 12:23:57 +03:00
|
|
|
import * as t from "io-ts";
|
|
|
|
import { pipe } from "fp-ts/lib/pipeable";
|
2020-07-22 22:56:21 +03:00
|
|
|
import { either, fold } from "fp-ts/lib/Either";
|
2019-07-22 00:09:45 +03:00
|
|
|
import { noop } from "./utils";
|
2019-07-22 13:09:05 +03:00
|
|
|
import deepDiff from "deep-diff";
|
2019-08-04 13:41:35 +03:00
|
|
|
import safeRegex from "safe-regex";
|
|
|
|
|
2019-08-04 15:44:41 +03:00
|
|
|
const regexWithFlags = /^\/(.*?)\/([i]*)$/;
|
|
|
|
|
2020-08-10 01:31:32 +03:00
|
|
|
export class InvalidRegexError extends Error {}
|
|
|
|
|
2019-08-04 15:44:41 +03:00
|
|
|
/**
|
2020-08-05 01:15:36 +03:00
|
|
|
* This function supports two input syntaxes for regexes: /<pattern>/<flags> and just <pattern>
|
2019-08-04 15:44:41 +03:00
|
|
|
*/
|
2020-08-05 01:15:36 +03:00
|
|
|
export function inputPatternToRegExp(pattern: string) {
|
|
|
|
const advancedSyntaxMatch = pattern.match(regexWithFlags);
|
|
|
|
const [finalPattern, flags] = advancedSyntaxMatch ? [advancedSyntaxMatch[1], advancedSyntaxMatch[2]] : [pattern, ""];
|
2020-08-10 01:31:32 +03:00
|
|
|
try {
|
|
|
|
return new RegExp(finalPattern, flags);
|
|
|
|
} catch (e) {
|
|
|
|
throw new InvalidRegexError(e.message);
|
|
|
|
}
|
2020-08-05 01:15:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
export const TRegex = new t.Type<RegExp, string>(
|
|
|
|
"TRegex",
|
2019-08-04 15:44:41 +03:00
|
|
|
(s): s is RegExp => s instanceof RegExp,
|
2019-08-04 13:41:35 +03:00
|
|
|
(from, to) =>
|
|
|
|
either.chain(t.string.validate(from, to), s => {
|
2020-08-05 01:15:36 +03:00
|
|
|
return t.success(inputPatternToRegExp(s));
|
2019-08-04 13:41:35 +03:00
|
|
|
}),
|
2019-08-04 15:44:41 +03:00
|
|
|
s => `/${s.source}/${s.flags}`,
|
2019-08-04 13:41:35 +03:00
|
|
|
);
|
2019-07-22 00:09:45 +03:00
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2019-08-04 15:44:41 +03:00
|
|
|
export class StrictValidationError extends Error {
|
2019-11-27 22:03:10 +02:00
|
|
|
private readonly errors;
|
2019-08-04 15:44:41 +03:00
|
|
|
|
|
|
|
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 => {
|
2019-07-22 00:09:45 +03:00
|
|
|
const context = err.context.map(c => c.key).filter(k => k && !k.startsWith("{"));
|
2019-08-04 15:44:41 +03:00
|
|
|
while (context.length > 0 && !isNaN(context[context.length - 1])) context.splice(-1);
|
2019-08-04 13:41:35 +03:00
|
|
|
|
2019-07-22 13:50:41 +03:00
|
|
|
const value = stringify(err.value);
|
|
|
|
return value === undefined
|
|
|
|
? `<${context.join("/")}> is required`
|
2019-08-04 15:44:41 +03:00
|
|
|
: `Invalid value supplied to <${context.join("/")}>${err.message ? `: ${err.message}` : ""}`;
|
2019-07-22 00:09:45 +03:00
|
|
|
});
|
2019-08-04 15:44:41 +03:00
|
|
|
|
|
|
|
return new StrictValidationError(errorStrings);
|
2019-07-22 00:09:45 +03:00
|
|
|
}, noop);
|
2019-07-11 12:23:57 +03:00
|
|
|
|
2019-11-28 02:34:41 +02:00
|
|
|
export function validate(schema: t.Type<any>, value: any): StrictValidationError | null {
|
|
|
|
const validationResult = schema.decode(value);
|
2020-11-09 20:03:57 +02:00
|
|
|
return (
|
|
|
|
pipe(
|
|
|
|
validationResult,
|
|
|
|
fold(
|
|
|
|
err => report(validationResult),
|
|
|
|
result => null,
|
|
|
|
),
|
|
|
|
) || null
|
2019-11-28 02:34:41 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-07-11 12:23:57 +03:00
|
|
|
/**
|
2019-08-04 15:44:41 +03:00
|
|
|
* Decodes and validates the given value against the given schema while also disallowing extra properties
|
2019-07-11 12:23:57 +03:00
|
|
|
* See: https://github.com/gcanti/io-ts/issues/322
|
|
|
|
*/
|
2020-07-30 00:29:44 +03:00
|
|
|
export function decodeAndValidateStrict<T extends t.HasProps>(schema: T, value: any): StrictValidationError | any {
|
2019-07-22 13:09:05 +03:00
|
|
|
const validationResult = t.exact(schema).decode(value);
|
2019-07-11 12:23:57 +03:00
|
|
|
return pipe(
|
|
|
|
validationResult,
|
|
|
|
fold(
|
2019-07-22 00:09:45 +03:00
|
|
|
err => report(validationResult),
|
2019-07-11 12:23:57 +03:00
|
|
|
result => {
|
|
|
|
// Make sure there are no extra properties
|
|
|
|
if (JSON.stringify(value) !== JSON.stringify(result)) {
|
2019-07-22 13:09:05 +03:00
|
|
|
const diff = deepDiff(result, value);
|
|
|
|
const errors = diff.filter(d => d.kind === "N").map(d => `Unknown property <${d.path.join(".")}>`);
|
2019-08-04 15:44:41 +03:00
|
|
|
if (errors.length) return new StrictValidationError(errors);
|
2019-07-11 12:23:57 +03:00
|
|
|
}
|
|
|
|
|
2019-08-04 15:44:41 +03:00
|
|
|
return result;
|
2019-07-11 12:23:57 +03:00
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|