use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Deserialize)]
pub struct ProjectConfig {
#[serde(default)]
pub service: ServiceConfig,
#[serde(default)]
pub environments: HashMap<String, EnvironmentConfig>,
#[serde(skip)]
pub project_dir: PathBuf,
}
#[derive(Debug, Deserialize, Default)]
pub struct ServiceConfig {
pub name: Option<String>,
pub mode: Option<String>,
pub default_env: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct EnvironmentConfig {
pub api_url: String,
pub tenant: String,
pub api_key_env: Option<String>,
pub api_key: Option<String>,
pub keycloak_url: Option<String>,
pub keycloak_realm: Option<String>,
pub keycloak_client_id: Option<String>,
#[serde(default)]
pub config: HashMap<String, String>,
#[serde(default)]
pub secrets: HashMap<String, String>,
}
impl ProjectConfig {
pub fn find_and_load() -> eyre::Result<Option<Self>> {
let cwd = std::env::current_dir()?;
let mut dir = cwd.as_path();
loop {
let candidate = dir.join("Cufflink.toml");
if candidate.exists() {
let content = std::fs::read_to_string(&candidate)?;
let mut config: ProjectConfig = toml::from_str(&content)
.map_err(|e| eyre::eyre!("Failed to parse {}: {}", candidate.display(), e))?;
config.project_dir = dir.to_path_buf();
return Ok(Some(config));
}
match dir.parent() {
Some(parent) => dir = parent,
None => return Ok(None),
}
}
}
pub fn get_env(&self, name: &str) -> eyre::Result<&EnvironmentConfig> {
self.environments.get(name).ok_or_else(|| {
let available: Vec<&str> = self.environments.keys().map(|k| k.as_str()).collect();
eyre::eyre!(
"Environment '{}' not found in Cufflink.toml. Available: {:?}",
name,
available
)
})
}
pub fn resolve_env(&self, env_arg: Option<&str>) -> eyre::Result<Option<&EnvironmentConfig>> {
let env_name = env_arg
.map(|s| s.to_string())
.or_else(|| self.service.default_env.clone());
match env_name {
Some(name) => Ok(Some(self.get_env(&name)?)),
None => Ok(None),
}
}
}