Skip to main content

obz_core/model/
response.rs

1//! Unified response envelope for all obz commands.
2//!
3//! All CLI output goes through this envelope to ensure consistent structure
4//! across all providers and commands. AI Agents can rely on the `status`
5//! field to determine success/failure.
6
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use super::error::ErrorDetail;
12use super::log::LogEntry;
13use super::metric::{MetricInfoDetail, MetricSeries};
14use super::trace::{Span, TraceDetail};
15use crate::provider::results::MetricResultType;
16
17/// Unified response envelope.
18///
19/// All obz commands produce this structure. The `status` field is always
20/// `"success"` or `"error"`, allowing AI Agents to reliably parse output.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(bound(
23    serialize = "T: Serialize",
24    deserialize = "T: serde::de::DeserializeOwned"
25))]
26pub struct Response<T> {
27    /// Response status: "success" or "error".
28    pub status: ResponseStatus,
29
30    /// Query metadata (success responses only).
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub metadata: Option<QueryMetadata>,
33
34    /// Response data (success responses only).
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub data: Option<T>,
37
38    /// Error detail (error responses only).
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub error: Option<ErrorDetail>,
41
42    /// Non-fatal warnings from the backend.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub warnings: Option<Vec<String>>,
45}
46
47/// Response status discriminator.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum ResponseStatus {
51    /// The request completed successfully.
52    Success,
53    /// The request failed.
54    Error,
55}
56
57/// Query metadata included in success responses.
58///
59/// Agent View includes only `provider` and `total_count`.
60/// Full View additionally includes `provider_type`, `query_language`,
61/// `query`, `time_range`, and `is_complete`.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct QueryMetadata {
64    /// Context name (user-configured), e.g., "dev-vm".
65    pub provider: String,
66
67    /// Provider type identifier (Full View only).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub provider_type: Option<String>,
70
71    /// Query language used (Full View only).
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub query_language: Option<String>,
74
75    /// Original query expression (Full View only).
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub query: Option<String>,
78
79    /// Query time range (Full View only).
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub time_range: Option<TimeRange>,
82
83    /// Total number of items returned by backend.
84    pub total_count: usize,
85
86    /// Number of items after view truncation.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub returned_series: Option<usize>,
89
90    /// Whether data is complete (Full View only).
91    /// Providers set this to `false` when more results exist beyond the
92    /// returned set (e.g., incomplete progress, cursor-based pagination).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub is_complete: Option<bool>,
95
96    /// Pagination cursor for next page (Full View only).
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub cursor: Option<String>,
99}
100
101/// Time range for a query.
102#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
103pub struct TimeRange {
104    /// Start timestamp (Unix seconds).
105    pub start: i64,
106    /// End timestamp (Unix seconds).
107    pub end: i64,
108}
109
110impl<T: Serialize> Response<T> {
111    /// Create a success response.
112    pub fn success(metadata: QueryMetadata, data: T) -> Self {
113        Self {
114            status: ResponseStatus::Success,
115            metadata: Some(metadata),
116            data: Some(data),
117            error: None,
118            warnings: None,
119        }
120    }
121}
122
123impl<T> Response<T> {
124    /// Create an error response.
125    ///
126    /// Does not require `T: Serialize` since `data` is always `None` for errors.
127    pub fn error(error: ErrorDetail) -> Self {
128        Self {
129            status: ResponseStatus::Error,
130            metadata: None,
131            data: None,
132            error: Some(error),
133            warnings: None,
134        }
135    }
136}
137
138// --- Result type discriminators ---
139// These constants are the API contract for the `result_type` field.
140
141/// Result type for `metric list`.
142pub const RESULT_TYPE_METRIC_LIST: &str = "metric_list";
143/// Result type for `metric info`.
144pub const RESULT_TYPE_METRIC_INFO: &str = "metric_info";
145/// Result type for `metric labels`.
146pub const RESULT_TYPE_LABEL_LIST: &str = "label_list";
147/// Result type for `metric label-values`.
148pub const RESULT_TYPE_LABEL_VALUES: &str = "label_values";
149/// Result type for `metric series`.
150pub const RESULT_TYPE_SERIES: &str = "series";
151/// Result type for `log search`.
152pub const RESULT_TYPE_LOG_ENTRIES: &str = "log_entries";
153/// Result type for `trace search`.
154pub const RESULT_TYPE_SPANS: &str = "spans";
155/// Result type for `trace get`.
156pub const RESULT_TYPE_TRACE_DETAIL: &str = "trace_detail";
157
158// --- Standard response data types ---
159// These define the JSON structure of `Response.data` for each command.
160// They are the API contract — AI Agents rely on these field names.
161
162/// Response data for `metric query` (vector/matrix results).
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct MetricQueryData {
165    /// Result type discriminator.
166    pub result_type: MetricResultType,
167    /// Query result series.
168    pub series: Vec<MetricSeries>,
169}
170
171/// Response data for `metric query` (scalar results).
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ScalarData {
174    /// Result type discriminator (always `scalar`).
175    pub result_type: MetricResultType,
176    /// Scalar value as `[timestamp, value]`.
177    pub scalar: (i64, f64),
178}
179
180/// Response data for string lists (`metric list`, `metric labels`).
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct StringListData {
183    /// Result type discriminator (e.g., `"metric_list"`, `"label_list"`).
184    pub result_type: String,
185    /// List of string values.
186    pub items: Vec<String>,
187}
188
189/// Response data for `metric label-values`.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct LabelValuesData {
192    /// Result type discriminator (always `"label_values"`).
193    pub result_type: String,
194    /// The label name that was queried.
195    pub label: String,
196    /// Label values.
197    pub items: Vec<String>,
198}
199
200/// Response data for `metric info`.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct MetricInfoData {
203    /// Result type discriminator (always `"metric_info"`).
204    pub result_type: String,
205    /// Metric metadata, or `None` if not found.
206    pub info: Option<MetricInfoDetail>,
207}
208
209/// Response data for `metric series`.
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct SeriesListData {
212    /// Result type discriminator (always `"series"`).
213    pub result_type: String,
214    /// Series label sets.
215    pub series: Vec<BTreeMap<String, String>>,
216}
217
218/// Response data for `log search`.
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct LogSearchData {
221    /// Result type discriminator (always `"log_entries"`).
222    pub result_type: String,
223    /// Log entries.
224    pub entries: Vec<LogEntry>,
225}
226
227/// Response data for `trace search`.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct TraceSearchData {
230    /// Result type discriminator (always `"spans"`).
231    pub result_type: String,
232    /// Matching spans.
233    pub spans: Vec<Span>,
234}
235
236/// Response data for `trace get`.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct TraceDetailData {
239    /// Result type discriminator (always `"trace_detail"`).
240    pub result_type: String,
241    /// Trace detail with summary and all spans.
242    #[serde(flatten)]
243    pub detail: TraceDetail,
244}
245
246/// Response data for extension commands.
247///
248/// `data` can be any valid JSON value — a string array for simple lists,
249/// an object array for structured results, or a single object.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ExtensionData {
252    /// Result type discriminator (e.g. `"services"`, `"operations"`).
253    pub result_type: String,
254    /// Result data — arbitrary JSON value.
255    pub data: serde_json::Value,
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_response_status_serialization() {
264        assert_eq!(
265            serde_json::to_string(&ResponseStatus::Success).unwrap(),
266            r#""success""#
267        );
268        assert_eq!(
269            serde_json::to_string(&ResponseStatus::Error).unwrap(),
270            r#""error""#
271        );
272    }
273
274    #[test]
275    fn test_success_response_structure() {
276        let metadata = QueryMetadata {
277            provider: "dev-vm".to_string(),
278            provider_type: None,
279            query_language: None,
280            query: None,
281            time_range: None,
282            total_count: 2,
283            returned_series: None,
284            is_complete: None,
285            cursor: None,
286        };
287
288        let resp = Response::success(
289            metadata,
290            serde_json::json!({"result_type": "matrix", "series": []}),
291        );
292
293        let json = serde_json::to_value(&resp).unwrap();
294        assert_eq!(json["status"], "success");
295        assert!(json.get("error").is_none());
296        assert_eq!(json["metadata"]["provider"], "dev-vm");
297        assert_eq!(json["metadata"]["total_count"], 2);
298    }
299
300    #[test]
301    fn test_error_response_structure() {
302        use super::super::error::{ErrorCategory, ErrorCode};
303
304        let detail = ErrorDetail {
305            category: ErrorCategory::Provider,
306            code: ErrorCode::QuerySyntax,
307            provider: Some("dev-vm".to_string()),
308            message: "invalid expression".to_string(),
309            raw_error: Some("bad_data".to_string()),
310            recoverable: false,
311            suggestion: Some("Check PromQL syntax".to_string()),
312            doc_url: None,
313            source_chain: None,
314        };
315
316        let resp: Response<serde_json::Value> = Response::error(detail);
317
318        let json = serde_json::to_value(&resp).unwrap();
319        assert_eq!(json["status"], "error");
320        assert!(json.get("data").is_none());
321        assert_eq!(json["error"]["category"], "provider");
322        assert_eq!(json["error"]["code"], "query_syntax");
323    }
324
325    #[test]
326    fn test_extension_data_serialization_string_list() {
327        let metadata = QueryMetadata {
328            provider: "dev-vt".to_string(),
329            provider_type: None,
330            query_language: None,
331            query: None,
332            time_range: None,
333            total_count: 3,
334            returned_series: None,
335            is_complete: None,
336            cursor: None,
337        };
338
339        let resp = Response::success(
340            metadata,
341            ExtensionData {
342                result_type: "services".to_string(),
343                data: serde_json::json!(["cart", "payment", "frontend"]),
344            },
345        );
346
347        let json = serde_json::to_value(&resp).unwrap();
348        assert_eq!(json["status"], "success");
349        assert_eq!(json["metadata"]["total_count"], 3);
350        assert_eq!(json["data"]["result_type"], "services");
351        let items = json["data"]["data"].as_array().unwrap();
352        assert_eq!(items.len(), 3);
353        assert_eq!(items[0], "cart");
354    }
355
356    #[test]
357    fn test_extension_data_serialization_structured() {
358        let metadata = QueryMetadata {
359            provider: "dev-vt".to_string(),
360            provider_type: None,
361            query_language: None,
362            query: None,
363            time_range: None,
364            total_count: 2,
365            returned_series: None,
366            is_complete: None,
367            cursor: None,
368        };
369
370        let resp = Response::success(
371            metadata,
372            ExtensionData {
373                result_type: "services".to_string(),
374                data: serde_json::json!([
375                    {"name": "cart", "spans": 100},
376                    {"name": "payment", "spans": 50}
377                ]),
378            },
379        );
380
381        let json = serde_json::to_value(&resp).unwrap();
382        assert_eq!(json["status"], "success");
383        assert_eq!(json["data"]["result_type"], "services");
384        let data = json["data"]["data"].as_array().unwrap();
385        assert_eq!(data.len(), 2);
386        assert_eq!(data[0]["name"], "cart");
387        assert_eq!(data[0]["spans"], 100);
388    }
389}