diff --git a/Cargo.toml b/Cargo.toml index e5f70eb7..1cfbb078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fred" -version = "4.2.2" +version = "4.2.3" authors = ["Alec Embke "] edition = "2018" description = "An async Redis client for Rust built on Futures and Tokio." diff --git a/src/error.rs b/src/error.rs index 73e21315..9954735d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,8 @@ pub enum RedisErrorKind { Parse, /// An error communicating with redis sentinel. Sentinel, + /// An error indicating a value was not found, often used when trying to cast a `nil` response from the server to a non-nullable type. + NotFound, } impl RedisErrorKind { @@ -66,6 +68,7 @@ impl RedisErrorKind { RedisErrorKind::Config => "Config Error", RedisErrorKind::Parse => "Parse Error", RedisErrorKind::Sentinel => "Sentinel Error", + RedisErrorKind::NotFound => "Not Found", } } } @@ -312,13 +315,21 @@ impl RedisError { RedisError::new(RedisErrorKind::Parse, details) } - /// Whether or not the error is a Canceled error. + /// Whether or not the error is a `Canceled` error. pub fn is_canceled(&self) -> bool { match self.kind { RedisErrorKind::Canceled => true, _ => false, } } + + /// Whether or not the error is a `NotFound` error. + pub fn is_not_found(&self) -> bool { + match self.kind { + RedisErrorKind::NotFound => true, + _ => false, + } + } } impl Error for RedisError { diff --git a/src/modules/response.rs b/src/modules/response.rs index 11417f3a..0815d353 100644 --- a/src/modules/response.rs +++ b/src/modules/response.rs @@ -1,4 +1,4 @@ -use crate::error::RedisError; +use crate::error::{RedisError, RedisErrorKind}; use crate::types::{RedisValue, QUEUED}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::{BuildHasher, Hash}; @@ -9,14 +9,16 @@ macro_rules! to_signed_number( match $v { RedisValue::Integer(i) => Ok(i as $t), RedisValue::String(s) => s.parse::<$t>().map_err(|e| e.into()), + RedisValue::Null => Err(RedisError::new(RedisErrorKind::NotFound, "Cannot convert nil to number.")), RedisValue::Array(mut a) => if a.len() == 1 { match a.pop().unwrap() { RedisValue::Integer(i) => Ok(i as $t), RedisValue::String(s) => s.parse::<$t>().map_err(|e| e.into()), + RedisValue::Null => Err(RedisError::new(RedisErrorKind::NotFound, "Cannot convert nil to number.")), _ => Err(RedisError::new_parse("Cannot convert to number.")) } }else{ - Err(RedisError::new_parse("Cannot convert to number.")) + Err(RedisError::new_parse("Cannot convert array to number.")) } _ => Err(RedisError::new_parse("Cannot convert to number.")), } @@ -39,12 +41,14 @@ macro_rules! to_unsigned_number( }else{ Ok(i as $t) }, + RedisValue::Null => Err(RedisError::new(RedisErrorKind::NotFound, "Cannot convert nil to number.")), RedisValue::String(s) => s.parse::<$t>().map_err(|e| e.into()), _ => Err(RedisError::new_parse("Cannot convert to number.")) } }else{ - Err(RedisError::new_parse("Cannot convert to number.")) - } + Err(RedisError::new_parse("Cannot convert array to number.")) + }, + RedisValue::Null => Err(RedisError::new(RedisErrorKind::NotFound, "Cannot convert nil to number.")), _ => Err(RedisError::new_parse("Cannot convert to number.")), } } @@ -123,34 +127,62 @@ impl_unsigned_number!(usize); impl RedisResponse for String { fn from_value(value: RedisValue) -> Result { - value - .into_string() - .ok_or(RedisError::new_parse("Could not convert to string.")) + if value.is_null() { + Err(RedisError::new( + RedisErrorKind::NotFound, + "Cannot convert nil response to string.", + )) + } else { + value + .into_string() + .ok_or(RedisError::new_parse("Could not convert to string.")) + } } } impl RedisResponse for f64 { fn from_value(value: RedisValue) -> Result { - value - .as_f64() - .ok_or(RedisError::new_parse("Could not convert to double.")) + if value.is_null() { + Err(RedisError::new( + RedisErrorKind::NotFound, + "Cannot convert nil response to double.", + )) + } else { + value + .as_f64() + .ok_or(RedisError::new_parse("Could not convert to double.")) + } } } impl RedisResponse for f32 { fn from_value(value: RedisValue) -> Result { - value - .as_f64() - .map(|f| f as f32) - .ok_or(RedisError::new_parse("Could not convert to float.")) + if value.is_null() { + Err(RedisError::new( + RedisErrorKind::NotFound, + "Cannot convert nil response to float.", + )) + } else { + value + .as_f64() + .map(|f| f as f32) + .ok_or(RedisError::new_parse("Could not convert to float.")) + } } } impl RedisResponse for bool { fn from_value(value: RedisValue) -> Result { - value - .as_bool() - .ok_or(RedisError::new_parse("Could not convert to bool.")) + if value.is_null() { + Err(RedisError::new( + RedisErrorKind::NotFound, + "Cannot convert nil response to bool.", + )) + } else { + value + .as_bool() + .ok_or(RedisError::new_parse("Could not convert to bool.")) + } } } @@ -336,6 +368,7 @@ impl_redis_response_tuple! { T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, #[cfg(test)] mod tests { + use crate::error::RedisError; use crate::types::RedisValue; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; @@ -404,6 +437,42 @@ mod tests { assert_eq!(_foo, 123); } + #[test] + fn should_return_not_found_with_null_scalar_values() { + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + } + + #[test] + fn should_return_not_found_with_null_strings_and_bools() { + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + let result: Result = RedisValue::Null.convert(); + assert!(result.unwrap_err().is_not_found()); + } + #[test] fn should_convert_strings() { let _foo: String = RedisValue::String("foo".into()).convert().unwrap();