sweet_potator/recipe/
list.rs

1use std::{fmt, slice};
2
3use serde::Serialize;
4
5use super::{
6    errors::{ParseError, ParseResult},
7    ParseFromStr,
8};
9
10#[derive(Debug, Serialize)]
11pub struct Section<D> {
12    pub name: String,
13    pub items: Vec<D>,
14}
15
16impl<D> Section<D> {
17    pub fn new(name: String, items: Vec<D>) -> Self {
18        Self { name, items }
19    }
20}
21
22impl<P: ParseFromStr> Section<P> {
23    fn parse_items<'a>(
24        iter: &'a mut slice::Iter<String>,
25    ) -> ParseResult<(Vec<P>, Option<&'a String>)> {
26        let mut items = Vec::new();
27        for line in iter {
28            if let Ok(line) = strip_prefix(line) {
29                items.push(P::parse_from_str(line)?);
30            } else {
31                return Ok((items, Some(line)));
32            }
33        }
34        Ok((items, None))
35    }
36}
37
38impl<D: fmt::Display> Section<D> {
39    fn format(&self, f: &mut fmt::Formatter<'_>, indentation: &str) -> fmt::Result {
40        writeln!(f, "{}{}", indentation, self.name)?;
41        format_items(&self.items, f, &format!("{indentation}{indentation}"))
42    }
43}
44
45#[derive(Debug)]
46pub enum List<D> {
47    Basic(Vec<D>),
48    Sectioned(Vec<Section<D>>),
49}
50
51impl<D> List<D> {
52    pub fn count(&self) -> usize {
53        match self {
54            Self::Basic(items) => items.len(),
55            Self::Sectioned(sections) => sections
56                .iter()
57                .fold(0, |acc, section| acc + section.items.len()),
58        }
59    }
60}
61
62impl<D: Serialize> Serialize for List<D> {
63    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64    where
65        S: serde::Serializer,
66    {
67        let count = self.count();
68        match self {
69            Self::Basic(items) => {
70                #[derive(Serialize)]
71                struct Extended<'a, D> {
72                    items: &'a Vec<D>,
73                    count: usize,
74                }
75                let ext = Extended { items, count };
76                Ok(ext.serialize(serializer)?)
77            }
78            Self::Sectioned(sections) => {
79                #[derive(Serialize)]
80                struct Extended<'a, D> {
81                    sections: &'a Vec<Section<D>>,
82                    count: usize,
83                }
84                let ext = Extended { sections, count };
85                Ok(ext.serialize(serializer)?)
86            }
87        }
88    }
89}
90
91impl<D: fmt::Display> List<D> {
92    pub fn format(&self, f: &mut fmt::Formatter<'_>, indentation: &str) -> fmt::Result {
93        match self {
94            Self::Basic(items) => {
95                format_items(items, f, indentation)?;
96            }
97            Self::Sectioned(sections) => {
98                for section in sections {
99                    section.format(f, indentation)?;
100                }
101            }
102        }
103        Ok(())
104    }
105}
106
107impl<P: ParseFromStr> List<P> {
108    pub fn parse_basic<S: ParseFromStr>(lines: &[String]) -> ParseResult<Vec<S>> {
109        lines
110            .iter()
111            .map(|line| strip_prefix(line).and_then(S::parse_from_str))
112            .collect()
113    }
114
115    fn parse_sectioned(lines: &[String]) -> ParseResult<Vec<Section<P>>> {
116        let mut sections = Vec::new();
117        let mut lines = lines.iter();
118        let mut section_name = if let Some(line) = lines.next() {
119            line.to_string()
120        } else {
121            return Ok(sections);
122        };
123        loop {
124            if section_name.is_empty() {
125                return Err(ParseError::empty("list section name"));
126            }
127            let (items, new_section_name) = Section::parse_items(&mut lines)?;
128            sections.push(Section::new(section_name, items));
129            if let Some(name) = new_section_name {
130                section_name = name.into();
131            } else {
132                break;
133            }
134        }
135        Ok(sections)
136    }
137}
138
139impl<D: ParseFromStr> TryFrom<Vec<String>> for List<D> {
140    type Error = ParseError;
141
142    fn try_from(lines: Vec<String>) -> Result<Self, Self::Error> {
143        if lines.first().map_or(true, |line| line.starts_with("- ")) {
144            Ok(Self::Basic(Self::parse_basic(&lines)?))
145        } else {
146            Ok(Self::Sectioned(Self::parse_sectioned(&lines)?))
147        }
148    }
149}
150
151pub fn format_items<D: fmt::Display>(
152    items: &[D],
153    f: &mut fmt::Formatter<'_>,
154    indentation: &str,
155) -> fmt::Result {
156    for item in items {
157        writeln!(f, "{indentation}- {item}")?;
158    }
159    Ok(())
160}
161
162fn strip_prefix(line: &str) -> ParseResult<&str> {
163    line.strip_prefix("- ")
164        .ok_or_else(|| "list item must start with '- '".into())
165        .map(str::trim_start)
166}
167
168#[cfg(test)]
169mod tests {
170
171    use super::*;
172
173    struct DisplayTest<'a, D> {
174        list: &'a List<D>,
175        indentation: String,
176    }
177
178    impl<'a, D> DisplayTest<'a, D> {
179        pub fn new(list: &'a List<D>, indent_size: usize) -> Self {
180            let indentation = " ".repeat(indent_size);
181            Self { list, indentation }
182        }
183    }
184
185    impl<'a, D: fmt::Display> fmt::Display for DisplayTest<'a, D> {
186        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187            self.list.format(f, &self.indentation)
188        }
189    }
190
191    const SECTIONED_LIST: &str = concat!(
192        "section 1\n",
193        "- item 1.1\n",
194        "- item 1.2\n",
195        "section 2\n",
196        "- item 2.1\n",
197        "- item 2.2\n",
198    );
199
200    const TWO_INDENTED_SECTIONED_LIST: &str = concat!(
201        "  section 1\n",
202        "    - item 1.1\n",
203        "    - item 1.2\n",
204        "  section 2\n",
205        "    - item 2.1\n",
206        "    - item 2.2\n",
207    );
208
209    #[test]
210    fn test_count() {
211        let list = List::Basic(vec!["item 1", "item 2"]);
212        assert_eq!(list.count(), 2);
213        let section1 = Section {
214            name: "section 1".into(),
215            items: vec!["item 1.1", "item 1.2"],
216        };
217        let section2 = Section {
218            name: "section 2".into(),
219            items: vec!["item 2.1"],
220        };
221        let list = List::Sectioned(vec![section1, section2]);
222        assert_eq!(list.count(), 3);
223    }
224
225    #[test]
226    fn test_display_basic() {
227        let list = List::Basic(vec!["item 1", "item 2"]);
228        let test = DisplayTest::new(&list, 0);
229        assert_eq!(test.to_string(), "- item 1\n- item 2\n");
230        let test = DisplayTest::new(&list, 2);
231        assert_eq!(test.to_string(), "  - item 1\n  - item 2\n");
232    }
233
234    #[test]
235    fn test_display_sectioned() {
236        let section1 = Section {
237            name: "section 1".into(),
238            items: vec!["item 1.1", "item 1.2"],
239        };
240        let section2 = Section {
241            name: "section 2".into(),
242            items: vec!["item 2.1", "item 2.2"],
243        };
244        let list = List::Sectioned(vec![section1, section2]);
245        let test = DisplayTest::new(&list, 0);
246        assert_eq!(test.to_string(), SECTIONED_LIST);
247        let test = DisplayTest::new(&list, 2);
248        assert_eq!(test.to_string(), TWO_INDENTED_SECTIONED_LIST);
249    }
250
251    #[test]
252    fn test_parse_basic() {
253        let list: List<String> = vec!["-  item 1".into(), "-  item 2".into()]
254            .try_into()
255            .unwrap();
256        assert!(matches!(list, List::Basic(items) if items == vec!("item 1", "item 2")))
257    }
258
259    #[test]
260    fn test_parse_basic_fail() {
261        assert!(List::<String>::try_from(vec![
262            "- item 1".into(),
263            "- item 2".into(),
264            String::new()
265        ])
266        .is_err());
267    }
268
269    #[test]
270    fn test_parse_basic_empty() {
271        assert!(List::<String>::try_from(vec![String::new()]).is_err());
272    }
273
274    #[test]
275    fn test_parse_sectioned() {
276        let list: List<String> = SECTIONED_LIST
277            .trim()
278            .split('\n')
279            .map(Into::into)
280            .collect::<Vec<_>>()
281            .try_into()
282            .unwrap();
283        assert!(matches!(list, List::Sectioned(_)));
284        if let List::Sectioned(sections) = list {
285            assert_eq!(sections.len(), 2);
286            let section = sections.get(0).unwrap();
287            assert_eq!(section.name, "section 1");
288            assert_eq!(section.items, vec!("item 1.1", "item 1.2"));
289            let section = sections.get(1).unwrap();
290            assert_eq!(section.name, "section 2");
291            assert_eq!(section.items, vec!("item 2.1", "item 2.2"));
292        }
293    }
294}