implement basic cli app template

This commit is contained in:
Aleksandr 2025-09-19 02:17:23 +03:00
parent 5ace79d1e6
commit 752c859fd7
5 changed files with 133 additions and 6 deletions

View file

@ -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 }

94
src/cli/mod.rs Normal file
View file

@ -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<A: Args> {
#[clap(long, short)]
/// Config file(s) for the application.
config: Vec<PathBuf>,
#[clap(long, short)]
/// Number of worker threads app will consume.
worker_threads: Option<NonZeroUsize>,
#[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<A, C>(
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::<A>::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(),
}
}
}

View file

@ -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;

View file

@ -1,5 +1 @@
pub use eva_utils::sync::{
chan,
notifies,
spin,
};
pub use eva_utils::sync::{chan, notifies, spin};

View file

@ -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("<secret>"))]
pub enum SecretString {
Plaintext(String),
File(PathBuf),
}
impl SecretString {
pub fn read(&self) -> eyre::Result<String> {
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<T>(v: T) -> T {