From 221e3256ee7e6f2c22f0d86ef91bc44afa075816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olav=20Sundf=C3=B8r?= Date: Mon, 10 May 2021 23:39:57 +0200 Subject: [PATCH 1/3] playing around with typed env --- packages/common/src/env.ts | 126 +++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 packages/common/src/env.ts diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts new file mode 100644 index 00000000..5be01eee --- /dev/null +++ b/packages/common/src/env.ts @@ -0,0 +1,126 @@ +import { logger } from "./logger/logger"; + +/** + * IF this works, create new module + */ +/* type Specification = + | "string" + | "number" + | string[] + | [min: number, max: number]; +const _env = (input: { [key in T]: Specification }) => {}; + +type SecondSpecification = + | "string" + | "number" + | `between ${number} and ${number}`; + +const _around = ( + input: { [key in SecondSpecification]: string } +) => {}; + +const envs = ["test", "development", "production"] as const; +type Env = typeof envs[number]; + +type MyEnvironment = "PORT" | "API_KEY" | "AGE"; +const env = _env({ + PORT: "number", + AGE: [13, 99], + API_KEY: "string", +}); + */ +/* type Validator = (key: string) => boolean; +const _validator = (input: { [key in T]: Validator }) => { + Object.keys(input).map((key) => { + const validator = input[key] as Validator; + const value = process.env[key]; + if (!validator(value)) + throw `${key} in env did not pass validator. (value: ${value})`; + }); +}; + */ +/* const guarantee = (value: any, boolean: boolean): value is T => boolean; + +const validators = { + string: (value: T): T => { + guarantee(value, typeof value === "string"); + return value; + }, + number: (value: T): number => { + if (!guarantee(value, parseInt(value) !== NaN)) throw ""; + return value; + }, + range: (from: number, to: number) => ( + value: any + ): number => { + if ( + !guarantee( + value, + validators.number(value) && value >= from && value < to + ) + ) + throw ""; + return value; + }, +}; + +_validator({ + PORT: validators.range(8000, 9000), + SERIAL: validators.number, + API_KEY: validators.string, +}); */ + +type Validators = { + number: () => Validator; + string: () => Validator; + range: (from: number, to: number) => Validator; + oneof: (values: T) => Validator; +}; +const validators: Validators = { + number: () => (value: string) => { + //TODO: validate + return () => -1; + }, + string: () => (value: string) => { + //TODO: validate + return () => value; + }, + range: (from: number, to: number) => (value: string) => { + //TODO: value + return () => -1; + }, + oneof: (values: string) => () => { + //TODO: validate + return () => null as T; + }, +}; + +type Validator = (input: string) => () => G; +const _env = ( + input: { [key in T]: Validator } +): { + [key in T]: any; +} => { + return Object.keys(input) + .map((key: T) => ({ key, get: input[key](key) })) + .map((a) => ({ + [a.key]: a.get, + })) + .reduce((a, b) => ({ + ...a, + ...b, + })) as { + [key in T]: any; + }; +}; + +const env = _env({ + PORT: validators.range(100, 200), + API_KEY: validators.range(1, 2), + //API_KEY: validators.string(), +}); + +//FIXME: values are any.. +const getPort = validators.range(1, 2)(process.env.PORT); +const port = getPort(); +console.log(port); From de7b0757a39f4b183e8e00fc590acb9147212c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olav=20Sundf=C3=B8r?= Date: Sat, 15 May 2021 16:55:55 +0000 Subject: [PATCH 2/3] practiced some tdd on env module --- packages/common/src/env.test.ts | 95 +++++++++++++++++++++++++++++++++ packages/common/src/env.ts | 6 +++ 2 files changed, 101 insertions(+) create mode 100644 packages/common/src/env.test.ts create mode 100644 packages/common/src/env.ts diff --git a/packages/common/src/env.test.ts b/packages/common/src/env.test.ts new file mode 100644 index 00000000..cd5ddc62 --- /dev/null +++ b/packages/common/src/env.test.ts @@ -0,0 +1,95 @@ +import { getEnv } from "./env"; +describe("the module for picking up env variables", () => { + it("is a function does not return null", () => { + const env = getEnv({}); + expect(env).not.toBeNull(); + }); + + it("is a function that does not return undefined", () => { + const env = getEnv({}); + expect(env).not.toBeUndefined(); + }); + + it("throws if one of its input is not in the env", () => { + expect(() => { + getEnv({ + NOT_IN_ENV: "string", + }); + }).toThrow(); + }); + + it("throws with appropriate error if key is not in the environment", () => { + expect(() => { + getEnv({ + NOT_IN_ENV: "string", + }); + }).toThrowError(`NOT_IN_ENV was expected to be in environment`); + }); + + const withEnvironment = ( + environment: T, + action: (environment: T) => void + ) => () => { + const previousEnv = process.env; + process.env = (environment as any) as NodeJS.ProcessEnv; + + action(environment); + process.env = previousEnv; + }; + + describe("withEnvironment helper", () => { + it( + "does change process.env", + withEnvironment( + { + TEST: "value", + }, + (environment) => { + expect(process.env.TEST).toEqual("value"); + } + ) + ); + + it( + "does return test key in callback", + withEnvironment( + { + KEY_NAME: "TEST_VALUE", + }, + (environment) => { + expect(environment.KEY_NAME).toEqual("TEST_VALUE"); + } + ) + ); + + it( + "returns environment exactly equal to process.env", + withEnvironment( + { + KEY: "VALUE", + }, + (environment) => { + expect(environment).toEqual(process.env); + } + ) + ); + + it( + "does not have other keys in env than the ones specified", + withEnvironment({ KEY: "VALUE" }, (environment) => { + expect(process.env).toEqual({ KEY: "VALUE" }); + }) + ); + it( + "does not have any other keys in object than the ones specified", + withEnvironment( + { + KEY: "VALUE", + }, + (environment) => { + expect(environment).toEqual({ KEY: "VALUE" }); + } + ) + ); + }); +}); diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts new file mode 100644 index 00000000..b557c725 --- /dev/null +++ b/packages/common/src/env.ts @@ -0,0 +1,6 @@ +export const getEnv = (input: T) => { + Object.keys(input).forEach((key) => { + if (!process.env[key]) throw `${key} was expected to be in environment`; + }); + return {}; +}; From a4cd7b8103199fb6990a223a47c7ea42cae9bc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olav=20Sundf=C3=B8r?= Date: Mon, 24 May 2021 16:05:55 +0200 Subject: [PATCH 3/3] matchers working, FIX: no typing --- packages/common/src/env.test.ts | 82 ++++++++++++++++++++++++++++----- packages/common/src/env.ts | 47 +++++++++++++++++-- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/packages/common/src/env.test.ts b/packages/common/src/env.test.ts index cd5ddc62..49996c0d 100644 --- a/packages/common/src/env.test.ts +++ b/packages/common/src/env.test.ts @@ -1,4 +1,5 @@ -import { getEnv } from "./env"; +import faker from "faker"; +import { getEnv, isString, isInteger } from "./env"; describe("the module for picking up env variables", () => { it("is a function does not return null", () => { const env = getEnv({}); @@ -13,7 +14,7 @@ describe("the module for picking up env variables", () => { it("throws if one of its input is not in the env", () => { expect(() => { getEnv({ - NOT_IN_ENV: "string", + NOT_IN_ENV: isString, }); }).toThrow(); }); @@ -21,21 +22,20 @@ describe("the module for picking up env variables", () => { it("throws with appropriate error if key is not in the environment", () => { expect(() => { getEnv({ - NOT_IN_ENV: "string", + NOT_IN_ENV: isString, }); }).toThrowError(`NOT_IN_ENV was expected to be in environment`); }); - const withEnvironment = ( - environment: T, - action: (environment: T) => void - ) => () => { - const previousEnv = process.env; - process.env = (environment as any) as NodeJS.ProcessEnv; + const withEnvironment = + (environment: T, action: (environment: T) => void) => + () => { + const previousEnv = process.env; + process.env = environment as any as NodeJS.ProcessEnv; - action(environment); - process.env = previousEnv; - }; + action(environment); + process.env = previousEnv; + }; describe("withEnvironment helper", () => { it( @@ -91,5 +91,63 @@ describe("the module for picking up env variables", () => { } ) ); + + it( + "Does return an object with the KEY from input", + withEnvironment({ KEY: "VALUE" }, () => { + const env = getEnv({ KEY: isString }); + expect(env.KEY).toBeDefined(); + expect(env.KEY).toEqual("VALUE"); + }) + ); + + it( + "Accepts key with type of Validator", + withEnvironment({ API_KEY: "some_key" }, () => { + const env = getEnv({ API_KEY: isString }); + expect(env.API_KEY).toBeDefined(); + }) + ); + + it( + "Does not accept an argument that's not a validator", + withEnvironment({ PORT: faker.datatype.number() }, () => { + expect(() => { + //@ts-expect-error + getEnv({ PORT: "NOT_A_VALIDATOR" }); + }).toThrow(); //will throw because "NOT_A_VALIDATOR" is not a function + }) + ); + + it("Does return a value with the expeted type", () => + withEnvironment({ PORT: faker.datatype.number() }, () => { + const acceptingNumber = (input: number) => {}; + const env = getEnv({ PORT: isInteger }); + + //NOTE: if this does not compile, consider test failing + acceptingNumber(env.PORT); + })); + + describe("isInteger validator", () => { + it( + "Fails if is not number", + withEnvironment({ PORT: faker.lorem.word(10) }, (environment) => { + expect(() => { + getEnv({ PORT: isInteger }); + }).toThrowError( + `PORT did not match its validator. Found: ${environment.PORT}` + ); + }) + ); + + it( + "Does not fail if it is an integer", + withEnvironment({ PORT: faker.datatype.number() }, () => { + expect(() => { + getEnv({ PORT: isInteger }); + }).not.toThrow(); + }) + ); + }); }); }); diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index b557c725..f34b6fbb 100644 --- a/packages/common/src/env.ts +++ b/packages/common/src/env.ts @@ -1,6 +1,43 @@ -export const getEnv = (input: T) => { - Object.keys(input).forEach((key) => { - if (!process.env[key]) throw `${key} was expected to be in environment`; - }); - return {}; +type PossibleTypes = string | number; +type Validator = ( + value: any +) => boolean /* value is T */; +//const a: Validator = (p: any): p is string => true; +//a("thing"); + +export const getEnv = ( + input: T +) => + Object.keys(input) + .map((key: G) => { + const actualValue = process.env[key]; + if (!actualValue) throw `${key} was expected to be in environment`; + + const validator = input[key]; + if (!validator(actualValue)) + throw `${key} did not match its validator. Found: ${actualValue}`; + + return { + [key as G]: actualValue, + }; + }) + .reduce((a, b) => ({ + ...a, + ...b, + })); + +export const isString = (input: any): boolean /* input is string */ => + typeof input === "string" && isNaN(parseInt(input)); + +const env = getEnv({ + KEY: isString, +}); + +export const isInteger = (input: any): boolean /* input is number */ => + !isNaN(parseInt(input)); + +const isFloat = (input: any) => parseFloat(input) !== NaN; +const isBetween = (min: number, max: number) => (input: string) => { + const number = parseFloat(input); + return number !== NaN || (number < max && number > min); };