Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95be9d8d4a |
@@ -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
@@ -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
@@ -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
@@ -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')
|
||||
Reference in New Issue
Block a user