Skip to content

Commit

Permalink
fix(tests): oh so many tests
Browse files Browse the repository at this point in the history
  • Loading branch information
djMax committed Oct 17, 2023
1 parent 027ded7 commit ca2fb35
Show file tree
Hide file tree
Showing 28 changed files with 1,238 additions and 59 deletions.
4 changes: 4 additions & 0 deletions __tests__/config/child.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "child",
"foo": "a value"
}
15 changes: 15 additions & 0 deletions __tests__/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "config",
"foo": "config:imported.foo",
"bar": "config:foo",
"baz": "config:path.to.nested.value",
"path": {
"to": {
"nested": {
"value": "config:value"
}
}
},
"value": false,
"imported": "import:./child.json"
}
4 changes: 4 additions & 0 deletions __tests__/config/error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "error",
"foo": "config:not.a.value"
}
5 changes: 5 additions & 0 deletions __tests__/defaults/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"default": "config",
"override": "config",
"misc": "path:./config.json"
}
4 changes: 4 additions & 0 deletions __tests__/defaults/development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"override": "development",
"path": "path:./development.json"
}
4 changes: 4 additions & 0 deletions __tests__/defaults/supplemental.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"override": "supplemental",
"path": "path:./supplemental.json"
}
5 changes: 5 additions & 0 deletions __tests__/import/child.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "child",
"grandchild": "import:./grandchild",
"grandchildJson": "import:./grandchild.json"
}
4 changes: 4 additions & 0 deletions __tests__/import/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "parent",
"child": "import:./child.json"
}
4 changes: 4 additions & 0 deletions __tests__/import/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "default",
"foo": "bar"
}
4 changes: 4 additions & 0 deletions __tests__/import/grandchild.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "grandchild",
"secret": "santa"
}
4 changes: 4 additions & 0 deletions __tests__/import/missing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "missing",
"child": "import:./orphan.json"
}
8 changes: 8 additions & 0 deletions __tests__/import/override.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"child" : {
"grandchild": {
"name": "surprise",
"another": "claus"
}
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@types/caller": "^1.0.0",
"@types/minimist": "^1.2.3",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.8.0",
Expand All @@ -64,6 +65,7 @@
"vitest": "^0.34.6"
},
"dependencies": {
"caller": "^1.1.0",
"comment-json": "^4.2.3",
"minimist": "^1.2.8"
}
Expand Down
40 changes: 40 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { merge } from './common';

import { IntermediateConfigValue } from '.';

