deb822_fast/
convert.rs

1//! Conversion between Deb822-like paragraphs and Rust objects.
2
3/// Abstract trait for accessing and modifying key-value pairs in a paragraph.
4pub trait Deb822LikeParagraph: FromIterator<(String, String)> {
5    /// Get the value for the given key.
6    fn get(&self, key: &str) -> Option<String>;
7
8    /// Insert a key-value pair.
9    fn set(&mut self, key: &str, value: &str);
10
11    /// Remove a key-value pair.
12    fn remove(&mut self, key: &str);
13}
14
15impl Deb822LikeParagraph for crate::Paragraph {
16    fn get(&self, key: &str) -> Option<String> {
17        crate::Paragraph::get(self, key).map(|v| v.to_string())
18    }
19
20    fn set(&mut self, key: &str, value: &str) {
21        crate::Paragraph::set(self, key, value);
22    }
23
24    fn remove(&mut self, key: &str) {
25        crate::Paragraph::remove(self, key);
26    }
27}
28
29/// Convert a paragraph to this object.
30pub trait FromDeb822Paragraph<P: Deb822LikeParagraph> {
31    /// Convert a paragraph to this object.
32    fn from_paragraph(paragraph: &P) -> Result<Self, String>
33    where
34        Self: Sized;
35}
36
37/// Convert this object to a paragraph.
38pub trait ToDeb822Paragraph<P: Deb822LikeParagraph> {
39    /// Convert this object to a paragraph.
40    fn to_paragraph(&self) -> P;
41
42    /// Update the given paragraph with the values from this object.
43    fn update_paragraph(&self, paragraph: &mut P);
44}
45
46/// Format a field value as a single line.
47///
48/// # Panics
49///
50/// Panics if the value contains newline characters.
51pub fn format_single_line(value: &str, field_name: &str) -> String {
52    assert!(
53        !value.contains('\n'),
54        "Field '{}' is marked as single_line but contains newlines",
55        field_name
56    );
57    value.to_string()
58}
59
60/// Format a field value as multi-line, ensuring continuation lines start with a space.
61///
62/// If the value is already single-line, it is returned as-is.
63/// For multi-line values:
64/// - The first line is kept as-is
65/// - Empty continuation lines are replaced with " ." (space followed by dot)
66/// - Non-empty continuation lines are prefixed with a space
67pub fn format_multi_line(value: &str) -> String {
68    if !value.contains('\n') {
69        value.to_string()
70    } else {
71        value
72            .lines()
73            .enumerate()
74            .map(|(i, line)| {
75                if i == 0 {
76                    line.to_string()
77                } else if line.is_empty() {
78                    " .".to_string()
79                } else {
80                    format!(" {}", line)
81                }
82            })
83            .collect::<Vec<_>>()
84            .join("\n")
85    }
86}
87
88/// Format a field value as folded, stripping whitespace and joining lines with spaces.
89///
90/// This implements RFC 822 folding behavior by:
91/// - Trimming leading and trailing whitespace from each line
92/// - Filtering out empty lines
93/// - Joining the remaining lines with single spaces
94pub fn format_folded(value: &str) -> String {
95    value
96        .lines()
97        .map(|line| line.trim())
98        .filter(|line| !line.is_empty())
99        .collect::<Vec<_>>()
100        .join(" ")
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_trait_impl_directly() {
109        // Test the trait methods directly to improve coverage
110        let mut para = crate::Paragraph {
111            fields: vec![crate::Field {
112                name: "Test".to_string(),
113                value: "Value".to_string(),
114            }],
115        };
116
117        // Test Deb822LikeParagraph::get
118        let result: Option<String> = Deb822LikeParagraph::get(&para, "Test");
119        assert_eq!(result, Some("Value".to_string()));
120
121        // Test Deb822LikeParagraph::set
122        Deb822LikeParagraph::set(&mut para, "Test", "NewValue");
123        assert_eq!(para.get("Test"), Some("NewValue"));
124
125        // Test Deb822LikeParagraph::remove
126        Deb822LikeParagraph::remove(&mut para, "Test");
127        assert_eq!(para.get("Test"), None);
128    }
129
130    #[test]
131    fn test_deb822like_paragraph_impl() {
132        // Create mock crate::Paragraph for tests
133        let mut para = crate::Paragraph {
134            fields: vec![crate::Field {
135                name: "Name".to_string(),
136                value: "Test".to_string(),
137            }],
138        };
139
140        // Test get() - this calls the implementation on line 16-17
141        assert_eq!(para.get("Name"), Some("Test"));
142        assert_eq!(para.get("NonExistent"), None);
143
144        // Test set() - this calls the implementation on line 20-21
145        para.set("Name", "NewValue");
146        assert_eq!(para.get("Name"), Some("NewValue"));
147
148        // Test set() with new key
149        para.set("NewKey", "Value");
150        assert_eq!(para.get("NewKey"), Some("Value"));
151
152        // Test remove() - this calls the implementation on line 24-25
153        para.remove("Name");
154        assert_eq!(para.get("Name"), None);
155        assert_eq!(para.get("NewKey"), Some("Value"));
156
157        // Create a new paragraph with multiple fields of the same name
158        let mut para = crate::Paragraph {
159            fields: vec![
160                crate::Field {
161                    name: "Duplicate".to_string(),
162                    value: "Value1".to_string(),
163                },
164                crate::Field {
165                    name: "Duplicate".to_string(),
166                    value: "Value2".to_string(),
167                },
168            ],
169        };
170
171        // Test remove() removes all matches
172        para.remove("Duplicate");
173        assert_eq!(para.get("Duplicate"), None);
174        assert_eq!(para.fields.len(), 0);
175    }
176
177    #[cfg(feature = "derive")]
178    mod derive {
179        use super::*;
180        use crate as deb822_fast;
181        use crate::{FromDeb822, ToDeb822};
182
183        #[test]
184        fn test_derive() {
185            #[derive(ToDeb822)]
186            struct Foo {
187                bar: String,
188                baz: i32,
189                blah: Option<String>,
190            }
191
192            let foo = Foo {
193                bar: "hello".to_string(),
194                baz: 42,
195                blah: None,
196            };
197
198            let paragraph: crate::Paragraph = foo.to_paragraph();
199            assert_eq!(paragraph.get("bar"), Some("hello"));
200            assert_eq!(paragraph.get("baz"), Some("42"));
201            assert_eq!(paragraph.get("blah"), None);
202        }
203
204        #[test]
205        fn test_optional_missing() {
206            #[derive(ToDeb822)]
207            struct Foo {
208                bar: String,
209                baz: Option<String>,
210            }
211
212            let foo = Foo {
213                bar: "hello".to_string(),
214                baz: None,
215            };
216
217            let paragraph: crate::Paragraph = foo.to_paragraph();
218            assert_eq!(paragraph.get("bar"), Some("hello"));
219            assert_eq!(paragraph.get("baz"), None);
220
221            assert_eq!("bar: hello\n", paragraph.to_string());
222        }
223
224        #[test]
225        fn test_deserialize_with() {
226            let mut para: crate::Paragraph = "bar: bar\n# comment\nbaz: blah\n".parse().unwrap();
227
228            fn to_bool(s: &str) -> Result<bool, String> {
229                Ok(s == "ja")
230            }
231
232            fn from_bool(s: &bool) -> String {
233                if *s {
234                    "ja".to_string()
235                } else {
236                    "nee".to_string()
237                }
238            }
239
240            #[derive(FromDeb822, ToDeb822)]
241            struct Foo {
242                bar: String,
243                #[deb822(deserialize_with = to_bool, serialize_with = from_bool)]
244                baz: bool,
245            }
246
247            let mut foo: Foo = Foo::from_paragraph(&para).unwrap();
248            assert_eq!(foo.bar, "bar");
249            assert!(!foo.baz);
250
251            foo.bar = "new".to_string();
252
253            foo.update_paragraph(&mut para);
254
255            assert_eq!(para.get("bar"), Some("new"));
256            assert_eq!(para.get("baz"), Some("nee"));
257            assert_eq!(para.to_string(), "bar: new\nbaz: nee\n");
258        }
259
260        #[test]
261        fn test_update_remove() {
262            let mut para: crate::Paragraph = "bar: bar\n# comment\nbaz: blah\n".parse().unwrap();
263
264            #[derive(FromDeb822, ToDeb822)]
265            struct Foo {
266                bar: Option<String>,
267                baz: String,
268            }
269
270            let mut foo: Foo = Foo::from_paragraph(&para).unwrap();
271            assert_eq!(foo.bar, Some("bar".to_string()));
272            assert_eq!(foo.baz, "blah");
273
274            foo.bar = None;
275
276            foo.update_paragraph(&mut para);
277
278            assert_eq!(para.get("bar"), None);
279            assert_eq!(para.get("baz"), Some("blah"));
280            assert_eq!(para.to_string(), "baz: blah\n");
281        }
282    }
283}