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}