diff --git a/services/comm-services-lib/src/database.rs b/services/comm-services-lib/src/database.rs --- a/services/comm-services-lib/src/database.rs +++ b/services/comm-services-lib/src/database.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; +use std::str::FromStr; #[derive( Debug, derive_more::Display, derive_more::From, derive_more::Error, @@ -55,6 +56,8 @@ #[display(...)] IncorrectType, #[display(...)] + TimestampOutOfRange, + #[display(...)] InvalidTimestamp(chrono::ParseError), #[display(...)] InvalidNumberFormat(ParseIntError), @@ -98,6 +101,30 @@ } } +pub fn parse_int_attribute( + attribute_name: &'static str, + attribute_value: Option, +) -> Result +where + T: FromStr, +{ + match &attribute_value { + Some(AttributeValue::N(numeric_str)) => { + parse_integer(attribute_name, &numeric_str) + } + Some(_) => Err(DBItemError::new( + attribute_name, + Value::AttributeValue(attribute_value), + DBItemAttributeError::IncorrectType, + )), + None => Err(DBItemError::new( + attribute_name, + Value::AttributeValue(attribute_value), + DBItemAttributeError::Missing, + )), + } +} + pub fn parse_datetime_attribute( attribute_name: &'static str, attribute_value: Option, @@ -120,6 +147,24 @@ } } +/// Parses the UTC timestamp in milliseconds from a DynamoDB numeric attribute +pub fn parse_timestamp_attribute( + attribute_name: &'static str, + attribute_value: Option, +) -> Result, DBItemError> { + let timestamp = + parse_int_attribute::(attribute_name, attribute_value.clone())?; + let naive_datetime = chrono::NaiveDateTime::from_timestamp_millis(timestamp) + .ok_or_else(|| { + DBItemError::new( + attribute_name, + Value::AttributeValue(attribute_value), + DBItemAttributeError::TimestampOutOfRange, + ) + })?; + Ok(DateTime::from_utc(naive_datetime, Utc)) +} + pub fn parse_map_attribute( attribute_name: &'static str, attribute_value: Option, @@ -139,11 +184,14 @@ } } -pub fn parse_number( +pub fn parse_integer( attribute_name: &'static str, attribute_value: &str, -) -> Result { - attribute_value.parse::().map_err(|e| { +) -> Result +where + T: FromStr, +{ + attribute_value.parse::().map_err(|e| { DBItemError::new( attribute_name, Value::String(attribute_value.to_string()), @@ -151,3 +199,50 @@ ) }) } + +#[cfg(test)] + +mod tests { + use super::*; + + #[test] + fn test_parse_integer() { + assert!(parse_integer::("some_attr", "123").is_ok()); + assert!(parse_integer::("negative", "-123").is_ok()); + + assert!(parse_integer::("float", "3.14").is_err()); + assert!(parse_integer::("NaN", "foo").is_err()); + + assert!(parse_integer::("negative_uint", "-123").is_err()); + assert!(parse_integer::("too_large", "65536").is_err()); + } + + #[test] + fn test_parse_timestamp() { + let timestamp = Utc::now().timestamp_millis(); + let attr = AttributeValue::N(timestamp.to_string()); + + let parsed_timestamp = parse_timestamp_attribute("some_attr", Some(attr)); + + assert!(parsed_timestamp.is_ok()); + assert_eq!(parsed_timestamp.unwrap().timestamp_millis(), timestamp); + } + + #[test] + fn test_parse_invalid_timestamp() { + let attr = AttributeValue::N("foo".to_string()); + let parsed_timestamp = parse_timestamp_attribute("some_attr", Some(attr)); + assert!(parsed_timestamp.is_err()); + } + + #[test] + fn test_parse_timestamp_out_of_range() { + let attr = AttributeValue::N(i64::MAX.to_string()); + let parsed_timestamp = parse_timestamp_attribute("some_attr", Some(attr)); + assert!(parsed_timestamp.is_err()); + assert!(matches!( + parsed_timestamp.unwrap_err().attribute_error, + DBItemAttributeError::TimestampOutOfRange + )); + } +} diff --git a/services/feature-flags/src/database.rs b/services/feature-flags/src/database.rs --- a/services/feature-flags/src/database.rs +++ b/services/feature-flags/src/database.rs @@ -52,7 +52,7 @@ let mut config = HashMap::new(); for (code_version_string, code_version_config) in config_map { let code_version: i32 = - database::parse_number("code_version", code_version_string.as_str())?; + database::parse_integer("code_version", code_version_string.as_str())?; let version_config = parse_code_version_specific_feature_config(Some(code_version_config))?; config.insert(code_version, version_config);