From 483e58a06febbdc0d196e600120424233773ab95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Wed, 11 Jan 2023 00:15:08 -0300 Subject: [PATCH] feat(envconfig): add output types --- examples/envconfig.ts | 196 ++++++++++++++++++++++++++++++++++++++++++ src/envconfig.ts | 190 +++++++++++++++++++++++++++++++--------- 2 files changed, 344 insertions(+), 42 deletions(-) create mode 100644 examples/envconfig.ts diff --git a/examples/envconfig.ts b/examples/envconfig.ts new file mode 100644 index 0000000..3f15166 --- /dev/null +++ b/examples/envconfig.ts @@ -0,0 +1,196 @@ +import { parse } from "../src/envconfig.ts"; + +const env = { + MYAPP_PORT: "3003", +}; + +const envconfig = { + myapp: { + bool: { + optionalAliasArray: "`bool[]", + requiredAliasArray: "`bool[]`required", + optional: "`bool", + required: "`bool`required", + optionalSplitValues: "`bool`split_values", + requiredSplitValues: "`bool`split_values`required", + }, + number: { + optionalAliasArray: "`number[]", + requiredAliasArray: "`number[]`required", + optional: "`number", + required: "`number`required", + optionalSplitValues: "`number`split_values", + requiredSplitValues: "`number`split_values`required", + }, + string: { + optionalAliasArray: "`string[]", + requiredAliasArray: "`string[]`required", + optional: "`string", + required: "`string`required", + optionalSplitValues: "`string`split_values", + requiredSplitValues: "`string`split_values`required", + }, + date: { + optionalAliasArray: "`date[]", + requiredAliasArray: "`date[]`required", + optional: "`date", + required: "`date`required", + optionalSplitValues: "`date`split_values", + requiredSplitValues: "`date`split_values`required", + }, + }, +} as const; + +const out = parse(env, envconfig); + +type OutType = typeof out; + +// bool[] +out.myapp.bool.optionalAliasArray; +let bool_optionalAliasArray: typeof out.myapp.bool.optionalAliasArray = + undefined as boolean[] | undefined; +void bool_optionalAliasArray; + +// bool[]`required +out.myapp.bool.requiredAliasArray; +let bool_requiredAliasArray: typeof out.myapp.bool.requiredAliasArray = [true]; +void bool_requiredAliasArray; + +// bool +out.myapp.bool.optional; +let bool_optional: typeof out.myapp.bool.optional = undefined as + | boolean + | undefined; +void bool_optional; + +// bool`required +out.myapp.bool.required; +let bool_required: typeof out.myapp.bool.required = true; +void bool_required; + +// `bool`split_values +out.myapp.bool.optionalSplitValues; +let bool_optionalSplitValues: typeof out.myapp.bool.optionalSplitValues = + undefined as boolean[] | undefined; +void bool_optionalSplitValues; + +// `bool`split_values`required +out.myapp.bool.requiredSplitValues; +let bool_requiredSplitValues: typeof out.myapp.bool.requiredSplitValues = [ + true, +]; +void bool_requiredSplitValues; + +// number[] +out.myapp.number.optionalAliasArray; +let number_optionalAliasArray: typeof out.myapp.number.optionalAliasArray = + undefined as number[] | undefined; +void number_optionalAliasArray; + +// number[]`required +out.myapp.number.requiredAliasArray; +let number_requiredAliasArray: typeof out.myapp.number.requiredAliasArray = [ + 1, +]; +void number_requiredAliasArray; + +// number +out.myapp.number.optional; +let number_optional: typeof out.myapp.number.optional = undefined as + | number + | undefined; +void number_optional; + +// number`required +out.myapp.number.required; +let number_required: typeof out.myapp.number.required = 1; +void number_required; + +// `number`split_values +out.myapp.number.optionalSplitValues; +let number_optionalSplitValues: typeof out.myapp.number.optionalSplitValues = + undefined as number[] | undefined; +void number_optionalSplitValues; + +// `number`split_values`required +out.myapp.number.requiredSplitValues; +let number_requiredSplitValues: typeof out.myapp.number.requiredSplitValues = [ + 1, +]; +void number_requiredSplitValues; + +// string[] +out.myapp.string.optionalAliasArray; +let string_optionalAliasArray: typeof out.myapp.string.optionalAliasArray = + undefined as string[] | undefined; +void string_optionalAliasArray; + +// string[]`required +out.myapp.string.requiredAliasArray; +let string_requiredAliasArray: typeof out.myapp.string.requiredAliasArray = [ + "", +]; +void string_requiredAliasArray; + +// string +out.myapp.string.optional; +let string_optional: typeof out.myapp.string.optional = undefined as + | string + | undefined; +void string_optional; + +// string`required +out.myapp.string.required; +let string_required: typeof out.myapp.string.required = ""; +void string_required; + +// `string`split_values +out.myapp.string.optionalSplitValues; +let string_optionalSplitValues: typeof out.myapp.string.optionalSplitValues = + undefined as string[] | undefined; +void string_optionalSplitValues; + +// `string`split_values`required +out.myapp.string.requiredSplitValues; +let string_requiredSplitValues: typeof out.myapp.string.requiredSplitValues = [ + "", +]; +void string_requiredSplitValues; + +// date[] +out.myapp.date.optionalAliasArray; +let date_optionalAliasArray: typeof out.myapp.date.optionalAliasArray = + undefined as Date[] | undefined; +void date_optionalAliasArray; + +// date[]`required +out.myapp.date.requiredAliasArray; +let date_requiredAliasArray: typeof out.myapp.date.requiredAliasArray = [ + new Date(), +]; +void date_requiredAliasArray; + +// date +out.myapp.date.optional; +let date_optional: typeof out.myapp.date.optional = undefined as + | Date + | undefined; +void date_optional; + +// date`required +out.myapp.date.required; +let date_required: typeof out.myapp.date.required = new Date(); +void date_required; + +// `date`split_values +out.myapp.date.optionalSplitValues; +let date_optionalSplitValues: typeof out.myapp.date.optionalSplitValues = + undefined as Date[] | undefined; +void date_optionalSplitValues; + +// `date`split_values`required +out.myapp.date.requiredSplitValues; +let date_requiredSplitValues: typeof out.myapp.date.requiredSplitValues = [ + new Date(), +]; +void date_requiredSplitValues; diff --git a/src/envconfig.ts b/src/envconfig.ts index 4bd9d5a..8bb18a1 100644 --- a/src/envconfig.ts +++ b/src/envconfig.ts @@ -8,8 +8,15 @@ import { } from "../deps.ts"; export class ConfigurationInvalidError extends Error {} + export class ArgumentInvalidError extends Error {} +type ConfigValue = undefined | string | { + [key: string]: string | undefined | Config; +}; + +export type Config = Record; + const configErrorMessage = (r: string, k: string, c: string) => `mark: "\`${r}" config: "${c}" on key: "${k}"`; @@ -523,18 +530,116 @@ describe("parseMarksConfig()", () => { }); }); +type IncludesMark< + Code extends string, + Mark extends string, + T = unknown, + F = unknown, +> = Code extends `${infer _}${Mark}${infer _}` ? T : F; + +type SplitValuesMark = + IncludesMark< + `${Code}`, + "`split_values", + T, + F + >; + +type RequiredMark = IncludesMark< + `${Code}`, + "`required", + T, + F +>; + +type GenericTypeMarkExtractor< + Mark extends string, + Code extends string, + T extends + | string + | boolean + | number + | Date, +> = IncludesMark< + `${Code}`, + `\`${Mark}[]`, + RequiredMark<`${Code}`, T[], T[] | undefined>, + IncludesMark< + `${Code}`, + `\`${Mark}`, + RequiredMark< + `${Code}`, + SplitValuesMark<`${Code}`, T[], T>, + SplitValuesMark<`${Code}`, T[] | undefined, T | undefined> + > + > +>; + +type BoolMarkExtractor = GenericTypeMarkExtractor< + "bool", + `${Code}`, + boolean +>; + +type NumberMarkExtractor = GenericTypeMarkExtractor< + "number", + `${Code}`, + number +>; + +type StringMarkExtractor = GenericTypeMarkExtractor< + "string", + `${Code}`, + string +>; + +type DateMarkExtractor = GenericTypeMarkExtractor< + "date", + `${Code}`, + Date +>; + +// type Bool2MarkExtractor = IncludesMark< +// `${Code}`, +// "`bool[]", +// RequiredMark<`${Code}`, boolean[], boolean[] | undefined>, +// IncludesMark< +// `${Code}`, +// "`bool", +// RequiredMark< +// `${Code}`, +// SplitValuesMark<`${Code}`, boolean[], boolean>, +// SplitValuesMark<`${Code}`, boolean[] | undefined, boolean | undefined> +// > +// > +// >; + +type TypeMarkExtractor = + & BoolMarkExtractor<`${Code}`> + & NumberMarkExtractor<`${Code}`> + & StringMarkExtractor<`${Code}`> + & DateMarkExtractor<`${Code}`>; + +type Output = { + [key in keyof Omit]: T[key] extends Config + ? Output + : T[key] extends `${infer _}\`${infer _}` ? TypeMarkExtractor<`${T[key]}`> + : T[key] extends string ? TypeMarkExtractor<`${T[key]}`> + : T[key]; +}; + export const parse = < - T = any, - C = any, + T = unknown, + C extends Config = Config, >( - vars: T, - config: C, + vars?: T, + config?: C, options: { debug?: boolean; prefix?: string[]; suffix?: string[] } = { debug: false, prefix: undefined, suffix: undefined, }, -): any => { +): Output => { if (typeof vars !== "object") { throw new ArgumentInvalidError("vars argument need to be an object"); } @@ -542,7 +647,7 @@ export const parse = < throw new ArgumentInvalidError("config argument need to be an object"); } const __env: any = vars || {}; - const env: Record = options.debug ? { __env } : {}, + const env: any = options.debug ? { __env } : {}, varsKeys = Object.keys(__env), configKeys = Object.keys(config); @@ -891,8 +996,8 @@ describe("parse()", () => { }; const conf = { - myEnvVar: "`split_values`date", - }; + myEnvVar: "`date`split_values", + } as const; const result = parse(env, conf); @@ -911,7 +1016,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`split_values`date`default:1989-05-20T00:00:00.000Z,1989-05-20T00:00:00.000Z,1989-05-20T00:00:00.000Z", - }; + } as const; const result = parse(env, conf); @@ -945,9 +1050,9 @@ describe("parse()", () => { const conf = { myEnvVar: `\`default:${input}\`date`, - }; + } as const; - if (Error !== expected) { + if (expected instanceof Date) { const result = parse(env, conf); assertEquals(result, { myEnvVar: expected }); @@ -963,11 +1068,11 @@ describe("parse()", () => { const conf = { myEnvVar: "`date", - }; + } as const; - if (Error !== expected) { + if (expected instanceof Date) { const result = parse(env, conf); - + result.myEnvVar; assertEquals(result, { myEnvVar: expected }); } else { assertThrows(() => parse(env, conf)); @@ -978,7 +1083,7 @@ describe("parse()", () => { describe("`bool", () => { for ( - const [input, expected] of [ + const [input, expected] of > [ ["true", true], ["yes", true], ["on", true], @@ -997,7 +1102,7 @@ describe("parse()", () => { const conf = { myEnvVar: `\`default:${input}\`bool`, - }; + } as const; const result = parse(env, conf); @@ -1011,7 +1116,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`bool", - }; + } as const; const result = parse(env, conf); @@ -1083,7 +1188,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`string`uppercase", - }; + } as const; const result = parse(env, conf); @@ -1096,7 +1201,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`string", - }; + } as const; const result = parse(env, conf); @@ -1109,7 +1214,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`string`default:hello hello", - }; + } as const; const result = parse(env, conf); @@ -1123,7 +1228,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`default:hello hello`string", - }; + } as const; const result = parse(env, conf); @@ -1137,7 +1242,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`string`lowercase", - }; + } as const; const result = parse(env, conf); @@ -1164,7 +1269,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`split_values`string", - }; + } as const; const result = parse(env, conf); @@ -1178,7 +1283,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`split_values`string`default:1,2,3", - }; + } as const; const result = parse(env, conf); @@ -1260,7 +1365,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`split_values`number", - }; + } as const; const result = parse(env, conf); @@ -1274,7 +1379,7 @@ describe("parse()", () => { const conf = { myEnvVar: "`split_values`number`default:1,2,3", - }; + } as const; const result = parse(env, conf); @@ -1315,9 +1420,9 @@ describe("parse()", () => { const conf = { myEnvVar: `\`default:${input}\`number`, - }; + } as const; - if (Error !== expected) { + if (typeof expected === "number") { const result = parse(env, conf); assertEquals(result, { myEnvVar: expected }); @@ -1338,8 +1443,9 @@ describe("parse()", () => { const conf = { myEnvVar: "`number", - }; - if (Error !== expected) { + } as const; + + if (typeof expected === "number") { const result = parse(env, conf); assertEquals(result, { myEnvVar: expected }); @@ -1388,7 +1494,7 @@ describe("complex marks", () => { superSecret: "`string`env:SECRETS`prefix:myapp`suffix:secret2", }, }, - }; + } as const; // parse marks const parsed = parse(env, config); @@ -1428,7 +1534,7 @@ describe("complex marks", () => { port: "`number", s1: "`string`env:SECRETS_SECRET1", s2: "`string`env:SECRETS_SECRET2", - }; + } as const; const parsed = parse(env, config, { prefix: ["myapp"] }); @@ -1444,7 +1550,7 @@ describe("complex marks", () => { const config = { port: undefined, - }; + } as const; const parsed = parse(env, config, { prefix: ["myapp"] }); @@ -1460,7 +1566,7 @@ describe("complex marks", () => { myapp: { port: undefined, }, - }; + } as const; const parsed = parse(env, config); @@ -1480,7 +1586,7 @@ describe("complex marks", () => { }, }, }, - }; + } as const; const parsed = parse(env, config); @@ -1500,7 +1606,7 @@ describe("complex marks", () => { }, }, }, - }; + } as const; const parsed = parse(env, config); @@ -1525,7 +1631,7 @@ describe("complex marks", () => { }, }, }, - }; + } as const; const parsed = parse(env, config); @@ -1573,7 +1679,7 @@ describe("complex marks", () => { maxIndexKeys: "`string`default:32", }, }, - }; + } as const; const e = parse({}, _envconfig); @@ -1620,7 +1726,7 @@ describe("complex marks", () => { myapp: { port: "`number[]", }, - }; + } as const; const parsed = parse(env, config); @@ -1636,7 +1742,7 @@ describe("complex marks", () => { myapp: { port: "`string[]", }, - }; + } as const; const parsed = parse(env, config); @@ -1652,7 +1758,7 @@ describe("complex marks", () => { myapp: { myFlags: "`env:PORT`bool[]", }, - }; + } as const; const parsed = parse(env, config); @@ -1682,7 +1788,7 @@ describe("complex marks", () => { myapp: { myDates: "`env:PORT`date[]", }, - }; + } as const; const parsed = parse(env, config);