Skip to content

Commit

Permalink
feat(cli): add cli package (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
tugrulates authored Feb 13, 2025
1 parent fc7e9eb commit 638a690
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 0 deletions.
82 changes: 82 additions & 0 deletions core/cli/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { basename, dirname, join } from "@std/path";

/**
* A key-value config store for the process.
*
* By default, the config store is persisted to a file in the user's home
* directory. It can be made in-memory by passing `path = ":memory:"` to the
* constructor. This allows for an easy setup for testing.
*
* @example
* ```ts
* import { Config } from "@roka/cli/config";
* import { assertEquals } from "@std/assert";
* using config = new Config<{ foo: string, bar: string }>({ path: ":memory:" });
* await config.set({ foo: "foo" });
* await config.set({ bar: "bar" });
* assertEquals(await config.get(), { foo: "foo", bar: "bar" });
* ```
*/
export class Config<T extends Record<string, unknown>> {
private kv: Deno.Kv | undefined;

/**
* Creates a new config store.
*
* @param name Name of the config store.
* @param options Configuration options.
* @param options.path The path to the database file.
*/
constructor(private readonly options?: { path?: string }) {}

/** Returns all data stored for this config. */
async get(): Promise<Partial<T>> {
const kv = await this.open();
const result = {} as Record<string, unknown>;
for await (const { key, value } of kv.list({ prefix: [] })) {
const [property] = key;
if (property) result[property.toString()] = value;
}
return result as Partial<T>;
}

/** Writes data to the configuration. Prior data is not deleted. */
async set(value: Partial<T>): Promise<void> {
const kv = await this.open();
const set = kv.atomic();
for (const property of Object.getOwnPropertyNames(value)) {
set.set([property], value[property]);
}
await set.commit();
}

/** Clear all stored configuration data. */
async clear(): Promise<void> {
const kv = await this.open();
const del = kv.atomic();
for await (const { key } of kv.list({ prefix: [] })) {
del.delete(key);
}
await del.commit();
}

/** Open the db connection to the persistent data. */
private async open(): Promise<Deno.Kv> {
if (!this.kv) {
const path = this.options?.path ??
join(
Deno.env.get("HOME") ?? ".",
"." + basename(dirname(Deno.mainModule)),
"config.db",
);
await Deno.mkdir(dirname(path), { recursive: true });
this.kv = await Deno.openKv(path);
}
return this.kv;
}

/** Close the db connection to the persistent data. */
[Symbol.dispose]() {
this.kv?.close();
}
}
57 changes: 57 additions & 0 deletions core/cli/config_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Config } from "@roka/cli/config";
import { assertEquals } from "@std/assert";

Deno.test("Config stores values", async () => {
type ConfigType = { foo: string; bar: string };
using config = new Config<ConfigType>({ path: ":memory:" });
await config.set({ foo: "value_foo", bar: "value_bar" });
assertEquals(await config.get(), { foo: "value_foo", bar: "value_bar" });
});

Deno.test("Config sets values partially", async () => {
type ConfigType = { foo: string; bar: string };
using config = new Config<ConfigType>({ path: ":memory:" });
await config.set({ foo: "value_foo" });
assertEquals(await config.get(), { foo: "value_foo" });
await config.set({ foo: "value_foo", bar: "value_bar" });
assertEquals(await config.get(), { foo: "value_foo", bar: "value_bar" });
});

Deno.test("Config isolates multiple configs", async () => {
type ConfigType = { foo: string; bar: string };
using config1 = new Config<ConfigType>({ path: ":memory:" });
using config2 = new Config<ConfigType>({ path: ":memory:" });
await config1.set({ foo: "value_foo" });
await config2.set({ bar: "value_bar" });
assertEquals(await config1.get(), { foo: "value_foo" });
assertEquals(await config2.get(), { bar: "value_bar" });
});

Deno.test("Config clears values", async () => {
type ConfigType = { foo: string; bar: string };
using config = new Config<ConfigType>({ path: ":memory:" });
await config.set({ foo: "value_foo", bar: "value_bar" });
await config.clear();
assertEquals(await config.get(), {});
});

Deno.test("Config stores numbers", async () => {
type ConfigType = { foo: number };
using config = new Config<ConfigType>({ path: ":memory:" });
await config.set({ foo: 5 });
assertEquals(await config.get(), { foo: 5 });
});

Deno.test("Config stores booleans", async () => {
type ConfigType = { foo: boolean };
using config = new Config<ConfigType>({ path: ":memory:" });
await config.set({ foo: true });
assertEquals(await config.get(), { foo: true });
});

Deno.test("Config stores objects", async () => {
type ConfigType = { foo: { bar: { baz: string } } };
using config = new Config<ConfigType>({ path: ":memory:" });
await config.set({ foo: { bar: { baz: "value" } } });
assertEquals(await config.get(), { foo: { bar: { baz: "value" } } });
});
6 changes: 6 additions & 0 deletions core/cli/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@roka/cli",
"exports": {
"./config": "./config.ts"
}
}
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"workspace": [
"./core/cli",
"./core/git",
"./core/github",
"./core/http",
Expand Down

0 comments on commit 638a690

Please sign in to comment.