Implement the whole thing

This commit is contained in:
2025-06-09 00:00:13 +02:00
parent ab7c8a3576
commit 5886934182
6 changed files with 163 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
# Config
Parse a configuration from env values using a description scheme (using Zod).
Keys are the name given in the TypeBox scheme, in capital case, separated with an underscore. Sub-objects' names will start with parent's name, followed by an underscore then the key.
**Any** key can be followed by `__FILE`. In this case, the given value is expected to be a path and the content of the file will be used to be the content of the config. The content of the file will be trimmed from any whitespace character. The file is read using UTF-8 encoding.
+27
View File
@@ -0,0 +1,27 @@
import {type Static, type TObject, type TProperties} from '@sinclair/typebox'
import {Value} from '@sinclair/typebox/value'
import logger from 'log'
import {read_object} from './src/read_type'
const log = logger('config')
export {Type} from '@sinclair/typebox' // Re-export Type so users can describe config
export async function parse<T extends TProperties>(scheme: TObject<T>): Promise<Static<TObject<T>> | null> {
log.info("Parse configuration from env")
log.debug("Read configuration from env")
const config = await read_object(scheme)
if (!config.ok) return null
// This check is kind of a duplicate, is it useful ?
log.debug("Validate config against scheme")
const config_parsed = Value.Check(scheme, config.data)
if (config_parsed) {
log.trace("Config is valid")
return config.data
} else {
log.warn("Invalid config, check failed")
return null
}
}
+3 -1
View File
@@ -1,13 +1,15 @@
{
"dependencies": {
"@sinclair/typebox": "^0.34.33",
"dotenv": "^16.5.0",
"log": "git+https://git.pband.ch/typescript/log.git"
},
"devDependencies": {
"@types/bun": "^1.2.15",
"@types/node": "^22.15.30",
"@types/tmp": "^0.2.6",
"bun": "^1.2.15",
"logger-console": "git+https://git.pband.ch/typescript/logger-console.git",
"tmp": "^0.2.3",
"typescript": "^5.8.3"
},
+1
View File
@@ -0,0 +1 @@
export type Ok<T> = {ok: true, data: T} | {ok: false}
+38
View File
@@ -0,0 +1,38 @@
import logger from 'log'
import fs from 'node:fs/promises'
import type {Ok} from './helpers'
const log = logger('config:read_env')
const FILE_EXT = "__FILE"
// Read from env or file
export async function read_env(key: string): Promise<Ok<string | undefined>> {
const path = process.env[key + FILE_EXT]
if (path !== undefined) return from_file(path)
else return {ok: true, data: process.env[key]}
}
async function from_file(path: string): Promise<Ok<string>> {
log.debug("Read a key from a file")
log.trace("Path :", path)
let content = await get_file_content(path)
if (!content.ok) return content
content.data = content.data.trim()
return content
}
async function get_file_content(path: string) : Promise<Ok<string>> {
log.debug('Read file content')
log.trace('Path :', path)
try {
const data = await fs.readFile(path, {encoding: 'utf-8'})
return {ok: true, data}
} catch (e) {
log.warn('Failed to read file', path)
log.debug('Error :', e)
return {ok: false}
}
}
+88
View File
@@ -0,0 +1,88 @@
import {
OptionalKind,
Type,
type Static,
type TDate,
type TObject,
type TProperties,
type TSchema, type TString
} from '@sinclair/typebox'
import {Value} from '@sinclair/typebox/value'
import logger from 'log'
import type {Ok} from './helpers'
import {read_env} from './read_env'
const log = logger('config:read_type')
// Read each TypeBox type from env
export async function read_object<T extends TProperties>(scheme: TObject<T>, base_name: string = "") : Promise<Ok<Static<TObject<T>>>> {
log.debug("Read object from env")
if (base_name.length !== 0) base_name = base_name + "_"
const data: Static<TObject<T>> = Value.Create(scheme)
for (const key in scheme.properties) {
const sub_key = base_name + key.toUpperCase()
// Get sub-scheme
const sub_scheme = scheme.properties[key]
// Read value
let value: Ok<unknown>;
if (sub_scheme.type === 'object') {
value = await read_object(sub_scheme as unknown as TObject<T>, sub_key)
} else if (sub_scheme.type === 'Date') {
value = await read_date(sub_scheme as unknown as TDate, sub_key)
} else {
value = await read_type(sub_scheme, sub_key)
}
// If read failed, early return the error
if (!value.ok) return value
// If optional, don't store useless data
if (value.data === undefined) continue
// Set value
// @ts-expect-error : it would be nice to fix this :)
data[key] = value.data
}
return {ok: true, data}
}
export async function read_type<T extends TSchema>(scheme: T, name: string) : Promise<Ok<Static<T> | undefined>> {
let value = await read_env(name)
if (!value.ok) return value
if (value.data === undefined && scheme[OptionalKind] !== undefined) return {ok: true, data: undefined}
try {
const data = Value.Parse(scheme, value.data)
return {ok: true, data}
} catch (e) {
log.warn(`Parsing of key '${name}' failed (invalid value : '${value.data}')`)
log.debug('Error :', e)
return {ok: false}
}
}
export async function read_date(scheme: TDate, name: string): Promise<Ok<Static<TDate> | undefined>> {
let string_scheme: TString;
if (scheme[OptionalKind] !== undefined) string_scheme = Type.Optional(Type.String())
else string_scheme = Type.String()
const value = await read_type(string_scheme, name)
if (!value.ok) return value
if (value.data === undefined) return {ok: true, data: undefined}
const data = new Date(value.data)
const checked = Value.Check(scheme, data)
if (!checked) {
log.warn(`Invalid date for '${name}' : ${value.data}`)
return {ok: false}
}
return {ok: true, data}
}