myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 448a68aa98ce009a6eb69221a6f471f379769b8e
parent 7e2fb7b739211dd5a653863aa44fb5d67bc002f3
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 21:17:12 +0000

app: establish bootstrap runtime

- add a thin binary entrypoint that delegates startup through the library surface
- introduce typed config, logging, and error modules for repo-root service bootstrap
- add runtime path preparation and startup snapshot wiring for signer-owned state
- validate with cargo metadata --format-version 1 --no-deps, cargo check, cargo test, and cargo fmt --check

Diffstat:
MCargo.lock | 667+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 10++++++++++
Asrc/app/mod.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/runtime.rs | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config.rs | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/error.rs | 35+++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 18++++++++++++++++++
Asrc/logging.rs | 38++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 7++++++-
9 files changed, 1150 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3,5 +3,672 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] name = "myc" version = "0.1.0" +dependencies = [ + "serde", + "tempfile", + "thiserror", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml @@ -12,3 +12,13 @@ resolver = "2" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0" +toml = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } + +[dev-dependencies] +tempfile = "3.17" diff --git a/src/app/mod.rs b/src/app/mod.rs @@ -0,0 +1,54 @@ +pub mod runtime; + +use crate::config::MycConfig; +use crate::error::MycError; + +pub use runtime::{MycRuntime, MycRuntimePaths, MycStartupSnapshot}; + +#[derive(Debug, Clone)] +pub struct MycApp { + runtime: MycRuntime, +} + +impl MycApp { + pub fn bootstrap(config: MycConfig) -> Result<Self, MycError> { + Ok(Self { + runtime: MycRuntime::bootstrap(config)?, + }) + } + + pub fn runtime(&self) -> &MycRuntime { + &self.runtime + } + + pub fn snapshot(&self) -> MycStartupSnapshot { + self.runtime.snapshot() + } + + pub fn run(self) -> Result<(), MycError> { + self.runtime.run() + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::config::MycConfig; + + use super::MycApp; + + #[test] + fn app_bootstrap_preserves_runtime_snapshot() { + let temp = tempfile::tempdir().expect("tempdir"); + let mut config = MycConfig::default(); + config.paths.state_dir = PathBuf::from(temp.path()).join("state"); + + let app = MycApp::bootstrap(config).expect("bootstrap"); + let snapshot = app.snapshot(); + + assert!(snapshot.state_dir.ends_with("state")); + assert!(snapshot.audit_dir.ends_with("audit")); + assert!(snapshot.signer_state_path.ends_with("signer-state.json")); + } +} diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -0,0 +1,128 @@ +use std::fs; +use std::path::PathBuf; + +use crate::config::MycConfig; +use crate::error::MycError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MycRuntimePaths { + pub state_dir: PathBuf, + pub audit_dir: PathBuf, + pub signer_state_path: PathBuf, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MycStartupSnapshot { + pub instance_name: String, + pub log_filter: String, + pub state_dir: PathBuf, + pub audit_dir: PathBuf, + pub signer_state_path: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct MycRuntime { + config: MycConfig, + paths: MycRuntimePaths, +} + +impl MycRuntime { + pub fn bootstrap(config: MycConfig) -> Result<Self, MycError> { + config.validate()?; + + let runtime = Self { + paths: MycRuntimePaths::from_config(&config), + config, + }; + runtime.prepare_filesystem()?; + Ok(runtime) + } + + pub fn paths(&self) -> &MycRuntimePaths { + &self.paths + } + + pub fn config(&self) -> &MycConfig { + &self.config + } + + pub fn snapshot(&self) -> MycStartupSnapshot { + MycStartupSnapshot { + instance_name: self.config.service.instance_name.clone(), + log_filter: self.config.logging.filter.clone(), + state_dir: self.paths.state_dir.clone(), + audit_dir: self.paths.audit_dir.clone(), + signer_state_path: self.paths.signer_state_path.clone(), + } + } + + pub fn run(self) -> Result<(), MycError> { + let snapshot = self.snapshot(); + tracing::info!( + instance_name = %snapshot.instance_name, + state_dir = %snapshot.state_dir.display(), + audit_dir = %snapshot.audit_dir.display(), + signer_state_path = %snapshot.signer_state_path.display(), + "myc runtime bootstrapped" + ); + Ok(()) + } + + fn prepare_filesystem(&self) -> Result<(), MycError> { + fs::create_dir_all(&self.paths.state_dir).map_err(|source| MycError::CreateDir { + path: self.paths.state_dir.clone(), + source, + })?; + fs::create_dir_all(&self.paths.audit_dir).map_err(|source| MycError::CreateDir { + path: self.paths.audit_dir.clone(), + source, + })?; + Ok(()) + } +} + +impl MycRuntimePaths { + fn from_config(config: &MycConfig) -> Self { + let state_dir = config.paths.state_dir.clone(); + Self { + signer_state_path: state_dir.join("signer-state.json"), + audit_dir: state_dir.join("audit"), + state_dir, + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::config::MycConfig; + + use super::MycRuntime; + + #[test] + fn bootstrap_creates_runtime_directories() { + let temp = tempfile::tempdir().expect("tempdir"); + let mut config = MycConfig::default(); + config.paths.state_dir = PathBuf::from(temp.path()).join("state"); + + let runtime = MycRuntime::bootstrap(config).expect("runtime"); + assert!(runtime.paths().state_dir.is_dir()); + assert!(runtime.paths().audit_dir.is_dir()); + assert!( + runtime + .paths() + .signer_state_path + .ends_with("signer-state.json") + ); + } + + #[test] + fn bootstrap_rejects_invalid_config() { + let mut config = MycConfig::default(); + config.service.instance_name.clear(); + + let err = MycRuntime::bootstrap(config).expect_err("invalid config"); + assert!(err.to_string().contains("service.instance_name")); + } +} diff --git a/src/config.rs b/src/config.rs @@ -0,0 +1,194 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; +use tracing_subscriber::EnvFilter; + +use crate::error::MycError; + +pub const DEFAULT_CONFIG_PATH: &str = "config.toml"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MycConfig { + pub service: MycServiceConfig, + pub logging: MycLoggingConfig, + pub paths: MycPathsConfig, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MycServiceConfig { + pub instance_name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MycLoggingConfig { + pub filter: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MycPathsConfig { + pub state_dir: PathBuf, +} + +impl Default for MycConfig { + fn default() -> Self { + Self { + service: MycServiceConfig::default(), + logging: MycLoggingConfig::default(), + paths: MycPathsConfig::default(), + } + } +} + +impl Default for MycServiceConfig { + fn default() -> Self { + Self { + instance_name: "myc".to_owned(), + } + } +} + +impl Default for MycLoggingConfig { + fn default() -> Self { + Self { + filter: "info,myc=info".to_owned(), + } + } +} + +impl Default for MycPathsConfig { + fn default() -> Self { + Self { + state_dir: PathBuf::from("var"), + } + } +} + +impl MycConfig { + pub fn load_from_default_path_if_exists() -> Result<Self, MycError> { + Self::load_from_path_if_exists(DEFAULT_CONFIG_PATH) + } + + pub fn load_from_path_if_exists(path: impl AsRef<Path>) -> Result<Self, MycError> { + let path = path.as_ref(); + if !path.exists() { + let config = Self::default(); + config.validate()?; + return Ok(config); + } + + Self::load_from_path(path) + } + + pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, MycError> { + let path = path.as_ref(); + let value = fs::read_to_string(path).map_err(|source| MycError::ConfigIo { + path: path.to_path_buf(), + source, + })?; + Self::from_toml_str_with_source(&value, path) + } + + pub fn from_toml_str(value: &str) -> Result<Self, MycError> { + Self::from_toml_str_with_source(value, Path::new("<inline>")) + } + + pub fn validate(&self) -> Result<(), MycError> { + if self.service.instance_name.trim().is_empty() { + return Err(MycError::InvalidConfig( + "service.instance_name must not be empty".to_owned(), + )); + } + + if self.logging.filter.trim().is_empty() { + return Err(MycError::InvalidConfig( + "logging.filter must not be empty".to_owned(), + )); + } + + EnvFilter::try_new(self.logging.filter.clone()).map_err(|source| { + MycError::InvalidLogFilter { + filter: self.logging.filter.clone(), + source, + } + })?; + + if self.paths.state_dir.as_os_str().is_empty() { + return Err(MycError::InvalidConfig( + "paths.state_dir must not be empty".to_owned(), + )); + } + + Ok(()) + } + + fn from_toml_str_with_source(value: &str, path: &Path) -> Result<Self, MycError> { + let config: Self = toml::from_str(value).map_err(|source| MycError::ConfigParse { + path: path.to_path_buf(), + source, + })?; + config.validate()?; + Ok(config) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_config_is_stable() { + let config = MycConfig::default(); + assert_eq!(config.service.instance_name, "myc"); + assert_eq!(config.logging.filter, "info,myc=info"); + assert_eq!(config.paths.state_dir, PathBuf::from("var")); + } + + #[test] + fn parse_config_from_toml_overrides_defaults() { + let config = MycConfig::from_toml_str( + r#" + [service] + instance_name = "myc-dev" + + [logging] + filter = "debug,myc=trace" + + [paths] + state_dir = "/tmp/myc" + "#, + ) + .expect("config"); + + assert_eq!(config.service.instance_name, "myc-dev"); + assert_eq!(config.logging.filter, "debug,myc=trace"); + assert_eq!(config.paths.state_dir, PathBuf::from("/tmp/myc")); + } + + #[test] + fn load_from_missing_path_returns_default_config() { + let temp = tempfile::tempdir().expect("tempdir"); + let config = MycConfig::load_from_path_if_exists(temp.path().join("missing.toml")) + .expect("missing path fallback"); + + assert_eq!(config, MycConfig::default()); + } + + #[test] + fn parse_rejects_unknown_fields() { + let err = MycConfig::from_toml_str( + r#" + [service] + instance_name = "myc-dev" + extra = "nope" + "#, + ) + .expect_err("unknown field"); + + assert!(err.to_string().contains("config parse error")); + } +} diff --git a/src/error.rs b/src/error.rs @@ -0,0 +1,35 @@ +use std::path::PathBuf; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MycError { + #[error("config io error at {path}: {source}")] + ConfigIo { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("config parse error at {path}: {source}")] + ConfigParse { + path: PathBuf, + #[source] + source: toml::de::Error, + }, + #[error("invalid config: {0}")] + InvalidConfig(String), + #[error("invalid log filter `{filter}`: {source}")] + InvalidLogFilter { + filter: String, + #[source] + source: tracing_subscriber::filter::ParseError, + }, + #[error("logging already initialized")] + LoggingAlreadyInitialized, + #[error("failed to create directory {path}: {source}")] + CreateDir { + path: PathBuf, + #[source] + source: std::io::Error, + }, +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,18 @@ +#![forbid(unsafe_code)] + +pub mod app; +pub mod config; +pub mod error; +pub mod logging; + +pub use app::{MycApp, MycRuntime, MycRuntimePaths, MycStartupSnapshot}; +pub use config::{ + DEFAULT_CONFIG_PATH, MycConfig, MycLoggingConfig, MycPathsConfig, MycServiceConfig, +}; +pub use error::MycError; + +pub fn run() -> Result<(), MycError> { + let config = MycConfig::load_from_default_path_if_exists()?; + logging::init_logging(&config.logging)?; + MycApp::bootstrap(config)?.run() +} diff --git a/src/logging.rs b/src/logging.rs @@ -0,0 +1,38 @@ +use tracing_subscriber::EnvFilter; + +use crate::config::MycLoggingConfig; +use crate::error::MycError; + +pub fn build_env_filter(filter: &str) -> Result<EnvFilter, MycError> { + EnvFilter::try_new(filter).map_err(|source| MycError::InvalidLogFilter { + filter: filter.to_owned(), + source, + }) +} + +pub fn init_logging(config: &MycLoggingConfig) -> Result<(), MycError> { + let filter = build_env_filter(&config.filter)?; + let subscriber = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_target(true) + .finish(); + + tracing::subscriber::set_global_default(subscriber) + .map_err(|_| MycError::LoggingAlreadyInitialized) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_env_filter_accepts_valid_filter() { + assert!(build_env_filter("info,myc=debug").is_ok()); + } + + #[test] + fn build_env_filter_rejects_invalid_filter() { + let err = build_env_filter("info,myc=[").expect_err("invalid filter"); + assert!(err.to_string().contains("invalid log filter")); + } +} diff --git a/src/main.rs b/src/main.rs @@ -1,3 +1,8 @@ #![forbid(unsafe_code)] -fn main() {} +fn main() { + if let Err(err) = myc::run() { + eprintln!("myc: {err}"); + std::process::exit(1); + } +}