1use std::time::Duration;
2
3use chrono::{SecondsFormat, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::error::DetailedError;
8
9#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11pub struct Envelope {
12 #[serde(skip_serializing_if = "is_absent_or_null")]
14 pub data: Option<Value>,
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub metadata: Option<Metadata>,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub error: Option<ErrorEnvelope>,
21 #[serde(default, skip_serializing_if = "Vec::is_empty")]
23 pub warnings: Vec<String>,
24 #[serde(default, skip)]
25 serialization_error: Option<String>,
26}
27
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct Metadata {
31 pub system: String,
33 pub timestamp: String,
35 #[serde(skip_serializing_if = "String::is_empty")]
37 pub request_id: String,
38 #[serde(skip_serializing_if = "is_false")]
40 pub dry_run: bool,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub pagination: Option<PaginationMeta>,
44 #[serde(skip_serializing_if = "String::is_empty")]
46 pub command: String,
47 #[serde(skip_serializing_if = "String::is_empty")]
49 pub duration: String,
50 #[serde(skip_serializing_if = "String::is_empty")]
52 pub env: String,
53 #[serde(skip_serializing_if = "String::is_empty")]
55 pub identity: String,
56 #[serde(skip_serializing_if = "is_absent_null_or_empty_object")]
58 pub args: Option<Value>,
59 #[serde(skip_serializing_if = "is_absent_null_or_empty_object")]
61 pub effective_args: Option<Value>,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
66pub struct PaginationMeta {
67 pub total: i64,
69 pub offset: i64,
71 pub limit: i64,
73 pub count: i64,
75}
76
77#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct ErrorEnvelope {
80 pub code: String,
82 pub message: String,
84 #[serde(skip_serializing_if = "String::is_empty")]
86 pub system: String,
87 #[serde(skip_serializing_if = "String::is_empty")]
89 pub request_id: String,
90}
91
92impl Envelope {
93 #[must_use]
95 pub fn success(data: impl Serialize, system: impl Into<String>) -> Self {
96 let (data, serialization_error) = match serde_json::to_value(data) {
97 Ok(data) => (Some(data), None),
98 Err(err) => (None, Some(err.to_string())),
99 };
100 Self {
101 data,
102 metadata: Some(Metadata::new(system)),
103 error: None,
104 warnings: Vec::new(),
105 serialization_error,
106 }
107 }
108
109 #[must_use]
111 pub fn error(
112 code: impl Into<String>,
113 message: impl Into<String>,
114 system: impl Into<String>,
115 ) -> Self {
116 let system = system.into();
117 Self {
118 data: None,
119 metadata: Some(Metadata::new(system.clone())),
120 error: Some(ErrorEnvelope {
121 code: code.into(),
122 message: message.into(),
123 system,
124 request_id: String::new(),
125 }),
126 warnings: Vec::new(),
127 serialization_error: None,
128 }
129 }
130
131 #[must_use]
133 pub fn error_detail(
134 code: impl Into<String>,
135 message: impl Into<String>,
136 system: impl Into<String>,
137 request_id: impl Into<String>,
138 ) -> Self {
139 let system = system.into();
140 let request_id = request_id.into();
141 Self {
142 data: None,
143 metadata: Some(Metadata {
144 request_id: request_id.clone(),
145 ..Metadata::new(system.clone())
146 }),
147 error: Some(ErrorEnvelope {
148 code: code.into(),
149 message: message.into(),
150 system,
151 request_id,
152 }),
153 warnings: Vec::new(),
154 serialization_error: None,
155 }
156 }
157
158 #[must_use]
160 pub fn with_dry_run(mut self) -> Self {
161 if let Some(metadata) = &mut self.metadata {
162 metadata.dry_run = true;
163 }
164 self
165 }
166
167 pub fn with_context(
169 &mut self,
170 command: &str,
171 env: &str,
172 identity: &str,
173 duration: Duration,
174 user_args: Option<Value>,
175 effective_args: Option<Value>,
176 ) {
177 if let Some(metadata) = &mut self.metadata {
178 metadata.command = command.to_owned();
179 metadata.env = env.to_owned();
180 metadata.identity = identity.to_owned();
181 metadata.duration = format_duration(duration);
182 metadata.args = user_args;
183 metadata.effective_args = effective_args;
184 }
185 }
186
187 #[must_use]
189 pub fn prepare_for_render(&self, verbose: &str) -> Self {
190 let mut copy = self.clone();
191 if verbose.is_empty() {
192 copy.metadata = None;
193 return copy;
194 }
195 if verbose == "all" {
196 return copy;
197 }
198 if let Some(metadata) = &self.metadata {
199 copy.metadata = Some(metadata.filter_fields(verbose));
200 }
201 copy
202 }
203
204 pub fn add_warning(&mut self, message: impl Into<String>) {
206 self.warnings.push(message.into());
207 }
208
209 pub(crate) fn serialization_result(&self) -> crate::Result<()> {
210 if let Some(error) = &self.serialization_error {
211 return Err(crate::CliCoreError::message(error.clone()));
212 }
213 Ok(())
214 }
215}
216
217impl Metadata {
218 #[must_use]
220 pub fn new(system: impl Into<String>) -> Self {
221 Self {
222 system: system.into(),
223 timestamp: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
224 request_id: String::new(),
225 dry_run: false,
226 pagination: None,
227 command: String::new(),
228 duration: String::new(),
229 env: String::new(),
230 identity: String::new(),
231 args: None,
232 effective_args: None,
233 }
234 }
235
236 fn filter_fields(&self, verbose: &str) -> Self {
237 let wanted = verbose
238 .split(',')
239 .map(str::trim)
240 .filter(|field| !field.is_empty())
241 .collect::<Vec<_>>();
242 Self {
243 system: keep_string(&wanted, "system", &self.system),
244 timestamp: keep_string(&wanted, "timestamp", &self.timestamp),
245 request_id: keep_string(&wanted, "request_id", &self.request_id),
246 dry_run: wanted.contains(&"dry_run") && self.dry_run,
247 pagination: wanted
248 .contains(&"pagination")
249 .then(|| self.pagination.clone())
250 .flatten(),
251 command: keep_string(&wanted, "command", &self.command),
252 duration: keep_string(&wanted, "duration", &self.duration),
253 env: keep_string(&wanted, "env", &self.env),
254 identity: keep_string(&wanted, "identity", &self.identity),
255 args: wanted
256 .contains(&"args")
257 .then(|| self.args.clone())
258 .flatten(),
259 effective_args: wanted
260 .contains(&"effective_args")
261 .then(|| self.effective_args.clone())
262 .flatten(),
263 }
264 }
265}
266
267#[must_use]
269pub fn build_error_envelope(err: &(dyn std::error::Error + 'static), system: &str) -> Envelope {
270 if let Some((code, mut sys, request_id)) = find_detailed_error(err) {
271 if sys.is_empty() {
272 sys = system.to_owned();
273 }
274 return Envelope {
275 data: None,
276 metadata: Some(Metadata {
277 request_id: request_id.clone(),
278 ..Metadata::new(sys.clone())
279 }),
280 error: Some(ErrorEnvelope {
281 code: if code.is_empty() {
282 "ERROR".to_owned()
283 } else {
284 code
285 },
286 message: err.to_string(),
287 system: sys,
288 request_id,
289 }),
290 warnings: Vec::new(),
291 serialization_error: None,
292 };
293 }
294 Envelope::error("ERROR", err.to_string(), system)
295}
296
297fn find_detailed_error(
298 err: &(dyn std::error::Error + 'static),
299) -> Option<(String, String, String)> {
300 let mut current = Some(err);
301 let mut fallback_system = None::<String>;
302 while let Some(error) = current {
303 if let Some(crate::CliCoreError::SystemMessage {
304 system,
305 code,
306 request_id,
307 ..
308 }) = error.downcast_ref::<crate::CliCoreError>()
309 {
310 return Some((code.clone(), system.clone(), request_id.clone()));
311 }
312 if let Some(crate::CliCoreError::System { system, .. }) =
313 error.downcast_ref::<crate::CliCoreError>()
314 && !system.is_empty()
315 && fallback_system.is_none()
316 {
317 fallback_system = Some(system.clone());
318 }
319 if let Some(crate::CliCoreError::Detailed {
320 code,
321 system,
322 request_id,
323 ..
324 }) = error.downcast_ref::<crate::CliCoreError>()
325 {
326 return Some((
327 code.clone(),
328 fallback_system
329 .clone()
330 .filter(|_| system.is_empty())
331 .unwrap_or_else(|| system.clone()),
332 request_id.clone(),
333 ));
334 }
335 let detailed_transport = error.downcast_ref::<crate::transport::Error>().or_else(|| {
336 match error.downcast_ref::<crate::CliCoreError>() {
337 Some(crate::CliCoreError::Transport(transport)) => Some(transport),
338 Some(
339 crate::CliCoreError::MissingAuthProvider(_)
340 | crate::CliCoreError::AuthProvider { .. }
341 | crate::CliCoreError::InvalidOutputFormat(_)
342 | crate::CliCoreError::Message(_)
343 | crate::CliCoreError::SystemMessage { .. }
344 | crate::CliCoreError::System { .. }
345 | crate::CliCoreError::Detailed { .. }
346 | crate::CliCoreError::ExitCode { .. }
347 | crate::CliCoreError::Io(_)
348 | crate::CliCoreError::Json(_),
349 )
350 | None => None,
351 }
352 });
353 if let Some(detailed) = detailed_transport {
354 let system = detailed
355 .error_system()
356 .map_or_else(String::new, std::borrow::Cow::into_owned);
357 return Some((
358 detailed.error_code().into_owned(),
359 fallback_system
360 .clone()
361 .filter(|_| system.is_empty())
362 .unwrap_or(system),
363 detailed
364 .error_request_id()
365 .map_or_else(String::new, std::borrow::Cow::into_owned),
366 ));
367 }
368 current = error.source();
369 }
370 fallback_system.map(|system| ("ERROR".to_owned(), system, String::new()))
371}
372
373#[must_use]
375pub fn build_detailed_error_envelope(err: &dyn DetailedError, system: &str) -> Envelope {
376 let code = err.error_code().into_owned();
377 let sys = err
378 .error_system()
379 .map_or_else(|| system.to_owned(), std::borrow::Cow::into_owned);
380 let request_id = err
381 .error_request_id()
382 .map_or_else(String::new, std::borrow::Cow::into_owned);
383 Envelope {
384 data: None,
385 metadata: Some(Metadata {
386 request_id: request_id.clone(),
387 ..Metadata::new(sys.clone())
388 }),
389 error: Some(ErrorEnvelope {
390 code: if code.is_empty() {
391 "ERROR".to_owned()
392 } else {
393 code
394 },
395 message: err.to_string(),
396 system: sys,
397 request_id,
398 }),
399 warnings: Vec::new(),
400 serialization_error: None,
401 }
402}
403
404fn keep_string(wanted: &[&str], field: &str, value: &str) -> String {
405 if wanted.contains(&field) {
406 value.to_owned()
407 } else {
408 String::new()
409 }
410}
411
412fn format_duration(duration: Duration) -> String {
413 let nanos = duration.as_nanos();
414 let millis = (nanos + 500_000) / 1_000_000;
415 if millis == 0 {
416 return "0s".to_owned();
417 }
418 if millis >= 1000 {
419 let secs = millis / 1000;
420 let rem = millis % 1000;
421 if rem == 0 {
422 format!("{secs}s")
423 } else {
424 let mut fraction = format!("{rem:03}");
425 while fraction.ends_with('0') {
426 fraction.pop();
427 }
428 format!("{secs}.{fraction}s")
429 }
430 } else {
431 format!("{millis}ms")
432 }
433}
434
435const fn is_false(value: &bool) -> bool {
436 !*value
437}
438
439fn is_absent_or_null(value: &Option<Value>) -> bool {
440 value.as_ref().is_none_or(Value::is_null)
441}
442
443fn is_absent_null_or_empty_object(value: &Option<Value>) -> bool {
444 match value {
445 None | Some(Value::Null) => true,
446 Some(Value::Object(map)) => map.is_empty(),
447 Some(Value::Array(_) | Value::Bool(_) | Value::Number(_) | Value::String(_)) => false,
448 }
449}