ptero/
context.rs

1use std::{error::Error, fmt};
2
3use log::error;
4
5use crate::text::{CoverTextLineIterator, CoverTextWordIterator};
6
7/// Context with essential methods for every encoder/decoder.
8pub trait Context {
9    /// Gets currently loaded cover text fragment *mutably*.
10    ///
11    /// # Returns
12    /// Result which is either `&mut String` or [ContextError].
13    fn get_current_text_mut(&mut self) -> Result<&mut String, ContextError>;
14
15    /// Gets currently loaded cover text fragment as read-only.
16    ///
17    /// # Returns
18    /// Result which is either `&String` or [ContextError].
19    fn get_current_text(&self) -> Result<&String, ContextError>;
20
21    /// Loads next cover text fragment.
22    ///
23    /// # Returns
24    /// Result which is either `&String` or [ContextError]. Returned string is the newly loaded fragment.
25    fn load_text(&mut self) -> Result<&str, ContextError>;
26}
27
28/// Context used by methods requiring pivot.
29/// Loads and returns cover text line by line - *not bound by pivot*.
30pub struct PivotByRawLineContext {
31    pivot: usize,
32    cover_text_iter: CoverTextLineIterator,
33    current_text: Option<String>,
34}
35/// Context used by methods requiring pivot.
36/// Loads cover text line by line, uses pivot and does not preserve original whitespace.
37/// It also exposes the word iterator for purpose of peeking and/or traversing.
38pub struct PivotByLineContext {
39    pivot: usize,
40    cover_text_iter: CoverTextWordIterator,
41    current_text: Option<String>,
42}
43
44impl PivotByRawLineContext {
45    pub fn new(cover_text: &str, pivot: usize) -> Self {
46        PivotByRawLineContext {
47            pivot,
48            cover_text_iter: CoverTextLineIterator::new(cover_text),
49            current_text: None,
50        }
51    }
52
53    pub fn get_pivot(&self) -> usize {
54        self.pivot
55    }
56}
57
58impl PivotByLineContext {
59    const WORD_DELIMITER: char = ' ';
60
61    pub fn new(cover_text: &str, pivot: usize) -> Self {
62        PivotByLineContext {
63            pivot,
64            cover_text_iter: CoverTextWordIterator::new(cover_text),
65            current_text: None,
66        }
67    }
68
69    pub fn get_pivot(&self) -> usize {
70        self.pivot
71    }
72
73    // Peeks the next word without forwarding the iterator.
74    //
75    // # Returns
76    // Returns the next word or none if the iterator gets out-of-bounds.
77    pub fn peek_word(&mut self) -> Option<String> {
78        self.cover_text_iter.peek()
79    }
80    
81    // Gets the next word and proceeds the iterator.
82    //
83    // # Returns
84    // Returns the next word or none if the iterator gets out-of-bounds.
85    pub fn next_word(&mut self) -> Option<String> {
86        self.cover_text_iter.next()
87    }
88
89    // Constructs line of maximum length determined by pivot.
90    //
91    // # Returns
92    // Returns the line or none if there are no words left. It is a result, 
93    // throws an error if line cannot be constructed but there are still words left.
94    fn construct_line_by_pivot(&mut self) -> Result<Option<String>, ContextError> {
95        let maybe_word = self.cover_text_iter.peek();
96
97        if maybe_word.is_none() {
98            return Ok(None); 
99        }
100
101        let mut word = maybe_word.unwrap();
102        
103        if word.len() > self.pivot {
104            error!("Stuck at word of length {}.", word.len());
105            return Err(ContextError { kind: ContextErrorKind::CannotConstructLine });
106        }
107        let mut line = String::new();
108        while line.len() + word.len() <= self.pivot {
109            line.push_str(&word);
110            line.push(Self::WORD_DELIMITER);
111            // Skip the peeked word
112            self.cover_text_iter.next();
113
114            if let Some(next_word) = self.cover_text_iter.peek() {
115                word = next_word;
116            } else {
117                return Ok(Some(line));
118            }
119        }
120        Ok(Some(line.trim_end().to_string()))
121    }
122}
123
124impl Context for PivotByLineContext {
125    fn get_current_text_mut(&mut self) -> Result<&mut String, ContextError> {
126        self.current_text
127            .as_mut()
128            .ok_or_else(|| ContextError::new(ContextErrorKind::NoTextLeft))
129    }
130
131    fn get_current_text(&self) -> Result<&String, ContextError> {
132        self.current_text
133            .as_ref()
134            .ok_or_else(|| ContextError::new(ContextErrorKind::NoTextLeft))
135    }
136
137    // Loads the line considering the pivot. Line length is smaller or equal the pivot value.
138    // This function does not preserve original whitespace. By default words are delimited by standard ASCII space (0x20). 
139    //
140    // # Returns
141    // Result which is either the line or [ContextError] if anything fails. 
142    fn load_text(&mut self) -> Result<&str, ContextError> {
143        self.current_text = self.construct_line_by_pivot()?;
144        self.current_text
145            .as_deref()
146            .ok_or_else(|| ContextError::new(ContextErrorKind::NoTextLeft))
147    }
148}
149
150impl Context for PivotByRawLineContext {
151    fn get_current_text_mut(&mut self) -> Result<&mut String, ContextError> {
152        self.current_text
153            .as_mut()
154            .ok_or_else(|| ContextError::new(ContextErrorKind::NoTextLeft))
155    }
156
157    fn get_current_text(&self) -> Result<&String, ContextError> {
158        self.current_text
159            .as_ref()
160            
161            .ok_or_else(|| ContextError::new(ContextErrorKind::NoTextLeft))
162    }
163
164    // Loads the raw line. By raw it means preserving the whitespace
165    //
166    // # Returns
167    // Result which is either the line or [ContextError] if anything fails. 
168    fn load_text(&mut self) -> Result<&str, ContextError> {
169        self.current_text = self.cover_text_iter.next();
170        self.current_text
171            .as_deref()
172            .ok_or_else(|| ContextError::new(ContextErrorKind::NoTextLeft))
173    }
174}
175
176/// Enum determining the exact context error.
177#[derive(Debug, Clone, Copy)]
178pub enum ContextErrorKind {
179    NoTextLeft,
180    CannotConstructLine
181}
182
183/// Error implementation for [Context]. Exact error message is determined by [ContextErrorKind].
184#[derive(Debug)]
185pub struct ContextError {
186    kind: ContextErrorKind,
187}
188
189impl ContextError {
190    fn new(kind: ContextErrorKind) -> Self {
191        ContextError { kind }
192    }
193
194    pub fn kind(&self) -> ContextErrorKind {
195        self.kind
196    }
197}
198
199#[cfg(not(tarpaulin_include))]
200impl fmt::Display for ContextError {
201    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
202        match self.kind {
203            ContextErrorKind::NoTextLeft => write!(f, "No cover text left.",),
204            ContextErrorKind::CannotConstructLine => write!(f, "Pivot is too small. Couldn't load the line no longer than the pivot.",),
205        }
206    }
207}
208
209impl Error for ContextError {}