export type ConfitDeepKeys<T> = {
[P in keyof T]: P extends string
? T[P] extends object
Expand Down Expand Up @@ -44,6 +48,10 @@ export class Config<ConfigurationSchema extends object> {
return undefined as ConfitPathValue<ConfigurationSchema, P>;
}

getUntypedValue(path: string) {
return this.getValue(path as ConfitDeepKeys<ConfigurationSchema>, false);
}

/**
* Get a value from the configuration store. Keys should be separated with
* colons. For example to get the value of c on object b on object a of the root
Expand Down Expand Up @@ -109,4 +117,36 @@ export class Config<ConfigurationSchema extends object> {

return;
}

setUntyped(path: string, value: IntermediateConfigValue) {
const pathParts = path.split(':');
let current: object = this.store as object;
while (pathParts.length - 1) {
const prop = pathParts.shift() as string;
if (!Object.prototype.hasOwnProperty.call(current, prop)) {
(current as Record<string, object>)[prop] = {};
}

current = (current as Record<string, object>)[prop];
if (current?.constructor !== Object) {
// Do not allow traversal into complex types,
// such as Buffer, Date, etc. So, this type
// of key will fail: 'foo:mystring:length'
return;
}
}
(current as Record<string, typeof value>)[pathParts.shift() as string] = value;
}

use(config: Partial<ConfigurationSchema>) {
return merge(config, this.store);
}

merge(config: Config<ConfigurationSchema>) {
return this.use(config.store);
}

toJSON() {
return JSON.stringify(this.store);
}
}
92 changes: 57 additions & 35 deletions src/Factory.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,91 @@
import path from 'path';

import { ConfitOptions, ShortstopHandler } from './types';
import caller from 'caller';

import { BaseConfitType, ConfitOptions, IntermediateConfigValue, ShortstopHandler } from './types';
import { Config } from './Config';
import { isAbsolutePath, loadJsonc, merge } from './common';
import { argv, convenience, environmentVariables } from './provider';
import { resolveImport } from './handlers';
import { resolveConfig, resolveCustom, resolveImport } from './handlers';

export class Factory<ConfigurationType extends object> {
private basedir: string;
private protocols: Record<string, ShortstopHandler<unknown>>;
private promise: Promise<ConfigurationType>;
private protocols: Record<string, ShortstopHandler | ShortstopHandler[]>;
private promise: Promise<ConfigurationType & BaseConfitType>;

constructor(options: ConfitOptions) {
const excludedEnvVariables = [...(options.excludeEnvVariables || []), 'env'];
this.protocols = options.protocols || {};
this.basedir = options.basedir;
this.basedir = options.basedir || path.dirname(caller());
this.promise = Promise.resolve({})
.then((store) => merge(convenience(), store))
.then(Factory.conditional((store) => {
const jsonPath = path.join(this.basedir, options.defaults || 'config.json');
return loadJsonc(jsonPath)
.then((json) => resolveImport(json as ConfigurationType, this.basedir))
.then((data) => merge(data, store));
}))
.then(Factory.conditional((store) => {
const jsonPath = path.join(this.basedir, `${store.env.env}.json`);
return loadJsonc(jsonPath)
.then((json) => resolveImport(json as ConfigurationType, this.basedir))
.then((data) => merge(data, store))
}))
.then((store) => merge(environmentVariables(options.excludeEnvVariables || []), store))
.then(
Factory.conditional((store) => {
const jsonPath = path.join(this.basedir, options.defaults || 'config.json');
return loadJsonc(jsonPath)
.then((json) => resolveImport(json, this.basedir))
.then((data) => merge(data, store));
}),
)
.then(
Factory.conditional((store) => {
const jsonPath = path.join(this.basedir, `${store.env.env}.json`);
return loadJsonc(jsonPath)
.then((json) => resolveImport(json as IntermediateConfigValue, this.basedir))
.then((data) => merge(data, store));
}),
)
.then((store) => merge(environmentVariables(excludedEnvVariables), store))
.then((store) => merge(argv(), store));
}

private async resolveFile(pathOrConfig: string | ConfigurationType): Promise<ConfigurationType> {
private async resolveFile(pathOrConfig: string | Partial<ConfigurationType>): Promise<ConfigurationType> {
if (typeof pathOrConfig === 'string') {
const file = isAbsolutePath(pathOrConfig) ? pathOrConfig : path.join(this.basedir, pathOrConfig);
return loadJsonc(file) as ConfigurationType;
const file = isAbsolutePath(pathOrConfig)
? pathOrConfig
: path.join(this.basedir, pathOrConfig);
return loadJsonc(file) as Promise<ConfigurationType>;
}
return path as ConfigurationType;
return pathOrConfig as unknown as ConfigurationType;
}

private add(fileOrDirOrConfig: string | ConfigurationType, fn: (store: ConfigurationType, update: ConfigurationType) => ConfigurationType) {
const dataPromise = this.resolveFile(fileOrDirOrConfig).then((data) => resolveImport(data, this.basedir));
this.promise = Promise.all([this.promise, dataPromise]).then(([store, data]) => fn(store, data));
private add(
fileOrDirOrConfig: string | Partial<ConfigurationType>,
fn: (store: ConfigurationType, update: ConfigurationType) => ConfigurationType & BaseConfitType,
) {
const dataPromise = this.resolveFile(fileOrDirOrConfig).then((data) =>
resolveImport(data, this.basedir),
);
this.promise = Promise.all([this.promise, dataPromise]).then(([store, data]) =>
fn(store, data as ConfigurationType & BaseConfitType),
);
}

addDefault(dir: string | ConfigurationType) {
this.add(dir, (store, data) => merge(store, data));
addDefault(pathOrConfig: string | Partial<ConfigurationType>) {
this.add(pathOrConfig, (store, data) => merge(store, data));
return this;
}

addOverride(dir: string) {
this.add(dir, (store, data) => merge(data, store));
addOverride(pathOrConfig: string | Partial<ConfigurationType>) {
this.add(pathOrConfig, (store, data) => merge(data, store));
return this;
}

async create() {
const finalStore = await this.promise;
return new Config<ConfigurationType>(finalStore);
const finalStore = await this.promise
.then((store) => resolveImport(store, this.basedir))
.then((store) => resolveCustom(store, this.protocols))
.then((store) => resolveConfig(store));
return new Config<ConfigurationType & BaseConfitType>(finalStore as ConfigurationType & BaseConfitType);
}

static conditional<ConfigurationType extends object, R>(fn: (store: ConfigurationType) => R) {
return (store: ConfigurationType) => {
static conditional<ConfigurationType extends BaseConfitType, R>(
fn: (store: ConfigurationType) => R,
) {
return async (store: ConfigurationType) => {
try {
return fn(store);
const result = await fn(store);
return result;
} catch (error) {
const err = error as { code?: string };
if (err.code && err.code === 'MODULE_NOT_FOUND') {
Expand All @@ -72,6 +94,6 @@ export class Factory<ConfigurationType extends object> {
}
throw err;
}
}
};
}
}
Loading

0 comments on commit ca2fb35

Please sign in to comment.