diff --git a/packages/common/src/env.test.ts b/packages/common/src/env.test.ts new file mode 100644 index 00000000..49996c0d --- /dev/null +++ b/packages/common/src/env.test.ts @@ -0,0 +1,153 @@ +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({}); + 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: isString, + }); + }).toThrow(); + }); + + it("throws with appropriate error if key is not in the environment", () => { + expect(() => { + getEnv({ + 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; + + 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" }); + } + ) + ); + + 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 new file mode 100644 index 00000000..f34b6fbb --- /dev/null +++ b/packages/common/src/env.ts @@ -0,0 +1,43 @@ +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); +}; diff --git a/packages/common/src/renamed_env_before_merge.ts b/packages/common/src/renamed_env_before_merge.ts new file mode 100644 index 00000000..5be01eee --- /dev/null +++ b/packages/common/src/renamed_env_before_merge.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);