0.0.1Updated 13 days ago
/**
 * A string-indexable object that returns the string value from an environment variable, or undefined if it does not exist.
 */
export const env = new Proxy({} as Record<string, string | undefined>, {
  get(_target, prop, _receiver) {
    return Deno.env.get(prop.toString());
  }
})

export const GenerateNamespaced = (namespace?: string) => {
  const prefix = namespace ? `${namespace}_` : '';

  /**
   * Returns a string from the environment.
   * 
   * Is sugar syntax for `env[key]` or `Deno.env.get(key)`
   */
  function string(key: string): string | undefined
  function string<T extends string | undefined>(key: string, default_value: T): TypedResponse<T, string>
  function string(key: string, default_value?: string) {
    return env[prefix + key] || default_value;
  }
  
  /**
   * ---
   * 
   * Returns `true` if the environment variable is one of the following truthy values: (Case insensitive)
   * 
   * `true`, `yes`, `y`, `on`, `1`
   * 
   * @param key The environment variable to use to find the truthy value.
   */
  function boolean(key: string) {
    const _key = prefix + key;
    return env[_key] !== undefined && ['true', '1', 'yes', 'y', 'on'].includes(env[_key].toLowerCase())
  }

  type TypedResponse<T, S> = T extends S ? S : S | undefined;

  /**
   * ---
   * 
   * Returns a numerical value from an environment variable. If it does not exist or is not a valid number, it returns either the fallback value or undefined.
   * 
   * Accepts a `fallback`, as well as `min` and `max` bounds if required.
   * 
   * @param key The environment variable to use to find the numerical value.
   */
  function number(key: string): number | undefined
  function number<T extends number | undefined>(key: string, default_value: T): TypedResponse<T, number>
  function number<T extends number | undefined>(key: string, default_value: T, min: number, max: number): TypedResponse<T, number>
  function number(key: string, default_value?: number, min?: number, max?: number) {
    let num = Number(env[prefix + key]);
    if(isNaN(num)) return default_value;
    if(typeof min == 'number' && num < min) num = min;
    if(typeof max == 'number' && num > max) num = max;
    return num;
  }

  /**
   * ---
   * 
   * Returns an integral value from an environment variable. If it does not exist or is not a valid integer, it returns either the fallback value or undefined.
   * 
   * Accepts a `fallback`, as well as `min` and `max` bounds if required.
   * 
   * @param key The environment variable to use to find the integer value.
   * @param default_port The fallback value if the environment does not contain the key, or the value is invalid. Default: `8080`
   * @param min The minimum acceptable value to allow
   * @param min The maximum acceptable value to allow
   */
  function integer(key: string): number | undefined
  function integer<T extends number | undefined>(key: string, default_value: T): TypedResponse<T, number>
  function integer<T extends number | undefined>(key: string, default_value: T, min: number, max: number): TypedResponse<T, number>
  function integer(key: string, default_value?: number, min?: number, max?: number) {
    let num = number(key, default_value as number);
    if(!num) return undefined;
  
    if(!Number.isSafeInteger(num) && default_value) num = default_value;
    if(typeof min == 'number' && num < min) num = min;
    if(typeof max == 'number' && num > max) num = max;
    return ~~num;
  }

  /**
   * ---
   * 
   * Returns a port-friendly integral value from an environment variable.
   * 
   * An optional `fallback` argument is accepted. An invalid `fallback` value will throw an error.
   * 
   * @param key The environment variable to use to find the port number. Default: `PORT`
   * @param default_port The fallback value if the environment does not contain the key, or the value is invalid. Default: `8080`
   */
  function port(key: string = 'PORT', default_port: number = 8080): number {
    if(!Number.isSafeInteger(default_port)) throw new Error('Default port is not an integer!');
    if(default_port < 0) throw new Error('Default port is negative');
    if(default_port > 65535) throw new Error('Default port is negative');
  
    const port = integer(key, default_port);
    if(port < 0) return default_port;
    if(port > 65535) return default_port;
  
    return port;
  }

  /**
   * Adds a layer of namespacing. Each namespace is delimited by underscores
   */
  const namespaced = (inner_namespace: string) => GenerateNamespaced(prefix + inner_namespace)

  return {
    string,
    get: string,

    boolean,
    bool: boolean,

    number,
    float: number,

    integer,
    int: integer,

    port,

    namespaced,

    [Symbol.dispose]() {}
  }
}


/**
 * A wrapper containing helpful methods for retrieving environment variables of specific data types, with automatic type formatting, type safety and fallback values.
 */
const ENV = GenerateNamespaced();

export const string = ENV.string;
export const integer = ENV.integer;
export const number = ENV.number;
export const port = ENV.port;
export const boolean = ENV.boolean;

export default ENV;