Changeset View
Changeset View
Standalone View
Standalone View
services/feature-flags/src/database.rs
use crate::constants::{ | use crate::constants::{ | ||||
FEATURE_FLAGS_CONFIG_FIELD, FEATURE_FLAGS_FEATURE_FIELD, | FEATURE_FLAGS_CONFIG_FIELD, FEATURE_FLAGS_FEATURE_FIELD, | ||||
FEATURE_FLAGS_NON_STAFF_FIELD, FEATURE_FLAGS_STAFF_FIELD, | FEATURE_FLAGS_NON_STAFF_FIELD, FEATURE_FLAGS_PLATFORM_FIELD, | ||||
FEATURE_FLAGS_STAFF_FIELD, FEATURE_FLAGS_TABLE_NAME, | |||||
}; | }; | ||||
use aws_sdk_dynamodb::model::Select; | |||||
use aws_sdk_dynamodb::{model::AttributeValue, Error as DynamoDBError}; | use aws_sdk_dynamodb::{model::AttributeValue, Error as DynamoDBError}; | ||||
use std::collections::HashMap; | use std::collections::HashMap; | ||||
use std::fmt::{Display, Formatter}; | use std::fmt::{Display, Formatter}; | ||||
use std::num::ParseIntError; | use std::num::ParseIntError; | ||||
use std::sync::Arc; | |||||
use tracing::error; | |||||
#[derive( | #[derive( | ||||
Debug, derive_more::Display, derive_more::From, derive_more::Error, | Debug, derive_more::Display, derive_more::From, derive_more::Error, | ||||
)] | )] | ||||
pub enum Error { | pub enum Error { | ||||
#[display(...)] | #[display(...)] | ||||
AwsSdk(DynamoDBError), | AwsSdk(DynamoDBError), | ||||
#[display(...)] | #[display(...)] | ||||
Show All 38 Lines | pub enum DBItemAttributeError { | ||||
#[display(...)] | #[display(...)] | ||||
Missing, | Missing, | ||||
#[display(...)] | #[display(...)] | ||||
IncorrectType, | IncorrectType, | ||||
#[display(...)] | #[display(...)] | ||||
InvalidNumberFormat(ParseIntError), | InvalidNumberFormat(ParseIntError), | ||||
} | } | ||||
fn _parse_string_attribute( | fn parse_string_attribute( | ||||
attribute_name: &'static str, | attribute_name: &'static str, | ||||
attribute_value: Option<AttributeValue>, | attribute_value: Option<AttributeValue>, | ||||
) -> Result<String, DBItemError> { | ) -> Result<String, DBItemError> { | ||||
match attribute_value { | match attribute_value { | ||||
Some(AttributeValue::S(value)) => Ok(value), | Some(AttributeValue::S(value)) => Ok(value), | ||||
Some(_) => Err(DBItemError::new( | Some(_) => Err(DBItemError::new( | ||||
attribute_name, | attribute_name, | ||||
Value::AttributeValue(attribute_value), | Value::AttributeValue(attribute_value), | ||||
DBItemAttributeError::IncorrectType, | DBItemAttributeError::IncorrectType, | ||||
)), | )), | ||||
None => Err(DBItemError::new( | None => Err(DBItemError::new( | ||||
attribute_name, | attribute_name, | ||||
Value::AttributeValue(attribute_value), | Value::AttributeValue(attribute_value), | ||||
DBItemAttributeError::Missing, | DBItemAttributeError::Missing, | ||||
)), | )), | ||||
} | } | ||||
} | } | ||||
fn _parse_bool_attribute( | fn parse_bool_attribute( | ||||
attribute_name: &'static str, | attribute_name: &'static str, | ||||
attribute_value: Option<AttributeValue>, | attribute_value: Option<AttributeValue>, | ||||
) -> Result<bool, DBItemError> { | ) -> Result<bool, DBItemError> { | ||||
match attribute_value { | match attribute_value { | ||||
Some(AttributeValue::Bool(value)) => Ok(value), | Some(AttributeValue::Bool(value)) => Ok(value), | ||||
Some(_) => Err(DBItemError::new( | Some(_) => Err(DBItemError::new( | ||||
attribute_name, | attribute_name, | ||||
Value::AttributeValue(attribute_value), | Value::AttributeValue(attribute_value), | ||||
DBItemAttributeError::IncorrectType, | DBItemAttributeError::IncorrectType, | ||||
)), | )), | ||||
None => Err(DBItemError::new( | None => Err(DBItemError::new( | ||||
attribute_name, | attribute_name, | ||||
Value::AttributeValue(attribute_value), | Value::AttributeValue(attribute_value), | ||||
DBItemAttributeError::Missing, | DBItemAttributeError::Missing, | ||||
)), | )), | ||||
} | } | ||||
} | } | ||||
fn _parse_map_attribute( | fn parse_map_attribute( | ||||
attribute_name: &'static str, | attribute_name: &'static str, | ||||
attribute_value: Option<AttributeValue>, | attribute_value: Option<AttributeValue>, | ||||
) -> Result<HashMap<String, AttributeValue>, DBItemError> { | ) -> Result<HashMap<String, AttributeValue>, DBItemError> { | ||||
if let Some(AttributeValue::M(map)) = attribute_value { | if let Some(AttributeValue::M(map)) = attribute_value { | ||||
Ok(map) | Ok(map) | ||||
} else { | } else { | ||||
Err(DBItemError::new( | Err(DBItemError::new( | ||||
attribute_name, | attribute_name, | ||||
Value::AttributeValue(attribute_value), | Value::AttributeValue(attribute_value), | ||||
DBItemAttributeError::Missing, | DBItemAttributeError::Missing, | ||||
)) | )) | ||||
} | } | ||||
} | } | ||||
fn _parse_number( | fn parse_number( | ||||
attribute_name: &'static str, | attribute_name: &'static str, | ||||
attribute_value: &str, | attribute_value: &str, | ||||
) -> Result<i32, DBItemError> { | ) -> Result<i32, DBItemError> { | ||||
let result = attribute_value.parse::<i32>().map_err(|e| { | let result = attribute_value.parse::<i32>().map_err(|e| { | ||||
DBItemError::new( | DBItemError::new( | ||||
attribute_name, | attribute_name, | ||||
Value::String(attribute_value.to_string()), | Value::String(attribute_value.to_string()), | ||||
DBItemAttributeError::InvalidNumberFormat(e), | DBItemAttributeError::InvalidNumberFormat(e), | ||||
) | ) | ||||
})?; | })?; | ||||
Ok(result) | Ok(result) | ||||
} | } | ||||
#[derive(Debug)] | #[derive(Debug)] | ||||
pub struct CodeVersionSpecificFeatureConfig { | pub struct CodeVersionSpecificFeatureConfig { | ||||
pub staff: bool, | pub staff: bool, | ||||
pub non_staff: bool, | pub non_staff: bool, | ||||
} | } | ||||
fn _parse_code_version_specific_feature_config( | fn parse_code_version_specific_feature_config( | ||||
value: Option<AttributeValue>, | value: Option<AttributeValue>, | ||||
) -> Result<CodeVersionSpecificFeatureConfig, DBItemError> { | ) -> Result<CodeVersionSpecificFeatureConfig, DBItemError> { | ||||
let mut code_version_config_map = | let mut code_version_config_map = | ||||
_parse_map_attribute(FEATURE_FLAGS_CONFIG_FIELD, value)?; | parse_map_attribute(FEATURE_FLAGS_CONFIG_FIELD, value)?; | ||||
let staff = _parse_bool_attribute( | let staff = parse_bool_attribute( | ||||
FEATURE_FLAGS_STAFF_FIELD, | FEATURE_FLAGS_STAFF_FIELD, | ||||
code_version_config_map.remove(FEATURE_FLAGS_STAFF_FIELD), | code_version_config_map.remove(FEATURE_FLAGS_STAFF_FIELD), | ||||
)?; | )?; | ||||
let non_staff = _parse_bool_attribute( | let non_staff = parse_bool_attribute( | ||||
FEATURE_FLAGS_NON_STAFF_FIELD, | FEATURE_FLAGS_NON_STAFF_FIELD, | ||||
code_version_config_map.remove(FEATURE_FLAGS_NON_STAFF_FIELD), | code_version_config_map.remove(FEATURE_FLAGS_NON_STAFF_FIELD), | ||||
)?; | )?; | ||||
Ok(CodeVersionSpecificFeatureConfig { staff, non_staff }) | Ok(CodeVersionSpecificFeatureConfig { staff, non_staff }) | ||||
} | } | ||||
#[derive(Debug)] | #[derive(Debug)] | ||||
pub struct FeatureConfig { | pub struct FeatureConfig { | ||||
pub name: String, | pub name: String, | ||||
pub config: HashMap<i32, CodeVersionSpecificFeatureConfig>, | pub config: HashMap<i32, CodeVersionSpecificFeatureConfig>, | ||||
} | } | ||||
fn _parse_feature_config( | fn parse_feature_config( | ||||
mut attribute_value: HashMap<String, AttributeValue>, | mut attribute_value: HashMap<String, AttributeValue>, | ||||
) -> Result<FeatureConfig, DBItemError> { | ) -> Result<FeatureConfig, DBItemError> { | ||||
let feature_name = _parse_string_attribute( | let feature_name = parse_string_attribute( | ||||
FEATURE_FLAGS_FEATURE_FIELD, | FEATURE_FLAGS_FEATURE_FIELD, | ||||
attribute_value.remove(FEATURE_FLAGS_FEATURE_FIELD), | attribute_value.remove(FEATURE_FLAGS_FEATURE_FIELD), | ||||
)?; | )?; | ||||
let config_map = _parse_map_attribute( | let config_map = parse_map_attribute( | ||||
FEATURE_FLAGS_CONFIG_FIELD, | FEATURE_FLAGS_CONFIG_FIELD, | ||||
attribute_value.remove(FEATURE_FLAGS_CONFIG_FIELD), | attribute_value.remove(FEATURE_FLAGS_CONFIG_FIELD), | ||||
)?; | )?; | ||||
let mut config = HashMap::new(); | let mut config = HashMap::new(); | ||||
for (code_version_string, code_version_config) in config_map { | for (code_version_string, code_version_config) in config_map { | ||||
let code_version: i32 = | let code_version: i32 = | ||||
_parse_number("code_version", code_version_string.as_str())?; | parse_number("code_version", code_version_string.as_str())?; | ||||
let version_config = | let version_config = | ||||
_parse_code_version_specific_feature_config(Some(code_version_config))?; | parse_code_version_specific_feature_config(Some(code_version_config))?; | ||||
config.insert(code_version, version_config); | config.insert(code_version, version_config); | ||||
} | } | ||||
Ok(FeatureConfig { | Ok(FeatureConfig { | ||||
name: feature_name, | name: feature_name, | ||||
config, | config, | ||||
}) | }) | ||||
} | } | ||||
pub enum Platform { | |||||
IOS, | |||||
ANDROID, | |||||
} | |||||
#[derive(Clone)] | |||||
pub struct DatabaseClient { | |||||
client: Arc<aws_sdk_dynamodb::Client>, | |||||
} | |||||
impl DatabaseClient { | |||||
pub fn new(aws_config: &aws_types::SdkConfig) -> Self { | |||||
DatabaseClient { | |||||
client: Arc::new(aws_sdk_dynamodb::Client::new(aws_config)), | |||||
} | |||||
} | |||||
pub async fn get_features_configuration( | |||||
&self, | |||||
platform: Platform, | |||||
) -> Result<HashMap<String, FeatureConfig>, Error> { | |||||
let platform_value = match platform { | |||||
Platform::IOS => "IOS", | |||||
Platform::ANDROID => "ANDROID", | |||||
bartek: Maybe we should extract these strings as constants?
```
lang=rust
pub const PLATFORM_IOS: &str… | |||||
tomekAuthorUnsubmitted Done Inline ActionsAgree that extracting as constants makes sense. And also, I don't think that introducing more complicated approach is a good idea - it's just too complicated for such a simple thing. tomek: Agree that extracting as constants makes sense. And also, I don't think that introducing more… | |||||
}; | |||||
let result = self | |||||
.client | |||||
.query() | |||||
.select(Select::AllAttributes) | |||||
.table_name(FEATURE_FLAGS_TABLE_NAME) | |||||
.consistent_read(true) | |||||
.key_condition_expression("#platform = :platform") | |||||
.expression_attribute_names("#platform", FEATURE_FLAGS_PLATFORM_FIELD) | |||||
.expression_attribute_values( | |||||
":platform", | |||||
AttributeValue::S(platform_value.to_string()), | |||||
) | |||||
.send() | |||||
.await | |||||
.map_err(|e| { | |||||
error!("DynamoDB client failed to find feature flags configuration"); | |||||
Error::AwsSdk(e.into()) | |||||
})?; | |||||
if let Some(items) = result.items { | |||||
let mut config = HashMap::new(); | |||||
for item in items { | |||||
let feature_config = parse_feature_config(item)?; | |||||
config.insert(feature_config.name.clone(), feature_config); | |||||
} | |||||
Ok(config) | |||||
} else { | |||||
Ok(HashMap::new()) | |||||
} | |||||
} | |||||
} |
Maybe we should extract these strings as constants?
Or even do an enum implementation
But this might be an overkill, it trades simplicity for flexibility/extensibility