Skip to content

Commit

Permalink
refactor: rewrite slow typings
Browse files Browse the repository at this point in the history
- Remove redundant type recursions
- Improve typing readability
- Avoid casting

Part of #2
  • Loading branch information
lcsmuller committed May 25, 2023
1 parent f1c68ff commit fb0f884
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 363 deletions.
2 changes: 1 addition & 1 deletion examples/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ try {
console.error('Failed to validate process.env variables:', error);

if (error instanceof EnvSchemaValidationError) {
const e: EnvSchemaValidationError<typeof appVars, typeof customize> = error;
const e: EnvSchemaValidationError<typeof appVars> = error;
// if you want to proceed anyway, the partial result is stored at:
console.log('partial result:', e.values);
console.log('errors:', JSON.stringify(e.errors, undefined, 2));
Expand Down
53 changes: 25 additions & 28 deletions lib/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ process.env.VALIDATED_ENV_SCHEMA_DEBUG = 'true';
import {
commonSchemas,
JSONSchema7,
TypeFromJSONSchema,
} from '@profusion/json-schema-to-typescript-definitions';

import {
EnvSchemaMaybeErrors,
EnvSchemaPartialValues,
schemaProperties,
} from './types';
import { EnvSchemaMaybeErrors, schemaProperties } from './types';
import commonConvert from './common-convert';
import createConvert from './convert';

Expand All @@ -34,17 +31,17 @@ describe('createConvert', (): void => {
required: ['REQ_VAR'],
type: 'object',
} as const;
type S = typeof schema;
type V = TypeFromJSONSchema<typeof schema>;

it('works without conversion', (): void => {
const values: EnvSchemaPartialValues<S> = {
const values = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
const container: Record<string, string | undefined> = {
} as const satisfies V;
const container = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
} as const satisfies V;
const convert = createConvert(schema, schemaProperties(schema), undefined);
const consoleSpy = getConsoleMock();
const [convertedValue, conversionErrors] = convert(
Expand All @@ -66,22 +63,22 @@ describe('createConvert', (): void => {
});

it('works with valid schema', (): void => {
const values: EnvSchemaPartialValues<S> = {
const values = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
} as const satisfies V;
const container = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
} as const;
} as const satisfies V;
const convert = createConvert(schema, schemaProperties(schema), {
OPT_VAR: (
value: string | undefined,
propertySchema: JSONSchema7,
key: string,
allSchema: JSONSchema7,
initialValues: EnvSchemaPartialValues<S>,
errors: EnvSchemaMaybeErrors<S>,
initialValues: Partial<V>,
errors: EnvSchemaMaybeErrors<typeof schema>,
): bigint | undefined =>
typeof value === 'string' &&
propertySchema === schema.properties.OPT_VAR &&
Expand Down Expand Up @@ -129,16 +126,16 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)}
});

it('works with missing values (keep undefined)', (): void => {
const values: EnvSchemaPartialValues<S> = {};
const values: Partial<V> = {};
const container: Record<string, string | undefined> = {};
const convert = createConvert(schema, schemaProperties(schema), {
OPT_VAR: (
value: string | undefined,
propertySchema: JSONSchema7,
key: string,
allSchema: JSONSchema7,
initialValues: EnvSchemaPartialValues<S>,
errors: EnvSchemaMaybeErrors<S>,
initialValues: Partial<V>,
errors: EnvSchemaMaybeErrors<typeof schema>,
): bigint | undefined =>
typeof value === 'string' &&
propertySchema === schema.properties.OPT_VAR &&
Expand Down Expand Up @@ -167,16 +164,16 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)}
});

it('works with missing values (return custom default)', (): void => {
const values: EnvSchemaPartialValues<S> = {};
const values: Partial<V> = {};
const container: Record<string, string | undefined> = {};
const convert = createConvert(schema, schemaProperties(schema), {
OPT_VAR: (
value: string | undefined,
propertySchema: JSONSchema7,
key: string,
allSchema: JSONSchema7,
initialValues: EnvSchemaPartialValues<S>,
errors: EnvSchemaMaybeErrors<S>,
initialValues: Partial<V>,
errors: EnvSchemaMaybeErrors<typeof schema>,
): bigint | undefined =>
typeof value === 'string' &&
propertySchema === schema.properties.OPT_VAR &&
Expand Down Expand Up @@ -222,14 +219,14 @@ New Value.....: ${new Date(0)}
});

it('removes properties converted to undefined', (): void => {
const values: EnvSchemaPartialValues<S> = {
const values = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
} as const satisfies V;
const container: Record<string, string | undefined> = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
} as const satisfies V;
const convert = createConvert(schema, schemaProperties(schema), {
OPT_VAR: (): bigint | undefined => undefined,
REQ_VAR: (): Date | undefined => undefined,
Expand All @@ -255,14 +252,14 @@ New Value.....: ${new Date(0)}
});

it('removes properties that conversion did throw', (): void => {
const values: EnvSchemaPartialValues<S> = {
const values = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
} as const satisfies V;
const container: Record<string, string | undefined> = {
OPT_VAR: '0x1fffffffffffff',
REQ_VAR: '2021-01-02T12:34:56.000Z',
};
} as const satisfies V;
const error = new Error('forced error');
const convert = createConvert(schema, schemaProperties(schema), {
OPT_VAR: (): bigint => {
Expand Down Expand Up @@ -303,7 +300,7 @@ New Value.....: ${new Date(0)}
},
type: 'object',
} as const;
const values: EnvSchemaPartialValues<typeof schemaNoRequired> = {};
const values: TypeFromJSONSchema<typeof schemaNoRequired> = {};
const container: Record<string, string | undefined> = {};
const convert = createConvert(
schemaNoRequired,
Expand Down
78 changes: 42 additions & 36 deletions lib/convert.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions';

import type {
BaseEnvParsed,
BaseEnvSchema,
EnvSchemaConvertedPartialValues,
EnvSchemaConvertedPartialValuesWithConvert,
EnvSchemaCustomConverters,
EnvSchemaMaybeErrors,
EnvSchemaProperties,
EnvSchemaPropertyValue,
EnvSchemaPartialValues,
KeyOf,
} from './types';

import { addErrors } from './errors';
Expand All @@ -15,50 +17,50 @@ import dbg from './dbg';
// DO NOT THROW HERE!
type EnvSchemaConvert<
S extends BaseEnvSchema,
Converters extends EnvSchemaCustomConverters<S> | undefined,
V extends BaseEnvParsed<S> = TypeFromJSONSchema<S>,
Converters extends EnvSchemaCustomConverters<S, V> | undefined = undefined,
> = (
value: EnvSchemaPartialValues<S>,
value: Partial<V>,
errors: EnvSchemaMaybeErrors<S>,
container: Record<string, string | undefined>,
) => [
EnvSchemaConvertedPartialValuesWithConvert<S, Converters>,
EnvSchemaConvertedPartialValuesWithConvert<S, Converters, V>,
EnvSchemaMaybeErrors<S>,
];

const noRequiredProperties: string[] = [];

const createConvert = <
S extends BaseEnvSchema,
Converters extends EnvSchemaCustomConverters<S>,
V extends BaseEnvParsed<S> = TypeFromJSONSchema<S>,
Converters extends EnvSchemaCustomConverters<
S,
V
> = EnvSchemaCustomConverters<S, V>,
>(
schema: S,
properties: Readonly<EnvSchemaProperties<S>>,
customize: Converters,
): EnvSchemaConvert<S, Converters> => {
): EnvSchemaConvert<S, V, Converters> => {
const convertedProperties = properties.filter(
([key]) => customize[key] !== undefined,
);

type ConverterKey = Extract<keyof Converters, string>;
const requiredProperties: readonly ConverterKey[] = schema.required
? (schema.required.filter(
key => customize[key] !== undefined,
) as ConverterKey[])
: (noRequiredProperties as ConverterKey[]);
type ConverterKey = KeyOf<Converters>;
const requiredProperties = schema.required
? schema.required.filter(key => customize[key] !== undefined)
: noRequiredProperties;

return (
initialValues: EnvSchemaPartialValues<S>,
initialValues: Partial<V>,
initialErrors: EnvSchemaMaybeErrors<S>,
container: Record<string, string | undefined>,
): [
EnvSchemaConvertedPartialValuesWithConvert<S, Converters>,
EnvSchemaConvertedPartialValuesWithConvert<S, Converters, V>,
EnvSchemaMaybeErrors<S>,
] => {
// alias the same object with a different type, save on casts
const values = initialValues as EnvSchemaConvertedPartialValuesWithConvert<
S,
Converters
>;
const values = initialValues;
let errors = initialErrors;

const removeValue = (key: ConverterKey): void => {
Expand All @@ -68,13 +70,14 @@ const createConvert = <
};

convertedProperties.forEach(([key, propertySchema]) => {
type K = typeof key;
// it was filtered before
const convert = customize[key] as Exclude<Converters[K], undefined>;
const convert = customize[key] as NonNullable<
(typeof customize)[typeof key]
>;
const oldValue = values[key];
try {
const newValue = convert(
values[key] as EnvSchemaPropertyValue<S, K> | undefined,
values[key],
propertySchema,
key,
schema,
Expand Down Expand Up @@ -118,36 +121,39 @@ New Value.....: ${newValue}
if (values[key] === undefined) {
errors = addErrors(
errors,
key as Extract<keyof S['properties'], string>,
key,
new Error(`required property "${key}" is undefined`),
);
}
});

return [values, errors];
return [
values as EnvSchemaConvertedPartialValuesWithConvert<S, Converters, V>,
errors,
];
};
};

const noConversion = <S extends BaseEnvSchema>(
values: EnvSchemaPartialValues<S>,
const noConversion = <
S extends BaseEnvSchema,
V extends BaseEnvParsed<S> = TypeFromJSONSchema<S>,
>(
values: Partial<V>,
errors: EnvSchemaMaybeErrors<S>,
): [EnvSchemaConvertedPartialValues<S, undefined>, EnvSchemaMaybeErrors<S>] => [
values as EnvSchemaConvertedPartialValues<S, undefined>,
): [EnvSchemaConvertedPartialValues<S, never, V>, EnvSchemaMaybeErrors<S>] => [
values as EnvSchemaConvertedPartialValues<S, never, V>,
errors,
];

export default <
S extends BaseEnvSchema,
Converters extends EnvSchemaCustomConverters<S> | undefined,
V extends BaseEnvParsed<S> = TypeFromJSONSchema<S>,
Converters extends EnvSchemaCustomConverters<S, V> | undefined = undefined,
>(
schema: Readonly<S>,
properties: Readonly<EnvSchemaProperties<S>>,
customize: Converters,
): EnvSchemaConvert<S, Converters> =>
): EnvSchemaConvert<S, V, Converters> =>
customize === undefined
? (noConversion as unknown as EnvSchemaConvert<S, Converters>)
: createConvert(
schema,
properties,
customize as Exclude<Converters, undefined>,
);
? (noConversion as unknown as EnvSchemaConvert<S, V, Converters>)
: createConvert<S, V, typeof customize>(schema, properties, customize);
24 changes: 15 additions & 9 deletions lib/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions';

import type {
BaseEnvSchema,
EnvSchemaConvertedPartialValues,
EnvSchemaCustomizations,
EnvSchemaMaybeErrors,
EnvSchemaErrors,
BaseEnvParsed,
KeyOf,
} from './types';

/* istanbul ignore next */
Expand All @@ -13,14 +17,12 @@ export const assertIsError: (e: unknown) => asserts e is Error = e => {

export const addErrors = <S extends BaseEnvSchema>(
initialErrors: EnvSchemaMaybeErrors<S>,
key: Extract<keyof S['properties'], string> | '$other',
key: KeyOf<S['properties']> | '$other',
exception: Error,
): EnvSchemaErrors<S> => {
let errors = initialErrors;
if (errors === undefined) errors = {};
let keyErrors = errors[key];
if (keyErrors === undefined) {
keyErrors = [];
const errors: EnvSchemaErrors<S> = initialErrors ?? {};
const keyErrors = errors[key] ?? [];
if (!keyErrors.length) {
errors[key] = keyErrors;
}
keyErrors.push(exception);
Expand All @@ -40,11 +42,15 @@ export const addErrors = <S extends BaseEnvSchema>(
*/
export class EnvSchemaValidationError<
S extends BaseEnvSchema,
Customizations extends EnvSchemaCustomizations<S>,
V extends BaseEnvParsed<S> = TypeFromJSONSchema<S>,
Customizations extends EnvSchemaCustomizations<
S,
V
> = EnvSchemaCustomizations<S, V>,
> extends Error {
readonly schema: S;

values: EnvSchemaConvertedPartialValues<S, Customizations>;
values: EnvSchemaConvertedPartialValues<S, V, Customizations>;

errors: EnvSchemaErrors<S>;

Expand All @@ -57,7 +63,7 @@ export class EnvSchemaValidationError<
customize: Customizations,
errors: EnvSchemaErrors<S>,
container: Record<string, string | undefined>,
values: EnvSchemaConvertedPartialValues<S, Customizations>,
values: EnvSchemaConvertedPartialValues<S, V, Customizations>,
) {
const names = Object.keys(errors).join(', ');
super(`Failed to validate environment variables against schema: ${names}`);
Expand Down
Loading

0 comments on commit fb0f884

Please sign in to comment.