exemplify_lib/layers/domain/
chunk_reader.rs

1use std::io::{BufRead, BufReader, Read};
2use std::pin::Pin;
3use std::str::FromStr;
4use std::sync::{Arc, Mutex};
5
6use futures::stream::Stream;
7use futures::task::{Context, Poll};
8
9use crate::layers::domain::entities::chunk::{Chunk, ChunkLine};
10use crate::layers::domain::parser_settings::ParserSettings;
11use crate::layers::domain::reader_factory::ReaderContext;
12
13pub struct ChunkReader<Reader> {
14    source_name: String,
15    reader: Arc<Mutex<BufReader<Reader>>>,
16    parser_settings: ParserSettings,
17    current_chunk: Option<Chunk>,
18    current_line: usize,
19}
20
21impl<Reader: Read> Stream for ChunkReader<Reader> {
22    type Item = Result<Vec<Chunk>, String>;
23
24    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
25        let mut completed_chunks = Vec::new();
26
27        let rc = self.reader.clone();
28        let mut reader = rc.lock().map_err(|e| { e.to_string() })?;
29
30        let taken_lines = reader.by_ref().lines().take(100);
31        let mut read_count = 0;
32
33        for line in taken_lines {
34            read_count += 1;
35            self.current_line += 1;
36
37            match line {
38                Err(err) => return Poll::Ready(Some(Err(err.to_string()))),
39                Ok(line) => {
40                    if let Some(result) = self.process_line(&line, self.current_line)? {
41                        completed_chunks.push(result);
42                    }
43                }
44            }
45        }
46
47        if read_count == 0 {
48            if self.current_chunk.is_some() {
49                if let Some(chunk) = self.finalize_chunk()? {
50                    completed_chunks.push(chunk);
51                }
52            }
53        }
54
55        if completed_chunks.len() > 0 {
56            Poll::Ready(Some(Ok(completed_chunks)))
57        } else if read_count == 0 {
58            Poll::Ready(None)
59        } else {
60            cx.waker().clone().wake();
61
62            Poll::Pending
63        }
64    }
65}
66
67impl<Reader: Read> ChunkReader<Reader> {
68    pub fn new(reader_context: ReaderContext<Reader>, parser_settings: ParserSettings) -> Self {
69        Self {
70            reader: Arc::new(Mutex::new(BufReader::new(reader_context.reader))),
71            parser_settings,
72            current_chunk: None,
73            source_name: reader_context.source_name,
74            current_line: 0,
75        }
76    }
77
78    fn process_line(self: &mut Pin<&mut Self>, line: &String, line_number: usize) -> Result<Option<Chunk>, String> {
79        let has_start = line.contains(&self.parser_settings.start_token);
80        let has_end = line.contains(&self.parser_settings.end_token);
81
82        match &mut self.current_chunk {
83            Some(chunk) => {
84                if has_start {
85                    return Err(format!("Error {}[{}]: attempting to start chunk-in-chunk", self.source_name, line_number));
86                }
87
88                if has_end {
89                    return self.finalize_chunk();
90                }
91
92                if !has_start && !has_end {
93                    {
94                        chunk.content.push(ChunkLine {
95                            value: line.clone(),
96                            line_number,
97                        });
98                    }
99                }
100
101                Ok(None)
102            }
103            None => {
104                if has_start {
105                    let params = Self::extract_chunk_params(&line, &self.source_name, line_number)?;
106
107                    self.current_chunk = Some(Chunk {
108                        example_name: params.name,
109                        content: vec![],
110                        part_number: params.part,
111                        indentation: params.indentation,
112                        source_name: self.source_name.clone(),
113                        start_line: line_number,
114                        title: params.title,
115                        language: params.language,
116                        id: params.id
117                    });
118                    return Ok(None);
119                }
120
121                if has_end {
122                    return Err(format!("Error {}[{}]: attempting to end chunk outside of chunk", self.source_name, line_number));
123                }
124
125                Ok(None)
126            }
127        }
128    }
129
130    fn extract_chunk_params(line: &String, source_name: &String, line_number: usize) -> Result<ChunkParams, String> {
131        lazy_static::lazy_static! {
132            static ref VAL_RE: regex::Regex = regex::Regex::new("(([a-zA-Z]+)\\s?=\\s?\"([a-zA-Z\\s0-9\\-\\(\\)_\\+\\.,'`@\\[\\]\\?!/]+)\")|(([a-zA-Z]+)\\s?=\\s?([0-9]+))").unwrap();
133        }
134
135        let mut name: String = "".into();
136        let mut part = None;
137        let mut indentation = None;
138        let mut title = None;
139        let mut language = None;
140        let mut id = None;
141
142        for val in VAL_RE.captures_iter(line) {
143            let param_name_name = val.get(2);
144            let param_name_val = val.get(3);
145
146            let param_part_name = val.get(5);
147            let param_part_val = val.get(6);
148
149
150            if let Some(pname) = param_name_name {
151                if let Some(n) = param_name_val {
152                    let val = n.as_str().to_string().clone();
153
154                    match pname.as_str().to_string().trim() {
155                        "name" => name = val,
156                        "title" => title = Some(val),
157                        "language" => language = Some(val),
158                        "id" => id = Some(val),
159                        _ => {}
160                    }
161                }
162            }
163
164            if let Some(pname) = param_part_name {
165                if let Some(part_val) = param_part_val {
166                    match pname.as_str().to_string().trim() {
167                        "part" => part = Some(u32::from_str(part_val.as_str()).map_err(|_| format!("{}[{}]: Failed to parse part number {}", source_name, line_number, part_val.as_str().to_string()))?),
168                        "indentation" => indentation = Some(u32::from_str(part_val.as_str())
169                            .map_err(|_| format!("{}[{}]: Failed to parse indentation number {}", source_name, line_number, part_val.as_str().to_string()))?),
170                        _ => {}
171                    }
172                }
173            }
174        }
175
176        if name.len() == 0 {
177            return Err(format!("{}[{}]: Missing name", source_name, line_number));
178        }
179
180        Ok(ChunkParams {
181            part,
182            name,
183            indentation,
184            title,
185            language,
186            id
187        })
188    }
189
190    fn finalize_chunk(self: &mut Pin<&mut Self>) -> Result<Option<Chunk>, String> {
191        Ok(self.current_chunk.take())
192    }
193}
194
195struct ChunkParams {
196    name: String,
197    part: Option<u32>,
198    indentation: Option<u32>,
199    title: Option<String>,
200    language: Option<String>,
201    id: Option<String>
202}