WIP: feat: api connection #6
@@ -7,3 +7,5 @@ resources/ai/sessions
|
||||
infra/Pulumi.*.yaml.backup
|
||||
infra/desi-explorer-infra
|
||||
api/target/
|
||||
.env
|
||||
.env.*
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ build-web: ## WebAssembly build -> build/web (needs emscripten)
|
||||
|
||||
test: ## Run Odin unit tests
|
||||
@mkdir -p lib/local
|
||||
$(ODIN) test src $(ODIN_FLAGS)
|
||||
$(ODIN) test test $(ODIN_FLAGS)
|
||||
|
||||
clean: ## Remove build artifacts
|
||||
rm -rf $(BIN) build
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package dotenv
|
||||
|
||||
import "core:os"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
|
||||
// parse parses dotenv-format source (KEY=VALUE lines) into a map allocated
|
||||
// with allocator. Blank lines, lines starting with '#', and lines without a
|
||||
// '=' are skipped. Keys and values are trimmed; values may be wrapped in
|
||||
// double quotes. Precedence with the real process environment is handled by
|
||||
// the get_* accessors (real env vars win over the file).
|
||||
parse :: proc(src: string, allocator := context.allocator) -> map[string]string {
|
||||
result := make(map[string]string, allocator)
|
||||
|
||||
it := src
|
||||
for line in strings.split_lines_iterator(&it) {
|
||||
tr := strings.trim_space(line)
|
||||
if len(tr) == 0 || strings.has_prefix(tr, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
eq := strings.index_byte(tr, '=')
|
||||
if eq < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.trim_space(tr[:eq])
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
value := strings.trim_space(tr[eq + 1:])
|
||||
if len(value) >= 2 && value[0] == '"' && value[len(value) - 1] == '"' {
|
||||
value = value[1:len(value) - 1]
|
||||
}
|
||||
|
||||
// clone so the map outlives the source buffer (e.g. a freed file read)
|
||||
result[strings.clone(key, allocator)] = strings.clone(value, allocator)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parse_file reads a dotenv file from disk and parses it. It returns
|
||||
// (nil, false) when the file cannot be read (e.g. it does not exist).
|
||||
parse_file :: proc(filename: string, allocator := context.allocator) -> (map[string]string, bool) {
|
||||
data, err := os.read_entire_file(filename, allocator)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer delete(data)
|
||||
|
||||
return parse(string(data), allocator), true
|
||||
}
|
||||
|
||||
// destroy frees the cloned keys/values and the map itself. Use it to release
|
||||
// a map returned by parse/parse_file (plain delete does not free the strings).
|
||||
destroy :: proc(env: map[string]string) {
|
||||
for key, value in env {
|
||||
delete(key)
|
||||
delete(value)
|
||||
}
|
||||
delete(env)
|
||||
}
|
||||
|
||||
// get_string returns the value for key from the real process environment if
|
||||
// set, otherwise from the parsed dotenv map (or "" if neither has it).
|
||||
get_string :: proc(env: map[string]string, key: string) -> string {
|
||||
if value, found := os.lookup_env(key, context.temp_allocator); found {
|
||||
return value
|
||||
}
|
||||
return env[key]
|
||||
}
|
||||
|
||||
// get_bool resolves key and parses it as a boolean (true/false, 1/0,
|
||||
// yes/no, on/off). Missing or unparsable values yield default.
|
||||
get_bool :: proc(env: map[string]string, key: string, default := false) -> bool {
|
||||
if v := get_string(env, key); v != "" {
|
||||
if parsed, ok := strconv.parse_bool(v); ok {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return default
|
||||
}
|
||||
|
||||
// get_int resolves key and parses it as an integer. Missing or unparsable
|
||||
// values yield default.
|
||||
get_int :: proc(env: map[string]string, key: string, default := 0) -> int {
|
||||
if v := get_string(env, key); v != "" {
|
||||
if parsed, ok := strconv.parse_int(v); ok {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return default
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import "lib:dotenv"
|
||||
|
||||
Config :: struct {
|
||||
api_url: string,
|
||||
}
|
||||
|
||||
get_config :: proc() -> (^Config, ^Error) {
|
||||
env, _ := dotenv.parse_file(".env", context.temp_allocator)
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
c := new(Config)
|
||||
|
||||
c.api_url = dotenv.get_string(env, "API_URL")
|
||||
|
||||
if err := validate_config(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
validate_config :: proc(c: ^Config) -> ^Error {
|
||||
err := new(Error)
|
||||
if c.api_url == "" do err.type = .Config; err.message = "'API_URL' is required"
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
ErrorType :: enum {
|
||||
Config,
|
||||
API,
|
||||
}
|
||||
|
||||
Error :: struct {
|
||||
type: ErrorType,
|
||||
message: string,
|
||||
}
|
||||
|
||||
format_error :: proc(err: ^Error) -> string {
|
||||
sb := strings.builder_make(context.temp_allocator)
|
||||
|
||||
return fmt.sbprintf(&sb, "[%s] => %s", err.type, err.message)
|
||||
}
|
||||
|
||||
+8
-7
@@ -24,6 +24,13 @@ universe: [dynamic]Galaxy
|
||||
rng: rand.Default_Random_State
|
||||
|
||||
main :: proc() {
|
||||
|
||||
c: ^Config
|
||||
|
||||
if c, err := get_config(); err != nil {
|
||||
panic(format_error(err))
|
||||
}
|
||||
|
||||
rng = rand.create(0xDE51_0000)
|
||||
context.random_generator = rand.default_random_generator(&rng)
|
||||
|
||||
@@ -103,11 +110,5 @@ draw :: proc() {
|
||||
}
|
||||
|
||||
rl.DrawFPS(10, 10)
|
||||
rl.DrawText(
|
||||
"DESI Explorer — drag to rotate, scroll to zoom",
|
||||
10,
|
||||
34,
|
||||
18,
|
||||
rl.RAYWHITE,
|
||||
)
|
||||
rl.DrawText("DESI Explorer — drag to rotate, scroll to zoom", 10, 34, 18, rl.RAYWHITE)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package dotenv_tests
|
||||
|
||||
import "core:os"
|
||||
import "core:testing"
|
||||
import "lib:dotenv"
|
||||
|
||||
@(test)
|
||||
test_parse_basic :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("API_URL=http://127.0.0.1:8080\nDEBUG=true\nPORT=8080\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, env["API_URL"] == "http://127.0.0.1:8080")
|
||||
testing.expect(t, env["DEBUG"] == "true")
|
||||
testing.expect(t, env["PORT"] == "8080")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_ignores_comments_and_blank_lines :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("# leading comment\n\n \nFOO=bar \nBAZ = qux \n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, env["FOO"] == "bar", "value should be trimmed")
|
||||
testing.expect(t, env["BAZ"] == "qux", "key and value should be trimmed around '='")
|
||||
testing.expect(t, "API_URL" not_in env, "comment-only lines should not be parsed")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_quoted_values :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("GREETING=\"hello world\"\nEMPTY=\"\"\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, env["GREETING"] == "hello world", "quoted value with inner space")
|
||||
testing.expect(t, env["EMPTY"] == "", "double-quoted empty value")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_skips_lines_without_equals :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("not-an-assignment\nOK=yep\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, env["OK"] == "yep")
|
||||
testing.expect(t, "not-an-assignment" not_in env, "line without '=' should be skipped")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_string_falls_back_to_file :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("FOO=from_file\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, dotenv.get_string(env, "FOO") == "from_file", "missing env var should use file value")
|
||||
testing.expect(t, dotenv.get_string(env, "MISSING") == "", "absent everywhere should be empty")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_string_prefers_real_env :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("FOO=from_file\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, os.set_env("FOO", "from_env") == nil)
|
||||
defer os.unset_env("FOO")
|
||||
|
||||
testing.expect(t, dotenv.get_string(env, "FOO") == "from_env", "real env var should win over file")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_bool :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("A=true\nB=0\nC=FALSE\nD=garbage\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, dotenv.get_bool(env, "A", false), "\"true\" should parse to true")
|
||||
testing.expect(t, !dotenv.get_bool(env, "B", true), "\"0\" should parse to false")
|
||||
testing.expect(t, !dotenv.get_bool(env, "C", true), "\"FALSE\" should parse to false")
|
||||
testing.expect(t, dotenv.get_bool(env, "D", true), "unparsable value should fall back to default")
|
||||
testing.expect(t, dotenv.get_bool(env, "MISSING", true), "missing key should fall back to default")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_int :: proc(t: ^testing.T) {
|
||||
env := dotenv.parse("PORT=8080\nNEG=-42\nHEX=0x1F\nGARBAGE=abc\n")
|
||||
defer dotenv.destroy(env)
|
||||
|
||||
testing.expect(t, dotenv.get_int(env, "PORT", -1) == 8080, "decimal int")
|
||||
testing.expect(t, dotenv.get_int(env, "NEG", 0) == -42, "negative int")
|
||||
testing.expect(t, dotenv.get_int(env, "HEX", 0) == 31, "hex int")
|
||||
testing.expect(t, dotenv.get_int(env, "GARBAGE", 7) == 7, "unparsable value should fall back to default")
|
||||
testing.expect(t, dotenv.get_int(env, "MISSING", 7) == 7, "missing key should fall back to default")
|
||||
}
|
||||
Reference in New Issue
Block a user