From 752c859fd7c2ffb371373b110210867ef6fd6e64 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Fri, 19 Sep 2025 02:17:23 +0300 Subject: [PATCH] implement basic cli app template --- Cargo.toml | 9 ++++- src/cli/mod.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 ++ src/sync/mod.rs | 6 +--- src/utils.rs | 27 ++++++++++++++ 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 src/cli/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 629538f..18b27cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = ["macros", "utils"] [features] default = [] +cli = ["dep:clap", "dep:figment", "dep:tokio", "dep:color-eyre", "dep:num_cpus"] # Very long running. get_time_test = [] @@ -43,4 +44,10 @@ bytes = { version = "1.10.1", features = ["serde"] } url = { version = "2.5.4", features = ["serde"] } blake3 = "1.8.2" slotmap = { version = "1.0.7", features = ["serde"] } -instant-acme = "0.7.2" + +clap = { version = "4.5.47", features = ["derive"], optional = true } +figment = { version = "0.10.19", features = ["json", "yaml", "env", "toml"], optional = true } +tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread"], optional = true } +color-eyre = { version = "0.6.5", optional = true } +eyre = { version = "0.6.12" } +num_cpus = { version = "1.17.0", optional = true } diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..4e08f49 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,94 @@ +use std::{num::NonZeroUsize, path::PathBuf}; + +use clap::{Args, Parser}; + +use eyre::Context; +use figment::{ + Figment, + providers::{Env, Format, Json, Toml, YamlExtended}, +}; + +use tokio::runtime as rt; + +#[derive(Parser)] +struct AppArgs { + #[clap(long, short)] + /// Config file(s) for the application. + config: Vec, + #[clap(long, short)] + /// Number of worker threads app will consume. + worker_threads: Option, + + #[clap(flatten)] + args: A, +} + +pub struct App { + figment: Figment, +} + +impl App { + pub fn env_prefix(mut self, pfx: &str) -> Self { + self.figment = self.figment.merge(Env::prefixed(pfx)); + + self + } + + pub fn run( + self, + f: impl Send + 'static + AsyncFnOnce(A, C) -> eyre::Result<()>, + ) -> eyre::Result<()> + where + A: Args, + C: serde::de::DeserializeOwned, + { + let Self { mut figment } = self; + color_eyre::install().wrap_err("failed to install color-eyre")?; + let AppArgs { + config: configs, + worker_threads, + args, + } = AppArgs::::parse(); + + for config in configs { + // TODO: support glob? Expand home? + let component = config.components().next_back().ok_or_else(|| { + eyre::eyre!("{} must contain at least one component", config.display()) + })?; + let last = component.as_os_str().as_encoded_bytes(); + + if config.is_dir() { + todo!("populate everything from the directory") + } else if last.ends_with(b".yaml") || last.ends_with(b".yml") { + figment = figment.merge(YamlExtended::file(config.as_path())); + } else if last.ends_with(b".json") { + figment = figment.merge(Json::file(config.as_path())); + } else if last.ends_with(b".toml") { + figment = figment.merge(Toml::file(config.as_path())); + } + } + + let config: C = figment.extract_lossy().wrap_err("failed to load config")?; + let rt = match worker_threads.map_or(num_cpus::get(), NonZeroUsize::get) { + 0 | 1 => rt::Builder::new_current_thread(), + n => { + let mut b = rt::Builder::new_multi_thread(); + b.worker_threads(n); + b + } + } + .enable_all() + .build() + .wrap_err("failed to create tokio runtime")?; + + rt.block_on(f(args, config)) + } +} + +impl Default for App { + fn default() -> Self { + Self { + figment: Figment::new(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7e43cb1..8d8870a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ pub use perfect_derive::perfect_derive; pub use slotmap; pub use trait_set::trait_set; +#[cfg(feature = "cli")] +pub mod cli; + pub mod array; pub mod error; pub mod utils; diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 7eb7957..86c0b15 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -1,5 +1 @@ -pub use eva_utils::sync::{ - chan, - notifies, - spin, -}; +pub use eva_utils::sync::{chan, notifies, spin}; diff --git a/src/utils.rs b/src/utils.rs index 4a6df54..00783f2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,32 @@ //! # Uncategorized useful functionality +use eyre::Context as _; + +use std::{fmt, path::PathBuf}; + +use crate::data; + +#[data(crate = crate, not(Debug), display(""))] +pub enum SecretString { + Plaintext(String), + File(PathBuf), +} + +impl SecretString { + pub fn read(&self) -> eyre::Result { + match self { + Self::Plaintext(t) => Ok(t.clone()), + Self::File(f) => std::fs::read_to_string(f).wrap_err("failed to read secret string"), + } + } +} + +impl fmt::Debug for SecretString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") + } +} + /// Hint that this branch is cold. #[inline(always)] pub const fn cold(v: T) -> T {