1use std::collections::{BTreeMap, BTreeSet};
11
12use serde::Deserialize;
13
14use super::error::DefError;
15use crate::types::{DnsName, HttpStatus};
16
17#[derive(Debug, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct StackDef {
22 pub stack: Stack,
23 #[serde(default)]
24 pub secrets: SecretsSpec,
25 #[serde(default)]
26 pub integrations: BTreeMap<String, Integration>,
27 #[serde(default)]
28 pub services: BTreeMap<String, Service>,
29 #[serde(skip)]
34 pub legacy_datastores: BTreeSet<String>,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct Stack {
39 pub name: DnsName,
40 #[serde(default)]
41 pub projects: ProjectsSpec,
42 pub verify: Option<VerifyRoot>,
43 #[serde(flatten)]
46 pub substrates: BTreeMap<String, toml::Value>,
47}
48
49#[derive(Debug, Default, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct ProjectsSpec {
52 pub stripe: Option<StripeProjectSpec>,
53}
54
55#[derive(Debug, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct StripeProjectSpec {
58 pub project: Option<String>,
59}
60
61#[derive(Debug, Clone, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct VerifySpec {
65 pub run: String,
66 #[serde(default)]
67 pub env: BTreeMap<String, String>,
68}
69
70#[derive(Debug, Default, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct VerifyRoot {
74 pub run: Option<String>,
75 #[serde(default)]
76 pub env: BTreeMap<String, String>,
77 #[serde(default)]
78 pub tiers: BTreeMap<String, VerifySpec>,
79}
80
81impl VerifyRoot {
82 pub fn is_declared(&self) -> bool {
83 self.run.is_some() || !self.tiers.is_empty()
84 }
85
86 pub fn resolve(&self, tier: Option<&str>) -> Option<VerifySpec> {
87 match tier {
88 None | Some("default") => self.run.as_ref().map(|run| VerifySpec {
89 run: run.clone(),
90 env: self.env.clone(),
91 }),
92 Some(name) => self.tiers.get(name).cloned(),
93 }
94 }
95}
96
97#[derive(Debug, Default, Deserialize)]
98#[serde(deny_unknown_fields)]
99pub struct SecretsSpec {
100 #[serde(default)]
101 pub required: Vec<String>,
102}
103
104#[derive(Debug, Deserialize)]
105pub struct Integration {
106 pub provider: String,
108 #[serde(flatten)]
112 pub fields: BTreeMap<String, toml::Value>,
113}
114
115impl Integration {
116 pub fn config_fields(&self, known_substrates: &[&str]) -> BTreeMap<String, toml::Value> {
120 self.fields
121 .iter()
122 .filter(|(key, _)| !known_substrates.contains(&key.as_str()))
123 .map(|(key, value)| (key.clone(), value.clone()))
124 .collect()
125 }
126
127 pub fn host_block(&self, host: &str) -> Option<&toml::Table> {
128 self.fields.get(host).and_then(toml::Value::as_table)
129 }
130
131 pub fn effective_config(
133 &self,
134 host: &str,
135 known_substrates: &[&str],
136 ) -> BTreeMap<String, toml::Value> {
137 let mut out = self.config_fields(known_substrates);
138 if let Some(override_table) = self.host_block(host) {
139 for (key, value) in override_table {
140 out.insert(key.clone(), value.clone());
141 }
142 }
143 out
144 }
145
146 pub fn host_blocks(&self, known_substrates: &[&str]) -> BTreeMap<String, &toml::Table> {
148 self.fields
149 .iter()
150 .filter_map(|(key, value)| {
151 if !known_substrates.contains(&key.as_str()) {
152 return None;
153 }
154 Some((key.clone(), value.as_table()?))
155 })
156 .collect()
157 }
158}
159
160#[derive(Debug, Deserialize)]
161pub struct Service {
162 pub source: Source,
163 pub setup: Option<String>,
165 pub prepare: Option<String>,
167 #[serde(default)]
169 pub secrets: Vec<String>,
170 #[serde(default)]
171 pub env: BTreeMap<String, String>,
172 pub health: Health,
174 #[serde(default)]
176 pub root_origin: bool,
177 #[serde(flatten)]
179 pub substrates: BTreeMap<String, toml::Value>,
180}
181
182#[derive(Debug, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub struct Source {
186 pub repo: String,
187 #[serde(rename = "ref")]
188 pub reference: String,
189}
190
191#[derive(Debug, Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct Health {
195 pub path: String,
196 #[serde(default = "default_health_status")]
197 pub status: HttpStatus,
198 pub contains: Option<String>,
199}
200
201fn default_health_status() -> HttpStatus {
202 HttpStatus::OK
203}
204
205impl Service {
206 pub fn substrate_env(
212 &self,
213 service_name: &str,
214 substrate: &str,
215 ) -> Result<BTreeMap<String, String>, DefError> {
216 let Some(block) = self.substrates.get(substrate) else {
217 return Ok(BTreeMap::new());
218 };
219 let location = format!("services.{service_name}.{substrate}.env");
220 let Some(table) = block.as_table() else {
221 return Ok(BTreeMap::new());
224 };
225 let Some(env) = table.get("env") else {
226 return Ok(BTreeMap::new());
227 };
228 let Some(env) = env.as_table() else {
229 return Err(DefError::EnvNotStrings { location });
230 };
231 let mut out = BTreeMap::new();
232 for (key, value) in env {
233 let Some(value) = value.as_str() else {
234 return Err(DefError::EnvNotStrings { location });
235 };
236 out.insert(key.clone(), value.to_owned());
237 }
238 Ok(out)
239 }
240
241 pub fn effective_env(
243 &self,
244 service_name: &str,
245 substrate: &str,
246 ) -> Result<BTreeMap<String, String>, DefError> {
247 let overlay = self.substrate_env(service_name, substrate)?;
248 if overlay.is_empty() {
249 return Ok(self.env.clone());
250 }
251 let mut env = self.env.clone();
252 env.extend(overlay);
253 Ok(env)
254 }
255}