Skip to content

Commit

Permalink
feat: add toml support / adapter, implements FR #17
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmarqs committed Jan 31, 2025
1 parent 0630f22 commit 5272125
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 10 deletions.
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ yarn add zod-config zod # yarn
- [Env Adapter](#env-adapter)
- [JSON Adapter](#json-adapter)
- [YAML Adapter](#yaml-adapter)
- [TOML Adapter](#toml-adapter)
- [Dotenv Adapter](#dotenv-adapter)
- [Script Adapter](#script-adapter)
- [Directory Adapter](#directory-adapter)
Expand Down Expand Up @@ -192,6 +193,41 @@ const customConfig = await loadConfig({
});
```

#### TOML Adapter

Loads the configuration from a `toml` file. In order to use this adapter, you need to install `smol-toml` (peer dependency), if you don't have it already.

```bash
npm install smol-toml
```

```ts
import { z } from 'zod';
import { loadConfig } from 'zod-config';
import { tomlAdapter } from 'zod-config/toml-adapter';

const schemaConfig = z.object({
port: z.string().regex(/^\d+$/),
host: z.string(),
});

const filePath = path.join(__dirname, 'config.toml');

const config = await loadConfig({
schema: schemaConfig,
adapters: tomlAdapter({ path: filePath }),
});

// using filter prefix key
const customConfig = await loadConfig({
schema: schemaConfig,
adapters: tomlAdapter({
path: filePath,
prefixKey: 'MY_APP_',
}),
});
```

#### Dotenv Adapter

Loads the configuration from a `.env` file. In order to use this adapter, you need to install `dotenv` (peer dependency), if you don't have it already.
Expand Down Expand Up @@ -406,7 +442,6 @@ const config = await loadConfig({
```



## Contributing notes

The goal is to have a helper to load configuration data from several srouces. If you have any source in mind, feel free to open a PR to add it or just open an issue to discuss it. More adapters are coming soon.
Expand Down
17 changes: 16 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
"import": "./dist/yaml-adapter.mjs",
"module": "./dist/yaml-adapter.mjs",
"require": "./dist/yaml-adapter.js"
},
"./toml-adapter": {
"types": "./dist/toml-adapter.d.ts",
"import": "./dist/toml-adapter.mjs",
"module": "./dist/toml-adapter.mjs",
"require": "./dist/toml-adapter.js"
}
},
"typesVersions": {
Expand All @@ -88,6 +94,9 @@
],
"yaml-adapter": [
"./dist/yaml-adapter.d.ts"
],
"toml-adapter": [
"./dist/toml-adapter.d.ts"
]
}
},
Expand All @@ -108,6 +117,7 @@
"env",
"json",
"yaml",
"toml",
"dotenv",
"typescript",
"adapters",
Expand All @@ -117,10 +127,12 @@
"@biomejs/biome": "1.7.3",
"@types/node": "20.10.7",
"dotenv": "16.4.1",
"smol-toml": "1.3.1",
"standard-version": "9.5.0",
"tsup": "6.7.0",
"typescript": "5.1.3",
"vitest": "1.1.3",
"yaml": "2.7.0",
"zod": "3.22.4"
},
"peerDependencies": {
Expand All @@ -134,10 +146,13 @@
},
"yaml": {
"optional": true
},
"smol-toml": {
"optional": true
}
},
"engines": {
"node": ">=14.0.0"
},
"packageManager": "[email protected]"
}
}
22 changes: 14 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions src/lib/adapters/toml-adapter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Adapter } from "../../../types";
import { filterByPrefixKey } from "../utils";
import { readFile } from "node:fs/promises";
import { parse as tomlParse } from "smol-toml";

export type TomlAdapterProps = {
path: string;
prefixKey?: string;
silent?: boolean;
};
const ADAPTER_NAME = "toml adapter";

