import {expect, test, describe, beforeEach, afterEach} from 'bun:test' import {type TDate, type TSchema, Type} from "@sinclair/typebox" import {type FileResult, fileSync} from 'tmp' import * as fs from 'node:fs/promises' import {read_date, read_object, read_type} from '../src/read_type' import {parse} from '..' beforeEach(() => process.env = {}) async function testing(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_type(scheme, key) if (should_be_successful) { expect(res).toEqual({ok: true, data}) } else { expect(res).toEqual({ok: false}) } } // Most important function : read_object describe('Object', () => { test('basic', async () => { const scheme = Type.Object({}) const res = await read_object(scheme) expect(res).toEqual({ok: true, data: {}}) }) test('One level', async () => { process.env = { NAME: 'test', PORT: '12', TS: '2025-06-08 23:51:12' } const scheme = Type.Object({name: Type.String(), port: Type.Integer(), ts: Type.Date()}) const res = await read_object(scheme) expect(res).toEqual({ok: true, data: {name: 'test', port: 12, ts: new Date('2025-06-08 23:51:12')}}) }) test('Two levels', async () => { process.env = { DB_PORT: '12', DB_HOST: 'localhost', THING_A: 'Hello!', THING_B: '212', NAME: 'Pascal', AGE: '27', } const expected = { name: 'Pascal', age: 27, db: { port: 12, host: 'localhost' }, thing: { a: 'Hello!', b: 212, } } const scheme = Type.Object({ name: Type.String(), age: Type.Number(), db: Type.Object({ port: Type.Integer(), host: Type.String() }), thing: Type.Object({ a: Type.String(), b: Type.Integer(), }) }) const res = await read_object(scheme) expect(res).toEqual({ok: true, data: expected}) const res2 = await parse(scheme) expect(res2).toEqual(expected) }) test('Three levels', async () => { process.env = { DB_HOST: 'localhost', THING_A: 'Hello!', AGE: '27', THING_THANG_A: '2025-06-06 23:56:12' } const expected = { age: 27, db: { host: 'localhost' }, thing: { a: 'Hello!', thang: { a: new Date('2025-06-06 23:56:12') } } } const scheme = Type.Object({ age: Type.Number(), db: Type.Object({ host: Type.String() }), thing: Type.Object({ a: Type.String(), thang: Type.Object({ a: Type.Date() }) }) }) const res = await read_object(scheme) expect(res).toEqual({ok: true, data: expected}) const res2 = await parse(scheme) expect(res2).toEqual(expected) }) }) // Read any from file describe('from_file', () => { let file: FileResult | undefined = undefined; afterEach(() => { file?.removeCallback() file = undefined }) async function testing(value: string | undefined, scheme: T, should_be_successful: boolean, data?: any) { file = fileSync() const name = file.name if (value !== undefined) { await fs.writeFile(file.name, value) } else { file.removeCallback() file = undefined } const key = 'DB_PORT' process.env[key + '__FILE'] = name const res = await read_type(scheme, key) if (should_be_successful) { expect(res).toEqual({ok: true, data}) } else { expect(res).toEqual({ok: false}) } } test('basic string', () => testing('Coucou', Type.String(), true, 'Coucou')) test('basic number', () => testing('12', Type.Integer(), true, 12)) test('file does not exist', () => testing(undefined, Type.String(), false)) test('Last empty line is trimmed', () => testing('Coucou\n\r\n ', Type.String(), true, 'Coucou')) }) // Read generic types describe('Integer', () => { test('basic', () => testing('12', Type.Integer(), true, 12)) test('not present', () => testing(undefined, Type.Integer(), false)) test('optional', () => testing(undefined, Type.Optional(Type.Integer()), true)) test('default', () => testing(undefined, Type.Integer({default: 21}), true, 21)) test('string', () => testing('coucou', Type.Integer(), false)) test('min-max OK', () => testing('12', Type.Integer({minimum: 11, maximum: 13}), true, 12)) test('min-max KO', () => testing('10', Type.Integer({minimum: 11, maximum: 13}), false, 10)) }) describe('String', () => { test('basic', () => testing('coucou', Type.String(), true, 'coucou')) test('not present', () => testing(undefined, Type.String(), false)) test('optional', () => testing(undefined, Type.Optional(Type.String()), true, undefined)) test('default', () => testing(undefined, Type.String({default: 'yeet'}), true, 'yeet')) test('Length min-max', () => testing('coucou', Type.String({maxLength: 7}), true, 'coucou')) test('KO: Length min-max', () => testing('coucou', Type.String({maxLength: 5}), false)) }) describe('boolean', () => { test('basic true', () => testing('true', Type.Boolean(), true, true)) test('basic 1', () => testing('1', Type.Boolean(), true, true)) test('not present', () => testing(undefined, Type.Boolean(), false)) test('optional', () => testing(undefined, Type.Optional(Type.Boolean()), true)) test('default', () => testing(undefined, Type.Boolean({default: false}), true, false)) }) describe('Enum', () => { enum TestNumber { A, B, C, D } enum TestString { A = "a", B = "b", } test('basic number', () => testing('1', Type.Enum(TestNumber), true, TestNumber.B)) test('number out', () => testing('4', Type.Enum(TestNumber), false)) test('basic string', () => testing('a', Type.Enum(TestString), true, TestString.A)) test('string out', () => testing('c', Type.Enum(TestString), false)) test('number with transform', async () => { const scheme = Type.Transform(Type.String()) .Decode(value => { value = value.toLowerCase() if (value === 'a') return TestNumber.A if (value === 'b') return TestNumber.B if (value === 'c') return TestNumber.C if (value === 'c') return TestNumber.D }) .Encode(value => { if (value === 0) return 'A' if (value === 1) return 'B' if (value === 2) return 'C' return 'D' }) await testing('A', scheme, true, TestNumber.A) }) }) // Read specific types describe('Date', () => { async function testing(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')