Skip to main content

cba/
bo.rs

1//! IO
2
3use std::{error::Error, fs, io, path::Path};
4
5use crate::{StringError, bait::ResultExt, bog::BogOkExt};
6
7// ------------ File read/write (bile) -------------
8
9/// Saves type to file.
10///
11/// Prints error.
12pub fn dump_type<'a, T, E: Error>(
13    path: impl AsRef<Path>,
14    input: &'a T,
15    string_maker: impl FnOnce(&'a T) -> Result<String, E>,
16) -> Result<(), StringError> {
17    let path = path.as_ref().with_extension("toml");
18    let type_name = std::any::type_name::<T>().rsplit("::").next().unwrap();
19    let error_prefix = format!("Failed to save {type_name} to {}", path.to_string_lossy());
20
21    let content = string_maker(input).prefix(&error_prefix)?;
22    fs::write(path, content).prefix(&error_prefix)
23}
24
25/// Returns error string if file could not be found/read/parsed.
26pub fn load_type<T, E: std::fmt::Display>(
27    path: impl AsRef<Path>,
28    str_loader: impl FnOnce(&str) -> Result<T, E>, // pass a closure here if u need to satisfy hrtb
29) -> Result<T, StringError> {
30    let path = path.as_ref().with_extension("toml");
31    let type_name = std::any::type_name::<T>().rsplit("::").next().unwrap();
32    let error_prefix = format!("Failed to load {type_name} from {}", path.to_string_lossy());
33
34    let mut file = fs::File::open(path).prefix(&error_prefix)?;
35
36    let mut contents = String::new();
37    io::Read::read_to_string(&mut file, &mut contents).prefix(&error_prefix)?;
38
39    str_loader(&contents).prefix(&error_prefix)
40}
41
42/// If the path exists, load from it, otherwise load from the provided default.
43///
44/// Prints error.
45///
46/// # Example
47/// ```rust, ignore
48/// #[derive(Debug, serde::Deserialize)]
49/// pub struct LessfilterConfig {
50///     #[serde(flatten, default)]
51///     pub test: TestSettings,
52///     #[serde(default)]
53///     pub rules: RulesConfig,
54///     #[serde(default)]
55///     pub actions: CustomActions,
56/// }
57///
58/// impl Default for LessfilterConfig {
59///     fn default() -> Self {
60///         let ret = toml::from_str(include_str!("../../assets/config/lessfilter.toml"));
61///         ret.unwrap()
62///     }
63/// }
64///
65/// let cfg: LessfilterConfig = load_type_or_default(lessfilter_cfg_path(), |s| toml::from_str(s));
66/// ```
67pub fn load_type_or_default<T: Default, E: std::fmt::Display>(
68    path: impl AsRef<Path>,
69    str_loader: impl Fn(&str) -> Result<T, E>,
70) -> T {
71    let path = path.as_ref();
72    if path.is_file() {
73        load_type(path, &str_loader)
74            .prefix("Using default config due to errors")
75            ._wbog()
76            .unwrap_or_else(T::default)
77    } else {
78        T::default()
79    }
80}
81
82/// Write string to file, creating parent directories as needed.
83pub fn write_str(path: &Path, contents: &str) -> io::Result<()> {
84    if let Some(p) = path.parent() {
85        std::fs::create_dir_all(p)?; // normalize should ensure parent always works
86    }
87    std::fs::write(path, contents)?;
88
89    Ok(())
90}
91
92// --------- READER ------------
93// todo: decide on how to handle max chunks
94use std::io::{BufRead, Read};
95
96#[derive(Debug, thiserror::Error)]
97#[non_exhaustive]
98pub enum MapReaderError<E> {
99    #[error("Failed to read chunk {0}: {1}")]
100    ChunkError(usize, std::io::Error),
101    #[error("Aborted: {0}")]
102    Custom(#[from] E),
103}
104
105/// Adapt a reader, splitting on the delim character.
106pub fn read_to_chunks<R: Read>(reader: R, delim: char) -> std::io::Split<std::io::BufReader<R>> {
107    io::BufReader::new(reader).split(delim as u8)
108}
109
110// note: stream means wrapping with closure passed stream::unfold and returning f() inside
111
112/// Map each chunk read from reader to a string, passing to f.
113/// Logs chunk reading errors.
114/// Use [`map_reader_lines`] instead for reading newlines.
115///
116///
117/// # Example
118/// ```rust,ignore
119/// pub fn map_reader<E: SSS + std::fmt::Display>(
120///     reader: impl Read + SSS,
121///     f: impl FnMut(String) -> Result<(), E> + SSS,
122///     input_separator: Option<char>,
123///     abort_empty: Option<RenderSender<NullActionExt>>,
124/// ) -> tokio::task::JoinHandle<Result<usize, MapReaderError<E>>> {
125///     tokio::task::spawn_blocking(move || {
126///         let ret = if let Some(delim) = input_separator {
127///             map_chunks::<true, E>(read_to_chunks(reader, delim), f).elog()
128///         } else {
129///             map_reader_lines::<true, E>(reader, f).elog()
130///         };
131///
132///         if let Some(render_tx) = abort_empty
133///             && matches!(ret, Ok(0))
134///         {
135///             let _ = render_tx.send(matchmaker::message::RenderCommand::QuitEmpty);
136///         }
137///         ret
138///     })
139/// }
140/// ```
141pub fn map_chunks<const INVALID_FAIL: bool, E>(
142    iter: impl Iterator<Item = std::io::Result<Vec<u8>>>,
143    mut f: impl FnMut(String) -> Result<(), E>,
144) -> Result<usize, MapReaderError<E>> {
145    let mut count = 0;
146    for (i, chunk_result) in iter.enumerate() {
147        let bytes = chunk_result.map_err(|e| MapReaderError::ChunkError(i, e))?;
148
149        match String::from_utf8(bytes) {
150            Ok(s) => {
151                if let Err(e) = f(s) {
152                    return Err(MapReaderError::Custom(e));
153                } else {
154                    count += 1;
155                }
156            }
157            Err(e) => {
158                let err = format!(
159                    "Invalid UTF-8 in stdin at byte {}: {}",
160                    e.utf8_error().valid_up_to(),
161                    e
162                );
163                // Skip but continue reading
164                if INVALID_FAIL {
165                    return Err(MapReaderError::ChunkError(i, std::io::Error::other(err)));
166                } else {
167                    continue;
168                }
169            }
170        }
171    }
172    Ok(count)
173}
174
175/// Map each line read from reader to a string, passing to f.
176/// Logs read errors.
177pub fn map_reader_lines<const INVALID_FAIL: bool, E>(
178    reader: impl Read,
179    mut f: impl FnMut(String) -> Result<(), E>,
180) -> Result<usize, MapReaderError<E>> {
181    let buf_reader = io::BufReader::new(reader);
182    let mut count = 0;
183
184    for (i, line) in buf_reader.lines().enumerate() {
185        match line {
186            Ok(l) => {
187                if let Err(e) = f(l) {
188                    return Err(MapReaderError::Custom(e));
189                } else {
190                    count += 1;
191                }
192            }
193            Err(e) => {
194                if INVALID_FAIL {
195                    return Err(MapReaderError::ChunkError(i, e));
196                } else {
197                    continue;
198                }
199            }
200        }
201    }
202    Ok(count)
203}