stackless_stripe_projects/
responses.rs1use std::collections::BTreeMap;
5
6use serde::Deserialize;
7use serde_json::Value;
8
9#[derive(Debug, Default, Deserialize)]
11pub struct StatusResponse {
12 #[serde(default)]
13 pub project: Option<ProjectRef>,
14}
15
16impl StatusResponse {
17 pub fn project_id(&self) -> Option<&str> {
19 self.project.as_ref().and_then(|p| p.id.as_deref())
20 }
21}
22
23#[derive(Debug, Deserialize)]
24pub struct ProjectRef {
25 #[serde(default)]
26 pub id: Option<String>,
27}
28
29#[derive(Debug, Clone, Deserialize)]
32pub struct PreflightCheck {
33 pub label: String,
34 pub pass: bool,
35 #[serde(default)]
36 pub code: Option<String>,
37 #[serde(default)]
38 pub remedy: Option<String>,
39}
40
41#[derive(Debug, Default, Deserialize)]
43pub struct PreflightReady {
44 #[serde(default)]
45 pub preflight: Vec<PreflightCheck>,
46 #[serde(default)]
47 pub ready: Option<bool>,
48}
49
50#[derive(Debug, Deserialize)]
55#[serde(untagged)]
56pub enum EnvListResponse {
57 Bare(Vec<EnvRef>),
60 Wrapped(EnvWrapper),
61}
62
63#[derive(Debug, Deserialize)]
64pub struct EnvWrapper {
65 #[serde(default)]
66 pub environments: Option<EnvCollection>,
67}
68
69#[derive(Debug, Deserialize)]
70#[serde(untagged)]
71pub enum EnvCollection {
72 Named(BTreeMap<String, Value>),
73 List(Vec<EnvRef>),
74}
75
76#[derive(Debug, Deserialize)]
77pub struct EnvRef {
78 #[serde(default)]
79 pub name: Option<String>,
80}
81
82impl EnvListResponse {
83 pub fn contains(&self, instance: &str) -> bool {
85 let in_list = |list: &[EnvRef]| list.iter().any(|e| e.name.as_deref() == Some(instance));
86 match self {
87 Self::Wrapped(wrapper) => match &wrapper.environments {
88 Some(EnvCollection::Named(map)) => map.contains_key(instance),
89 Some(EnvCollection::List(list)) => in_list(list),
90 None => false,
91 },
92 Self::Bare(list) => in_list(list),
93 }
94 }
95}
96
97#[derive(Debug, Default, Deserialize)]
99pub struct ServicesListResponse {
100 #[serde(default)]
101 pub services: Vec<ServiceRef>,
102}
103
104impl ServicesListResponse {
105 pub fn contains(&self, name: &str) -> bool {
107 self.services
108 .iter()
109 .any(|s| s.name.as_deref() == Some(name))
110 }
111}
112
113#[derive(Debug, Deserialize)]
114pub struct ServiceRef {
115 #[serde(default)]
116 pub name: Option<String>,
117}
118
119pub fn preflight_checks_from_envelope(raw: &str) -> Vec<PreflightCheck> {
121 #[derive(Deserialize)]
122 struct Envelope {
123 #[serde(default)]
124 ok: bool,
125 #[serde(default)]
126 data: Option<Value>,
127 #[serde(default)]
128 error: Option<PreflightError>,
129 }
130 #[derive(Deserialize)]
131 struct PreflightError {
132 #[serde(default)]
133 details: Option<Value>,
134 }
135 let envelope: Envelope = match serde_json::from_str(raw) {
136 Ok(e) => e,
137 Err(_) => return Vec::new(),
138 };
139 let data = envelope.data.unwrap_or(Value::Null);
140 let details = envelope.error.and_then(|e| e.details);
141 preflight_checks_from_parts(&data, envelope.ok, details.as_ref())
142}
143
144pub fn preflight_checks_from_parts(
146 data: &Value,
147 ok: bool,
148 error_details: Option<&Value>,
149) -> Vec<PreflightCheck> {
150 if ok {
151 return serde_json::from_value::<PreflightReady>(data.clone())
152 .map(|ready| ready.preflight)
153 .unwrap_or_default();
154 }
155 let Some(details) = error_details else {
156 return Vec::new();
157 };
158 #[derive(Deserialize)]
159 struct Details {
160 #[serde(default)]
161 preflight: Vec<PreflightCheck>,
162 }
163 serde_json::from_value::<Details>(details.clone())
164 .map(|d| d.preflight)
165 .unwrap_or_default()
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use serde_json::json;
172
173 const PREFLIGHT_INIT_BLOCKED: &str = include_str!(concat!(
174 env!("CARGO_MANIFEST_DIR"),
175 "/tests/fixtures/probes/preflight-init-blocked.json"
176 ));
177 const PREFLIGHT_INIT_READY: &str = include_str!(concat!(
178 env!("CARGO_MANIFEST_DIR"),
179 "/tests/fixtures/probes/preflight-init-ready.json"
180 ));
181 const ENV_LIST: &str = include_str!(concat!(
182 env!("CARGO_MANIFEST_DIR"),
183 "/tests/fixtures/probes/env-list.json"
184 ));
185
186 #[test]
187 fn status_reads_project_id() {
188 let r: StatusResponse =
189 serde_json::from_value(json!({"project": {"id": "proj_1"}})).unwrap();
190 assert_eq!(r.project_id(), Some("proj_1"));
191 let empty: StatusResponse = serde_json::from_value(json!({})).unwrap();
192 assert_eq!(empty.project_id(), None);
193 }
194
195 #[test]
196 fn env_list_handles_all_three_shapes() {
197 let named: EnvListResponse =
198 serde_json::from_value(json!({"environments": {"feat-x": {}}})).unwrap();
199 assert!(named.contains("feat-x"));
200 let listed: EnvListResponse =
201 serde_json::from_value(json!({"environments": [{"name": "feat-x"}]})).unwrap();
202 assert!(listed.contains("feat-x"));
203 let bare: EnvListResponse = serde_json::from_value(json!([{"name": "feat-x"}])).unwrap();
204 assert!(bare.contains("feat-x"));
205 assert!(!bare.contains("other"));
206 }
207
208 #[test]
209 fn env_list_parses_probe_fixture() {
210 #[derive(Deserialize)]
211 struct Envelope {
212 data: EnvListResponse,
213 }
214 let start = ENV_LIST.find('{').expect("fixture contains JSON object");
215 let envelope: Envelope = serde_json::from_str(&ENV_LIST[start..]).unwrap();
216 assert!(envelope.data.contains("default"));
217 }
218
219 #[test]
220 fn preflight_blocked_fixture_lists_failures() {
221 let checks = preflight_checks_from_envelope(PREFLIGHT_INIT_BLOCKED);
222 let failures: Vec<_> = checks.iter().filter(|c| !c.pass).collect();
223 assert_eq!(failures.len(), 2);
224 assert!(
225 failures
226 .iter()
227 .any(|c| c.code.as_deref() == Some("TOS_ACCEPTANCE_REQUIRED"))
228 );
229 }
230
231 #[test]
232 fn preflight_ready_fixture_passes() {
233 let checks = preflight_checks_from_envelope(PREFLIGHT_INIT_READY);
234 assert!(checks.iter().all(|c| c.pass));
235 }
236
237 #[test]
238 fn services_list_contains() {
239 let r: ServicesListResponse =
240 serde_json::from_value(json!({"services": [{"name": "atto-web"}]})).unwrap();
241 assert!(r.contains("atto-web"));
242 assert!(!r.contains("missing"));
243 }
244}