1#[allow(deprecated)]
2use crate::formatter::DataFormat;
3use crate::formatter::{RecordFormatter, ValueFormatter};
4use wp_model_core::model::{DataRecord, DataType, FieldStorage, Value, types::value::ObjectValue};
5
6#[derive(Default)]
7pub struct ProtoTxt;
8
9impl ProtoTxt {
10 pub fn new() -> Self {
11 Self
12 }
13}
14
15#[allow(deprecated)]
16impl DataFormat for ProtoTxt {
17 type Output = String;
18 fn format_null(&self) -> String {
19 String::new()
20 }
21 fn format_bool(&self, v: &bool) -> String {
22 v.to_string()
23 }
24 fn format_string(&self, v: &str) -> String {
25 format!("\"{}\"", v.replace('"', "\\\""))
26 }
27 fn format_i64(&self, v: &i64) -> String {
28 v.to_string()
29 }
30 fn format_f64(&self, v: &f64) -> String {
31 v.to_string()
32 }
33 fn format_ip(&self, v: &std::net::IpAddr) -> String {
34 self.format_string(&v.to_string())
35 }
36 fn format_datetime(&self, v: &chrono::NaiveDateTime) -> String {
37 self.format_string(&v.to_string())
38 }
39 fn format_object(&self, value: &ObjectValue) -> String {
40 let items: Vec<String> = value
41 .iter()
42 .map(|(k, v)| format!("{}: {}", k, self.fmt_value(v.get_value())))
43 .collect();
44 items.join(" ")
45 }
46 fn format_array(&self, value: &[FieldStorage]) -> String {
47 let items: Vec<String> = value
48 .iter()
49 .map(|f| self.fmt_value(f.get_value()))
50 .collect();
51 format!("[{}]", items.join(", "))
52 }
53 fn format_field(&self, field: &FieldStorage) -> String {
54 if *field.get_meta() == DataType::Ignore {
55 String::new()
56 } else {
57 match field.get_value() {
58 Value::Obj(_) | Value::Array(_) => format!(
59 "{}: {}",
60 field.get_name(),
61 self.fmt_value(field.get_value())
62 ),
63 _ => format!(
64 "{}: {}",
65 field.get_name(),
66 self.fmt_value(field.get_value())
67 ),
68 }
69 }
70 }
71 fn format_record(&self, record: &DataRecord) -> String {
72 let items = record
73 .items
74 .iter()
75 .filter(|f| *f.get_meta() != DataType::Ignore)
76 .map(|f| self.format_field(f))
77 .collect::<Vec<_>>();
78 format!("{{ {} }}", items.join(" "))
80 }
81}
82
83#[cfg(test)]
84#[allow(deprecated)]
85mod tests {
86 use super::*;
87 use std::net::IpAddr;
88 use std::str::FromStr;
89 use wp_model_core::model::DataField;
90
91 #[test]
92 fn test_proto_new() {
93 let proto = ProtoTxt::new();
94 assert_eq!(proto.format_null(), "");
95 }
96
97 #[test]
98 fn test_proto_default() {
99 let proto = ProtoTxt;
100 assert_eq!(proto.format_null(), "");
101 }
102
103 #[test]
104 fn test_format_null() {
105 let proto = ProtoTxt;
106 assert_eq!(proto.format_null(), "");
107 }
108
109 #[test]
110 fn test_format_bool() {
111 let proto = ProtoTxt;
112 assert_eq!(proto.format_bool(&true), "true");
113 assert_eq!(proto.format_bool(&false), "false");
114 }
115
116 #[test]
117 fn test_format_string() {
118 let proto = ProtoTxt;
119 assert_eq!(proto.format_string("hello"), "\"hello\"");
120 assert_eq!(proto.format_string(""), "\"\"");
121 }
122
123 #[test]
124 fn test_format_string_escape_quotes() {
125 let proto = ProtoTxt;
126 assert_eq!(proto.format_string("say \"hi\""), "\"say \\\"hi\\\"\"");
127 }
128
129 #[test]
130 fn test_format_i64() {
131 let proto = ProtoTxt;
132 assert_eq!(proto.format_i64(&0), "0");
133 assert_eq!(proto.format_i64(&42), "42");
134 assert_eq!(proto.format_i64(&-100), "-100");
135 }
136
137 #[test]
138 fn test_format_f64() {
139 let proto = ProtoTxt;
140 assert_eq!(proto.format_f64(&3.24), "3.24");
141 assert_eq!(proto.format_f64(&0.0), "0");
142 }
143
144 #[test]
145 fn test_format_ip() {
146 let proto = ProtoTxt;
147 let ip = IpAddr::from_str("192.168.1.1").unwrap();
148 assert_eq!(proto.format_ip(&ip), "\"192.168.1.1\"");
149 }
150
151 #[test]
152 fn test_format_datetime() {
153 let proto = ProtoTxt;
154 let dt = chrono::NaiveDateTime::parse_from_str("2024-01-15 10:30:45", "%Y-%m-%d %H:%M:%S")
155 .unwrap();
156 let result = proto.format_datetime(&dt);
157 assert!(result.starts_with('"'));
158 assert!(result.ends_with('"'));
159 assert!(result.contains("2024"));
160 }
161
162 #[test]
163 fn test_format_field() {
164 let proto = ProtoTxt;
165 let field = FieldStorage::from_owned(DataField::from_chars("name", "Alice"));
166 let result = proto.format_field(&field);
167 assert_eq!(result, "name: \"Alice\"");
168 }
169
170 #[test]
171 fn test_format_field_digit() {
172 let proto = ProtoTxt;
173 let field = FieldStorage::from_owned(DataField::from_digit("age", 30));
174 let result = proto.format_field(&field);
175 assert_eq!(result, "age: 30");
176 }
177
178 #[test]
179 fn test_format_record() {
180 let proto = ProtoTxt;
181 let record = DataRecord {
182 id: Default::default(),
183 items: vec![
184 FieldStorage::from_owned(DataField::from_chars("name", "Alice")),
185 FieldStorage::from_owned(DataField::from_digit("age", 30)),
186 ],
187 };
188 let result = proto.format_record(&record);
189 assert!(result.starts_with("{ "));
190 assert!(result.ends_with(" }"));
191 assert!(result.contains("name: \"Alice\""));
192 assert!(result.contains("age: 30"));
193 }
194
195 #[test]
196 fn test_format_array() {
197 let proto = ProtoTxt;
198 let arr = vec![
199 FieldStorage::from_owned(DataField::from_digit("x", 1)),
200 FieldStorage::from_owned(DataField::from_digit("y", 2)),
201 ];
202 let result = proto.format_array(&arr);
203 assert!(result.starts_with('['));
204 assert!(result.ends_with(']'));
205 }
206
207 fn make_record_with_nested() -> DataRecord {
209 let mut obj = ObjectValue::new();
210 obj.insert(
211 "ssl_cipher".to_string(),
212 FieldStorage::from_owned(DataField::from_chars("ssl_cipher", "ECDHE")),
213 );
214 obj.insert(
215 "ssl_protocol".to_string(),
216 FieldStorage::from_owned(DataField::from_chars("ssl_protocol", "TLSv1.3")),
217 );
218 let arr = vec![
219 DataField::from_chars("", "foo"),
220 DataField::from_digit("", 42),
221 ];
222 DataRecord {
223 id: Default::default(),
224 items: vec![
225 FieldStorage::from_owned(DataField::from_digit("sent_bytes", 200)),
226 FieldStorage::from_owned(DataField::from_obj("extends", obj)),
227 FieldStorage::from_owned(DataField::from_arr("tags", arr)),
228 FieldStorage::from_owned(DataField::from_digit("match_chars", 50)),
229 ],
230 }
231 }
232
233 #[test]
236 fn test_format_record_with_obj_no_newlines() {
237 let proto = ProtoTxt;
238 let record = make_record_with_nested();
239 let result = proto.format_record(&record);
240 assert!(
241 !result.contains('\n'),
242 "record output should not contain newlines: {}",
243 result
244 );
245 assert!(result.contains("ssl_cipher: \"ECDHE\""));
246 assert!(result.contains("ssl_protocol: \"TLSv1.3\""));
247 assert!(result.contains("sent_bytes: 200"));
248 assert!(result.contains("match_chars: 50"));
249 }
250
251 #[test]
252 fn test_fmt_record_with_obj_no_newlines() {
253 let proto = ProtoTxt;
254 let record = make_record_with_nested();
255 let result = proto.fmt_record(&record);
256 assert!(
257 !result.contains('\n'),
258 "record output should not contain newlines: {}",
259 result
260 );
261 assert!(result.contains("ssl_cipher: \"ECDHE\""));
262 assert!(result.contains("ssl_protocol: \"TLSv1.3\""));
263 assert!(result.contains("tags: [\"foo\", 42]"));
264 }
265
266 #[test]
268 fn test_old_new_api_consistency_nested() {
269 let proto = ProtoTxt;
270 let record = make_record_with_nested();
271 assert_eq!(proto.format_record(&record), proto.fmt_record(&record));
272 }
273
274 #[test]
275 fn test_old_new_api_consistency_scalar() {
276 let proto = ProtoTxt;
277 let record = DataRecord {
278 id: Default::default(),
279 items: vec![
280 FieldStorage::from_owned(DataField::from_chars("name", "Alice")),
281 FieldStorage::from_owned(DataField::from_digit("age", 30)),
282 ],
283 };
284 assert_eq!(proto.format_record(&record), proto.fmt_record(&record));
285 }
286}
287
288#[allow(clippy::items_after_test_module)]
293impl ValueFormatter for ProtoTxt {
294 type Output = String;
295
296 fn format_value(&self, value: &Value) -> String {
297 match value {
298 Value::Null => String::new(),
299 Value::Bool(v) => v.to_string(),
300 Value::Chars(v) => format!("\"{}\"", v.replace('"', "\\\"")),
301 Value::Digit(v) => v.to_string(),
302 Value::Float(v) => v.to_string(),
303 Value::IpAddr(v) => format!("\"{}\"", v),
304 Value::Time(v) => format!("\"{}\"", v),
305 Value::Obj(obj) => {
306 let items: Vec<String> = obj
307 .iter()
308 .map(|(k, field)| format!("{}: {}", k, self.format_value(field.get_value())))
309 .collect();
310 items.join(" ")
311 }
312 Value::Array(arr) => {
313 let items: Vec<String> = arr
314 .iter()
315 .map(|field| self.format_value(field.get_value()))
316 .collect();
317 format!("[{}]", items.join(", "))
318 }
319 _ => format!("\"{}\"", value.to_string().replace('"', "\\\"")),
320 }
321 }
322}
323
324impl RecordFormatter for ProtoTxt {
325 fn fmt_field(&self, field: &FieldStorage) -> String {
326 if *field.get_meta() == DataType::Ignore {
327 String::new()
328 } else {
329 format!(
330 "{}: {}",
331 field.get_name(),
332 self.format_value(field.get_value())
333 )
334 }
335 }
336
337 fn fmt_record(&self, record: &DataRecord) -> String {
338 let items = record
339 .items
340 .iter()
341 .filter(|f| *f.get_meta() != DataType::Ignore)
342 .map(|f| self.fmt_field(f))
343 .collect::<Vec<_>>();
344 format!("{{ {} }}", items.join(" "))
345 }
346}