From b5fad7c8d9c9c2fe595a19db12c716a3d058e382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=BCller?= Date: Tue, 9 May 2023 15:02:56 -0300 Subject: [PATCH] wip: redundant type recursions --- lib/convert.test.ts | 53 +++++------ lib/convert.ts | 68 ++++++++------ lib/errors.ts | 21 +++-- lib/index.test.ts | 21 +++-- lib/index.ts | 57 +++++++++--- lib/parse.ts | 50 +++++----- lib/serialize.test.ts | 58 ++++++------ lib/serialize.ts | 38 ++++---- lib/types.ts | 207 +++++++++++++++++++++++++----------------- lib/validate.test.ts | 81 +++++++---------- lib/validate.ts | 130 +++++++++++++++----------- 11 files changed, 442 insertions(+), 342 deletions(-) diff --git a/lib/convert.test.ts b/lib/convert.test.ts index cb6f981..d7416db 100644 --- a/lib/convert.test.ts +++ b/lib/convert.test.ts @@ -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'; @@ -34,17 +31,17 @@ describe('createConvert', (): void => { required: ['REQ_VAR'], type: 'object', } as const; - type S = typeof schema; + type V = TypeFromJSONSchema; it('works without conversion', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; - const container: Record = { + } 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( @@ -66,22 +63,22 @@ describe('createConvert', (): void => { }); it('works with valid schema', (): void => { - const values: EnvSchemaPartialValues = { + 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, - errors: EnvSchemaMaybeErrors, + initialValues: Partial, + errors: EnvSchemaMaybeErrors, ): bigint | undefined => typeof value === 'string' && propertySchema === schema.properties.OPT_VAR && @@ -129,7 +126,7 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} }); it('works with missing values (keep undefined)', (): void => { - const values: EnvSchemaPartialValues = {}; + const values: Partial = {}; const container: Record = {}; const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: ( @@ -137,8 +134,8 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} propertySchema: JSONSchema7, key: string, allSchema: JSONSchema7, - initialValues: EnvSchemaPartialValues, - errors: EnvSchemaMaybeErrors, + initialValues: Partial, + errors: EnvSchemaMaybeErrors, ): bigint | undefined => typeof value === 'string' && propertySchema === schema.properties.OPT_VAR && @@ -167,7 +164,7 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} }); it('works with missing values (return custom default)', (): void => { - const values: EnvSchemaPartialValues = {}; + const values: Partial = {}; const container: Record = {}; const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: ( @@ -175,8 +172,8 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} propertySchema: JSONSchema7, key: string, allSchema: JSONSchema7, - initialValues: EnvSchemaPartialValues, - errors: EnvSchemaMaybeErrors, + initialValues: Partial, + errors: EnvSchemaMaybeErrors, ): bigint | undefined => typeof value === 'string' && propertySchema === schema.properties.OPT_VAR && @@ -222,14 +219,14 @@ New Value.....: ${new Date(0)} }); it('removes properties converted to undefined', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const container: Record = { 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, @@ -255,14 +252,14 @@ New Value.....: ${new Date(0)} }); it('removes properties that conversion did throw', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const container: Record = { 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 => { @@ -303,7 +300,7 @@ New Value.....: ${new Date(0)} }, type: 'object', } as const; - const values: EnvSchemaPartialValues = {}; + const values: TypeFromJSONSchema = {}; const container: Record = {}; const convert = createConvert( schemaNoRequired, diff --git a/lib/convert.ts b/lib/convert.ts index f9bded7..ac20919 100644 --- a/lib/convert.ts +++ b/lib/convert.ts @@ -1,4 +1,7 @@ +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + import type { + BaseEnvParsed, BaseEnvSchema, EnvSchemaConvertedPartialValues, EnvSchemaConvertedPartialValuesWithConvert, @@ -6,7 +9,6 @@ import type { EnvSchemaMaybeErrors, EnvSchemaProperties, EnvSchemaPropertyValue, - EnvSchemaPartialValues, } from './types'; import { addErrors } from './errors'; @@ -15,13 +17,14 @@ import dbg from './dbg'; // DO NOT THROW HERE! type EnvSchemaConvert< S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters | undefined, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters | undefined = undefined, > = ( - value: EnvSchemaPartialValues, + value: Partial, errors: EnvSchemaMaybeErrors, container: Record, ) => [ - EnvSchemaConvertedPartialValuesWithConvert, + EnvSchemaConvertedPartialValuesWithConvert, EnvSchemaMaybeErrors, ]; @@ -29,35 +32,40 @@ const noRequiredProperties: string[] = []; const createConvert = < S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters< + S, + V + > = EnvSchemaCustomConverters, >( schema: S, - properties: Readonly>, + properties: Readonly>, customize: Converters, -): EnvSchemaConvert => { +): EnvSchemaConvert => { const convertedProperties = properties.filter( ([key]) => customize[key] !== undefined, ); - type ConverterKey = Extract; - const requiredProperties: readonly ConverterKey[] = schema.required - ? (schema.required.filter( - key => customize[key] !== undefined, - ) as ConverterKey[]) - : (noRequiredProperties as ConverterKey[]); + type ConverterKey = Extract; + const requiredProperties = ( + schema.required + ? schema.required.filter(key => customize[key] !== undefined) + : noRequiredProperties + ) as ConverterKey[]; return ( - initialValues: EnvSchemaPartialValues, + initialValues: Partial, initialErrors: EnvSchemaMaybeErrors, container: Record, ): [ - EnvSchemaConvertedPartialValuesWithConvert, + EnvSchemaConvertedPartialValuesWithConvert, EnvSchemaMaybeErrors, ] => { // alias the same object with a different type, save on casts const values = initialValues as EnvSchemaConvertedPartialValuesWithConvert< S, - Converters + Converters, + V >; let errors = initialErrors; @@ -74,7 +82,7 @@ const createConvert = < const oldValue = values[key]; try { const newValue = convert( - values[key] as EnvSchemaPropertyValue | undefined, + values[key] as EnvSchemaPropertyValue, propertySchema, key, schema, @@ -128,26 +136,26 @@ New Value.....: ${newValue} }; }; -const noConversion = ( - values: EnvSchemaPartialValues, +const noConversion = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( + values: Partial, errors: EnvSchemaMaybeErrors, -): [EnvSchemaConvertedPartialValues, EnvSchemaMaybeErrors] => [ - values as EnvSchemaConvertedPartialValues, +): [EnvSchemaConvertedPartialValues, EnvSchemaMaybeErrors] => [ + values as EnvSchemaConvertedPartialValues, errors, ]; export default < S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters | undefined, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters | undefined = undefined, >( schema: Readonly, - properties: Readonly>, + properties: Readonly>, customize: Converters, -): EnvSchemaConvert => +): EnvSchemaConvert => customize === undefined - ? (noConversion as unknown as EnvSchemaConvert) - : createConvert( - schema, - properties, - customize as Exclude, - ); + ? (noConversion as unknown as EnvSchemaConvert) + : createConvert(schema, properties, customize); diff --git a/lib/errors.ts b/lib/errors.ts index c6f5161..a6d57ab 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -1,9 +1,12 @@ +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + import type { BaseEnvSchema, EnvSchemaConvertedPartialValues, EnvSchemaCustomizations, EnvSchemaMaybeErrors, EnvSchemaErrors, + BaseEnvParsed, } from './types'; export const addErrors = ( @@ -11,11 +14,9 @@ export const addErrors = ( key: Extract | '$other', exception: Error, ): EnvSchemaErrors => { - let errors = initialErrors; - if (errors === undefined) errors = {}; - let keyErrors = errors[key]; - if (keyErrors === undefined) { - keyErrors = []; + const errors: EnvSchemaErrors = initialErrors ?? {}; + const keyErrors = errors[key] ?? []; + if (!keyErrors.length) { errors[key] = keyErrors; } keyErrors.push(exception); @@ -35,11 +36,15 @@ export const addErrors = ( */ export class EnvSchemaValidationError< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > extends Error { readonly schema: S; - values: EnvSchemaConvertedPartialValues; + values: EnvSchemaConvertedPartialValues; errors: EnvSchemaErrors; @@ -52,7 +57,7 @@ export class EnvSchemaValidationError< customize: Customizations, errors: EnvSchemaErrors, container: Record, - values: EnvSchemaConvertedPartialValues, + values: EnvSchemaConvertedPartialValues, ) { const names = Object.keys(errors).join(', '); super(`Failed to validate environment variables against schema: ${names}`); diff --git a/lib/index.test.ts b/lib/index.test.ts index 25d397a..cf510bc 100644 --- a/lib/index.test.ts +++ b/lib/index.test.ts @@ -2,6 +2,7 @@ process.env.VALIDATED_ENV_SCHEMA_DEBUG = 'true'; import { + TypeFromJSONSchema, commonSchemas, schemaHelpers, } from '@profusion/json-schema-to-typescript-definitions'; @@ -37,6 +38,7 @@ describe('validateEnvSchema', (): void => { required: ['REQ_VAR'], type: 'object', } as const; + type V = TypeFromJSONSchema; it('works with valid data', (): void => { const container: Record = { @@ -65,7 +67,7 @@ describe('validateEnvSchema', (): void => { validateEnvSchema(schema, container); expect(true).toBe(false); // should throw } catch (e) { - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(e).toBeInstanceOf(EnvSchemaValidationError); expect(err.schema).toBe(schema); expect(err.container).toBe(container); @@ -88,10 +90,10 @@ describe('validateEnvSchema', (): void => { }); it('works with customizations', (): void => { - const container: Record = { + const container = { OPT_VAR: '1.23', REQ_VAR: '{"a": [2, 3], "s": "hello"}', - }; + } as const; const consoleSpy = getConsoleMock(); const values = validateEnvSchema(schema, container, { convert: { @@ -99,7 +101,7 @@ describe('validateEnvSchema', (): void => { value !== undefined ? BigInt(value * 1e6) : undefined, }, parse: { - OPT_VAR: (str: string): number => Number(str) * 1000, + OPT_VAR: str => Number(str) * 1000, }, postValidate: { OPT_VAR: (value: number | undefined): number | undefined => @@ -110,7 +112,8 @@ describe('validateEnvSchema', (): void => { serialize: { OPT_VAR: (value: number): string => String(value / 1000), }, - } as const); + // TODO: try as const once type is fixed + }); type ValueType = typeof values; type ExpectedType = { OPT_VAR: bigint | undefined; @@ -182,8 +185,8 @@ New Value.....: 1000000000 } as const); expect(true).toBe(false); } catch (e) { - const err = e as EnvSchemaValidationError; expect(e).toBeInstanceOf(EnvSchemaValidationError); + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -219,8 +222,8 @@ New Value.....: 1000000000 } as const); expect(true).toBe(false); } catch (e) { - const err = e as EnvSchemaValidationError; expect(e).toBeInstanceOf(EnvSchemaValidationError); + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -255,8 +258,8 @@ New Value.....: 1000000000 } as const); expect(true).toBe(false); } catch (e) { - const err = e as EnvSchemaValidationError; expect(e).toBeInstanceOf(EnvSchemaValidationError); + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -292,8 +295,8 @@ New Value.....: 1000000000 } as const); expect(true).toBe(false); } catch (e) { - const err = e as EnvSchemaValidationError; expect(e).toBeInstanceOf(EnvSchemaValidationError); + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ diff --git a/lib/index.ts b/lib/index.ts index 96428e7..62b404a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,7 @@ +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + import type { + BaseEnvParsed, BaseEnvSchema, EnvSchemaConvertedValues, EnvSchemaConverters, @@ -28,10 +31,14 @@ export const commonConvert = providedConverters; type ValidateEnvSchema< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > = ( container: Record, -) => EnvSchemaConvertedValues; +) => EnvSchemaConvertedValues; /** * Creates the validator based on JSON Schema 7 and customizations. @@ -53,23 +60,39 @@ type ValidateEnvSchema< */ export const createValidateEnvSchema = < S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, >( schema: S, customize?: Customizations, -): ValidateEnvSchema => { - const properties = schemaProperties(schema); - const parse = createParse(schema, properties, customize?.parse); - const validate = createValidate(schema, properties, customize?.postValidate); - const serialize = createSerialize(schema, properties, customize?.serialize); - const convert = createConvert>( +): ValidateEnvSchema => { + const properties = schemaProperties(schema); + const parse = createParse(schema, properties, customize?.parse); + const validate = createValidate( + schema, + properties, + customize?.postValidate, + ); + const serialize = createSerialize( + schema, + properties, + customize?.serialize, + ); + const convert = createConvert< + S, + V, + EnvSchemaConverters + >( schema, properties, - customize?.convert as EnvSchemaConverters, + customize?.convert as EnvSchemaConverters, ); return ( container: Record, - ): EnvSchemaConvertedValues => { + ): EnvSchemaConvertedValues => { const [parsedValues, parseErrors] = parse(container); const [validatedValues, validationErrors] = validate( @@ -100,7 +123,7 @@ export const createValidateEnvSchema = < } // no errors means the partial object is actually complete - return values as EnvSchemaConvertedValues; + return values as EnvSchemaConvertedValues; }; }; @@ -135,12 +158,16 @@ export const createValidateEnvSchema = < */ export const validateEnvSchema = < S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, >( schema: S, container: Record = process.env, customize: Customizations | undefined = undefined, -): EnvSchemaConvertedValues => - createValidateEnvSchema(schema, customize)(container); +): EnvSchemaConvertedValues => + createValidateEnvSchema(schema, customize)(container); export default validateEnvSchema; diff --git a/lib/parse.ts b/lib/parse.ts index 16bffd3..f2f45f0 100644 --- a/lib/parse.ts +++ b/lib/parse.ts @@ -1,16 +1,20 @@ import type { JSONSchema7Definition, JSONSchema7Type, + TypeFromJSONSchema, } from '@profusion/json-schema-to-typescript-definitions'; import dbg from './dbg'; -import { EnvSchemaMaybeErrors, EnvSchemaProperties } from './types'; +import { + BaseEnvParsed, + EnvSchemaMaybeErrors, + EnvSchemaProperties, +} from './types'; import type { BaseEnvSchema, EnvSchemaCustomParsers, EnvSchemaParserFn, - EnvSchemaPartialValues, } from './types'; import { addErrors } from './errors'; @@ -30,35 +34,39 @@ const defaultParser = ( // Do its best to parse values, however Ajv will handle most // of the specific conversions itself during validate() // DO NOT THROW HERE! -type EnvSchemaParse = ( +type EnvSchemaParse> = ( container: Readonly>, -) => [EnvSchemaPartialValues, EnvSchemaMaybeErrors]; +) => [Partial, EnvSchemaMaybeErrors]; -export default ( +export default < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, + >( schema: Readonly, - properties: Readonly>, - customize: EnvSchemaCustomParsers | undefined, - ): EnvSchemaParse => + properties: Readonly>, + customize: EnvSchemaCustomParsers | undefined, + ): EnvSchemaParse => ( container: Readonly>, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => - properties.reduce( + ): [Partial, EnvSchemaMaybeErrors] => + properties.reduce<[Partial, EnvSchemaMaybeErrors]>( ( [values, initialErrors], [key, propertySchema], - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { - type K = typeof key; - const str = container[key]; + ): [Partial, EnvSchemaMaybeErrors] => { + const containerKey = container[key]; let errors = initialErrors; - if (typeof str === 'string') { - const parser = - // we already checked for not undefined, but TS doesn't get it :-( - ((customize && customize[key]) as EnvSchemaParserFn) || - (defaultParser as unknown as EnvSchemaParserFn); + if (typeof containerKey === 'string') { + const parser = (customize?.[key] || + defaultParser) as EnvSchemaParserFn; try { - const value = parser(str, propertySchema, key, schema); // eslint-disable-next-line no-param-reassign - values[key] = value; + values[key as keyof V] = parser( + containerKey, + propertySchema, + key, + schema, + ); } catch (e) { dbg(`failed to parse "${key}": ${e}`, e); errors = addErrors(errors, key, e as Error); @@ -66,5 +74,5 @@ export default ( } return [values, errors]; }, - [{}, undefined] as [EnvSchemaPartialValues, EnvSchemaMaybeErrors], + [{}, undefined], ); diff --git a/lib/serialize.test.ts b/lib/serialize.test.ts index 5829252..ff55b0f 100644 --- a/lib/serialize.test.ts +++ b/lib/serialize.test.ts @@ -28,10 +28,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: true, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -50,10 +50,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: 123, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -72,10 +72,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: 'hello', - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -94,12 +94,12 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values = { MY_VAR: 123 }; + type V = TypeFromJSONSchema; + const values = { MY_VAR: 123 } as const; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( - values as unknown as TypeFromJSONSchema, + values as unknown as V, container, undefined, ), @@ -114,10 +114,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: null, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -136,10 +136,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: ['abc', 'def'] as string[], - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -164,13 +164,13 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: { b: true, s: 'hello', }, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -189,8 +189,8 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: Partial> = {} as const; + type V = TypeFromJSONSchema; + const values = {} as const satisfies V; const container: Record = { MY_VAR: 'will be removed', }; @@ -211,10 +211,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: Partial> = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: true, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), { @@ -243,10 +243,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: Partial> = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: true, - } as const; + } as const satisfies V; const container: Record = {}; const consoleSpy = getConsoleMock(); const error = new Error('forced error'); diff --git a/lib/serialize.ts b/lib/serialize.ts index 52b62f4..32e90d9 100644 --- a/lib/serialize.ts +++ b/lib/serialize.ts @@ -1,15 +1,15 @@ import type { JSONSchema7Definition, JSONSchema7Type, + TypeFromJSONSchema, } from '@profusion/json-schema-to-typescript-definitions'; -import { EnvSchemaProperties } from './types'; +import { BaseEnvParsed, EnvSchemaProperties } from './types'; import type { BaseEnvSchema, EnvSchemaCustomSerializers, EnvSchemaSerializeFn, EnvSchemaMaybeErrors, - EnvSchemaPartialValues, } from './types'; import dbg from './dbg'; import { addErrors } from './errors'; @@ -17,41 +17,43 @@ import { addErrors } from './errors'; const defaultSerialize = ( value: JSONSchema7Type, _propertySchema: JSONSchema7Definition, -): string => { - if (typeof value === 'string') return value; // no double-quotes - return JSON.stringify(value); -}; +): string => (typeof value === 'string' ? value : JSON.stringify(value)); // no double quotes // Serialize the parsed and validated values back to container. // DO NOT THROW HERE! -type EnvSchemaSerialize = ( - values: Readonly>, +type EnvSchemaSerialize< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = ( + values: Readonly>, container: Record, errors: EnvSchemaMaybeErrors, -) => [EnvSchemaPartialValues, EnvSchemaMaybeErrors]; +) => [Partial, EnvSchemaMaybeErrors]; -export default ( +export default < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, + >( schema: Readonly, - properties: Readonly>, - customize: EnvSchemaCustomSerializers | undefined, - ): EnvSchemaSerialize => + properties: Readonly>, + customize: EnvSchemaCustomSerializers | undefined, + ): EnvSchemaSerialize => ( - givenValues: Readonly>, + givenValues: Readonly>, container: Record, givenErrors: EnvSchemaMaybeErrors, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => + ): [Partial, EnvSchemaMaybeErrors] => properties.reduce( ( [values, initialErrors], [key, propertySchema], - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { - type K = typeof key; + ): [Partial, EnvSchemaMaybeErrors] => { const value = values[key]; let errors = initialErrors; if (value !== undefined) { const serialize = // we already checked for not undefined, but TS doesn't get it :-( - ((customize && customize[key]) as EnvSchemaSerializeFn) || + ((customize && customize[key]) as EnvSchemaSerializeFn) || defaultSerialize; try { diff --git a/lib/types.ts b/lib/types.ts index 04e221c..c0dc649 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,41 +1,47 @@ -import type { - TypeFromJSONSchema, - JSONSchema7, - JSONSchema7Definition, -} from '@profusion/json-schema-to-typescript-definitions'; +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; +import type { JSONSchema7, JSONSchema7Definition } from 'json-schema'; +import type { DeepReadonly } from 'json-schema-to-ts/lib/types/type-utils/readonly'; + +type ReadonlyJSONSchema = DeepReadonly; +type ReadonlyJSONSchemaDefinition = DeepReadonly; export type BaseEnvSchema = { type: 'object'; - properties: Readonly<{ [key: string]: JSONSchema7 }>; + properties: Readonly<{ [key: string]: ReadonlyJSONSchema }>; required?: readonly string[]; - dependencies?: { - readonly [key: string]: JSONSchema7Definition | readonly string[]; - }; + dependencies?: Readonly<{ + [key: string]: ReadonlyJSONSchemaDefinition | string[]; + }>; additionalProperties?: true; // if provided, must be true, otherwise it may hurt process.env }; -export type EnvSchemaProperties = { - [K in Extract]: [K, S['properties'][K]]; -}[Extract][]; +export type BaseEnvParsed = { + [P in keyof S['properties']]?: unknown; +}; -export const schemaProperties = ( - schema: S, -): Readonly> => - Object.entries(schema.properties) as EnvSchemaProperties; +export type EnvSchemaProperties< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, + Keys extends Extract = Extract< + keyof S['properties'] & keyof V, + string + >, +> = { + [K in Keys]: [K, S['properties'][K]]; +}[Keys][]; -/** - * Subset of valid (post-validate) properties. If there are no - * errors, then all required properties should be present. - */ -export type EnvSchemaPartialValues = Partial< - TypeFromJSONSchema ->; +export const schemaProperties = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, + Ret = EnvSchemaProperties, +>({ + properties, +}: S): Readonly => Object.entries(properties) as Ret; export type EnvSchemaPropertyValue< S extends BaseEnvSchema, - K extends keyof S['properties'], - V = TypeFromJSONSchema, -> = K extends keyof V ? V[K] : never; + V extends BaseEnvParsed, +> = V[Extract]; /** * Errors are stored per-property/variable, if something @@ -51,20 +57,20 @@ export type EnvSchemaMaybeErrors = | undefined; /** - * Parse one property from string to the best JSONSchema7Type. + * Parse one property from string to the best ReadonlyJSONSchema. * * There is no need to coerce types as Ajv.validate() will do that * for you. */ export type EnvSchemaParserFn< S extends BaseEnvSchema, - K extends keyof S['properties'], + V extends BaseEnvParsed = TypeFromJSONSchema, > = ( str: string, - propertySchema: Readonly, - key: K, + propertySchema: Readonly, + key: keyof S['properties'], schema: Readonly, -) => EnvSchemaPropertyValue; +) => EnvSchemaPropertyValue; /** * Customize the parser to be used for each property. @@ -72,24 +78,27 @@ export type EnvSchemaParserFn< * If not provided, the default is to `JSON.parse()` and, if that fails, * keep the original value as a string. */ -export type EnvSchemaCustomParsers = Readonly< +export type EnvSchemaCustomParsers< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Readonly< Partial<{ - [K in keyof S['properties']]: EnvSchemaParserFn; + [K in keyof S['properties']]: EnvSchemaParserFn; }> >; /** - * Serialize one property from validated JSONSchema7Type to string. + * Serialize one property from validated ReadonlyJSONSchema to string. * * The types will be validated by Ajv.validate() */ export type EnvSchemaSerializeFn< S extends BaseEnvSchema, - K extends keyof S['properties'], + V extends BaseEnvParsed = TypeFromJSONSchema, > = ( - value: Exclude, undefined>, - propertySchema: Readonly, - key: K, + value: Exclude, undefined>, + propertySchema: Readonly, + key: keyof S['properties'], schema: Readonly, ) => string; @@ -99,9 +108,12 @@ export type EnvSchemaSerializeFn< * If not provided, the default is to `JSON.stringify()`, * unless it's already a string. */ -export type EnvSchemaCustomSerializers = Readonly< +export type EnvSchemaCustomSerializers< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Readonly< Partial<{ - [K in keyof S['properties']]: EnvSchemaSerializeFn; + [K in keyof S['properties']]: EnvSchemaSerializeFn; }> >; @@ -119,16 +131,16 @@ export type EnvSchemaCustomSerializers = Readonly< */ export type EnvSchemaPostValidateFn< S extends BaseEnvSchema, - K extends keyof S['properties'], - V = EnvSchemaPropertyValue, + V extends BaseEnvParsed = TypeFromJSONSchema, + PV extends EnvSchemaPropertyValue = EnvSchemaPropertyValue, > = ( - value: V | undefined, - propertySchema: S['properties'][K], - key: K, + value: PV | undefined, + propertySchema: S['properties'][keyof S['properties']], + key: keyof S['properties'], schema: Readonly, - allValues: EnvSchemaPartialValues, + allValues: Partial, errors: Readonly>, -) => V | undefined; +) => PV | undefined; /** * Customize the validator to be executed for each property, @@ -136,43 +148,48 @@ export type EnvSchemaPostValidateFn< * * Each validator will receive the other properties for convenience. */ -export type EnvSchemaCustomPostValidators = Readonly< +export type EnvSchemaCustomPostValidators< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Readonly< Partial<{ - [K in keyof S['properties']]: EnvSchemaPostValidateFn; + [K in keyof S['properties']]: EnvSchemaPostValidateFn; }> >; /** - * Convert JSONSchema7Type to high-level values, such as 'Date'. + * Convert ReadonlyJSONSchema to high-level values, such as 'Date'. * * This is executed with the post-Validated data. */ export type EnvSchemaConvertFn< S extends BaseEnvSchema, - K extends keyof S['properties'], - R, - V = EnvSchemaPropertyValue, + Ret, + V extends BaseEnvParsed = TypeFromJSONSchema, > = ( - value: V | undefined, - propertySchema: S['properties'][K], - key: K, + value: EnvSchemaPropertyValue | undefined, + propertySchema: S['properties'][keyof S['properties']], + key: keyof S['properties'], schema: Readonly, - allValues: EnvSchemaPartialValues, + allValues: Partial, errors: Readonly>, -) => R; +) => Ret; /** * Each converter will receive the other properties for convenience. * * If not provided, the value will be kept as the post-validated - * JSONSchema7Type that matches the type (TypeFromJSONSchema), + * ReadonlyJSONSchema that matches the type (TypeFromJSONSchema), * otherwise it may be converted to high-level type, such as `Date`. */ -export type EnvSchemaCustomConverters = Readonly< +export type EnvSchemaCustomConverters< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Readonly< Partial<{ // we must match any return type // eslint-disable-next-line @typescript-eslint/no-explicit-any - [K in keyof S['properties']]: EnvSchemaConvertFn; + [K in keyof S['properties']]: EnvSchemaConvertFn; }> >; @@ -183,12 +200,15 @@ export type EnvSchemaCustomConverters = Readonly< * - serialize the post-validated value back to string * - convert the JSON type to native type (ie: `Date`) */ -export type EnvSchemaCustomizations = +export type EnvSchemaCustomizations< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = | Readonly<{ - convert?: EnvSchemaCustomConverters; - parse?: EnvSchemaCustomParsers; - postValidate?: EnvSchemaCustomPostValidators; - serialize?: EnvSchemaCustomSerializers; + convert?: EnvSchemaCustomConverters; + parse?: EnvSchemaCustomParsers; + postValidate?: EnvSchemaCustomPostValidators; + serialize?: EnvSchemaCustomSerializers; }> | undefined; @@ -196,8 +216,8 @@ type EnvSchemaConvertedValue< S extends BaseEnvSchema, K extends keyof S['properties'], Convert, - V = TypeFromJSONSchema, -> = Convert extends EnvSchemaConvertFn + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Convert extends EnvSchemaConvertFn ? N : K extends keyof V ? V[K] @@ -205,28 +225,40 @@ type EnvSchemaConvertedValue< type EnvSchemaConvertedValuesWithConvertInternal< S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters< + S, + V + > = EnvSchemaCustomConverters, > = { -readonly [K in keyof S['properties']]: EnvSchemaConvertedValue< S, K, - Converters[K] + Converters[K], + V >; }; export type EnvSchemaConverters< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, -> = Customizations extends { readonly convert: EnvSchemaCustomConverters } + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, +> = Customizations extends { readonly convert: EnvSchemaCustomConverters } ? Customizations['convert'] : undefined; export type EnvSchemaConvertedPartialValuesWithConvert< S extends BaseEnvSchema, Converters, -> = Converters extends EnvSchemaCustomConverters - ? Partial> - : EnvSchemaPartialValues; + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Partial< + Converters extends EnvSchemaCustomConverters + ? EnvSchemaConvertedValuesWithConvertInternal + : V +>; /** * Subset of converted properties. If there are no @@ -234,18 +266,24 @@ export type EnvSchemaConvertedPartialValuesWithConvert< */ export type EnvSchemaConvertedPartialValues< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > = EnvSchemaConvertedPartialValuesWithConvert< S, - EnvSchemaConverters + EnvSchemaConverters, + V >; type EnvSchemaConvertedValuesWithConvert< S extends BaseEnvSchema, Converters, -> = Converters extends EnvSchemaCustomConverters - ? EnvSchemaConvertedValuesWithConvertInternal - : TypeFromJSONSchema; + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Converters extends EnvSchemaCustomConverters + ? EnvSchemaConvertedValuesWithConvertInternal + : V; /** * All converted properties. It assumes there @@ -254,8 +292,13 @@ type EnvSchemaConvertedValuesWithConvert< */ export type EnvSchemaConvertedValues< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > = EnvSchemaConvertedValuesWithConvert< S, - EnvSchemaConverters + EnvSchemaConverters, + V >; diff --git a/lib/validate.test.ts b/lib/validate.test.ts index 516e659..8b4b577 100644 --- a/lib/validate.test.ts +++ b/lib/validate.test.ts @@ -3,11 +3,9 @@ process.env.VALIDATED_ENV_SCHEMA_DEBUG = 'true'; import Ajv from 'ajv'; -import { - EnvSchemaMaybeErrors, - EnvSchemaPartialValues, - schemaProperties, -} from './types'; +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + +import { schemaProperties } from './types'; import createValidate from './validate'; const getConsoleMock = (): jest.SpyInstance => @@ -37,7 +35,7 @@ describe('createValidate', (): void => { required: ['REQ_VAR'], type: 'object', } as const; - type S = typeof schema; + type V = TypeFromJSONSchema; it('works with valid data', (): void => { expect( @@ -107,8 +105,7 @@ describe('createValidate', (): void => { a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -138,8 +135,7 @@ describe('createValidate', (): void => { a: [2, 3], s: 'hello', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -171,8 +167,7 @@ describe('createValidate', (): void => { )( { OPT_VAR: 1, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -201,8 +196,7 @@ describe('createValidate', (): void => { { OPT_VAR: 1, REQ_VAR: '{"bug":"not-an-object"}', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -233,8 +227,7 @@ describe('createValidate', (): void => { a: [2, 'bug'], s: 'hello', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -264,8 +257,7 @@ describe('createValidate', (): void => { REQ_VAR: { a: [1, 2], }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -292,18 +284,17 @@ describe('createValidate', (): void => { a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; expect( createValidate(schema, schemaProperties(schema), { OPT_VAR: ( value: number | undefined, - propertySchema: S['properties']['OPT_VAR'], - key: string, - allSchema: S, - allValues: EnvSchemaPartialValues, - errors: Readonly>, - ): number | undefined => + propertySchema, + key, + allSchema, + allValues, + errors, + ) => value === 1 && propertySchema === schema.properties.OPT_VAR && key === 'OPT_VAR' && @@ -332,19 +323,18 @@ describe('createValidate', (): void => { a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const consoleSpy = getConsoleMock(); expect( createValidate(schema, schemaProperties(schema), { OPT_VAR: ( value: number | undefined, - propertySchema: S['properties']['OPT_VAR'], - key: string, - allSchema: S, - allValues: EnvSchemaPartialValues, - errors: Readonly>, - ): number | undefined => + propertySchema, + key, + allSchema, + allValues, + errors, + ) => value === 1 && propertySchema === schema.properties.OPT_VAR && key === 'OPT_VAR' && @@ -382,19 +372,18 @@ New Value.....: 1234 a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const consoleSpy = getConsoleMock(); expect( createValidate(schema, schemaProperties(schema), { OPT_VAR: ( value: number | undefined, - propertySchema: S['properties']['OPT_VAR'], - key: string, - allSchema: S, - allValues: EnvSchemaPartialValues, - errors: Readonly>, - ): number | undefined => + propertySchema, + key, + allSchema, + allValues, + errors, + ) => value === 1 && propertySchema === schema.properties.OPT_VAR && key === 'OPT_VAR' && @@ -425,8 +414,7 @@ New Value.....: 1234 a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const error = new Error('forced error'); const consoleSpy = getConsoleMock(); expect( @@ -457,13 +445,12 @@ New Value.....: 1234 const values = { OPT_VAR: '1', REQ_VAR: '{"bug":"not-an-object"}', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const error = new Error('forced error'); const consoleSpy = getConsoleMock(); expect( createValidate(schema, schemaProperties(schema), { - REQ_VAR: (): EnvSchemaPartialValues['REQ_VAR'] => { + REQ_VAR: (): Partial['REQ_VAR'] => { throw error; }, })(values, undefined), diff --git a/lib/validate.ts b/lib/validate.ts index 97d7f65..b817074 100644 --- a/lib/validate.ts +++ b/lib/validate.ts @@ -1,13 +1,13 @@ import Ajv, { ErrorObject } from 'ajv'; +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; import type { + BaseEnvParsed, BaseEnvSchema, EnvSchemaCustomPostValidators, EnvSchemaMaybeErrors, - EnvSchemaPostValidateFn, EnvSchemaProperties, EnvSchemaPropertyValue, - EnvSchemaPartialValues, } from './types'; import { addErrors } from './errors'; @@ -34,60 +34,65 @@ try { } // DO NOT THROW HERE! -type EnvSchemaValidate = ( - value: EnvSchemaPartialValues, +type EnvSchemaValidate< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = ( + value: Partial, errors: EnvSchemaMaybeErrors, -) => [EnvSchemaPartialValues, EnvSchemaMaybeErrors]; +) => [Partial, EnvSchemaMaybeErrors]; -const createPostValidation = ( +const createPostValidation = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: S, - properties: Readonly>, - customize: EnvSchemaCustomPostValidators, -): EnvSchemaValidate => { + properties: Readonly>, + customize: EnvSchemaCustomPostValidators, +): EnvSchemaValidate => { const postValidatedProperties = properties.filter( ([key]) => customize[key] !== undefined, ); return ( - values: EnvSchemaPartialValues, + values: Partial, initialErrors: EnvSchemaMaybeErrors, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { + ): [Partial, EnvSchemaMaybeErrors] => { let errors = initialErrors; postValidatedProperties.forEach(([key, propertySchema]) => { - type K = typeof key; // it was filtered before - const validate = customize[key] as EnvSchemaPostValidateFn; + const validate = customize[key] as NonNullable< + (typeof customize)[string] + >; const oldValue = values[key]; try { const newValue = validate( - oldValue as EnvSchemaPropertyValue | undefined, + oldValue, propertySchema, key, schema, values, errors, ); - if (oldValue !== newValue) { - if (newValue === undefined) { - dbg( - () => - `Post validation of "${key}" removed property. Was ${JSON.stringify( - oldValue, - )}`, - ); - // eslint-disable-next-line no-param-reassign - delete values[key]; - } else { - dbg( - () => - `\ + if (oldValue !== newValue && newValue === undefined) { + dbg( + () => + `Post validation of "${key}" removed property. Was ${JSON.stringify( + oldValue, + )}`, + ); + // eslint-disable-next-line no-param-reassign + delete values[key]; + } else { + dbg( + () => + `\ Post validation of "${key}" changed property from: Previous Value: ${JSON.stringify(oldValue)} New Value.....: ${JSON.stringify(newValue)} `, - ); - // eslint-disable-next-line no-param-reassign - values[key] = newValue; - } + ); + // eslint-disable-next-line no-param-reassign + values[key as keyof V] = newValue; } } catch (e) { dbg( @@ -105,33 +110,39 @@ New Value.....: ${JSON.stringify(newValue)} }; }; -const noPostValidation = ( - values: EnvSchemaPartialValues, +const noPostValidation = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( + values: Partial, errors: EnvSchemaMaybeErrors, -): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => [values, errors]; +): [Partial, EnvSchemaMaybeErrors] => [values, errors]; const createExceptionForAjvError = (ajvError: ErrorObject): Error => new Ajv.ValidationError([ajvError]); -const processAjvTopLevelError = ( +const processAjvTopLevelError = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: S, - key: Extract, + key: Extract, _path: string[], - values: EnvSchemaPartialValues, + values: Partial, ajvError: ErrorObject, ): void => { - const defVal = schema.properties[key].default; - if (defVal !== undefined) { + const defaultValue = schema.properties[key].default; + if (defaultValue !== undefined) { dbg( () => `Ajv failed the validation of "${key}": ${ajv.errorsText([ ajvError, - ])}. Use default ${JSON.stringify(defVal)}. Was ${JSON.stringify( + ])}. Use default ${JSON.stringify(defaultValue)}. Was ${JSON.stringify( values[key], )}`, ); // eslint-disable-next-line no-param-reassign - values[key] = defVal as EnvSchemaPropertyValue; + values[key] = defaultValue as EnvSchemaPropertyValue; return; } @@ -150,11 +161,14 @@ const processAjvTopLevelError = ( // array, const, oneOf/allOf/anyOf/not... const processAjvNestedError = processAjvTopLevelError; -const processSingleAjvError = ( +const processSingleAjvError = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: S, - key: Extract, + key: Extract, path: string[], - values: EnvSchemaPartialValues, + values: Partial, ajvError: ErrorObject, errors: EnvSchemaMaybeErrors, ): EnvSchemaMaybeErrors => { @@ -178,10 +192,13 @@ const processSpuriousAjvError = ( return addErrors(errors, '$other', createExceptionForAjvError(ajvError)); }; -const processAjvErrors = ( +const processAjvErrors = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: Readonly, - schemaKeys: Readonly>>, - values: EnvSchemaPartialValues, + schemaKeys: Readonly>>, + values: Partial, ajvErrors: readonly ErrorObject[], ): EnvSchemaMaybeErrors => ajvErrors.reduce( @@ -192,7 +209,7 @@ const processAjvErrors = ( /* istanbul ignore else */ if (ajvError.instancePath.startsWith('/')) { const path = ajvError.instancePath.substr(1).split('/'); - const key = path[0] as Extract; + const key = path[0] as Extract; /* istanbul ignore else */ if (schemaKeys.has(key)) { return processSingleAjvError( @@ -224,11 +241,14 @@ const processAjvErrors = ( undefined, ); -export default ( +export default < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: Readonly, - properties: Readonly>, - customize: EnvSchemaCustomPostValidators | undefined, -): EnvSchemaValidate => { + properties: Readonly>, + customize: EnvSchemaCustomPostValidators | undefined, +): EnvSchemaValidate => { const validate = ajv.compile(schema); const postValidate = customize === undefined @@ -236,9 +256,9 @@ export default ( : createPostValidation(schema, properties, customize); const schemaKeys = new Set(properties.map(([key]) => key)); return ( - values: EnvSchemaPartialValues, + values: Partial, initialErrors: EnvSchemaMaybeErrors, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { + ): [Partial, EnvSchemaMaybeErrors] => { let errors = initialErrors; if (!validate(values)) { /* istanbul ignore else */