3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

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.
This commit is contained in:
Dragory 2019-11-28 02:34:41 +02:00
parent 279a8fe7ae
commit ba2873a29a
4 changed files with 124 additions and 1 deletions

View file

@ -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<TConfig extends {} = IBasePluginConfig> 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;

View file

@ -48,6 +48,69 @@ export function tNullable<T extends t.Type<any, any>>(type: T) {
return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`);
}
function typeHasProps(type: any): type is t.TypeC<any> {
return type.props != null;
}
function typeIsArray(type: any): type is t.ArrayC<any> {
return type._tag === "ArrayType";
}
export type TDeepPartial<T> = T extends t.InterfaceType<any>
? TDeepPartialProps<T["props"]>
: T extends t.DictionaryType<any, any>
? t.DictionaryType<T["domain"], TDeepPartial<T["codomain"]>>
: T extends t.UnionType<any[]>
? t.UnionType<Array<TDeepPartial<T["types"][number]>>>
: T extends t.IntersectionType<any>
? t.IntersectionType<Array<TDeepPartial<T["types"][number]>>>
: T extends t.ArrayType<any>
? t.ArrayType<TDeepPartial<T["type"]>>
: T;
// Based on t.PartialC
export interface TDeepPartialProps<P extends t.Props>
extends t.PartialType<
P,
{
[K in keyof P]?: TDeepPartial<t.TypeOf<P[K]>>;
},
{
[K in keyof P]?: TDeepPartial<t.OutputOf<P[K]>>;
}
> {}
export function tDeepPartial<T>(type: T): TDeepPartial<T> {
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<T>;
} else if (type instanceof t.DictionaryType) {
return t.record(type.domain, tDeepPartial(type.codomain)) as TDeepPartial<T>;
} else if (type instanceof t.UnionType) {
return t.union(type.types.map(unionType => tDeepPartial(unionType))) as TDeepPartial<T>;
} 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<T>;
} else if (type instanceof t.ArrayType) {
return t.array(tDeepPartial(type.type)) as TDeepPartial<T>;
} else {
return type as TDeepPartial<T>;
}
}
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
*/

View file

@ -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);
});

View file

@ -83,6 +83,17 @@ const report = fold((errors: any): StrictValidationError | void => {
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