ssube/salty-dog

View on GitHub
src/config/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
import { doesExist, NotFoundError } from '@apextoaster/js-utils';
import { Stream } from 'bunyan';
import { LogLevel } from 'noicejs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

import { YamlParser } from '../parser/YamlParser.js';
import { readSource } from '../source.js';

export const CONFIG_ENV = 'SALTY_HOME';

export interface ConfigData {
  data: {
    logger: {
      level: LogLevel;
      name: string;
      streams: Array<Stream>;
    };
  };
}

/**
 * Path to project root directory.
 */
export function dirName(): string {
  if (doesExist(import.meta) && doesExist(import.meta.url)) {
    return join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
  } else {
    return join(dirname(process.argv[1]), '..', '..');
  }
}

/**
 * With the given name, generate all potential config paths in their complete, absolute form.
 *
 * This will include the value of `SALTY_HOME`, `HOME`, the current working directory, and any extra paths
 * passed as the final arguments.
 */
export function completePaths(name: string, extras: Array<string>): Array<string> {
  const paths = [];

  const env = process.env[CONFIG_ENV];
  if (typeof env === 'string') {
    paths.push(join(env, name));
  }

  const home = process.env.HOME;
  if (typeof home === 'string') {
    paths.push(join(home, name));
  }

  const cwd = dirName();
  if (cwd !== '') {
    paths.push(join(cwd, name));
  }

  for (const e of extras) {
    paths.push(join(e, name));
  }

  return paths;
}

export async function loadConfig(name: string, ...extras: Array<string>): Promise<ConfigData> {
  const paths = completePaths(name, extras);

  for (const p of paths) {
    const data = await readConfig(p);
    if (doesExist(data)) {
      const parser = new YamlParser();
      const [head] = parser.parse({
        data,
        path: p,
      });

      return head.data as ConfigData; // TODO: validate config
    }
  }

  throw new NotFoundError('unable to load config');
}

export function errorCode(err: unknown): string | undefined {
  if (err instanceof Error) {
    return (err as NodeJS.ErrnoException).code; // === 'ENOENT'
  }

  /* eslint-disable-next-line sonarjs/no-redundant-jump */
  return;
}

export async function readConfig(path: string): Promise<string | undefined> {
  try {
    // need to await this read to catch the error, need to catch the error to check the code
    /* eslint-disable-next-line sonarjs/prefer-immediate-return */
    const data = await readSource(path);
    return data;
  } catch (err) {
    if (errorCode(err) === 'ENOENT') {
      return;
    } else {
      throw err;
    }
  }
}