relay_core_api/
modification.rs1pub use crate::flow_query::{
2 flow_matches_filter, flow_matches_query, parse_flow_filter, FlowFilter,
3};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
11pub struct FlowQuery {
12 pub host: Option<String>,
14 pub path_contains: Option<String>,
16 pub method: Option<String>,
18 pub status_min: Option<u16>,
20 pub status_max: Option<u16>,
22 pub has_error: Option<bool>,
24 pub is_websocket: Option<bool>,
26 pub limit: Option<usize>,
28 pub offset: Option<usize>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct FlowSummary {
35 pub id: String,
36 pub method: String,
37 pub url: String,
38 pub host: String,
39 pub path: String,
40 pub status: Option<u16>,
41 pub duration_ms: Option<u64>,
42 pub tags: Vec<String>,
43 pub start_time_ms: i64,
44 pub has_error: bool,
45 pub is_websocket: bool,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct FlowModification {
53 pub method: Option<String>,
55 pub url: Option<String>,
56 pub request_headers: Option<HashMap<String, String>>,
57 pub request_body: Option<String>,
58
59 pub status_code: Option<u16>,
61 pub response_headers: Option<HashMap<String, String>>,
62 pub response_body: Option<String>,
63
64 pub message_content: Option<String>,
66}
67
68impl FlowModification {
69 pub fn is_empty(&self) -> bool {
70 self.method.is_none()
71 && self.url.is_none()
72 && self.request_headers.is_none()
73 && self.request_body.is_none()
74 && self.status_code.is_none()
75 && self.response_headers.is_none()
76 && self.response_body.is_none()
77 && self.message_content.is_none()
78 }
79
80 pub fn into_option(self) -> Option<Self> {
81 if self.is_empty() { None } else { Some(self) }
82 }
83
84 pub fn from_json_value(value: &Value) -> Self {
85 Self {
86 method: value
87 .get("method")
88 .and_then(Value::as_str)
89 .map(str::to_string),
90 url: value.get("url").and_then(Value::as_str).map(str::to_string),
91 request_headers: string_map_from_json(value.get("request_headers")),
92 request_body: value
93 .get("request_body")
94 .and_then(Value::as_str)
95 .map(str::to_string),
96 status_code: value
97 .get("status_code")
98 .and_then(Value::as_u64)
99 .map(|code| code as u16),
100 response_headers: string_map_from_json(value.get("response_headers")),
101 response_body: value
102 .get("response_body")
103 .and_then(Value::as_str)
104 .map(str::to_string),
105 message_content: value
106 .get("message_content")
107 .and_then(Value::as_str)
108 .map(str::to_string),
109 }
110 }
111}
112
113fn string_map_from_json(value: Option<&Value>) -> Option<HashMap<String, String>> {
114 value?.as_object().map(|entries| {
115 entries
116 .iter()
117 .filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
118 .collect()
119 })
120}
121
122#[cfg(test)]
123mod tests {
124 use super::FlowModification;
125 use serde_json::json;
126 use std::collections::HashMap;
127
128 #[test]
129 fn flow_modification_into_option_returns_none_when_empty() {
130 assert!(FlowModification::default().into_option().is_none());
131 }
132
133 #[test]
134 fn flow_modification_into_option_preserves_non_empty_payload() {
135 let modification = FlowModification {
136 request_body: Some("patched".to_string()),
137 ..Default::default()
138 };
139
140 assert_eq!(
141 modification.clone().into_option().unwrap().request_body,
142 modification.request_body
143 );
144 }
145
146 #[test]
147 fn flow_modification_from_json_value_reads_supported_fields() {
148 let modification = FlowModification::from_json_value(&json!({
149 "method": "PATCH",
150 "url": "http://example.com/new",
151 "request_headers": {
152 "X-Test": "1",
153 "X-Ignore": 2
154 },
155 "response_headers": {
156 "Content-Type": "application/json"
157 },
158 "request_body": "body",
159 "status_code": 202,
160 "response_body": "ok",
161 "message_content": "ws"
162 }));
163
164 assert_eq!(modification.method.as_deref(), Some("PATCH"));
165 assert_eq!(modification.url.as_deref(), Some("http://example.com/new"));
166 assert_eq!(
167 modification.request_headers,
168 Some(HashMap::from([("X-Test".to_string(), "1".to_string())]))
169 );
170 assert_eq!(
171 modification.response_headers,
172 Some(HashMap::from([(
173 "Content-Type".to_string(),
174 "application/json".to_string()
175 )]))
176 );
177 assert_eq!(modification.request_body.as_deref(), Some("body"));
178 assert_eq!(modification.status_code, Some(202));
179 assert_eq!(modification.response_body.as_deref(), Some("ok"));
180 assert_eq!(modification.message_content.as_deref(), Some("ws"));
181 }
182}