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