feat(core): utility - env getters
ADD
packages/core/src/+internal/utils/env.util.ts
+import * as IO from 'fp-ts/IO';
+import { Option, fromNullable, getOrElse } from 'fp-ts/Option';
+import { pipe } from 'fp-ts/function';
+
+function memoize<A>(ma: IO.IO<A>): IO.IO<A> {
+ let cache: A;
+ let isMemoized = false;
+
+ return () => {
+ if (!isMemoized) {
+ cache = ma();
+ isMemoized = true;
+ }
+
+ return cache;
+ };
+}
+
+/**
+ * Read env config (but only once, value is cached)
+ *
+ * @param key - env variable to read.
+ * @see getEnvConfigOrElse
+ */
+export const getEnvConfig = (envKey: string): IO.IO<Option<string>> =>
+ memoize(() => fromNullable(process.env[envKey]));
+
+/**
+ * Read env config with fallback value in case it is not defined.
+ *
+ * @param envKey
+ * @param onNone
+ * @see getEnvConfig
+ */
+export const getEnvConfigOrElse = (envKey: string, onNone: string): IO.IO<string> =>
+ pipe(
+ getEnvConfig(envKey),
+ IO.map(getOrElse(() => onNone)),
+ );
+
+/**
+ * Read env config using #getEnvConfigOrElse and converts the value to boolean
+ * defined value must be "true" (case insensitive) to be true.
+ *
+ * @param envKey
+ * @param onNone
+ * @see getEnvConfigOrElse
+ */
+export const getEnvConfigOrElseAsBoolean = (envKey: string, onNone: 'false' | 'true' | true | false): IO.IO<boolean> =>
+ pipe(
+ getEnvConfigOrElse(envKey, String(onNone)),
+ IO.map(val => val.toLowerCase() === 'true'),
+ );

MODIFY
packages/core/src/+internal/utils/index.ts
packages/core/src/+internal/utils/index.ts
@@ -4,3 +4,4 @@ export * from './error.util';
export * from './any.util';
export * from './stream.util';
export * from './type.util';
+export * from './env.util';

ADD
packages/core/src/+internal/utils/spec/env.util.spec.ts
+import { IO } from 'fp-ts/IO';
+import { pipe } from 'fp-ts/function';
+import { Option, getOrElse } from 'fp-ts/Option';
+import { getEnvConfig, getEnvConfigOrElse, getEnvConfigOrElseAsBoolean } from '../env.util';
+
+beforeEach(() => delete process.env.Foo);
+
+test('#getEnvConfig - reads env once', () => {
+ const getFooEnv: IO<Option<string>> = getEnvConfig('Foo');
+
+ process.env.Foo = 'Foo';
+ expect(pipe(getFooEnv(), getOrElse(() => 'Env Foo not defined'))).toBe('Foo');
+
+ delete process.env.Foo;
+ // env variable is only read once so it should return the old value
+ expect(pipe(getFooEnv(), getOrElse(() => 'Env Foo not defined'))).toBe('Foo');
+});
+
+test('#getEnvConfig - reads nullable env once', () => {
+ const getFooEnv: IO<Option<string>> = getEnvConfig('Foo');
+
+ delete process.env.Foo;
+ expect(pipe(getFooEnv(), getOrElse(() => 'Env Foo not defined'))).toBe('Env Foo not defined');
+
+ process.env.Foo = 'Foo';
+ // env variable is only read once so it should return the old value
+ expect(pipe(getFooEnv(), getOrElse(() => 'Env Foo not defined'))).toBe('Env Foo not defined');
+});
+
+test('#getEnvConfigOrElse - reads nullable env once', () => {
+ const getFooEnv: IO<string> = getEnvConfigOrElse('Foo', 'Fallback Value');
+
+ process.env.Foo = 'Foo';
+ expect(getFooEnv()).toBe('Foo');
+
+ delete process.env.Foo;
+ expect(getFooEnv()).toBe('Foo');
+});
+
+test('#getEnvConfigOrElse - reads nullable env with fallback once', () => {
+ const getFooEnv: IO<string> = getEnvConfigOrElse('Foo', 'Fallback Value');
+
+ delete process.env.Foo;
+ expect(getFooEnv()).toBe('Fallback Value');
+
+ process.env.Foo = 'Foo';
+ expect(getFooEnv()).toBe('Fallback Value');
+});
+
+test('#getEnvConfigOrElseAsBoolean - reads truthy env with fallback once', () => {
+ const getFooEnv: IO<boolean> = getEnvConfigOrElseAsBoolean('Foo', false);
+
+ process.env.Foo = 'true';
+ expect(getFooEnv()).toBe(true);
+
+ delete process.env.Foo;
+ expect(getFooEnv()).toBe(true);
+});
+
+test('#getEnvConfigOrElseAsBoolean - reads falsy env with fallback once', () => {
+ const getFooEnv: IO<boolean> = getEnvConfigOrElseAsBoolean('Foo', true);
+
+ process.env.Foo = 'FALSE';
+ expect(getFooEnv()).toBe(false);
+
+ delete process.env.Foo;
+ expect(getFooEnv()).toBe(false);
+});
+
+test('#getEnvConfigOrElseAsBoolean - reads boolean env with fallback once', () => {
+ const getFooEnv: IO<boolean> = getEnvConfigOrElseAsBoolean('Foo', false);
+
+ delete process.env.Foo;
+ expect(getFooEnv()).toBe(false);
+
+ process.env.Foo = 'true';
+ expect(getFooEnv()).toBe(false);
+});
+

feat(core): utility - env getters