io_resp/
value.rs

1use crate::error::Error as TError;
2use crate::Node::{ARRAY, BULK_STRING, ERROR, INTEGER, SIMPLE_STRING, SIZE, UNKNOWN};
3
4/// A wrapper type for a RESP value.
5///
6/// This enum implements the `TryFrom` trait (`TryFrom<&str>`), to provide
7/// on-the-fly parsing and validation of RESP strings.
8///
9/// # Examples
10///
11/// ```rust
12/// use io_resp::{
13///     Node::{self, NIL, SIZE, ARRAY, ERROR, INTEGER, UNKNOWN, SIMPLE_STRING, BULK_STRING},
14///     Value::{self, Nil, Error, Array, String, Integer},
15///     Error as VError,
16///     ValueResult,
17/// };
18///
19/// assert_eq!( // Empty RESP
20///     "".try_into() as ValueResult,
21///     Err(VError::Unexpected {node: UNKNOWN, index: 0}));
22///
23/// assert_eq!( // Unterminated number: missing "\r\n"
24///     ":0".try_into() as ValueResult,
25///     Err(VError::Unexpected {node: INTEGER, index: 2}));
26///
27/// assert_eq!( // Not enough elements in the array
28///     "*2\r\n$-1\r\n".try_into() as ValueResult,
29///     Err(VError::Size {node: ARRAY, index: 9}));
30///
31/// assert_eq!( // Longer bulk string: got more that 2-bytes
32///     "$2\r\nHello\r\n".try_into() as ValueResult,
33///     Err(VError::Size {node: BULK_STRING, index: 6}));
34///
35/// assert_eq!( // Sorter bulk string: shorter by 1-byte (capital A acute is 2-bytes)
36///     "$3\r\nÂ\r\n".try_into() as ValueResult,
37///     Err(VError::Size {node: BULK_STRING, index: 7}));
38///
39/// // JSON: null
40/// assert_eq!(
41///     Value::try_from("$-1\r\n"),
42///     Ok(Nil)
43/// );
44///
45/// // JSON: 10
46/// assert_eq!(
47///     Value::try_from(":10\r\n"),
48///     Ok(Integer(10))
49/// );
50///
51/// // JSON: "Nina Simone"
52/// assert_eq!(
53///     Value::try_from("+Nina Simone\r\n"),
54///     Ok(String("Nina Simone".into()))
55/// );
56///
57/// // JSON: "Lorem ipsum...\r\nDolor sit amet..."
58/// assert_eq!(
59///     Value::try_from("$33\r\nLorem ipsum...\r\nDolor sit amet...\r\n"),
60///     Ok(String("Lorem ipsum...\r\nDolor sit amet...".into()))
61/// );
62///
63/// // JavaScript: [null, 447, new Error("Oh oh!"), "Hourly", "Si vis pacem,\r\npara bellum"]
64/// assert_eq!(
65///     Value::try_from("*5\r\n$-1\r\n:447\r\n-Oh oh!\r\n+Hourly\r\n$26\r\nSi vis pacem,\r\npara bellum\r\n"),
66///     Ok(Array(vec![
67///         Nil,
68///         Integer(447),
69///         Error("Oh oh!".into()),
70///         String("Hourly".into()),
71///         String("Si vis pacem,\r\npara bellum".into())
72///     ]))
73/// );
74///
75/// // NOTE: Even recursive arrays - we leave that for you to try out.
76/// ```
77#[derive(Debug, PartialEq)]
78pub enum Value {
79    /// Denote the absence of value.
80    Nil,
81    /// Denote and integer value, wrapped as singleton tuple.
82    Integer(i64),
83    /// Denote an error, wrapped as descriptive message string.
84    Error(String),
85    /// Denote a string value, wrapped as singleton tuple.
86    String(String),
87    /// Denote a non-nil list of values, wrapped as singleton vector of Value.
88    Array(Vec<Value>),
89}
90
91#[derive(Debug)]
92struct Input<'a> {
93    /// String range to be processed.
94    source: &'a str,
95    /// Bytes count of this range first [`char`], in the original [`&str`].
96    position: usize,
97}
98
99type InnerResult<'a> = (ValueResult<'a>, usize);
100
101/// Just a type alias
102pub type ValueResult<'a> = Result<Value, <Value as TryFrom<&'a str>>::Error>;
103
104impl TryFrom<&str> for Value {
105    type Error = TError;
106
107    fn try_from(source: &str) -> ValueResult {
108        Value::internal_try_from(Input {
109            position: 0,
110            source,
111        })
112        .0
113    }
114}
115
116impl Value {
117    fn internal_try_from(input: Input) -> InnerResult {
118        match input.source.chars().next() {
119            Some('*') => Value::extract_array(input),
120            Some('-') => Value::extract_error(input),
121            Some(':') => Value::extract_integer(input),
122            Some('$') => Value::extract_bulk_string(input),
123            Some('+') => Value::extract_simple_string(input),
124            _ => (Err(TError::of_unexpected(UNKNOWN, input.position)), 0),
125        }
126    }
127
128    fn extract_array(input: Input) -> InnerResult {
129        let integer_input = Input {
130            position: input.position,
131            ..input
132        };
133        match Value::extract_integer(integer_input) {
134            (Ok(Value::Integer(len)), size) => {
135                let mut values = vec![];
136                let len = len as usize;
137                let mut offset = size;
138
139                while values.len() < len {
140                    let next_input = Input {
141                        position: input.position + offset,
142                        source: &input.source[offset..input.source.len()],
143                    };
144
145                    if "" == next_input.source {
146                        return (Err(TError::of_size(ARRAY, offset)), offset);
147                    }
148
149                    match Value::internal_try_from(next_input) {
150                        (Ok(value), size) => {
151                            values.push(value);
152                            offset += size;
153                        }
154                        r#else => return r#else,
155                    }
156                }
157
158                if len == values.len() {
159                    (Ok(Value::Array(values)), offset)
160                } else {
161                    (Err(TError::of_size(ARRAY, offset + 1)), offset + 1)
162                }
163            }
164            r#else => return r#else,
165        }
166    }
167
168    fn extract_error(input: Input) -> InnerResult {
169        match Value::extract_simple_string(input) {
170            (Ok(Value::String(message)), size) => (Ok(Value::Error(message)), size),
171            r#else => r#else,
172        }
173    }
174
175    fn extract_integer(input: Input) -> InnerResult {
176        // TODO: Support negative numbers
177        let node = match &input.source[0..1] {
178            ":" => INTEGER,
179            _ => SIZE,
180        };
181        let position = input.position + 1;
182
183        if let Some(i) = input.source.find("\r\n") {
184            return match input.source[1..i].parse::<i64>().ok() {
185                Some(value) => (Ok(Value::Integer(value)), i + 2),
186                _ => (Err(TError::of_type(node, position)), position),
187            };
188        }
189
190        (
191            Err(TError::of_unexpected(node, input.source.len())),
192            input.source.len(),
193        )
194    }
195
196    fn extract_bulk_string(input: Input) -> InnerResult {
197        if input.source.starts_with("$-1\r\n") {
198            return (Ok(Value::Nil), 5);
199        }
200
201        match Self::extract_integer(Input { ..input }) {
202            (Ok(Value::Integer(size)), _) => {
203                let start = 1 + size.to_string().len() + 2;
204                let end = start + size as usize;
205
206                return if input.source[end..input.source.len()].starts_with("\r\n") {
207                    (
208                        Ok(Value::String(input.source[start..end].to_string())),
209                        end + 2,
210                    )
211                } else if end < input.source.len() {
212                    let position = input.position + end;
213                    (Err(TError::of_size(BULK_STRING, position)), position)
214                } else {
215                    let position = input.position + end + 1;
216                    (Err(TError::of_size(BULK_STRING, position)), position)
217                };
218            }
219            (Err(error), size) => (Err(error), size),
220            _ => (
221                Err(TError::of_unexpected(BULK_STRING, input.position + 1)),
222                input.position + 1,
223            ),
224        }
225    }
226
227    fn extract_simple_string(input: Input) -> InnerResult {
228        let node = match &input.source[0..1] {
229            "+" => SIMPLE_STRING,
230            _ => ERROR,
231        };
232        let mut position = input.position + 1;
233
234        if let Some(i) = input.source.find("\r\n") {
235            // @formatter::off
236            match input.source.find('\r').filter(|&p| p < i)
237                .or_else(|| input.source.find('\n').filter(|&p| p < i))
238            // @formatter::on
239            {
240                Some(shift) => position = input.position + shift,
241                _ => return (Ok(Value::String(input.source[1..i].into())), i + 2),
242            }
243        }
244
245        (Err(TError::of_unexpected(node, position)), position)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use crate::Node::{ARRAY, BULK_STRING, INTEGER, SIMPLE_STRING, SIZE};
252
253    use super::super::{Error, Value};
254
255    #[test]
256    fn value_implement_try_from_resp_nil() {
257        assert_eq!("$-1\r\n".try_into(), Ok(Value::Nil));
258    }
259
260    #[test]
261    fn value_implement_try_from() {
262        let _value: Result<Value, Error> = "".try_into();
263    }
264
265    #[test]
266    fn value_implement_try_from_resp_array() {
267        assert_eq!("*0\r\n".try_into(), Ok(Value::Array(vec![])));
268        assert_eq!(
269            "*5\r\n$-1\r\n:447\r\n-Oh oh!\r\n+Hourly\r\n$26\r\nSi vis pacem,\r\npara bellum\r\n"
270                .try_into(),
271            Ok(Value::Array(vec![
272                Value::Nil,
273                Value::Integer(447),
274                Value::Error("Oh oh!".into()),
275                Value::String("Hourly".into()),
276                Value::String("Si vis pacem,\r\npara bellum".into()),
277            ]))
278        );
279    }
280
281    #[test]
282    fn value_implement_try_from_resp_nested_array() {
283        let got = "*2\r\n*3\r\n+A\r\n+B\r\n+C\r\n*3\r\n:1\r\n:2\r\n:3\r\n".try_into()
284            as Result<Value, Error>;
285        let expected = Ok(Value::Array(vec![
286            Value::Array(vec![
287                Value::String("A".into()),
288                Value::String("B".into()),
289                Value::String("C".into()),
290            ]),
291            Value::Array(vec![
292                Value::Integer(1),
293                Value::Integer(2),
294                Value::Integer(3),
295            ]),
296        ])) as Result<Value, Error>;
297
298        assert_eq!(got, expected);
299    }
300
301    #[test]
302    fn value_implement_try_from_resp_array_with_mismatching_size() {
303        assert_eq!(
304            "*2\r\n$-1\r\n".try_into() as Result<Value, Error>,
305            Err(Error::of_size(ARRAY, 9))
306        );
307    }
308
309    #[test]
310    fn value_implement_try_from_resp_error() {
311        assert_eq!("-My bad\r\n".try_into(), Ok(Value::Error("My bad".into())));
312    }
313
314    #[test]
315    fn value_implement_try_from_resp_integer() {
316        assert_eq!(":10\r\n".try_into(), Ok(Value::Integer(10i64)));
317    }
318
319    #[test]
320    fn value_implement_try_from_resp_integer_with_invalid_integer() {
321        assert_eq!(
322            ":Yikes\r\n".try_into() as Result<Value, Error>,
323            Err(Error::of_type(INTEGER, 1))
324        );
325        assert_eq!(
326            ":0".try_into() as Result<Value, Error>,
327            Err(Error::of_unexpected(INTEGER, 2))
328        );
329    }
330
331    #[test]
332    fn value_implement_try_from_resp_bulk_string() {
333        assert_eq!(
334            "$4\r\nOops\r\n".try_into(),
335            Ok(Value::String("Oops".into()))
336        );
337        assert_eq!(
338            "$7\r\nOh\r\nOh!\r\n".try_into(),
339            Ok(Value::String("Oh\r\nOh!".into()))
340        );
341    }
342
343    #[test]
344    fn value_implement_try_from_resp_bulk_string_with_mismatching_len() {
345        assert_eq!(
346            "$5\r\nOops\r\n".try_into() as Result<Value, Error>,
347            Err(Error::of_size(BULK_STRING, 9))
348        );
349        assert_eq!(
350            "$3\r\nOops\r\n".try_into() as Result<Value, Error>,
351            Err(Error::of_size(BULK_STRING, 7))
352        );
353    }
354
355    #[test]
356    fn value_implement_try_from_resp_simple_string() {
357        assert_eq!(
358            "+Anatomy\r\n".try_into(),
359            Ok(Value::String("Anatomy".into()))
360        );
361    }
362
363    #[test]
364    fn value_implement_try_from_resp_simple_string_with_line_feed_or_carriage_return_in_value() {
365        assert_eq!(
366            "+Top\nBottom\r\n".try_into() as Result<Value, Error>,
367            Err(Error::of_unexpected(SIMPLE_STRING, 4))
368        );
369        assert_eq!(
370            "+Top\rBottom\r\n".try_into() as Result<Value, Error>,
371            Err(Error::of_unexpected(SIMPLE_STRING, 4))
372        );
373    }
374
375    #[test]
376    fn value_implement_try_from_resp_with_invalid_size_type() {
377        assert_eq!(
378            "*!\r\n$-1\r\n".try_into() as Result<Value, Error>,
379            Err(Error::of_type(SIZE, 1))
380        );
381        assert_eq!(
382            "$!\r\n$-1\r\n".try_into() as Result<Value, Error>,
383            Err(Error::of_type(SIZE, 1))
384        );
385    }
386}