From 6ec72638358c534fc57ad37f7877a8b9cc9bc803 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 --- examples/usage.ts | 2 +- lib/convert.test.ts | 53 +++++------ lib/convert.ts | 77 ++++++++------- lib/errors.ts | 24 +++-- lib/index.test.ts | 19 ++-- lib/index.ts | 54 ++++++++--- lib/parse.ts | 45 +++++---- lib/serialize.test.ts | 58 +++++------ lib/serialize.ts | 38 ++++---- lib/types.ts | 217 +++++++++++++++++++++++++----------------- lib/validate.test.ts | 81 +++++++--------- lib/validate.ts | 139 ++++++++++++++++----------- 12 files changed, 447 insertions(+), 360 deletions(-) diff --git a/examples/usage.ts b/examples/usage.ts index 5854b42..73bf361 100644 --- a/examples/usage.ts +++ b/examples/usage.ts @@ -97,7 +97,7 @@ try { console.error('Failed to validate process.env variables:', error); if (error instanceof EnvSchemaValidationError) { - const e: EnvSchemaValidationError = error; + const e: EnvSchemaValidationError = 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)); 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..77b84fc 100644 --- a/lib/convert.ts +++ b/lib/convert.ts @@ -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'; @@ -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,36 +32,35 @@ 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 = KeyOf; + const requiredProperties = schema.required + ? schema.required.filter(key => customize[key] !== undefined) + : noRequiredProperties; 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 - >; + const values = initialValues; let errors = initialErrors; const removeValue = (key: ConverterKey): void => { @@ -68,13 +70,14 @@ const createConvert = < }; convertedProperties.forEach(([key, propertySchema]) => { - type K = typeof key; // it was filtered before - const convert = customize[key] as Exclude; + const convert = customize[key] as NonNullable< + (typeof customize)[typeof key] + >; const oldValue = values[key]; try { const newValue = convert( - values[key] as EnvSchemaPropertyValue | undefined, + values[key], propertySchema, key, schema, @@ -118,7 +121,7 @@ New Value.....: ${newValue} if (values[key] === undefined) { errors = addErrors( errors, - key as Extract, + key, new Error(`required property "${key}" is undefined`), ); } @@ -128,26 +131,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 82bb332..422d334 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -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'; type IsError = (error: unknown) => asserts error is Error; @@ -15,14 +19,12 @@ export const assertIsError: IsError = error => { export const addErrors = ( initialErrors: EnvSchemaMaybeErrors, - key: Extract | '$other', + key: KeyOf | '$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); @@ -42,11 +44,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; @@ -59,7 +65,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 ad424ea..c5b0140 100644 --- a/lib/index.test.ts +++ b/lib/index.test.ts @@ -66,7 +66,7 @@ describe('validateEnvSchema', (): void => { validateEnvSchema(schema, container); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -88,10 +88,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 +99,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 +110,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; @@ -183,7 +184,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -220,7 +221,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -256,7 +257,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -293,7 +294,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as 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..db618ac 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,13 @@ export const createValidateEnvSchema = < */ export const validateEnvSchema = < S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends TypeFromJSONSchema, + Customizations extends 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 0c4f5d4..c4c0f70 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, assertIsError } from './errors'; @@ -30,35 +34,34 @@ 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] = parser(containerKey, propertySchema, key, schema); } catch (e) { dbg(`failed to parse "${key}": ${e}`, e); assertIsError(e); @@ -67,5 +70,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 a79a13d..34cfd46 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, assertIsError } from './errors'; @@ -17,41 +17,43 @@ import { addErrors, assertIsError } 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..077f2af 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; + +// string-only keys +export type KeyOf = keyof T & string; 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 = { + [key: string]: unknown; +} & Record, 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 KeyOf = KeyOf, +> = { + [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[KeyOf]; /** * Errors are stored per-property/variable, if something @@ -44,27 +50,27 @@ export type EnvSchemaPropertyValue< * Multiple phases may produce errors, then it's stored as array. */ export type EnvSchemaErrors = Partial<{ - [K in keyof S['properties'] | '$other']: Error[]; + [K in KeyOf | '$other']: Error[]; }>; export type EnvSchemaMaybeErrors = | EnvSchemaErrors | 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, 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]: 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, 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]: 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], + key: KeyOf, 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]: 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], + key: KeyOf, 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]: EnvSchemaConvertFn; }> >; @@ -183,50 +200,61 @@ 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; type EnvSchemaConvertedValue< S extends BaseEnvSchema, - K extends keyof S['properties'], + K extends KeyOf, Convert, - V = TypeFromJSONSchema, -> = Convert extends EnvSchemaConvertFn - ? N - : K extends keyof V - ? V[K] - : never; + V extends BaseEnvParsed = TypeFromJSONSchema, +> = Convert extends EnvSchemaConvertFn ? N : V[K]; 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< + -readonly [K in KeyOf]: 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 +262,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 +288,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..f638979 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: (): V['REQ_VAR'] => { throw error; }, })(values, undefined), diff --git a/lib/validate.ts b/lib/validate.ts index e618bd1..544482a 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, + KeyOf, } from './types'; import { addErrors, assertIsError } from './errors'; @@ -45,60 +45,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] = newValue; } } catch (e) { dbg( @@ -117,33 +122,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: KeyOf, _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) { 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; return; } @@ -162,11 +173,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: KeyOf, path: string[], - values: EnvSchemaPartialValues, + values: Partial, ajvError: ErrorObject, errors: EnvSchemaMaybeErrors, ): EnvSchemaMaybeErrors => { @@ -190,10 +204,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( @@ -204,7 +221,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]; /* istanbul ignore else */ if (schemaKeys.has(key)) { return processSingleAjvError( @@ -236,11 +253,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 @@ -248,14 +268,19 @@ 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 */ if (validate.errors && validate.errors.length > 0) { - errors = processAjvErrors(schema, schemaKeys, values, validate.errors); + errors = processAjvErrors( + schema, + schemaKeys, + values, + validate.errors, + ); } } return postValidate(values, errors);