use std::collections::{BTreeMap, BTreeSet};
use serde::Deserialize;
use super::error::DefError;
use crate::types::{DnsName, HttpStatus};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StackDef {
pub stack: Stack,
#[serde(default)]
pub secrets: SecretsSpec,
#[serde(default)]
pub integrations: BTreeMap<String, Integration>,
#[serde(default)]
pub services: BTreeMap<String, Service>,
#[serde(skip)]
pub legacy_datastores: BTreeSet<String>,
}
#[derive(Debug, Deserialize)]
pub struct Stack {
pub name: DnsName,
#[serde(default)]
pub projects: ProjectsSpec,
pub verify: Option<VerifyRoot>,
#[serde(flatten)]
pub substrates: BTreeMap<String, toml::Value>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectsSpec {
pub stripe: Option<StripeProjectSpec>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StripeProjectSpec {
pub project: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VerifySpec {
pub run: String,
#[serde(default)]
pub env: BTreeMap<String, String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VerifyRoot {
pub run: Option<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
#[serde(default)]
pub tiers: BTreeMap<String, VerifySpec>,
}
impl VerifyRoot {
pub fn is_declared(&self) -> bool {
self.run.is_some() || !self.tiers.is_empty()
}
pub fn resolve(&self, tier: Option<&str>) -> Option<VerifySpec> {
match tier {
None | Some("default") => self.run.as_ref().map(|run| VerifySpec {
run: run.clone(),
env: self.env.clone(),
}),
Some(name) => self.tiers.get(name).cloned(),
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SecretsSpec {
#[serde(default)]
pub required: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Integration {
pub provider: String,
#[serde(flatten)]
pub fields: BTreeMap<String, toml::Value>,
}
impl Integration {
pub fn config_fields(&self, known_substrates: &[&str]) -> BTreeMap<String, toml::Value> {
self.fields
.iter()
.filter(|(key, _)| !known_substrates.contains(&key.as_str()))
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
}
pub fn host_block(&self, host: &str) -> Option<&toml::Table> {
self.fields.get(host).and_then(toml::Value::as_table)
}
pub fn effective_config(
&self,
host: &str,
known_substrates: &[&str],
) -> BTreeMap<String, toml::Value> {
let mut out = self.config_fields(known_substrates);
if let Some(override_table) = self.host_block(host) {
for (key, value) in override_table {
out.insert(key.clone(), value.clone());
}
}
out
}
pub fn host_blocks(&self, known_substrates: &[&str]) -> BTreeMap<String, &toml::Table> {
self.fields
.iter()
.filter_map(|(key, value)| {
if !known_substrates.contains(&key.as_str()) {
return None;
}
Some((key.clone(), value.as_table()?))
})
.collect()
}
}
#[derive(Debug, Deserialize)]
pub struct Service {
pub source: Source,
pub setup: Option<String>,
pub prepare: Option<String>,
#[serde(default)]
pub secrets: Vec<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
pub health: Health,
#[serde(default)]
pub root_origin: bool,
#[serde(flatten)]
pub substrates: BTreeMap<String, toml::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Source {
pub repo: String,
#[serde(rename = "ref")]
pub reference: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Health {
pub path: String,
#[serde(default = "default_health_status")]
pub status: HttpStatus,
pub contains: Option<String>,
}
fn default_health_status() -> HttpStatus {
HttpStatus::OK
}
impl Service {
pub fn substrate_env(
&self,
service_name: &str,
substrate: &str,
) -> Result<BTreeMap<String, String>, DefError> {
let Some(block) = self.substrates.get(substrate) else {
return Ok(BTreeMap::new());
};
let location = format!("services.{service_name}.{substrate}.env");
let Some(table) = block.as_table() else {
return Ok(BTreeMap::new());
};
let Some(env) = table.get("env") else {
return Ok(BTreeMap::new());
};
let Some(env) = env.as_table() else {
return Err(DefError::EnvNotStrings { location });
};
let mut out = BTreeMap::new();
for (key, value) in env {
let Some(value) = value.as_str() else {
return Err(DefError::EnvNotStrings { location });
};
out.insert(key.clone(), value.to_owned());
}
Ok(out)
}
pub fn effective_env(
&self,
service_name: &str,
substrate: &str,
) -> Result<BTreeMap<String, String>, DefError> {
let overlay = self.substrate_env(service_name, substrate)?;
if overlay.is_empty() {
return Ok(self.env.clone());
}
let mut env = self.env.clone();
env.extend(overlay);
Ok(env)
}
}