use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FlowQuery {
pub host: Option<String>,
pub path_contains: Option<String>,
pub method: Option<String>,
pub status_min: Option<u16>,
pub status_max: Option<u16>,
pub has_error: Option<bool>,
pub is_websocket: Option<bool>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowSummary {
pub id: String,
pub method: String,
pub url: String,
pub host: String,
pub path: String,
pub status: Option<u16>,
pub duration_ms: Option<u64>,
pub tags: Vec<String>,
pub start_time_ms: i64,
pub has_error: bool,
pub is_websocket: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FlowModification {
pub method: Option<String>,
pub url: Option<String>,
pub request_headers: Option<HashMap<String, String>>,
pub request_body: Option<String>,
pub status_code: Option<u16>,
pub response_headers: Option<HashMap<String, String>>,
pub response_body: Option<String>,
pub message_content: Option<String>,
}
impl FlowModification {
pub fn is_empty(&self) -> bool {
self.method.is_none()
&& self.url.is_none()
&& self.request_headers.is_none()
&& self.request_body.is_none()
&& self.status_code.is_none()
&& self.response_headers.is_none()
&& self.response_body.is_none()
&& self.message_content.is_none()
}
pub fn into_option(self) -> Option<Self> {
if self.is_empty() {
None
} else {
Some(self)
}
}
pub fn from_json_value(value: &Value) -> Self {
Self {
method: value.get("method").and_then(Value::as_str).map(str::to_string),
url: value.get("url").and_then(Value::as_str).map(str::to_string),
request_headers: string_map_from_json(value.get("request_headers")),
request_body: value.get("request_body").and_then(Value::as_str).map(str::to_string),
status_code: value.get("status_code").and_then(Value::as_u64).map(|code| code as u16),
response_headers: string_map_from_json(value.get("response_headers")),
response_body: value.get("response_body").and_then(Value::as_str).map(str::to_string),
message_content: value.get("message_content").and_then(Value::as_str).map(str::to_string),
}
}
}
fn string_map_from_json(value: Option<&Value>) -> Option<HashMap<String, String>> {
value?.as_object().map(|entries| {
entries
.iter()
.filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
.collect()
})
}
#[cfg(test)]
mod tests {
use super::FlowModification;
use serde_json::json;
use std::collections::HashMap;
#[test]
fn flow_modification_into_option_returns_none_when_empty() {
assert!(FlowModification::default().into_option().is_none());
}
#[test]
fn flow_modification_into_option_preserves_non_empty_payload() {
let modification = FlowModification {
request_body: Some("patched".to_string()),
..Default::default()
};
assert_eq!(
modification.clone().into_option().unwrap().request_body,
modification.request_body
);
}
#[test]
fn flow_modification_from_json_value_reads_supported_fields() {
let modification = FlowModification::from_json_value(&json!({
"method": "PATCH",
"url": "http://example.com/new",
"request_headers": {
"X-Test": "1",
"X-Ignore": 2
},
"response_headers": {
"Content-Type": "application/json"
},
"request_body": "body",
"status_code": 202,
"response_body": "ok",
"message_content": "ws"
}));
assert_eq!(modification.method.as_deref(), Some("PATCH"));
assert_eq!(modification.url.as_deref(), Some("http://example.com/new"));
assert_eq!(
modification.request_headers,
Some(HashMap::from([("X-Test".to_string(), "1".to_string())]))
);
assert_eq!(
modification.response_headers,
Some(HashMap::from([(
"Content-Type".to_string(),
"application/json".to_string()
)]))
);
assert_eq!(modification.request_body.as_deref(), Some("body"));
assert_eq!(modification.status_code, Some(202));
assert_eq!(modification.response_body.as_deref(), Some("ok"));
assert_eq!(modification.message_content.as_deref(), Some("ws"));
}
}