export const tomlAdapter = ({ path, prefixKey, silent }: TomlAdapterProps): Adapter => {
return {
name: ADAPTER_NAME,
read: async () => {
try {
const data = await readFile(path, "utf-8");

const parsedData = tomlParse(data) || {};

if (prefixKey) {
return filterByPrefixKey(parsedData, prefixKey);
}

return parsedData;
} catch (error) {
throw new Error(
`Failed to parse / read TOML file at ${path}: ${
error instanceof Error ? error.message : error
}`,
);
}
},
silent,
};
};
177 changes: 177 additions & 0 deletions tests/toml-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { tomlAdapter } from "@/lib/adapters/toml-adapter";
import { loadConfig } from "@/lib/config";
import type { Logger } from "@/types";
import { unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { z } from "zod";

describe("toml adapter", () => {
const testFilePath = path.join(__dirname, "test-toml-adapter.toml");

beforeAll(async () => {
await writeFile(
testFilePath,
`
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
API_KEY = "secret123"
DEBUG = true
[[kv_namespaces]]
binding = "MY_KV"
id = "xxx111xxx"
preview_id = "xxx222xxx"
[triggers]
crons = ["0 0 * * *"]
`,
);
});

afterAll(async () => {
await unlink(testFilePath);
});

it("should return parsed data when schema is valid", async () => {
// given
const schema = z.object({
name: z.string(),
main: z.string(),
compatibility_date: z.string(),
vars: z.object({
API_KEY: z.string(),
DEBUG: z.boolean(),
}),
kv_namespaces: z.array(
z.object({
binding: z.string(),
id: z.string(),
preview_id: z.string(),
}),
),
triggers: z.object({
crons: z.array(z.string()),
}),
});

// when
const config = await loadConfig({
schema,
adapters: tomlAdapter({
path: testFilePath,
}),
});

// then
expect(config.name).toBe("my-worker");
expect(config.main).toBe("src/index.ts");
expect(config.compatibility_date).toBe("2024-01-01");
expect(config.vars.API_KEY).toBe("secret123");
expect(config.vars.DEBUG).toBe(true);
expect(config.kv_namespaces).toEqual([
{
binding: "MY_KV",
id: "xxx111xxx",
preview_id: "xxx222xxx",
},
]);
expect(config.triggers.crons).toEqual(["0 0 * * *"]);
});
it("should throw zod error when schema is invalid", async () => {
// given
const schema = z.object({
HOST: z.string(),
PORT: z.number(),
});

// when
// then
expect(
loadConfig({
schema,
adapters: tomlAdapter({
path: testFilePath,
}),
}),
).rejects.toThrowError(z.ZodError);
});
it("should log error from adapter errors + throw zod error when schema is invalid", async () => {
// given
const schema = z.object({
HOST: z.string(),
PORT: z.number(),
});
const consoleErrorSpy = vi.spyOn(console, "warn");

// when
// then
await expect(
loadConfig({
schema,
adapters: tomlAdapter({
path: "not-exist.toml",
}),
}),
).rejects.toThrowError(z.ZodError);

expect(consoleErrorSpy).toHaveBeenCalledWith(
"Cannot read data from toml adapter: Failed to parse / read TOML file at not-exist.toml: ENOENT: no such file or directory, open 'not-exist.toml'",
);
});
it("should log error from adapter errors (custom logger) + throw zod error when schema is invalid", async () => {
// given
const schema = z.object({
HOST: z.string(),
PORT: z.number(),
});

const customLogger: Logger = {
warn: (_msg) => {},
};

const customLoggerWarnSpy = vi.spyOn(customLogger, "warn");

// when
// then
await expect(
loadConfig({
schema,
adapters: tomlAdapter({
path: "not-exist.toml",
}),
logger: customLogger,
}),
).rejects.toThrowError(z.ZodError);

expect(customLoggerWarnSpy).toHaveBeenCalledWith(
"Cannot read data from toml adapter: Failed to parse / read TOML file at not-exist.toml: ENOENT: no such file or directory, open 'not-exist.toml'",
);
});
it("throw zod error when schema is invalid but not log error from adapter errors when silent is true", async () => {
// given
const schema = z.object({
HOST: z.string(),
PORT: z.number(),
});

const consoleErrorSpy = vi.spyOn(console, "warn");

// when
// then
expect(
loadConfig({
schema,
adapters: tomlAdapter({
path: "not-exist.toml",
silent: true,
}),
}),
).rejects.toThrowError(z.ZodError);

expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
Loading

0 comments on commit 5272125

Please sign in to comment.