-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fc7e9eb
commit 638a690
Showing
4 changed files
with
146 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } } }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "@roka/cli", | ||
"exports": { | ||
"./config": "./config.ts" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
{ | ||
"workspace": [ | ||
"./core/cli", | ||
"./core/git", | ||
"./core/github", | ||
"./core/http", | ||
|