From ba2873a29a9899082014489f9abab502b57f06db Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 28 Nov 2019 02:34:41 +0200 Subject: [PATCH] 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. --- backend/src/plugins/ZeppelinPlugin.ts | 10 ++++- backend/src/utils.ts | 63 +++++++++++++++++++++++++++ backend/src/validation.test.ts | 41 +++++++++++++++++ backend/src/validatorUtils.ts | 11 +++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 backend/src/validation.test.ts diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index 3bf8e340..f2cc6481 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -12,6 +12,7 @@ import { resolveMember, resolveUser, resolveUserId, + tDeepPartial, trimEmptyStartEndLines, trimIndents, UnknownUser, @@ -19,7 +20,7 @@ import { import { Invite, Member, User } from "eris"; import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line import { performance } from "perf_hooks"; -import { decodeAndValidateStrict, StrictValidationError } from "../validatorUtils"; +import { decodeAndValidateStrict, StrictValidationError, validate } from "../validatorUtils"; import { SimpleCache } from "../SimpleCache"; const SLOW_RESOLVE_THRESHOLD = 1500; @@ -121,6 +122,13 @@ export class ZeppelinPlugin extends Plug ? options.overrides : (defaultOptions.overrides || []).concat(options.overrides || []); + // Before preprocessing the static config, do a loose check by checking the schema as deeply partial. + // This way the preprocessing function can trust that if a property exists, its value will be the correct (partial) type. + const initialLooseCheck = this.configSchema ? validate(tDeepPartial(this.configSchema), mergedConfig) : null; + if (initialLooseCheck) { + throw initialLooseCheck; + } + mergedConfig = this.preprocessStaticConfig(mergedConfig); const decodedConfig = this.configSchema ? decodeAndValidateStrict(this.configSchema, mergedConfig) : mergedConfig; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index bb7599ab..e5e86863 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -48,6 +48,69 @@ export function tNullable>(type: T) { return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`); } +function typeHasProps(type: any): type is t.TypeC { + return type.props != null; +} + +function typeIsArray(type: any): type is t.ArrayC { + return type._tag === "ArrayType"; +} + +export type TDeepPartial = T extends t.InterfaceType + ? TDeepPartialProps + : T extends t.DictionaryType + ? t.DictionaryType> + : T extends t.UnionType + ? t.UnionType>> + : T extends t.IntersectionType + ? t.IntersectionType>> + : T extends t.ArrayType + ? t.ArrayType> + : T; + +// Based on t.PartialC +export interface TDeepPartialProps

+ extends t.PartialType< + P, + { + [K in keyof P]?: TDeepPartial>; + }, + { + [K in keyof P]?: TDeepPartial>; + } + > {} + +export function tDeepPartial(type: T): TDeepPartial { + if (type instanceof t.InterfaceType) { + const newProps = {}; + for (const [key, prop] of Object.entries(type.props)) { + newProps[key] = tDeepPartial(prop); + } + return t.partial(newProps) as TDeepPartial; + } else if (type instanceof t.DictionaryType) { + return t.record(type.domain, tDeepPartial(type.codomain)) as TDeepPartial; + } else if (type instanceof t.UnionType) { + return t.union(type.types.map(unionType => tDeepPartial(unionType))) as TDeepPartial; + } else if (type instanceof t.IntersectionType) { + const types = type.types.map(intersectionType => tDeepPartial(intersectionType)); + return (t.intersection(types as [t.Mixed, t.Mixed]) as unknown) as TDeepPartial; + } else if (type instanceof t.ArrayType) { + return t.array(tDeepPartial(type.type)) as TDeepPartial; + } else { + return type as TDeepPartial; + } +} + +function tDeepPartialProp(prop: any) { + if (typeHasProps(prop)) { + return tDeepPartial(prop); + } else if (typeIsArray(prop)) { + return t.array(tDeepPartialProp(prop.type)); + } else { + return prop; + } +} + /** * Mirrors EmbedOptions from Eris */ diff --git a/backend/src/validation.test.ts b/backend/src/validation.test.ts new file mode 100644 index 00000000..a916fff9 --- /dev/null +++ b/backend/src/validation.test.ts @@ -0,0 +1,41 @@ +import { tDeepPartial } from "./utils"; +import * as t from "io-ts"; +import * as validatorUtils from "./validatorUtils"; +import test from "ava"; +import util from "util"; + +test("tDeepPartial works", ava => { + const originalSchema = t.type({ + listOfThings: t.record( + t.string, + t.type({ + enabled: t.boolean, + someValue: t.number, + }), + ), + }); + + const deepPartialSchema = tDeepPartial(originalSchema); + + const partialValidValue = { + listOfThings: { + myThing: { + someValue: 5, + }, + }, + }; + + const partialErrorValue = { + listOfThings: { + myThing: { + someValue: "test", + }, + }, + }; + + const result1 = validatorUtils.validate(deepPartialSchema, partialValidValue); + ava.is(result1, null); + + const result2 = validatorUtils.validate(deepPartialSchema, partialErrorValue); + ava.not(result2, null); +}); diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts index 1495d8b5..b5743030 100644 --- a/backend/src/validatorUtils.ts +++ b/backend/src/validatorUtils.ts @@ -83,6 +83,17 @@ const report = fold((errors: any): StrictValidationError | void => { return new StrictValidationError(errorStrings); }, noop); +export function validate(schema: t.Type, 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