add dev resources; plumb GUI_ENV_FILE/API_ENV_FILE/API_DESI_DATA into run targets
CI / Detect changed paths (pull_request) Failing after 33m59s
CI / Infra unit tests, vet, and preview (pull_request) Has been skipped
CI / API unit tests and lint (pull_request) Failing after 31m0s
CI / Odin unit tests and build (pull_request) Failing after 31m9s

This commit is contained in:
2026-09-07 14:55:49 -06:00
parent ba24005bfb
commit 1443369f6c
18 changed files with 490 additions and 58 deletions
+23 -9
View File
@@ -80,13 +80,26 @@ destroy :: proc(env: map[string]string) {
delete(env)
}
// env_key returns the env key that should bind to a struct field. It prefers
// an explicit `env:"NAME"` tag; when the tag is absent or empty it falls
// back to the field's name. Matching against the parsed map is
// case-insensitive, so API_URL maps onto api_url (or an `env:"API_URL"` tag).
@(private)
env_key_for_field :: proc(field: reflect.Struct_Field) -> string {
if tag_key, ok := reflect.struct_tag_lookup(field.tag, "env");
ok && tag_key != "" {
return tag_key
}
return field.name
}
// decode populates dest's fields from env, matching each field by name
// (case-insensitively, so API_URL maps onto api_url). Values are converted
// to the field's type: string is cloned as-is into allocator, integers are
// parsed with strconv.parse_int (decimal/hex/negative), booleans with
// strconv.parse_bool, and floats with strconv.parse_f64. Keys missing from
// env leave the field at its zero value. It returns false if a present value
// cannot be converted to the field's type.
// (or by an `env:"NAME"` struct tag). Values are converted to the field's
// type: string is cloned as-is into allocator, integers are parsed with
// strconv.parse_int (decimal/hex/negative), booleans with strconv.parse_bool,
// and floats with strconv.parse_f64. Keys missing from env leave the field
// at its zero value. It returns false if a present value cannot be converted
// to the field's type.
decode :: proc(
env: map[string]string,
dest: ^$T,
@@ -98,11 +111,12 @@ decode :: proc(
return false
}
name: string
value: string
field_ptr := rawptr(dest)
st: reflect.Struct_Field
for _, i in fields.names[:fields.field_count] {
name = fields.names[i]
st = reflect.struct_field_at(T, i)
name := env_key_for_field(st)
value = ""
found := false
for key, v in env {
@@ -157,7 +171,7 @@ decode :: proc(
return false
}
case:
// unsupported field type (slices, pointers, ...) is left untouched
// unsupported field type (slices, pointers, ...) is left untouched
}
}
@@ -12,6 +12,16 @@ Test_Config :: struct {
ratio: f64,
}
Tagged_Config :: struct {
api_url: string `env:"API_URL"`,
port: int `env:"PORT"`,
debug: bool `env:"DEBUG"`,
}
Tagged_Empty :: struct {
api_url: string `env:""`,
}
// load_env writes src to a unique temp file and parses it via parse_file.
// The returned map owns its strings; callers must destroy it.
load_env :: proc(t: ^testing.T, src: string) -> map[string]string {
@@ -199,3 +209,37 @@ test_decode_hex_and_negative_ints :: proc(t: ^testing.T) {
testing.expect(t, dotenv.decode(env, &cfg))
testing.expect(t, cfg.port == 31, "hex int should decode")
}
@(test)
test_decode_honors_env_tags :: proc(t: ^testing.T) {
env := load_env(t, "API_URL=http://127.0.0.1:8080\nPORT=9090\nDEBUG=true\n")
defer dotenv.destroy(env)
cfg := Tagged_Config{}
testing.expect(t, dotenv.decode(env, &cfg))
defer delete(cfg.api_url)
testing.expect(
t,
cfg.api_url == "http://127.0.0.1:8080",
"env tag should bind API_URL",
)
testing.expect(t, cfg.port == 9090, "env tag should bind PORT")
testing.expect(t, cfg.debug == true, "env tag should bind DEBUG")
}
@(test)
test_decode_empty_env_tag_falls_back_to_field_name :: proc(t: ^testing.T) {
env := load_env(t, "api_url=http://fallback\n")
defer dotenv.destroy(env)
cfg := Tagged_Empty{}
testing.expect(t, dotenv.decode(env, &cfg))
defer delete(cfg.api_url)
testing.expect(
t,
cfg.api_url == "http://fallback",
"empty env tag should use field name",
)
}
+12 -2
View File
@@ -4,11 +4,21 @@ import "core:os"
import dotenv "lib:dotenv/src"
Config :: struct {
api_url: string,
api_url: string `env:"API_URL"`,
}
get_config :: proc(env_file: ^string = nil) -> (^Config, ^Error) {
env, _ := dotenv.parse_file(".env", context.temp_allocator)
path := ".env"
if env_file != nil && env_file^ != "" {
path = env_file^
} else if from_env, ok := os.lookup_env(
"GUI_ENV_FILE",
context.temp_allocator,
); ok {
path = from_env
}
env, _ := dotenv.parse_file(path, context.temp_allocator)
defer dotenv.destroy(env)
c := new(Config)