An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

feat(MM-69): configuration system — relay.toml parsing #7

Summary#

  • Adds Config struct to the common crate (serde-deserializable from TOML) with v0.1 fields: bind_address, port, data_dir,database_url, public_url, and empty stub sections [blobs], [oauth], [iroh]
  • EZPDS_* env var overrides (prefix: EZPDS_) applied on top of TOML via pure apply_env_overrides — no I/O in the Functional Core
  • Relay binary gains --config/EZPDS_CONFIG CLI arg (clap), loads config on startup, fails fast with clear error message on invalid config; logging via RUST_LOG-aware EnvFilter
  • 13 tests: TOML parsing, env var overrides, missing required field errors, I/O and parse error paths

Architecture notes#

FCIS pattern applied:

  • config.rsFunctional Core: Config, RawConfig, ConfigError, apply_env_overrides (takes explicit env HashMap), validate_and_build
  • config_loader.rsImperative Shell: load_config (reads file + real env), load_config_with_env (pub(crate) for test isolation)
  • relay/src/main.rsImperative Shell: CLI parsing, config loading, structured logging

Test plan#

  • cargo test --workspace — 13 tests pass, 0 failures
  • cargo clippy --workspace -- -D warnings — no warnings
  • cargo fmt --all --check — clean
  • cargo build --package relay — builds successfully
  • Manual smoke test: run ./target/debug/relay --config /nonexistent.toml and confirm error message is error: failed to load config from /nonexistent.toml: failed to read config file: No such file or directory (os error 2)
  • Manual smoke test: create a minimal relay.toml with data_dir and public_url, run ./target/debug/relay and confirm structured log line at startup

