use std::fmt::Debug;
use serde::de::DeserializeOwned;
use super::error::CliError;
use super::version::VersionInfo;
use super::{CommonArgs, StandardCommand, output};
pub trait DfeApp: Sized {
type Config: DeserializeOwned + Debug + Send + Sync;
fn name(&self) -> &str;
fn env_prefix(&self) -> &str;
fn version_info(&self) -> VersionInfo;
fn common_args(&self) -> &CommonArgs;
fn command(&self) -> Option<&StandardCommand> {
None
}
fn load_config(&self, path: Option<&str>) -> Result<Self::Config, CliError>;
fn run_service(
&self,
config: Self::Config,
runtime: super::ServiceRuntime,
) -> impl std::future::Future<Output = Result<(), CliError>> + Send;
#[cfg(feature = "scaling")]
fn scaling_components(&self, _config: &Self::Config) -> Vec<crate::ScalingComponent> {
vec![]
}
#[cfg(any(feature = "metrics", feature = "otel-metrics"))]
fn register_metrics(&self, _manager: &crate::metrics::MetricsManager) {}
#[cfg(feature = "deployment")]
fn deployment_contract(&self) -> Option<crate::deployment::DeploymentContract> {
None
}
}
pub async fn run_app<A: DfeApp>(app: A) -> Result<(), CliError> {
let command = app.command().cloned().unwrap_or(StandardCommand::Run);
let args = app.common_args();
match command {
StandardCommand::Version => {
let info = app.version_info();
println!("{info}");
Ok(())
}
StandardCommand::ConfigCheck => {
init_logger(args)?;
let config_path = args.config.as_deref();
match app.load_config(config_path) {
Ok(config) => {
output::print_success("configuration is valid");
if !args.quiet {
eprintln!();
output::print_kv("service", &app.name());
output::print_kv("config", &config_path.unwrap_or("(defaults)"));
output::print_kv("log_level", &args.effective_log_level());
output::print_kv("log_format", &args.log_format);
output::print_kv("metrics_addr", &args.metrics_addr);
eprintln!();
eprintln!(" config: {config:#?}");
}
Ok(())
}
Err(e) => {
output::print_error(&format!("configuration invalid: {e}"));
Err(e)
}
}
}
#[cfg(any(feature = "metrics", feature = "otel-metrics"))]
StandardCommand::MetricsManifest => {
let mgr = crate::metrics::MetricsManager::new(app.name());
app.register_metrics(&mgr);
let manifest = mgr.registry().manifest();
println!(
"{}",
serde_json::to_string_pretty(&manifest)
.map_err(|e| CliError::Service(format!("JSON serialisation failed: {e}")))?
);
Ok(())
}
#[cfg(not(any(feature = "metrics", feature = "otel-metrics")))]
StandardCommand::MetricsManifest => {
output::print_error("metrics feature not enabled — no manifest available");
Err(CliError::Service("metrics feature not enabled".into()))
}
StandardCommand::GenerateArtefacts(ref artefact_args) => {
generate_artefacts(&app, artefact_args)?;
Ok(())
}
StandardCommand::Run => {
let version_info = app.version_info();
init_logger_for_service(args, app.name(), &version_info.version)?;
tracing::info!(
service = app.name(),
version = version_info.version,
"starting service"
);
let config_path = args.config.as_deref();
let config = app.load_config(config_path)?;
tracing::debug!(?config, "configuration loaded");
let commit = option_env!("GIT_COMMIT").unwrap_or("unknown");
let runtime = super::ServiceRuntime::build(
app.name(),
app.env_prefix(),
&args.metrics_addr,
&version_info.version,
commit,
#[cfg(feature = "scaling")]
app.scaling_components(&config),
)
.await?;
app.run_service(config, runtime).await
}
#[cfg(feature = "top")]
StandardCommand::Top(ref top_args) => {
let top_config = crate::top::TopConfig::from_args(top_args);
crate::top::run_top(&top_config).map_err(|e| CliError::Service(e.to_string()))
}
}
}
#[cfg(feature = "logger")]
fn init_logger(args: &CommonArgs) -> Result<(), CliError> {
let opts = args.to_logger_options()?;
crate::logger::setup(opts)?;
Ok(())
}
#[cfg(feature = "logger")]
fn init_logger_for_service(
args: &CommonArgs,
service_name: &str,
service_version: &str,
) -> Result<(), CliError> {
let opts = args.to_logger_options()?;
crate::logger::setup(crate::logger::LoggerOptions {
service_name: Some(service_name.to_string()),
service_version: Some(service_version.to_string()),
..opts
})?;
Ok(())
}
#[cfg(not(feature = "logger"))]
fn init_logger(_args: &CommonArgs) -> Result<(), CliError> {
Ok(())
}
#[cfg(not(feature = "logger"))]
fn init_logger_for_service(
_args: &CommonArgs,
_service_name: &str,
_service_version: &str,
) -> Result<(), CliError> {
Ok(())
}
fn generate_artefacts<A: DfeApp>(
app: &A,
args: &super::commands::GenerateArtefactsArgs,
) -> Result<(), CliError> {
let output_dir = std::path::Path::new(&args.output_dir);
std::fs::create_dir_all(output_dir)
.map_err(|e| CliError::Service(format!("failed to create output dir: {e}")))?;
let mut generated: Vec<String> = Vec::new();
#[cfg(any(feature = "metrics", feature = "otel-metrics"))]
{
let mgr = crate::metrics::MetricsManager::new(app.name());
app.register_metrics(&mgr);
let manifest = mgr.registry().manifest();
let path = output_dir.join("metrics-manifest.json");
let json = serde_json::to_string_pretty(&manifest)
.map_err(|e| CliError::Service(format!("metrics manifest JSON failed: {e}")))?;
std::fs::write(&path, &json)
.map_err(|e| CliError::Service(format!("failed to write {}: {e}", path.display())))?;
generated.push(format!(
"metrics-manifest.json ({} metrics)",
manifest.metrics.len()
));
}
#[cfg(feature = "deployment")]
if let Some(contract) = app.deployment_contract() {
let path = output_dir.join("deployment-contract.json");
let json = serde_json::to_string_pretty(&contract)
.map_err(|e| CliError::Service(format!("deployment contract JSON failed: {e}")))?;
std::fs::write(&path, &json)
.map_err(|e| CliError::Service(format!("failed to write {}: {e}", path.display())))?;
generated.push("deployment-contract.json".to_string());
let cm_path = output_dir.join("container-manifest.json");
let cm_json = crate::deployment::generate::generate_container_manifest(&contract)
.map_err(|e| CliError::Service(format!("container manifest failed: {e}")))?;
std::fs::write(&cm_path, &cm_json).map_err(|e| {
CliError::Service(format!("failed to write {}: {e}", cm_path.display()))
})?;
generated.push("container-manifest.json".to_string());
let rt_path = output_dir.join("Dockerfile.runtime");
let rt_content = crate::deployment::generate::generate_runtime_stage(&contract);
std::fs::write(&rt_path, &rt_content).map_err(|e| {
CliError::Service(format!("failed to write {}: {e}", rt_path.display()))
})?;
generated.push("Dockerfile.runtime".to_string());
}
if generated.is_empty() {
output::print_warn("no artefacts generated (no metrics or deployment features enabled)");
} else {
output::print_success(&format!(
"generated {} artefact(s) in {}",
generated.len(),
output_dir.display()
));
for name in &generated {
output::print_kv(" wrote", name);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_command_default_is_run() {
let cmd = StandardCommand::Run;
assert!(matches!(cmd, StandardCommand::Run));
}
}