Skip to content

Commit

Permalink
Merge pull request #9 from joschi/yaml-adapter
Browse files Browse the repository at this point in the history
feat: add YAML adapter
  • Loading branch information
alexmarqs authored Oct 3, 2024
2 parents 03eb3b0 + 6802dbc commit a32feda
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 4 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ jobs:

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8


- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ yarn add zod-config zod # yarn
- [Built In Adapters](#built-in-adapters)
- [Env Adapter](#env-adapter)
- [JSON Adapter](#json-adapter)
- [YAML Adapter](#yaml-adapter)
- [Dotenv Adapter](#dotenv-adapter)
- [Script Adapter](#script-adapter)
- [Directory Adapter](#directory-adapter)
Expand Down Expand Up @@ -156,6 +157,41 @@ const customConfig = await loadConfig({
});
```

#### YAML Adapter

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

```bash
npm install yaml
```

```ts
import { z } from 'zod';
import { loadConfig } from 'zod-config';
import { yamlAdapter } from 'zod-config/yaml-adapter';

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

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

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

// using filter prefix key
const customConfig = await loadConfig({
schema: schemaConfig,
adapters: yamlAdapter({
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
17 changes: 16 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
"import": "./dist/directory-adapter.mjs",
"module": "./dist/directory-adapter.mjs",
"require": "./dist/directory-adapter.js"
},
"./yaml-adapter": {
"types": "./dist/yaml-adapter.d.ts",
"import": "./dist/yaml-adapter.mjs",
"module": "./dist/yaml-adapter.mjs",
"require": "./dist/yaml-adapter.js"
}
},
"typesVersions": {
Expand All @@ -79,6 +85,9 @@
],
"directory-adapter": [
"./dist/directory-adapter.d.ts"
],
"yaml-adapter": [
"./dist/yaml-adapter.d.ts"
]
}
},
Expand All @@ -98,6 +107,7 @@
"config",
"env",
"json",
"yaml",
"dotenv",
"typescript",
"adapters",
Expand All @@ -115,14 +125,19 @@
},
"peerDependencies": {
"dotenv": ">=15",
"yaml": "^2.x",
"zod": "^3.x"
},
"peerDependenciesMeta": {
"dotenv": {
"optional": true
},
"yaml": {
"optional": true
}
},
"engines": {
"node": ">=14.0.0"
}
},
"packageManager": "[email protected]"
}
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

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

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

export type YamlAdapterProps = {
path: string;
prefixKey?: string;
silent?: boolean;
};

const ADAPTER_NAME = "yaml adapter";

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

const parsedData = YAML.parse(data) || {};

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

return parsedData;
} catch (error) {
throw new Error(
`Failed to parse / read YAML file at ${path}: ${
error instanceof Error ? error.message : error
}`,
);
}
},
silent,
};
};
133 changes: 133 additions & 0 deletions tests/yaml-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { yamlAdapter } from "@/lib/adapters/yaml-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";
import YAML from "yaml";

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

beforeAll(async () => {
await writeFile(testFilePath, YAML.stringify({ HOST: "localhost", PORT: "3000" }));
});

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

it("should return parsed data when schema is valid", async () => {
// given
const schema = z.object({
HOST: z.string(),
PORT: z.string().regex(/^\d+$/),
});

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

// then
expect(config.HOST).toBe("localhost");
expect(config.PORT).toBe("3000");
});
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: yamlAdapter({
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: yamlAdapter({
path: "not-exist.yaml",
}),
}),
).rejects.toThrowError(z.ZodError);

expect(consoleErrorSpy).toHaveBeenCalledWith(
"Cannot read data from yaml adapter: Failed to parse / read YAML file at not-exist.yaml: ENOENT: no such file or directory, open 'not-exist.yaml'",
);
});
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: yamlAdapter({
path: "not-exist.yaml",
}),
logger: customLogger,
}),
).rejects.toThrowError(z.ZodError);

expect(customLoggerWarnSpy).toHaveBeenCalledWith(
"Cannot read data from yaml adapter: Failed to parse / read YAML file at not-exist.yaml: ENOENT: no such file or directory, open 'not-exist.yaml'",
);
});
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: yamlAdapter({
path: "not-exist.yaml",
silent: true,
}),
}),
).rejects.toThrowError(z.ZodError);

expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig([
"dotenv-adapter": "src/lib/adapters/dotenv-adapter/index.ts",
"script-adapter": "src/lib/adapters/script-adapter/index.ts",
"directory-adapter": "src/lib/adapters/directory-adapter/index.ts",
"yaml-adapter": "src/lib/adapters/yaml-adapter/index.ts",
},
format: ["cjs", "esm"],
target: "node14",
Expand Down

0 comments on commit a32feda

Please sign in to comment.