Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Replace with json-schema-to-ts package #4

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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