Closes MM-69

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:web:malpercio.dev/sh.tangled.repo.pull/3mglmncyxxf22
+1273 -1
Diff #1
+792
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 15 + name = "anstream" 16 + version = "0.6.21" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 19 + dependencies = [ 20 + "anstyle", 21 + "anstyle-parse", 22 + "anstyle-query", 23 + "anstyle-wincon", 24 + "colorchoice", 25 + "is_terminal_polyfill", 26 + "utf8parse", 27 + ] 28 + 29 + [[package]] 30 + name = "anstyle" 31 + version = "1.0.13" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 34 + 35 + [[package]] 36 + name = "anstyle-parse" 37 + version = "0.2.7" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 40 + dependencies = [ 41 + "utf8parse", 42 + ] 43 + 44 + [[package]] 45 + name = "anstyle-query" 46 + version = "1.1.5" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 49 + dependencies = [ 50 + "windows-sys", 51 + ] 52 + 53 + [[package]] 54 + name = "anstyle-wincon" 55 + version = "3.0.11" 56 + source = "registry+https://github.com/rust-lang/crates.io-index" 57 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 58 + dependencies = [ 59 + "anstyle", 60 + "once_cell_polyfill", 61 + "windows-sys", 62 + ] 63 + 64 + [[package]] 65 + name = "anyhow" 66 + version = "1.0.102" 67 + source = "registry+https://github.com/rust-lang/crates.io-index" 68 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 69 + 70 + [[package]] 71 + name = "bitflags" 72 + version = "2.11.0" 73 + source = "registry+https://github.com/rust-lang/crates.io-index" 74 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 75 + 76 + [[package]] 77 + name = "cfg-if" 78 + version = "1.0.4" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 81 + 82 + [[package]] 83 + name = "clap" 84 + version = "4.5.60" 85 + source = "registry+https://github.com/rust-lang/crates.io-index" 86 + checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" 87 + dependencies = [ 88 + "clap_builder", 89 + "clap_derive", 90 + ] 91 + 92 + [[package]] 93 + name = "clap_builder" 94 + version = "4.5.60" 95 + source = "registry+https://github.com/rust-lang/crates.io-index" 96 + checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" 97 + dependencies = [ 98 + "anstream", 99 + "anstyle", 100 + "clap_lex", 101 + "strsim", 102 + ] 103 + 104 + [[package]] 105 + name = "clap_derive" 106 + version = "4.5.55" 107 + source = "registry+https://github.com/rust-lang/crates.io-index" 108 + checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" 109 + dependencies = [ 110 + "heck", 111 + "proc-macro2", 112 + "quote", 113 + "syn", 114 + ] 115 + 116 + [[package]] 117 + name = "clap_lex" 118 + version = "1.0.0" 119 + source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" 121 + 122 + [[package]] 123 + name = "colorchoice" 124 + version = "1.0.4" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 127 + 128 + [[package]] 6 129 name = "common" 7 130 version = "0.1.0" 131 + dependencies = [ 132 + "serde", 133 + "tempfile", 134 + "thiserror", 135 + "toml", 136 + ] 8 137 9 138 [[package]] 10 139 name = "crypto" 11 140 version = "0.1.0" 12 141 13 142 [[package]] 143 + name = "equivalent" 144 + version = "1.0.2" 145 + source = "registry+https://github.com/rust-lang/crates.io-index" 146 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 147 + 148 + [[package]] 149 + name = "errno" 150 + version = "0.3.14" 151 + source = "registry+https://github.com/rust-lang/crates.io-index" 152 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 153 + dependencies = [ 154 + "libc", 155 + "windows-sys", 156 + ] 157 + 158 + [[package]] 159 + name = "fastrand" 160 + version = "2.3.0" 161 + source = "registry+https://github.com/rust-lang/crates.io-index" 162 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 163 + 164 + [[package]] 165 + name = "foldhash" 166 + version = "0.1.5" 167 + source = "registry+https://github.com/rust-lang/crates.io-index" 168 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 169 + 170 + [[package]] 171 + name = "getrandom" 172 + version = "0.4.2" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" 175 + dependencies = [ 176 + "cfg-if", 177 + "libc", 178 + "r-efi", 179 + "wasip2", 180 + "wasip3", 181 + ] 182 + 183 + [[package]] 184 + name = "hashbrown" 185 + version = "0.15.5" 186 + source = "registry+https://github.com/rust-lang/crates.io-index" 187 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 188 + dependencies = [ 189 + "foldhash", 190 + ] 191 + 192 + [[package]] 193 + name = "hashbrown" 194 + version = "0.16.1" 195 + source = "registry+https://github.com/rust-lang/crates.io-index" 196 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 197 + 198 + [[package]] 199 + name = "heck" 200 + version = "0.5.0" 201 + source = "registry+https://github.com/rust-lang/crates.io-index" 202 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 203 + 204 + [[package]] 205 + name = "id-arena" 206 + version = "2.3.0" 207 + source = "registry+https://github.com/rust-lang/crates.io-index" 208 + checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 209 + 210 + [[package]] 211 + name = "indexmap" 212 + version = "2.13.0" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" 215 + dependencies = [ 216 + "equivalent", 217 + "hashbrown 0.16.1", 218 + "serde", 219 + "serde_core", 220 + ] 221 + 222 + [[package]] 223 + name = "is_terminal_polyfill" 224 + version = "1.70.2" 225 + source = "registry+https://github.com/rust-lang/crates.io-index" 226 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 227 + 228 + [[package]] 229 + name = "itoa" 230 + version = "1.0.17" 231 + source = "registry+https://github.com/rust-lang/crates.io-index" 232 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 233 + 234 + [[package]] 235 + name = "lazy_static" 236 + version = "1.5.0" 237 + source = "registry+https://github.com/rust-lang/crates.io-index" 238 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 239 + 240 + [[package]] 241 + name = "leb128fmt" 242 + version = "0.1.0" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 245 + 246 + [[package]] 247 + name = "libc" 248 + version = "0.2.183" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 251 + 252 + [[package]] 253 + name = "linux-raw-sys" 254 + version = "0.12.1" 255 + source = "registry+https://github.com/rust-lang/crates.io-index" 256 + checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" 257 + 258 + [[package]] 259 + name = "log" 260 + version = "0.4.29" 261 + source = "registry+https://github.com/rust-lang/crates.io-index" 262 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 263 + 264 + [[package]] 265 + name = "matchers" 266 + version = "0.2.0" 267 + source = "registry+https://github.com/rust-lang/crates.io-index" 268 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 269 + dependencies = [ 270 + "regex-automata", 271 + ] 272 + 273 + [[package]] 274 + name = "memchr" 275 + version = "2.8.0" 276 + source = "registry+https://github.com/rust-lang/crates.io-index" 277 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 278 + 279 + [[package]] 280 + name = "nu-ansi-term" 281 + version = "0.50.3" 282 + source = "registry+https://github.com/rust-lang/crates.io-index" 283 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 284 + dependencies = [ 285 + "windows-sys", 286 + ] 287 + 288 + [[package]] 289 + name = "once_cell" 290 + version = "1.21.3" 291 + source = "registry+https://github.com/rust-lang/crates.io-index" 292 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 293 + 294 + [[package]] 295 + name = "once_cell_polyfill" 296 + version = "1.70.2" 297 + source = "registry+https://github.com/rust-lang/crates.io-index" 298 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 299 + 300 + [[package]] 301 + name = "pin-project-lite" 302 + version = "0.2.17" 303 + source = "registry+https://github.com/rust-lang/crates.io-index" 304 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 305 + 306 + [[package]] 307 + name = "prettyplease" 308 + version = "0.2.37" 309 + source = "registry+https://github.com/rust-lang/crates.io-index" 310 + checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 311 + dependencies = [ 312 + "proc-macro2", 313 + "syn", 314 + ] 315 + 316 + [[package]] 317 + name = "proc-macro2" 318 + version = "1.0.106" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 321 + dependencies = [ 322 + "unicode-ident", 323 + ] 324 + 325 + [[package]] 326 + name = "quote" 327 + version = "1.0.45" 328 + source = "registry+https://github.com/rust-lang/crates.io-index" 329 + checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" 330 + dependencies = [ 331 + "proc-macro2", 332 + ] 333 + 334 + [[package]] 335 + name = "r-efi" 336 + version = "6.0.0" 337 + source = "registry+https://github.com/rust-lang/crates.io-index" 338 + checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 339 + 340 + [[package]] 341 + name = "regex-automata" 342 + version = "0.4.14" 343 + source = "registry+https://github.com/rust-lang/crates.io-index" 344 + checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" 345 + dependencies = [ 346 + "aho-corasick", 347 + "memchr", 348 + "regex-syntax", 349 + ] 350 + 351 + [[package]] 352 + name = "regex-syntax" 353 + version = "0.8.10" 354 + source = "registry+https://github.com/rust-lang/crates.io-index" 355 + checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 356 + 357 + [[package]] 14 358 name = "relay" 15 359 version = "0.1.0" 360 + dependencies = [ 361 + "anyhow", 362 + "clap", 363 + "common", 364 + "tracing", 365 + "tracing-subscriber", 366 + ] 16 367 17 368 [[package]] 18 369 name = "repo-engine" 19 370 version = "0.1.0" 371 + 372 + [[package]] 373 + name = "rustix" 374 + version = "1.1.4" 375 + source = "registry+https://github.com/rust-lang/crates.io-index" 376 + checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" 377 + dependencies = [ 378 + "bitflags", 379 + "errno", 380 + "libc", 381 + "linux-raw-sys", 382 + "windows-sys", 383 + ] 384 + 385 + [[package]] 386 + name = "semver" 387 + version = "1.0.27" 388 + source = "registry+https://github.com/rust-lang/crates.io-index" 389 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 390 + 391 + [[package]] 392 + name = "serde" 393 + version = "1.0.228" 394 + source = "registry+https://github.com/rust-lang/crates.io-index" 395 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 396 + dependencies = [ 397 + "serde_core", 398 + "serde_derive", 399 + ] 400 + 401 + [[package]] 402 + name = "serde_core" 403 + version = "1.0.228" 404 + source = "registry+https://github.com/rust-lang/crates.io-index" 405 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 406 + dependencies = [ 407 + "serde_derive", 408 + ] 409 + 410 + [[package]] 411 + name = "serde_derive" 412 + version = "1.0.228" 413 + source = "registry+https://github.com/rust-lang/crates.io-index" 414 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 415 + dependencies = [ 416 + "proc-macro2", 417 + "quote", 418 + "syn", 419 + ] 420 + 421 + [[package]] 422 + name = "serde_json" 423 + version = "1.0.149" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 426 + dependencies = [ 427 + "itoa", 428 + "memchr", 429 + "serde", 430 + "serde_core", 431 + "zmij", 432 + ] 433 + 434 + [[package]] 435 + name = "serde_spanned" 436 + version = "0.6.9" 437 + source = "registry+https://github.com/rust-lang/crates.io-index" 438 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 439 + dependencies = [ 440 + "serde", 441 + ] 442 + 443 + [[package]] 444 + name = "sharded-slab" 445 + version = "0.1.7" 446 + source = "registry+https://github.com/rust-lang/crates.io-index" 447 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 448 + dependencies = [ 449 + "lazy_static", 450 + ] 451 + 452 + [[package]] 453 + name = "smallvec" 454 + version = "1.15.1" 455 + source = "registry+https://github.com/rust-lang/crates.io-index" 456 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 457 + 458 + [[package]] 459 + name = "strsim" 460 + version = "0.11.1" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 463 + 464 + [[package]] 465 + name = "syn" 466 + version = "2.0.117" 467 + source = "registry+https://github.com/rust-lang/crates.io-index" 468 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 469 + dependencies = [ 470 + "proc-macro2", 471 + "quote", 472 + "unicode-ident", 473 + ] 474 + 475 + [[package]] 476 + name = "tempfile" 477 + version = "3.26.0" 478 + source = "registry+https://github.com/rust-lang/crates.io-index" 479 + checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" 480 + dependencies = [ 481 + "fastrand", 482 + "getrandom", 483 + "once_cell", 484 + "rustix", 485 + "windows-sys", 486 + ] 487 + 488 + [[package]] 489 + name = "thiserror" 490 + version = "2.0.18" 491 + source = "registry+https://github.com/rust-lang/crates.io-index" 492 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 493 + dependencies = [ 494 + "thiserror-impl", 495 + ] 496 + 497 + [[package]] 498 + name = "thiserror-impl" 499 + version = "2.0.18" 500 + source = "registry+https://github.com/rust-lang/crates.io-index" 501 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 502 + dependencies = [ 503 + "proc-macro2", 504 + "quote", 505 + "syn", 506 + ] 507 + 508 + [[package]] 509 + name = "thread_local" 510 + version = "1.1.9" 511 + source = "registry+https://github.com/rust-lang/crates.io-index" 512 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 513 + dependencies = [ 514 + "cfg-if", 515 + ] 516 + 517 + [[package]] 518 + name = "toml" 519 + version = "0.8.23" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 522 + dependencies = [ 523 + "serde", 524 + "serde_spanned", 525 + "toml_datetime", 526 + "toml_edit", 527 + ] 528 + 529 + [[package]] 530 + name = "toml_datetime" 531 + version = "0.6.11" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 534 + dependencies = [ 535 + "serde", 536 + ] 537 + 538 + [[package]] 539 + name = "toml_edit" 540 + version = "0.22.27" 541 + source = "registry+https://github.com/rust-lang/crates.io-index" 542 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 543 + dependencies = [ 544 + "indexmap", 545 + "serde", 546 + "serde_spanned", 547 + "toml_datetime", 548 + "toml_write", 549 + "winnow", 550 + ] 551 + 552 + [[package]] 553 + name = "toml_write" 554 + version = "0.1.2" 555 + source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 557 + 558 + [[package]] 559 + name = "tracing" 560 + version = "0.1.44" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 563 + dependencies = [ 564 + "pin-project-lite", 565 + "tracing-attributes", 566 + "tracing-core", 567 + ] 568 + 569 + [[package]] 570 + name = "tracing-attributes" 571 + version = "0.1.31" 572 + source = "registry+https://github.com/rust-lang/crates.io-index" 573 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 574 + dependencies = [ 575 + "proc-macro2", 576 + "quote", 577 + "syn", 578 + ] 579 + 580 + [[package]] 581 + name = "tracing-core" 582 + version = "0.1.36" 583 + source = "registry+https://github.com/rust-lang/crates.io-index" 584 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 585 + dependencies = [ 586 + "once_cell", 587 + "valuable", 588 + ] 589 + 590 + [[package]] 591 + name = "tracing-log" 592 + version = "0.2.0" 593 + source = "registry+https://github.com/rust-lang/crates.io-index" 594 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 595 + dependencies = [ 596 + "log", 597 + "once_cell", 598 + "tracing-core", 599 + ] 600 + 601 + [[package]] 602 + name = "tracing-subscriber" 603 + version = "0.3.22" 604 + source = "registry+https://github.com/rust-lang/crates.io-index" 605 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 606 + dependencies = [ 607 + "matchers", 608 + "nu-ansi-term", 609 + "once_cell", 610 + "regex-automata", 611 + "sharded-slab", 612 + "smallvec", 613 + "thread_local", 614 + "tracing", 615 + "tracing-core", 616 + "tracing-log", 617 + ] 618 + 619 + [[package]] 620 + name = "unicode-ident" 621 + version = "1.0.24" 622 + source = "registry+https://github.com/rust-lang/crates.io-index" 623 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 624 + 625 + [[package]] 626 + name = "unicode-xid" 627 + version = "0.2.6" 628 + source = "registry+https://github.com/rust-lang/crates.io-index" 629 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 630 + 631 + [[package]] 632 + name = "utf8parse" 633 + version = "0.2.2" 634 + source = "registry+https://github.com/rust-lang/crates.io-index" 635 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 636 + 637 + [[package]] 638 + name = "valuable" 639 + version = "0.1.1" 640 + source = "registry+https://github.com/rust-lang/crates.io-index" 641 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 642 + 643 + [[package]] 644 + name = "wasip2" 645 + version = "1.0.2+wasi-0.2.9" 646 + source = "registry+https://github.com/rust-lang/crates.io-index" 647 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 648 + dependencies = [ 649 + "wit-bindgen", 650 + ] 651 + 652 + [[package]] 653 + name = "wasip3" 654 + version = "0.4.0+wasi-0.3.0-rc-2026-01-06" 655 + source = "registry+https://github.com/rust-lang/crates.io-index" 656 + checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" 657 + dependencies = [ 658 + "wit-bindgen", 659 + ] 660 + 661 + [[package]] 662 + name = "wasm-encoder" 663 + version = "0.244.0" 664 + source = "registry+https://github.com/rust-lang/crates.io-index" 665 + checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" 666 + dependencies = [ 667 + "leb128fmt", 668 + "wasmparser", 669 + ] 670 + 671 + [[package]] 672 + name = "wasm-metadata" 673 + version = "0.244.0" 674 + source = "registry+https://github.com/rust-lang/crates.io-index" 675 + checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 676 + dependencies = [ 677 + "anyhow", 678 + "indexmap", 679 + "wasm-encoder", 680 + "wasmparser", 681 + ] 682 + 683 + [[package]] 684 + name = "wasmparser" 685 + version = "0.244.0" 686 + source = "registry+https://github.com/rust-lang/crates.io-index" 687 + checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 688 + dependencies = [ 689 + "bitflags", 690 + "hashbrown 0.15.5", 691 + "indexmap", 692 + "semver", 693 + ] 694 + 695 + [[package]] 696 + name = "windows-link" 697 + version = "0.2.1" 698 + source = "registry+https://github.com/rust-lang/crates.io-index" 699 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 700 + 701 + [[package]] 702 + name = "windows-sys" 703 + version = "0.61.2" 704 + source = "registry+https://github.com/rust-lang/crates.io-index" 705 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 706 + dependencies = [ 707 + "windows-link", 708 + ] 709 + 710 + [[package]] 711 + name = "winnow" 712 + version = "0.7.15" 713 + source = "registry+https://github.com/rust-lang/crates.io-index" 714 + checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" 715 + dependencies = [ 716 + "memchr", 717 + ] 718 + 719 + [[package]] 720 + name = "wit-bindgen" 721 + version = "0.51.0" 722 + source = "registry+https://github.com/rust-lang/crates.io-index" 723 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 724 + dependencies = [ 725 + "wit-bindgen-rust-macro", 726 + ] 727 + 728 + [[package]] 729 + name = "wit-bindgen-core" 730 + version = "0.51.0" 731 + source = "registry+https://github.com/rust-lang/crates.io-index" 732 + checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" 733 + dependencies = [ 734 + "anyhow", 735 + "heck", 736 + "wit-parser", 737 + ] 738 + 739 + [[package]] 740 + name = "wit-bindgen-rust" 741 + version = "0.51.0" 742 + source = "registry+https://github.com/rust-lang/crates.io-index" 743 + checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" 744 + dependencies = [ 745 + "anyhow", 746 + "heck", 747 + "indexmap", 748 + "prettyplease", 749 + "syn", 750 + "wasm-metadata", 751 + "wit-bindgen-core", 752 + "wit-component", 753 + ] 754 + 755 + [[package]] 756 + name = "wit-bindgen-rust-macro" 757 + version = "0.51.0" 758 + source = "registry+https://github.com/rust-lang/crates.io-index" 759 + checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" 760 + dependencies = [ 761 + "anyhow", 762 + "prettyplease", 763 + "proc-macro2", 764 + "quote", 765 + "syn", 766 + "wit-bindgen-core", 767 + "wit-bindgen-rust", 768 + ] 769 + 770 + [[package]] 771 + name = "wit-component" 772 + version = "0.244.0" 773 + source = "registry+https://github.com/rust-lang/crates.io-index" 774 + checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 775 + dependencies = [ 776 + "anyhow", 777 + "bitflags", 778 + "indexmap", 779 + "log", 780 + "serde", 781 + "serde_derive", 782 + "serde_json", 783 + "wasm-encoder", 784 + "wasm-metadata", 785 + "wasmparser", 786 + "wit-parser", 787 + ] 788 + 789 + [[package]] 790 + name = "wit-parser" 791 + version = "0.244.0" 792 + source = "registry+https://github.com/rust-lang/crates.io-index" 793 + checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" 794 + dependencies = [ 795 + "anyhow", 796 + "id-arena", 797 + "indexmap", 798 + "log", 799 + "semver", 800 + "serde", 801 + "serde_derive", 802 + "serde_json", 803 + "unicode-xid", 804 + "wasmparser", 805 + ] 806 + 807 + [[package]] 808 + name = "zmij" 809 + version = "1.0.21" 810 + source = "registry+https://github.com/rust-lang/crates.io-index" 811 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+7
Cargo.toml
··· 24 24 # Serialization 25 25 serde = { version = "1", features = ["derive"] } 26 26 serde_json = "1" 27 + toml = "0.8" 28 + 29 + # CLI argument parsing (relay) 30 + clap = { version = "4", features = ["derive", "env"] } 27 31 28 32 # Error handling 29 33 anyhow = "1" ··· 39 43 40 44 # Crypto (crypto) 41 45 # rsky-crypto = "0.2" 46 + 47 + # Testing 48 + tempfile = "3" 42 49 43 50 # Intra-workspace 44 51 common = { path = "crates/common" }
+8
crates/common/Cargo.toml
··· 5 5 publish.workspace = true 6 6 7 7 # common: shared types, error envelope, config parsing. 8 + 9 + [dependencies] 10 + serde = { workspace = true } 11 + toml = { workspace = true } 12 + thiserror = { workspace = true } 13 + 14 + [dev-dependencies] 15 + tempfile = { workspace = true }
+292
crates/common/src/config.rs
··· 1 + // pattern: Functional Core 2 + 3 + use serde::Deserialize; 4 + use std::collections::HashMap; 5 + use std::path::PathBuf; 6 + 7 + /// Validated, fully-resolved relay configuration. 8 + #[derive(Debug, Clone)] 9 + pub struct Config { 10 + pub bind_address: String, 11 + pub port: u16, 12 + pub data_dir: PathBuf, 13 + pub database_url: String, 14 + pub public_url: String, 15 + pub blobs: BlobsConfig, 16 + pub oauth: OAuthConfig, 17 + pub iroh: IrohConfig, 18 + } 19 + 20 + /// Stub for future blob storage configuration. 21 + #[derive(Debug, Clone, Deserialize, Default)] 22 + pub struct BlobsConfig {} 23 + 24 + /// Stub for future OAuth configuration. 25 + #[derive(Debug, Clone, Deserialize, Default)] 26 + pub struct OAuthConfig {} 27 + 28 + /// Stub for future Iroh networking configuration. 29 + #[derive(Debug, Clone, Deserialize, Default)] 30 + pub struct IrohConfig {} 31 + 32 + /// Raw TOML-deserialized config with all fields optional to support env-var overlays. 33 + #[derive(Debug, Deserialize, Default)] 34 + pub(crate) struct RawConfig { 35 + pub(crate) bind_address: Option<String>, 36 + pub(crate) port: Option<u16>, 37 + pub(crate) data_dir: Option<String>, 38 + pub(crate) database_url: Option<String>, 39 + pub(crate) public_url: Option<String>, 40 + #[serde(default)] 41 + pub(crate) blobs: BlobsConfig, 42 + #[serde(default)] 43 + pub(crate) oauth: OAuthConfig, 44 + #[serde(default)] 45 + pub(crate) iroh: IrohConfig, 46 + } 47 + 48 + #[derive(Debug, thiserror::Error)] 49 + pub enum ConfigError { 50 + #[error("failed to read config file {path}: {source}")] 51 + Io { 52 + path: PathBuf, 53 + #[source] 54 + source: std::io::Error, 55 + }, 56 + #[error("failed to parse config file: {0}")] 57 + Parse(#[from] toml::de::Error), 58 + #[error("invalid configuration: missing required field '{field}'")] 59 + MissingField { field: &'static str }, 60 + #[error("invalid configuration: {0}")] 61 + Invalid(String), 62 + } 63 + 64 + /// Apply `EZPDS_*` environment variable overrides to a [`RawConfig`], returning the updated config. 65 + /// 66 + /// Receives the environment as a map so this function stays isolated from I/O (no `std::env` 67 + /// access). Takes `raw` by value and returns it so callers can chain calls without mutation. 68 + pub(crate) fn apply_env_overrides( 69 + mut raw: RawConfig, 70 + env: &HashMap<String, String>, 71 + ) -> Result<RawConfig, ConfigError> { 72 + if let Some(v) = env.get("EZPDS_BIND_ADDRESS") { 73 + raw.bind_address = Some(v.clone()); 74 + } 75 + if let Some(v) = env.get("EZPDS_PORT") { 76 + raw.port = Some(v.parse::<u16>().map_err(|e| { 77 + ConfigError::Invalid(format!("EZPDS_PORT is not a valid port number: '{v}': {e}")) 78 + })?); 79 + } 80 + if let Some(v) = env.get("EZPDS_DATA_DIR") { 81 + raw.data_dir = Some(v.clone()); 82 + } 83 + if let Some(v) = env.get("EZPDS_DATABASE_URL") { 84 + raw.database_url = Some(v.clone()); 85 + } 86 + if let Some(v) = env.get("EZPDS_PUBLIC_URL") { 87 + raw.public_url = Some(v.clone()); 88 + } 89 + Ok(raw) 90 + } 91 + 92 + /// Validate a [`RawConfig`] and build a [`Config`], applying defaults for optional fields. 93 + /// 94 + /// Required fields: `data_dir`, `public_url`. 95 + /// Defaults: `bind_address = "0.0.0.0"`, `port = 8080`, 96 + /// `database_url = "{data_dir}/relay.db"` (derived; fails if `data_dir` is non-UTF-8). 97 + pub(crate) fn validate_and_build(raw: RawConfig) -> Result<Config, ConfigError> { 98 + let bind_address = raw.bind_address.unwrap_or_else(|| "0.0.0.0".to_string()); 99 + let port = raw.port.unwrap_or(8080); 100 + let data_dir: PathBuf = raw 101 + .data_dir 102 + .ok_or(ConfigError::MissingField { field: "data_dir" })? 103 + .into(); 104 + let database_url = match raw.database_url { 105 + Some(url) => url, 106 + None => data_dir 107 + .join("relay.db") 108 + .to_str() 109 + .ok_or_else(|| { 110 + ConfigError::Invalid( 111 + "data_dir contains non-UTF-8 characters, cannot derive database_url" 112 + .to_string(), 113 + ) 114 + })? 115 + .to_owned(), 116 + }; 117 + let public_url = raw.public_url.ok_or(ConfigError::MissingField { 118 + field: "public_url", 119 + })?; 120 + 121 + Ok(Config { 122 + bind_address, 123 + port, 124 + data_dir, 125 + database_url, 126 + public_url, 127 + blobs: raw.blobs, 128 + oauth: raw.oauth, 129 + iroh: raw.iroh, 130 + }) 131 + } 132 + 133 + #[cfg(test)] 134 + mod tests { 135 + use super::*; 136 + 137 + fn minimal_raw() -> RawConfig { 138 + RawConfig { 139 + data_dir: Some("/var/pds".to_string()), 140 + public_url: Some("https://pds.example.com".to_string()), 141 + ..Default::default() 142 + } 143 + } 144 + 145 + #[test] 146 + fn parses_minimal_toml() { 147 + let toml = r#" 148 + data_dir = "/var/pds" 149 + public_url = "https://pds.example.com" 150 + "#; 151 + let raw: RawConfig = toml::from_str(toml).unwrap(); 152 + let config = validate_and_build(raw).unwrap(); 153 + 154 + assert_eq!(config.bind_address, "0.0.0.0"); 155 + assert_eq!(config.port, 8080); 156 + assert_eq!(config.data_dir, PathBuf::from("/var/pds")); 157 + assert_eq!(config.database_url, "/var/pds/relay.db"); 158 + assert_eq!(config.public_url, "https://pds.example.com"); 159 + } 160 + 161 + #[test] 162 + fn parses_full_toml() { 163 + let toml = r#" 164 + bind_address = "127.0.0.1" 165 + port = 3000 166 + data_dir = "/data" 167 + database_url = "sqlite:///data/custom.db" 168 + public_url = "https://pds.example.com" 169 + "#; 170 + let raw: RawConfig = toml::from_str(toml).unwrap(); 171 + let config = validate_and_build(raw).unwrap(); 172 + 173 + assert_eq!(config.bind_address, "127.0.0.1"); 174 + assert_eq!(config.port, 3000); 175 + assert_eq!(config.data_dir, PathBuf::from("/data")); 176 + assert_eq!(config.database_url, "sqlite:///data/custom.db"); 177 + } 178 + 179 + #[test] 180 + fn parses_stub_sections() { 181 + let toml = r#" 182 + data_dir = "/var/pds" 183 + public_url = "https://pds.example.com" 184 + 185 + [blobs] 186 + 187 + [oauth] 188 + 189 + [iroh] 190 + "#; 191 + let raw: RawConfig = toml::from_str(toml).unwrap(); 192 + let config = validate_and_build(raw).unwrap(); 193 + 194 + assert_eq!(config.public_url, "https://pds.example.com"); 195 + } 196 + 197 + #[test] 198 + fn database_url_defaults_to_data_dir() { 199 + let config = validate_and_build(minimal_raw()).unwrap(); 200 + assert_eq!(config.database_url, "/var/pds/relay.db"); 201 + } 202 + 203 + #[test] 204 + fn env_override_port() { 205 + let env = HashMap::from([("EZPDS_PORT".to_string(), "9090".to_string())]); 206 + let raw = apply_env_overrides(minimal_raw(), &env).unwrap(); 207 + let config = validate_and_build(raw).unwrap(); 208 + 209 + assert_eq!(config.port, 9090); 210 + } 211 + 212 + #[test] 213 + fn env_override_wins_over_toml_value() { 214 + // env always takes precedence over explicit TOML values 215 + let toml = r#" 216 + data_dir = "/var/pds" 217 + port = 3000 218 + public_url = "https://pds.example.com" 219 + "#; 220 + let raw: RawConfig = toml::from_str(toml).unwrap(); 221 + let env = HashMap::from([("EZPDS_PORT".to_string(), "9999".to_string())]); 222 + let raw = apply_env_overrides(raw, &env).unwrap(); 223 + let config = validate_and_build(raw).unwrap(); 224 + 225 + assert_eq!(config.port, 9999); 226 + } 227 + 228 + #[test] 229 + fn env_override_all_fields() { 230 + let env = HashMap::from([ 231 + ("EZPDS_BIND_ADDRESS".to_string(), "127.0.0.1".to_string()), 232 + ("EZPDS_PORT".to_string(), "4000".to_string()), 233 + ("EZPDS_DATA_DIR".to_string(), "/tmp/pds".to_string()), 234 + ( 235 + "EZPDS_DATABASE_URL".to_string(), 236 + "sqlite:///tmp/relay.db".to_string(), 237 + ), 238 + ( 239 + "EZPDS_PUBLIC_URL".to_string(), 240 + "https://pds.test".to_string(), 241 + ), 242 + ]); 243 + let raw = apply_env_overrides(RawConfig::default(), &env).unwrap(); 244 + let config = validate_and_build(raw).unwrap(); 245 + 246 + assert_eq!(config.bind_address, "127.0.0.1"); 247 + assert_eq!(config.port, 4000); 248 + assert_eq!(config.data_dir, PathBuf::from("/tmp/pds")); 249 + assert_eq!(config.database_url, "sqlite:///tmp/relay.db"); 250 + assert_eq!(config.public_url, "https://pds.test"); 251 + } 252 + 253 + #[test] 254 + fn env_override_invalid_port_returns_error() { 255 + let env = HashMap::from([("EZPDS_PORT".to_string(), "not_a_port".to_string())]); 256 + let err = apply_env_overrides(minimal_raw(), &env).unwrap_err(); 257 + 258 + assert!(matches!(err, ConfigError::Invalid(_))); 259 + assert!(err.to_string().contains("EZPDS_PORT")); 260 + assert!(err.to_string().contains("not_a_port")); 261 + } 262 + 263 + #[test] 264 + fn missing_data_dir_returns_error() { 265 + let raw = RawConfig { 266 + public_url: Some("https://pds.example.com".to_string()), 267 + ..Default::default() 268 + }; 269 + let err = validate_and_build(raw).unwrap_err(); 270 + 271 + assert!(matches!( 272 + err, 273 + ConfigError::MissingField { field: "data_dir" } 274 + )); 275 + } 276 + 277 + #[test] 278 + fn missing_public_url_returns_error() { 279 + let raw = RawConfig { 280 + data_dir: Some("/var/pds".to_string()), 281 + ..Default::default() 282 + }; 283 + let err = validate_and_build(raw).unwrap_err(); 284 + 285 + assert!(matches!( 286 + err, 287 + ConfigError::MissingField { 288 + field: "public_url" 289 + } 290 + )); 291 + } 292 + }
+121
crates/common/src/config_loader.rs
··· 1 + // pattern: Imperative Shell 2 + 3 + use std::collections::HashMap; 4 + use std::path::Path; 5 + 6 + use crate::config::{apply_env_overrides, validate_and_build, Config, ConfigError, RawConfig}; 7 + 8 + /// Collect only `EZPDS_*` env vars from the process environment, rejecting any with non-UTF-8 9 + /// values rather than panicking (as `std::env::vars()` would on non-UTF-8 data). 10 + fn collect_ezpds_env() -> Result<HashMap<String, String>, ConfigError> { 11 + let mut map = HashMap::new(); 12 + for (key_os, val_os) in std::env::vars_os() { 13 + let key = match key_os.to_str() { 14 + Some(k) if k.starts_with("EZPDS_") => k.to_owned(), 15 + _ => continue, 16 + }; 17 + let val = val_os.into_string().map_err(|_| { 18 + ConfigError::Invalid(format!( 19 + "environment variable {key} contains non-UTF-8 data" 20 + )) 21 + })?; 22 + map.insert(key, val); 23 + } 24 + Ok(map) 25 + } 26 + 27 + /// Load [`Config`] from a TOML file with an explicit environment map. 28 + /// 29 + /// Prefer [`load_config`] for production use. This variant is `pub(crate)` so tests can pass a 30 + /// controlled environment without leaking real `EZPDS_*` vars. 31 + pub(crate) fn load_config_with_env( 32 + path: &Path, 33 + env: &HashMap<String, String>, 34 + ) -> Result<Config, ConfigError> { 35 + let contents = std::fs::read_to_string(path).map_err(|source| ConfigError::Io { 36 + path: path.to_owned(), 37 + source, 38 + })?; 39 + let raw: RawConfig = toml::from_str(&contents)?; 40 + let raw = apply_env_overrides(raw, env)?; 41 + validate_and_build(raw) 42 + } 43 + 44 + /// Load [`Config`] from a TOML file, applying `EZPDS_*` environment variable overrides. 45 + pub fn load_config(path: &Path) -> Result<Config, ConfigError> { 46 + let env = collect_ezpds_env()?; 47 + load_config_with_env(path, &env) 48 + } 49 + 50 + #[cfg(test)] 51 + mod tests { 52 + use super::*; 53 + use std::io::Write; 54 + 55 + fn empty_env() -> HashMap<String, String> { 56 + HashMap::new() 57 + } 58 + 59 + #[test] 60 + fn loads_config_from_file() { 61 + let mut tmp = tempfile::NamedTempFile::new().unwrap(); 62 + writeln!( 63 + tmp, 64 + r#"data_dir = "/var/pds" 65 + public_url = "https://pds.example.com""# 66 + ) 67 + .unwrap(); 68 + 69 + let config = load_config_with_env(tmp.path(), &empty_env()).unwrap(); 70 + 71 + assert_eq!(config.public_url, "https://pds.example.com"); 72 + assert_eq!(config.bind_address, "0.0.0.0"); 73 + assert_eq!(config.port, 8080); 74 + } 75 + 76 + #[test] 77 + fn loads_minimal_valid_toml_produces_missing_field_error() { 78 + // An empty file is valid TOML but missing required fields. 79 + let tmp = tempfile::NamedTempFile::new().unwrap(); 80 + 81 + let err = load_config_with_env(tmp.path(), &empty_env()).unwrap_err(); 82 + 83 + assert!(matches!( 84 + err, 85 + ConfigError::MissingField { field: "data_dir" } 86 + )); 87 + } 88 + 89 + #[test] 90 + fn env_overrides_applied_from_file() { 91 + let mut tmp = tempfile::NamedTempFile::new().unwrap(); 92 + writeln!( 93 + tmp, 94 + r#"data_dir = "/var/pds" 95 + public_url = "https://pds.example.com""# 96 + ) 97 + .unwrap(); 98 + let env = HashMap::from([("EZPDS_PORT".to_string(), "9999".to_string())]); 99 + 100 + let config = load_config_with_env(tmp.path(), &env).unwrap(); 101 + 102 + assert_eq!(config.port, 9999); 103 + } 104 + 105 + #[test] 106 + fn returns_error_for_missing_file() { 107 + let result = load_config_with_env(Path::new("/nonexistent/relay.toml"), &empty_env()); 108 + 109 + assert!(matches!(result, Err(ConfigError::Io { .. }))); 110 + } 111 + 112 + #[test] 113 + fn returns_error_for_invalid_toml() { 114 + let mut tmp = tempfile::NamedTempFile::new().unwrap(); 115 + writeln!(tmp, "not valid toml = [[[").unwrap(); 116 + 117 + let result = load_config_with_env(tmp.path(), &empty_env()); 118 + 119 + assert!(matches!(result, Err(ConfigError::Parse(_)))); 120 + } 121 + }
+6
crates/common/src/lib.rs
··· 1 1 // common: shared types, error envelope, config parsing. 2 + 3 + mod config; 4 + mod config_loader; 5 + 6 + pub use config::{BlobsConfig, Config, ConfigError, IrohConfig, OAuthConfig}; 7 + pub use config_loader::load_config;
+7
crates/relay/Cargo.toml
··· 9 9 [[bin]] 10 10 name = "relay" 11 11 path = "src/main.rs" 12 + 13 + [dependencies] 14 + common = { workspace = true } 15 + clap = { workspace = true } 16 + anyhow = { workspace = true } 17 + tracing = { workspace = true } 18 + tracing-subscriber = { workspace = true }
+40 -1
crates/relay/src/main.rs
··· 1 + // pattern: Imperative Shell 2 + 3 + use anyhow::Context; 4 + use clap::Parser; 5 + use std::path::PathBuf; 6 + 7 + #[derive(Parser)] 8 + #[command(name = "relay", about = "ezpds relay server")] 9 + struct Cli { 10 + /// Path to relay.toml config file 11 + #[arg(long, env = "EZPDS_CONFIG")] 12 + config: Option<PathBuf>, 13 + } 14 + 1 15 fn main() { 2 - println!("relay starting"); 16 + if let Err(err) = run() { 17 + eprintln!("error: {err:#}"); 18 + std::process::exit(1); 19 + } 20 + } 21 + 22 + fn run() -> anyhow::Result<()> { 23 + tracing_subscriber::fmt() 24 + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 25 + .try_init() 26 + .map_err(|e| anyhow::anyhow!("failed to initialize tracing subscriber: {e}"))?; 27 + 28 + let cli = Cli::parse(); 29 + let config_path = cli.config.unwrap_or_else(|| PathBuf::from("relay.toml")); 30 + 31 + let config = common::load_config(&config_path) 32 + .with_context(|| format!("failed to load config from {}", config_path.display()))?; 33 + 34 + tracing::info!( 35 + bind_address = %config.bind_address, 36 + port = config.port, 37 + public_url = %config.public_url, 38 + "relay starting" 39 + ); 40 + 41 + Ok(()) 3 42 }

History

2 rounds 0 comments
sign up or login to add to the discussion
2 commits
expand
feat(MM-69): configuration system — relay.toml parsing
fix(MM-69): address PR review — UTF-8 safety, error context, pure core
expand 0 comments
pull request successfully merged
1 commit
expand
feat(MM-69): configuration system — relay.toml parsing
expand 0 comments