exemplify_lib/layers/domain/
chunk_reader.rs1use 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}