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)]
103pub struct ServicesListResponse {
104 #[serde(default)]
105 pub services: Vec<ServiceRef>,
106 #[serde(default)]
107 pub plans: Vec<ServiceRef>,
108}
109
110impl ServicesListResponse {
111 pub fn contains(&self, name: &str) -> bool {
113 self.iter().any(|r| r.name.as_deref() == Some(name))
114 }
115
116 pub fn iter(&self) -> impl Iterator<Item = &ServiceRef> {
118 self.services.iter().chain(self.plans.iter())
119 }
120}
121
122#[derive(Debug, Deserialize)]
123pub struct ServiceRef {
124 #[serde(default)]
125 pub name: Option<String>,
126 #[serde(default)]
128 pub service_id: Option<String>,
129 #[serde(default, alias = "provider")]
131 pub provider_name: Option<String>,
132}
133
134pub fn preflight_checks_from_envelope(raw: &str) -> Vec<PreflightCheck> {
136 #[derive(Deserialize)]
137 struct Envelope {
138 #[serde(default)]
139 ok: bool,
140 #[serde(default)]
141 data: Option<Value>,
142 #[serde(default)]
143 error: Option<PreflightError>,
144 }
145 #[derive(Deserialize)]
146 struct PreflightError {
147 #[serde(default)]
148 details: Option<Value>,
149 }
150 let envelope: Envelope = match serde_json::from_str(raw) {
151 Ok(e) => e,
152 Err(_) => return Vec::new(),
153 };
154 let data = envelope.data.unwrap_or(Value::Null);
155 let details = envelope.error.and_then(|e| e.details);
156 preflight_checks_from_parts(&data, envelope.ok, details.as_ref())
157}
158
159pub fn preflight_checks_from_parts(
161 data: &Value,
162 ok: bool,
163 error_details: Option<&Value>,
164) -> Vec<PreflightCheck> {
165 if ok {
166 return serde_json::from_value::<PreflightReady>(data.clone())
167 .map(|ready| ready.preflight)
168 .unwrap_or_default();
169 }
170 let Some(details) = error_details else {
171 return Vec::new();
172 };
173 #[derive(Deserialize)]
174 struct Details {
175 #[serde(default)]
176 preflight: Vec<PreflightCheck>,
177 }
178 serde_json::from_value::<Details>(details.clone())
179 .map(|d| d.preflight)
180 .unwrap_or_default()
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use serde_json::json;
187
188 const PREFLIGHT_INIT_BLOCKED: &str = include_str!(concat!(
189 env!("CARGO_MANIFEST_DIR"),
190 "/tests/fixtures/probes/preflight-init-blocked.json"
191 ));
192 const PREFLIGHT_INIT_READY: &str = include_str!(concat!(
193 env!("CARGO_MANIFEST_DIR"),
194 "/tests/fixtures/probes/preflight-init-ready.json"
195 ));
196 const ENV_LIST: &str = include_str!(concat!(
197 env!("CARGO_MANIFEST_DIR"),
198 "/tests/fixtures/probes/env-list.json"
199 ));
200
201 #[test]
202 fn status_reads_project_id() {
203 let r: StatusResponse =
204 serde_json::from_value(json!({"project": {"id": "proj_1"}})).unwrap();
205 assert_eq!(r.project_id(), Some("proj_1"));
206 let empty: StatusResponse = serde_json::from_value(json!({})).unwrap();
207 assert_eq!(empty.project_id(), None);
208 }
209
210 #[test]
211 fn env_list_handles_all_three_shapes() {
212 let named: EnvListResponse =
213 serde_json::from_value(json!({"environments": {"feat-x": {}}})).unwrap();
214 assert!(named.contains("feat-x"));
215 let listed: EnvListResponse =
216 serde_json::from_value(json!({"environments": [{"name": "feat-x"}]})).unwrap();
217 assert!(listed.contains("feat-x"));
218 let bare: EnvListResponse = serde_json::from_value(json!([{"name": "feat-x"}])).unwrap();
219 assert!(bare.contains("feat-x"));
220 assert!(!bare.contains("other"));
221 }
222
223 #[test]
224 fn env_list_parses_probe_fixture() {
225 #[derive(Deserialize)]
226 struct Envelope {
227 data: EnvListResponse,
228 }
229 let start = ENV_LIST.find('{').expect("fixture contains JSON object");
230 let envelope: Envelope = serde_json::from_str(&ENV_LIST[start..]).unwrap();
231 assert!(envelope.data.contains("default"));
232 }
233
234 #[test]
235 fn preflight_blocked_fixture_lists_failures() {
236 let checks = preflight_checks_from_envelope(PREFLIGHT_INIT_BLOCKED);
237 let failures: Vec<_> = checks.iter().filter(|c| !c.pass).collect();
238 assert_eq!(failures.len(), 2);
239 assert!(
240 failures
241 .iter()
242 .any(|c| c.code.as_deref() == Some("TOS_ACCEPTANCE_REQUIRED"))
243 );
244 }
245
246 #[test]
247 fn preflight_ready_fixture_passes() {
248 let checks = preflight_checks_from_envelope(PREFLIGHT_INIT_READY);
249 assert!(checks.iter().all(|c| c.pass));
250 }
251
252 #[test]
253 fn services_list_contains() {
254 let r: ServicesListResponse =
255 serde_json::from_value(json!({"services": [{"name": "atto-web"}]})).unwrap();
256 assert!(r.contains("atto-web"));
257 assert!(!r.contains("missing"));
258 }
259
260 #[test]
261 fn services_list_contains_plans_by_name() {
262 let r: ServicesListResponse = serde_json::from_value(json!({
263 "services": [{"name": "clerk-auth", "service_id": "auth", "provider_name": "Clerk"}],
264 "plans": [{"name": "hobby", "service_id": "hobby", "provider_name": "Clerk"}]
265 }))
266 .unwrap();
267 assert!(r.contains("hobby"));
268 assert!(r.contains("clerk-auth"));
269 }
270
271 #[test]
272 fn services_list_accepts_provider_alias() {
273 let r: ServicesListResponse = serde_json::from_value(json!({
274 "plans": [{"name": "hobby", "service_id": "hobby", "provider": "vercel"}]
275 }))
276 .unwrap();
277 assert_eq!(r.plans[0].provider_name.as_deref(), Some("vercel"));
278 }
279}