Skip to content

Commit

Permalink
Expanding test coverage (#70)
Browse files Browse the repository at this point in the history
* expanding test coverage

* lint

* config coverate

* config & utils

* tests

* sliding

* bump
  • Loading branch information
zoe-codez authored Sep 28, 2024
1 parent b282db4 commit 5171fb7
Show file tree
Hide file tree
Showing 17 changed files with 1,539 additions and 832 deletions.
41 changes: 14 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,36 @@

[![codecov](https://codecov.io/github/Digital-Alchemy-TS/core/graph/badge.svg?token=IBGLY3RY68)](https://codecov.io/github/Digital-Alchemy-TS/core)
[![version](https://img.shields.io/github/package-json/version/Digital-Alchemy-TS/core)](https://www.npmjs.com/package/@digital-alchemy/core)

---

# 🚀 Project Overview - Digital Alchemy TypeScript Libraries

Welcome to `@digital-alchemy/core`!
This repository contains the boilerplate code for building applications and wiring logic.

- [Extended docs](https://docs.digital-alchemy.app/docs/core/)
- [Discord](https://discord.gg/JkZ35Gv97Y)

## 🌐 Core Library Overview
Welcome to `@digital-alchemy/core`

### 🗃️ Cache
The core library is a minimal dependency framework for building Typescript based applications and libraries.
It aims to provide some basic workflows and tools that can be used to create a variety of different types of projects.

Flexible caching solutions with support for memory and Redis cache drivers.
> 📚 [Extended docs](https://docs.digital-alchemy.app/docs/core/)
### ⚙️ Configuration
## 🌐 Tools Overview

Dynamic application settings management, prioritizing configuration from multiple sources for maximum flexibility.
### ⚙️ [Configuration](https://docs.digital-alchemy.app/docs/core/configuration)

### 🌍 Fetch Wrapper
Define and load structured configuration data from files, merge data from environment variables, and more.

Simplifies RESTful service integration, streamlining the development of verbose adapters.

### 📝 Logger
### 📝 [Logger](https://docs.digital-alchemy.app/docs/core/logger/api)

Advanced logging interface for detailed and customizable output, compatible with external libraries for specialized logging needs.

### ⏲️ Scheduler
### ⏲️ [Scheduler](https://docs.digital-alchemy.app/docs/core/scheduler)

Lifecycle-aware task scheduling, featuring flexible timing functions and robust error handling.

### 🔄 Application Lifecycle Management

Comprehensive support for managing the application's lifecycle, from initialization to shutdown.
### 🔄 [Lifecycle Hooks](https://docs.digital-alchemy.app/docs/core/lifecycle)

## 📚 Explore Our Libraries and Applications
Run commands at a variety of predetermined times during your application's boot or shutdown sequence.

Dive deeper into the ecosystem by exploring related libraries and applications published under the Digital Alchemy organization.
Each is designed to extend the capabilities the core library, offering specialized functionalities for a wide range of use cases.
### ⁉️ [Testing Utilities](https://docs.digital-alchemy.app/docs/testing/)

| Library/Application | Description | GitHub | npm |
| ------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
| `hass` | Home Assistant integration for smart home automation. | [GitHub](https://github.com/Digital-Alchemy-TS/hass) | [npm](https://www.npmjs.com/package/@digital-alchemy/hass) |
| `synapse` | Tools for generating entities within home assistant | [GitHub](https://github.com/Digital-Alchemy-TS/synapse) | [npm](https://www.npmjs.com/package/@digital-alchemy/synapse) |
| `automation` | Advanced automation tools for creating dynamic workflows. | [GitHub](https://github.com/Digital-Alchemy-TS/automation) | [npm](https://www.npmjs.com/package/@digital-alchemy/automation) |
Convert your application into a testing module - append extra libraries and reconfigure modules to get the coverage you're looking for.
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default [
"unicorn/switch-case-braces": "off",
"unicorn/prefer-module": "off",
"@typescript-eslint/no-magic-numbers": "warn",
"unicorn/no-process-exit": "off",
"unicorn/no-object-as-default-parameter": "off",
"@cspell/spellchecker": ["warn", { checkComments: false, autoFix: true }],
"unicorn/no-null": "off",
Expand Down
21 changes: 10 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"repository": {
"url": "git+https://github.com/Digital-Alchemy-TS/core"
},
"version": "24.9.3",
"version": "24.9.4",
"author": {
"url": "https://github.com/zoe-codez",
"name": "Zoe Codez"
Expand Down Expand Up @@ -46,43 +46,42 @@
"engines": {
"node": ">=20"
},
"peerDependencies": {
"dependencies": {
"chalk": "^5",
"dayjs": "^1",
"dotenv": "^16",
"ini": "^4",
"js-yaml": "^4",
"minimist": "^1",
"node-cron": "^3",
"uuid": "*"
"uuid": "^9 || ^10"
},
"devDependencies": {
"@cspell/eslint-plugin": "^8.14.4",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.10.0",
"@faker-js/faker": "^9.0.1",
"@eslint/js": "^9.11.1",
"@faker-js/faker": "^9.0.3",
"@jest/globals": "^29.7.0",
"@types/dotenv": "^8.2.0",
"@types/ini": "^4.1.1",
"@types/jest": "^29.5.13",
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/node": "^22.5.5",
"@types/node": "^22.7.3",
"@types/node-cron": "^3.0.11",
"@types/semver": "^7.5.8",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "8.6.0",
"@typescript-eslint/parser": "8.6.0",
"@typescript-eslint/eslint-plugin": "8.7.0",
"@typescript-eslint/parser": "8.7.0",
"chalk": "^5.3.0",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"eslint": "9.10.0",
"eslint": "9.11.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-no-unsanitized": "^4.1.0",
"eslint-plugin-no-unsanitized": "^4.1.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand Down
57 changes: 26 additions & 31 deletions src/helpers/config-file-loader.helper.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { existsSync, readFileSync, statSync } from "fs";
import { decode } from "ini";
import { load } from "js-yaml";
import fs from "fs";
import ini from "ini";
import yaml from "js-yaml";
import minimist from "minimist";
import { homedir } from "os";
import { join } from "path";
import { cwd, exit, platform } from "process";
import { cwd, platform } from "process";

import { deepExtend, INVERT_VALUE, is, ServiceMap, START } from "..";
import {
AbstractConfig,
ConfigLoaderParams,
ConfigLoaderReturn,
ModuleConfiguration,
} from "./config.helper";
import { deepExtend, INVERT_VALUE, is, PartialConfiguration, ServiceMap, START } from "..";
import { ConfigLoaderParams, ConfigLoaderReturn, ModuleConfiguration } from "./config.helper";

const isWindows = platform === "win32";

export const SUPPORTED_CONFIG_EXTENSIONS = ["json", "ini", "yaml", "yml"];
function withExtensions(path: string): string[] {
export function withExtensions(path: string): string[] {
return [path, join(path, "config")].flatMap(path => [
path,
...SUPPORTED_CONFIG_EXTENSIONS.map(i => `${path}.${i}`),
]);
}

export function configFilePaths(name = "digital-alchemy"): string[] {
export function configFilePaths(name: string): string[] {
const out: string[] = [];
if (!isWindows) {
out.push(...withExtensions(join(`/etc`, `${name}`)));
Expand All @@ -40,10 +35,10 @@ export function configFilePaths(name = "digital-alchemy"): string[] {
current = next;
}
out.push(...withExtensions(join(homedir(), ".config", name)));
return out.filter(filePath => existsSync(filePath) && statSync(filePath).isFile());
return out.filter(filePath => fs.existsSync(filePath) && fs.statSync(filePath).isFile());
}

export async function ConfigLoaderFile<
export async function configLoaderFile<
S extends ServiceMap = ServiceMap,
C extends ModuleConfiguration = ModuleConfiguration,
>({ application, logger }: ConfigLoaderParams<S, C>): ConfigLoaderReturn {
Expand All @@ -52,45 +47,45 @@ export async function ConfigLoaderFile<
let files: string[];
if (is.empty(configFile)) {
files = configFilePaths(application.name);
logger.trace({ files, name: ConfigLoaderFile }, `identified config files`);
logger.trace({ files, name: configLoaderFile }, `identified config files`);
} else {
if (!existsSync(configFile)) {
if (!fs.existsSync(configFile)) {
logger.fatal(
{ configFile, name: ConfigLoaderFile },
{ configFile, name: configLoaderFile },
`used {--config} to specify path that does not exist`,
);
exit();
process.exit();
}
files = [configFile];
logger.debug(
{ configFile, name: ConfigLoaderFile },
{ configFile, name: configLoaderFile },
`used {--config}, loading from target file`,
);
}

if (is.empty(files)) {
return {};
}
const out: Partial<AbstractConfig> = {};
logger.trace({ files, name: ConfigLoaderFile }, `loading configuration files`);
const out: Partial<PartialConfiguration> = {};
logger.trace({ files, name: configLoaderFile }, `loading configuration files`);
files.forEach(file => loadConfigFromFile(out, file));
return out;
}

function loadConfigFromFile(out: Partial<AbstractConfig>, filePath: string) {
const fileContent = readFileSync(filePath, "utf8").trim();
export function loadConfigFromFile(out: PartialConfiguration, filePath: string) {
const fileContent = fs.readFileSync(filePath, "utf8").trim();
const hasExtension = SUPPORTED_CONFIG_EXTENSIONS.some(extension => {
if (filePath.slice(extension.length * INVERT_VALUE).toLowerCase() === extension) {
switch (extension) {
case "ini":
deepExtend(out, decode(fileContent) as unknown as AbstractConfig);
deepExtend(out, ini.decode(fileContent) as PartialConfiguration);
return true;
case "yaml":
case "yml":
deepExtend(out, load(fileContent) as AbstractConfig);
deepExtend(out, yaml.load(fileContent) as PartialConfiguration);
return true;
case "json":
deepExtend(out, JSON.parse(fileContent) as unknown as AbstractConfig);
deepExtend(out, JSON.parse(fileContent) as PartialConfiguration);
return true;
}
}
Expand All @@ -101,19 +96,19 @@ function loadConfigFromFile(out: Partial<AbstractConfig>, filePath: string) {
}
// Guessing JSON
if (fileContent[START] === "{") {
deepExtend(out, JSON.parse(fileContent) as unknown as AbstractConfig);
deepExtend(out, JSON.parse(fileContent) as PartialConfiguration);
return;
}
// Guessing yaml
try {
const content = load(fileContent);
const content = yaml.load(fileContent);
if (is.object(content)) {
deepExtend(out, content as unknown as AbstractConfig);
deepExtend(out, content as PartialConfiguration);
return;
}
} catch {
// Is not a yaml file
}
// Final fallback: INI
deepExtend(out, decode(fileContent) as unknown as AbstractConfig);
deepExtend(out, ini.decode(fileContent) as PartialConfiguration);
}
13 changes: 8 additions & 5 deletions src/helpers/config.helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dotenv from "dotenv";
import fs from "fs";
import { ParsedArgs } from "minimist";
import { isAbsolute, join, normalize } from "path";
import path from "path";
import { cwd } from "process";

import {
Expand Down Expand Up @@ -147,7 +147,7 @@ export type ConfigLoader = <S extends ServiceMap, C extends OptionalModuleConfig
params: ConfigLoaderParams<S, C>,
) => ConfigLoaderReturn;

export function cast<T = unknown>(data: string | string[], type: string): T {
export function cast<T = unknown>(data: boolean | number[] | string | string[], type: string): T {
switch (type) {
case "boolean": {
data ??= "";
Expand Down Expand Up @@ -208,7 +208,8 @@ export function loadDotenv(
CLI_SWITCHES: ParsedArgs,
logger: ILogger,
) {
let { envFile } = internal.boot.options ?? {};
internal.boot.options ??= {};
let { envFile } = internal.boot.options;
const switchKeys = Object.keys(CLI_SWITCHES);
const searched = iSearchKey("env-file", switchKeys);

Expand All @@ -221,7 +222,9 @@ export function loadDotenv(

// * was provided an --env-file or something via boot
if (!is.empty(envFile)) {
const checkFile = isAbsolute(envFile) ? normalize(envFile) : join(cwd(), envFile);
const checkFile = path.isAbsolute(envFile)
? path.normalize(envFile)
: path.join(cwd(), envFile);
if (fs.existsSync(checkFile)) {
file = checkFile;
} else {
Expand All @@ -231,7 +234,7 @@ export function loadDotenv(

// * attempt default file
if (is.empty(file)) {
const defaultFile = join(cwd(), ".env");
const defaultFile = path.join(cwd(), ".env");
if (fs.existsSync(defaultFile)) {
file = defaultFile;
} else {
Expand Down
18 changes: 9 additions & 9 deletions src/helpers/extend.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function isSpecificValue(value: unknown) {
return value instanceof Date || value instanceof RegExp;
}

function cloneSpecificValue(value: unknown) {
export function cloneSpecificValue(value: unknown) {
if (value instanceof Date) {
return new Date(value.getTime());
}
Expand All @@ -17,25 +17,25 @@ function cloneSpecificValue(value: unknown) {
export function deepCloneArray<TYPE = unknown>(array: Array<TYPE>): Array<TYPE> {
// eslint-disable-next-line sonarjs/function-return-type
return array.map(item => {
if (is.array(item)) {
return deepCloneArray(item);
}
if (isSpecificValue(item)) {
return cloneSpecificValue(item);
}
if (is.object(item)) {
if (is.array(item)) {
return deepCloneArray(item);
}
if (isSpecificValue(item)) {
return cloneSpecificValue(item);
}
return deepExtend({}, item);
}
return item;
}) as Array<TYPE>;
}

function safeGetProperty(object: unknown, key: string) {
export function safeGetProperty(object: unknown, key: string) {
return key === "__proto__" ? undefined : (object as Record<string, unknown>)[key];
}

export function deepExtend<A, B>(target: A, object: B): A & B {
if (typeof object !== "object" || object === null || is.array(object)) {
if (!is.object(object)) {
return target as A & B;
}
Object.keys(object).forEach(key => {
Expand Down
Loading

0 comments on commit 5171fb7

Please sign in to comment.