Skip to main content

wp_data_fmt/
proto.rs

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        // 生成标准的 proto-text 格式:消息用花括号包围
79        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    /// 构造一个包含 Obj 和 Array 字段的 record,用于嵌套格式化测试
208    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    /// 回归测试:format_value(Value::Obj) 曾使用 \n 作为分隔符,
234    /// 嵌入 record 后导致输出中出现意外换行
235    #[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    /// 新旧 API 对含嵌套类型的 record 输出一致性
267    #[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// ============================================================================
289// 新 trait 实现:ValueFormatter + RecordFormatter
290// ============================================================================
291
292#[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}