exemplify_lib/layers/domain/
collect_examples.rs

1use std::cmp::{min, Ordering};
2use std::collections::{HashMap, HashSet};
3use std::io::Read;
4use std::pin::Pin;
5
6use futures::{Stream, StreamExt};
7
8use crate::layers::domain::entities::chunk::Chunk;
9use crate::layers::domain::chunk_reader::ChunkReader;
10use crate::layers::domain::parser_settings::ParserSettings;
11use crate::layers::domain::reader_factory::ReaderContext;
12use crate::layers::domain::entities::example::Example;
13
14
15/// Transform a stream of file readers into a stream of examples
16/// Note: this will exhaust all readers before starting the stream of examples
17pub async fn collect_examples<Reader: Read>(mut reader_factory: Pin<Box<dyn Stream<Item=Result<ReaderContext<Reader>, String>>>>, parser_settings: ParserSettings)
18                                            -> Result<Pin<Box<dyn Stream<Item=Example>>>, String> {
19    let mut chunk_cache: HashMap<String, Vec<Chunk>> = Default::default();
20
21    while let Some(reader_context) = reader_factory.next().await {
22        let reader_context = reader_context?;
23
24        let chunk_reader = ChunkReader::new(reader_context, parser_settings.clone());
25
26        chunk_cache = exhaust_reader(chunk_reader, chunk_cache).await?;
27    }
28
29    let examples = finalize_examples(chunk_cache)?;
30
31    Ok(Box::pin(futures::stream::iter(examples.into_iter())))
32}
33
34fn finalize_examples(chunk_cache: HashMap<String, Vec<Chunk>>) -> Result<Vec<Example>, String> {
35    let mut examples = Vec::new();
36
37    for v in &chunk_cache {
38        verify_example(&v.1)?;
39
40        let mut chunks: Vec<Chunk> = v.1[..].to_vec();
41
42        chunks.sort_by(|lhs, rhs| {
43            if let Some(r) = rhs.part_number {
44                if let Some(l) = lhs.part_number {
45                    if l < r {
46                        return Ordering::Less;
47                    } else {
48                        return Ordering::Greater;
49                    }
50                }
51            }
52
53            return Ordering::Equal;
54        });
55
56        let mut example_title = None;
57        let mut example_language = None;
58        let mut example_id = None;
59
60        let content = chunks.into_iter().flat_map(|v| {
61            if let Some(title) = v.title {
62                if example_title.is_none() {
63                    example_title = Some(title)
64                }
65            }
66
67            if let Some(language) = v.language {
68                if example_language.is_none() {
69                    example_language = Some(language)
70                }
71            }
72
73            if let Some(id) = v.id {
74                if example_id.is_none() {
75                    example_id = Some(id)
76                }
77            }
78
79
80            let content = v.content.into_iter().map(|l| l.value).collect();
81
82            match v.indentation {
83                Some(indentation) => indent(left_align(content), indentation),
84                _ => content
85            }
86        }).collect();
87
88        let example = Example::new(v.0.clone(), content, example_title, example_language, example_id);
89
90        examples.push(example)
91    }
92
93    Ok(examples)
94}
95
96/// Align the string of the content vector so that the least indented
97/// line has indentation 0
98pub fn left_align(content: Vec<String>) -> Vec<String> {
99    let mut min_indent = usize::MAX;
100
101    for line in &content {
102        if line.len() == 0 {
103            continue;
104        }
105
106        let mut ws_end = 0;
107
108        for c in line.chars() {
109            if c != ' ' {
110                break;
111            }
112
113            ws_end += 1;
114        }
115
116        min_indent = min(min_indent, ws_end);
117    }
118
119    content.into_iter()
120        .map(|mut line| {
121            line.drain(..min(min_indent, line.len()));
122            line
123        }).collect()
124}
125
126fn indent(content: Vec<String>, indentation: u32) -> Vec<String> {
127    content.into_iter().map(|line| format!("{}{}", (0..indentation).map(|_| " ").collect::<String>(), line)).collect()
128}
129
130async fn exhaust_reader<Reader: Read>(mut chunk_reader: ChunkReader<Reader>, mut chunk_cache: HashMap<String, Vec<Chunk>>) -> Result<HashMap<String, Vec<Chunk>>, String> {
131    while let Some(chunks) = chunk_reader.next().await {
132        let chunks = chunks?;
133
134        for chunk in chunks {
135            let chunk_name = chunk.example_name.clone();
136
137            let cache = match chunk_cache.remove(chunk.example_name.as_str()) {
138                Some(mut cache) => {
139                    cache.push(chunk);
140                    cache
141                }
142                _ => vec![chunk]
143            };
144
145            chunk_cache.insert(chunk_name, cache);
146        }
147    }
148
149    Ok(chunk_cache)
150}
151
152fn verify_example(chunks: &Vec<Chunk>) -> Result<(), String> {
153    let mut part_set = HashSet::new();
154
155    for chunk in chunks {
156        if let Some(part) = chunk.part_number {
157            if part_set.contains(&part) {
158                return Err(format!("{}[{}]: Duplicate part {} ", chunk.source_name, chunk.start_line, part).into());
159            }
160            part_set.insert(part);
161        } else if chunks.len() > 1 {
162            return Err(format!("{}[{}]: You must provide a part number for chunks in examples with more than one chunk", chunk.source_name, chunk.start_line));
163        }
164    }
165
166    Ok(())
167}
168
169
170impl Example {
171    pub fn lines(&self) -> &Vec<String> {
172        &self.content
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use stringreader::StringReader;
179
180    use crate::layers::domain::reader_factory::ReaderFactory;
181    use crate::layers::domain::reader_stream::reader_stream;
182
183    use super::*;
184
185    struct StringReaderFactory {}
186
187    impl ReaderFactory<StringReader<'static>> for StringReaderFactory {
188        fn make_reader(&self, name: String) -> Result<ReaderContext<StringReader<'static>>, String> {
189            let content = match name.as_str() {
190                "a" => CONTENT_A,
191                "b" => CONTENT_B,
192                "c" => CONTENT_C,
193                "d" => CONTENT_FAIL_D,
194                "e" => CONTENT_FAIL_E,
195                _ => panic!()
196            };
197
198            Ok(ReaderContext { source_name: name.clone(), reader: StringReader::new(content) })
199        }
200    }
201
202    #[test]
203    fn test_left_align() {
204        let mut data = vec![
205            "    a".to_string(),
206            "   b".to_string()
207        ];
208
209        data = left_align(data);
210
211        assert_eq!(data[0], " a");
212        assert_eq!(data[1], "b");
213    }
214
215    #[tokio::test]
216    async fn test_example_producer() {
217        let parser_settings = ParserSettings { start_token: "##exemplify-start##".into(), end_token: "##exemplify-end##".into() };
218
219        let file_name_stream = Box::pin(futures::stream::iter(
220            vec![
221                Ok("a".into()),
222                Ok("b".into()),
223                Ok("c".into())
224            ].into_iter()));
225
226        let file_reader_factory = reader_stream(Box::new(StringReaderFactory {}), file_name_stream);
227        let _result = collect_examples(file_reader_factory, parser_settings.clone()).await.unwrap();
228
229        let file_name_stream = Box::pin(futures::stream::iter(
230            vec![
231                Ok("d".into())
232            ].into_iter()));
233
234        let file_reader_factory = reader_stream(Box::new(StringReaderFactory {}), file_name_stream);
235        let result = collect_examples(file_reader_factory, parser_settings.clone()).await;
236
237        assert_eq!(result.is_err(), true);
238
239        let file_name_stream = Box::pin(futures::stream::iter(
240            vec![
241                Ok("e".into())
242            ].into_iter()));
243
244        let file_reader_factory = reader_stream(Box::new(StringReaderFactory {}), file_name_stream);
245        let result = collect_examples(file_reader_factory, parser_settings.clone()).await;
246
247        assert_eq!(result.is_err(), true);
248    }
249
250    const CONTENT_A: &str = "\
251//##exemplify-start##{name=\"example-1\" part=1}
252class ExampleClass {}
253//##exemplify-end##
254class NotIncludedInExample {}
255//##exemplify-start##{name=\"example-1\" part=2}
256// This is also part of example-1
257//##exemplify-end##
258//##exemplify-start##{name=\"example-2\" part=1}
259//This chunk has no explicit end
260        ";
261
262    const CONTENT_B: &str = "\
263//##exemplify-start##{name=\"example-3\" part=1}
264class ExampleClass {}
265        ";
266
267    const CONTENT_C: &str = "\
268//##exemplify-start##{name=\"example-4\"}
269class ExampleClass {}
270        ";
271
272    const CONTENT_FAIL_D: &str = "\
273//##exemplify-start##{name=\"example-5\"}
274class ExampleClass {}
275//##exemplify-end##
276//##exemplify-start##{name=\"example-5\"}
277        ";
278
279    const CONTENT_FAIL_E: &str = "\
280//##exemplify-start##{name=\"example-5\"}
281class ExampleClass {}
282//##exemplify-start##{name=\"example-5\"}
283        ";
284}