1 Commits

Author SHA1 Message Date
pascal 95be9d8d4a Dump WIP 2025-08-14 17:31:04 +02:00
4 changed files with 42 additions and 73 deletions
+8 -8
View File
@@ -1,14 +1,13 @@
import {type Static, type TObject, type TProperties} from '@sinclair/typebox'
import {Value} from '@sinclair/typebox/value'
import {z, type ZodObject, type ZodRawShape} from 'zod'
import logger from 'log'
import {read_object} from './src/read_type'
const log = logger('config')
export {Type, type Static, type StaticDecode} from '@sinclair/typebox' // Re-export Type so users can describe config
export {Value} from '@sinclair/typebox/value'
// TODO : export {Type, type Static, type StaticDecode} from '@sinclair/typebox' // Re-export Type so users can describe config
// TODO : export {Value} from '@sinclair/typebox/value'
export async function parse<T extends TProperties>(scheme: TObject<T>): Promise<Static<TObject<T>> | null> {
export async function parse<T extends ZodRawShape>(scheme: ZodObject<T>): Promise<z.infer<ZodObject<T>> | null> {
log.info("Parse configuration from env")
log.debug("Read configuration from env")
@@ -17,12 +16,13 @@ export async function parse<T extends TProperties>(scheme: TObject<T>): Promise<
// 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) {
const config_parsed = await scheme.safeParseAsync(config.data)
if (config_parsed.success) {
log.trace("Config is valid")
return config.data
return config_parsed.data
} else {
log.warn("Invalid config, check failed")
log.debug("Error :", config_parsed.error)
return null
}
}
+2 -3
View File
@@ -1,7 +1,8 @@
{
"dependencies": {
"@sinclair/typebox": "^0.34.33",
"log": "git+https://git.pband.ch/typescript/log.git"
"log": "git+https://git.pband.ch/typescript/log.git",
"zod": "^3.25.60"
},
"devDependencies": {
"@types/bun": "^1.2.15",
@@ -12,12 +13,10 @@
"tmp": "^0.2.3",
"typescript": "^5.8.3"
},
"scripts": {
"test": "bun test",
"check": "tsc --noEmit"
},
"name": "config",
"description": "Parse env to create a configuration from a description",
"version": "1.0.0",
+19 -43
View File
@@ -1,13 +1,4 @@
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 z, {type $ZodObject, $ZodType} from 'zod/v4/core'
import logger from 'log'
import type {Ok} from './helpers'
@@ -17,24 +8,23 @@ 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>>>> {
export async function read_object<T extends $ZodObject>(scheme: T, base_name: string = "") : Promise<Ok<z.infer<T>>> {
log.debug("Read object from env")
if (base_name.length !== 0) base_name = base_name + "_"
const data: Static<TObject<T>> = Value.Create(scheme)
const data: z.infer<T> = Object.create({})
for (const key in scheme.properties) {
for (const key in scheme.shape) {
const sub_key = base_name + key.toUpperCase()
// Get sub-scheme
const sub_scheme = scheme.properties[key]
const sub_scheme = scheme.shape[key]
const sub_type = sub_scheme.def.type as string
// 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)
if (sub_type === 'object') {
value = await read_object(sub_scheme as any, sub_key)
//} else if (sub_type === 'Date') {
//value = await read_date(sub_scheme as any, sub_key)
} else {
value = await read_type(sub_scheme, sub_key)
}
@@ -52,37 +42,23 @@ export async function read_object<T extends TProperties>(scheme: TObject<T>, bas
return {ok: true, data}
}
export async function read_type<T extends TSchema>(scheme: T, name: string) : Promise<Ok<Static<T> | undefined>> {
export async function read_type<T extends $ZodType>(scheme: T, name: string) : Promise<Ok<z.infer<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}
if (value.data === undefined && scheme.isOptional()) return {ok: true, data: undefined}
try {
const data = Value.Parse(scheme, value.data)
return {ok: true, data}
const data = await scheme.safeParseAsync(value.data)
if (data.success) return {ok: true, data: data.data}
else {
log.warn(`Parsing of '${name}' failed`)
log.debug('Error :', data.error)
return {ok: false}
}
} 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}
}
+13 -19
View File
@@ -1,9 +1,10 @@
import {expect, test, describe, beforeEach, afterEach} from 'bun:test'
import {type TDate, type TSchema, Type} from "@sinclair/typebox"
import {type TSchema, Type} from "@sinclair/typebox"
import {type FileResult, fileSync} from 'tmp'
import * as fs from 'node:fs/promises'
import {z} from 'zod/v4'
import {read_date, read_object, read_type} from '../src/read_type'
import {read_object, read_type} from '../src/read_type'
import {parse} from '..'
beforeEach(() => process.env = {})
@@ -21,6 +22,16 @@ async function testing<T extends TSchema>(value: string | undefined, scheme: T,
}
}
test('tweak', async () => {
const ts = '2025-06-11 09:36:12'
process.env['NAME'] = 'coucou'
process.env['TS'] = ts
process.env['PORT'] = '212'
const scheme = z.object({name: z.string(), port: z.number(), ts: z.date()})
const res = await read_object(scheme)
expect(res).toEqual({ok: true, data: {name: 'coucou', port: 212, ts: new Date(ts)}})
})
// Most important function : read_object
describe('Object', () => {
test('basic', async () => {
@@ -219,22 +230,5 @@ describe('Enum', () => {
})
// Read specific types
describe('Date', () => {
async function testing<T extends TDate>(value: string | undefined, scheme: T, should_be_successful: boolean, data?: any) {
const key = 'DB_PORT'
if (value !== undefined) process.env[key] = value
const res = await read_date(scheme, key)
if (should_be_successful) {
expect(res).toEqual({ok: true, data})
} else {
expect(res).toEqual({ok: false})
}
}
test('basic', () => testing('2025-06-08 22:40:17', Type.Date(), true, new Date('2025-06-08 22:40:17')))
test('optional', () => testing(undefined, Type.Optional(Type.Date()), true))
})
describe.todo('Array')
describe.todo('Tuple')