exemplify_lib/layers/domain/
collect_examples.rs1use 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
15pub 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
96pub 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}