Skip to main content

hataori/
map.rs

1use std::fmt::{self, Display, Formatter};
2
3pub(crate) const MAX_ERROR_MESSAGE_BYTES: usize = 4096;
4
5/// The first callback error returned by [`map`].
6#[derive(Debug)]
7pub struct MapError {
8    index: usize,
9    message: String,
10}
11
12impl MapError {
13    /// Returns the zero-based index of the input that failed.
14    pub fn index(&self) -> usize {
15        self.index
16    }
17
18    /// Returns the callback error message, at most 4096 bytes and truncated at a UTF-8 character boundary.
19    pub fn message(&self) -> &str {
20        &self.message
21    }
22
23    fn new<E: Display>(index: usize, error: E) -> Self {
24        Self {
25            index,
26            message: truncate_message(error.to_string()),
27        }
28    }
29}
30
31impl Display for MapError {
32    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
33        write!(
34            formatter,
35            "map failed at index {}: {}",
36            self.index, self.message
37        )
38    }
39}
40
41impl std::error::Error for MapError {}
42
43pub(crate) fn truncate_message(mut message: String) -> String {
44    if message.len() > MAX_ERROR_MESSAGE_BYTES {
45        let mut end = MAX_ERROR_MESSAGE_BYTES;
46        while !message.is_char_boundary(end) {
47            end -= 1;
48        }
49        message.truncate(end);
50    }
51    message
52}
53
54/// Applies `f` to `items` sequentially and returns results in their original order.
55///
56/// Each successful input is evaluated exactly once. Evaluation stops at the first
57/// callback error, and trailing unconsumed items are dropped naturally. Errors
58/// report a zero-based input index and a message truncated at a UTF-8 character
59/// boundary to at most 4096 bytes. This serial operation has no serde, `Send`,
60/// `Sync`, or `'static` requirements.
61///
62/// # Example
63///
64/// ```
65/// use hataori::map;
66///
67/// let doubled = map(vec![1, 2, 3], |item| {
68///     Ok::<_, std::convert::Infallible>(item * 2)
69/// })
70/// .unwrap();
71/// assert_eq!(doubled, vec![2, 4, 6]);
72/// ```
73pub fn map<T, U, E, F>(items: Vec<T>, mut f: F) -> Result<Vec<U>, MapError>
74where
75    F: FnMut(T) -> Result<U, E>,
76    E: Display,
77{
78    let mut output = Vec::with_capacity(items.len());
79    for (index, item) in items.into_iter().enumerate() {
80        match f(item) {
81            Ok(value) => output.push(value),
82            Err(error) => return Err(MapError::new(index, error)),
83        }
84    }
85    Ok(output)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::{map, MapError};
91
92    #[test]
93    fn empty_input_succeeds() {
94        let result = map(Vec::<i32>::new(), |_| Ok::<_, &str>(0));
95        assert_eq!(result.unwrap(), Vec::<i32>::new());
96    }
97
98    #[test]
99    fn successful_inputs_are_ordered_and_called_once() {
100        let calls = std::cell::Cell::new(0);
101        let result = map(vec![3, 1, 2], |item| {
102            calls.set(calls.get() + 1);
103            Ok::<_, &str>(item * 2)
104        });
105
106        assert_eq!(result.unwrap(), vec![6, 2, 4]);
107        assert_eq!(calls.get(), 3);
108    }
109
110    #[test]
111    fn first_error_stops_later_callbacks() {
112        let calls = std::cell::Cell::new(0);
113        let result = map(vec![10, 20, 30], |item| {
114            calls.set(calls.get() + 1);
115            if item == 20 {
116                Err::<i32, _>("bad item")
117            } else {
118                Ok(item)
119            }
120        });
121
122        let error = result.unwrap_err();
123        assert_eq!(error.index(), 1);
124        assert_eq!(error.message(), "bad item");
125        assert_eq!(calls.get(), 2);
126    }
127
128    #[test]
129    fn ascii_error_is_capped() {
130        let result = map(vec![()], |_| Err::<(), _>("x".repeat(5000)));
131        assert_eq!(result.unwrap_err().message().len(), 4096);
132    }
133
134    #[test]
135    fn multibyte_error_is_truncated_at_utf8_boundary() {
136        let message = format!("{}é", "a".repeat(4095));
137        let error = map(vec![()], |_| Err::<(), _>(message.clone())).unwrap_err();
138
139        assert_eq!(error.message().len(), 4095);
140        assert!(error.message().is_char_boundary(error.message().len()));
141    }
142
143    #[test]
144    fn serial_map_accepts_borrowed_non_static_values_and_state() {
145        use std::{cell::RefCell, rc::Rc};
146
147        let input_text = String::from("borrowed input");
148        let error_text = String::from("borrowed error");
149        let calls = Rc::new(RefCell::new(0));
150        let captured = Rc::clone(&calls);
151        let input = Rc::new(input_text.as_str());
152        let output: Result<Vec<Rc<&str>>, MapError> = map(vec![input], |item| {
153            *captured.borrow_mut() += 1;
154            Ok::<_, Rc<&str>>(item)
155        });
156
157        assert_eq!(output.unwrap().as_slice(), &[Rc::new("borrowed input")]);
158        assert_eq!(*calls.borrow(), 1);
159
160        let error = map(vec![Rc::new(input_text.as_str())], |_| {
161            Err::<(), _>(Rc::new(error_text.as_str()))
162        })
163        .unwrap_err();
164        assert_eq!(error.message(), "borrowed error");
165    }
166}