-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
960 additions
and
314 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,145 @@ | ||
use crate::errors::ErrorKind; | ||
use crate::eval::details::EvaluationDetails; | ||
use crate::eval::evaluator::{eval, EvalResult}; | ||
use crate::fetch::service::{ConfigResult, ConfigService}; | ||
use crate::options::{Options, OptionsBuilder}; | ||
use crate::ClientError; | ||
use crate::value::{OptionalValueDisplay, Value}; | ||
use crate::{ClientError, User}; | ||
use log::{error, warn}; | ||
use std::sync::Arc; | ||
|
||
pub struct Client { | ||
options: Arc<Options>, | ||
service: ConfigService, | ||
} | ||
|
||
impl Client { | ||
pub fn from_builder(builder: OptionsBuilder) -> Result<Self, ClientError> { | ||
let result = builder.build(); | ||
match result { | ||
Ok(opts) => Ok(Client::new(opts)), | ||
Err(err) => Err(err), | ||
pub(crate) fn with_options(options: Options) -> Self { | ||
let opts = Arc::new(options); | ||
Self { | ||
options: Arc::clone(&opts), | ||
service: ConfigService::new(Arc::clone(&opts)), | ||
} | ||
} | ||
|
||
pub fn new(options: Options) -> Self { | ||
Self { | ||
options: Arc::new(options), | ||
pub fn builder(sdk_key: &str) -> OptionsBuilder { | ||
OptionsBuilder::new(sdk_key) | ||
} | ||
|
||
pub fn new(sdk_key: &str) -> Result<Self, ClientError> { | ||
OptionsBuilder::new(sdk_key).build() | ||
} | ||
|
||
pub async fn refresh(&self) -> Result<(), ClientError> { | ||
if self.options.offline() { | ||
let err = ClientError::new( | ||
ErrorKind::OfflineClient, | ||
"Client is in offline mode, it cannot initiate HTTP calls.".to_owned(), | ||
); | ||
warn!(event_id = err.kind.as_u8(); "{}", err); | ||
return Err(err); | ||
} | ||
self.service.refresh().await | ||
} | ||
|
||
pub async fn get_bool_value(&self, key: &str, user: Option<User>, default: bool) -> bool { | ||
self.get_bool_details(key, user, default).await.value | ||
} | ||
|
||
pub fn refresh() -> Result<(), ClientError> { | ||
Ok(()) | ||
pub async fn get_bool_details( | ||
&self, | ||
key: &str, | ||
user: Option<User>, | ||
default: bool, | ||
) -> EvaluationDetails<bool> { | ||
let result = self.service.config().await; | ||
match self | ||
.eval_flag(&result, key, &user, Some(default.into())) | ||
.await | ||
{ | ||
Ok(eval_result) => match eval_result.value.as_bool() { | ||
Some(val) => EvaluationDetails { | ||
value: val, | ||
key: key.to_owned(), | ||
user, | ||
..EvaluationDetails::from_results(eval_result, &result) | ||
}, | ||
None => { | ||
let err = ClientError::new(ErrorKind::SettingValueTypeMismatch, format!("The type of a setting must match the requested type. Setting's type was '{}' but the requested type was 'bool'. Learn more: https://configcat.com/docs/sdk-reference/rust/#setting-type-mapping", eval_result.setting_type)); | ||
error!(event_id = err.kind.as_u8(); "{}", err); | ||
EvaluationDetails::from_err(default, key, user, err) | ||
} | ||
}, | ||
Err(err) => { | ||
error!(event_id = err.kind.as_u8(); "{}", err); | ||
EvaluationDetails::from_err(default, key, user, err) | ||
} | ||
} | ||
} | ||
|
||
pub async fn get_flag_details( | ||
&self, | ||
key: &str, | ||
user: Option<User>, | ||
) -> EvaluationDetails<Option<Value>> { | ||
let result = self.service.config().await; | ||
match self.eval_flag(&result, key, &user, None).await { | ||
Ok(eval_result) => EvaluationDetails { | ||
value: Some(eval_result.value), | ||
key: key.to_owned(), | ||
is_default_value: false, | ||
variation_id: eval_result.variation_id, | ||
user, | ||
error: None, | ||
fetch_time: Some(*result.fetch_time()), | ||
matched_targeting_rule: eval_result.rule, | ||
matched_percentage_option: eval_result.option, | ||
}, | ||
Err(err) => { | ||
error!(event_id = err.kind.as_u8(); "{}", err); | ||
EvaluationDetails::from_err(None, key, user, err) | ||
} | ||
} | ||
} | ||
|
||
async fn eval_flag( | ||
&self, | ||
config_result: &ConfigResult, | ||
key: &str, | ||
user: &Option<User>, | ||
default: Option<Value>, | ||
) -> Result<EvalResult, ClientError> { | ||
if config_result.config().settings.is_empty() { | ||
return Err(ClientError::new(ErrorKind::ConfigJsonNotAvailable, format!("Config JSON is not present when evaluating setting '{key}'. Returning the `defaultValue` parameter that you specified in your application: '{}'.", default.to_str()))); | ||
} | ||
|
||
match config_result.config().settings.get(key) { | ||
None => { | ||
let keys = config_result | ||
.config() | ||
.settings | ||
.keys() | ||
.map(|k| format!("'{k}'")) | ||
.collect::<Vec<String>>() | ||
.join(", "); | ||
Err(ClientError::new(ErrorKind::SettingKeyMissing, format!("Failed to evaluate setting '{key}' (the key was not found in config JSON). Returning the `defaultValue` parameter that you specified in your application: '{}'. Available keys: [{keys}].", default.to_str()))) | ||
} | ||
Some(setting) => { | ||
let eval_result = eval( | ||
setting, | ||
key, | ||
user, | ||
&config_result.config().settings, | ||
&default, | ||
); | ||
match eval_result { | ||
Ok(result) => Ok(result), | ||
Err(err) => Err(ClientError::new( | ||
ErrorKind::EvaluationFailure, | ||
format!("Failed to evaluate setting '{key}' ({err})"), | ||
)), | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,62 @@ | ||
use thiserror::Error; | ||
use std::error::Error; | ||
use std::fmt::{Display, Formatter}; | ||
|
||
#[derive(Error, PartialEq, Debug)] | ||
pub enum ClientError { | ||
#[error("SDK key is invalid. ({0})")] | ||
InvalidSdkKey(String), | ||
#[error("{0}")] | ||
Fetch(String), | ||
/// Error kind that represents failures reported by the [`crate::Client`]. | ||
#[derive(Debug, Copy, Clone, PartialEq)] | ||
pub enum ErrorKind { | ||
/// No error occurred. | ||
NoError, | ||
/// The evaluation failed because the config JSON was not available locally. | ||
ConfigJsonNotAvailable = 1000, | ||
/// The evaluation failed because the key of the evaluated setting was not found in the config JSON. | ||
SettingKeyMissing = 1001, | ||
/// The evaluation failed because the key of the evaluated setting was not found in the config JSON. | ||
EvaluationFailure = 1002, | ||
/// An HTTP response indicating an invalid SDK Key was received (403 Forbidden or 404 Not Found). | ||
InvalidSdkKey = 1100, | ||
/// Invalid HTTP response was received (unexpected HTTP status code). | ||
UnexpectedHttpResponse = 1101, | ||
/// The HTTP request timed out. | ||
HttpRequestTimeout = 1102, | ||
/// The HTTP request failed (most likely, due to a local network issue). | ||
HttpRequestFailure = 1103, | ||
/// Redirection loop encountered while trying to fetch config JSON. | ||
RedirectLoop = 1104, | ||
/// An invalid HTTP response was received (200 OK with an invalid content). | ||
InvalidHttpResponseContent = 1105, | ||
/// An invalid HTTP response was received (304 Not Modified when no config JSON was cached locally). | ||
InvalidHttpResponseWhenLocalCacheIsEmpty = 1106, | ||
/// The evaluation failed because of a type mismatch between the evaluated setting value and the specified default value. | ||
SettingValueTypeMismatch = 2002, | ||
/// The client is in offline mode, it cannot initiate HTTP requests. | ||
OfflineClient = 3200, | ||
/// The refresh operation failed because the client is configured to use the [`crate::OverrideBehavior::LocalOnly`] override behavior, | ||
LocalOnlyClient = 3202, | ||
} | ||
|
||
#[derive(Error, PartialEq, Debug)] | ||
pub enum InternalError { | ||
#[error("JSON parsing failed. ({0})")] | ||
Parse(String), | ||
#[error("{0}")] | ||
Http(String), | ||
impl ErrorKind { | ||
pub(crate) fn as_u8(&self) -> u8 { | ||
*self as u8 | ||
} | ||
} | ||
|
||
/// Error struct that holds the [ErrorKind] and message of the reported failure. | ||
#[derive(Debug, PartialEq)] | ||
pub struct ClientError { | ||
pub kind: ErrorKind, | ||
pub message: String, | ||
} | ||
|
||
impl ClientError { | ||
pub fn new(kind: ErrorKind, message: String) -> Self { | ||
Self { message, kind } | ||
} | ||
} | ||
|
||
impl Display for ClientError { | ||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
f.write_str(self.message.as_str()) | ||
} | ||
} | ||
|
||
impl Error for ClientError {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
use crate::eval::evaluator::EvalResult; | ||
use crate::fetch::service::ConfigResult; | ||
use crate::{ClientError, PercentageOption, TargetingRule, User}; | ||
use chrono::{DateTime, Utc}; | ||
use std::sync::Arc; | ||
|
||
/// Details of the flag evaluation's result. | ||
#[derive(Default)] | ||
pub struct EvaluationDetails<T> { | ||
pub value: T, | ||
/// Key of the feature flag or setting. | ||
pub key: String, | ||
/// Indicates whether the default value passed to the setting evaluation methods is used as the result of the evaluation. | ||
pub is_default_value: bool, | ||
/// Variation ID of the feature flag or setting (if available). | ||
pub variation_id: Option<String>, | ||
/// The User Object used for the evaluation (if available). | ||
pub user: Option<User>, | ||
/// Error in case evaluation failed. | ||
pub error: Option<ClientError>, | ||
/// Time of last successful config download. | ||
pub fetch_time: Option<DateTime<Utc>>, | ||
/// The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value. | ||
pub matched_targeting_rule: Option<Arc<TargetingRule>>, | ||
/// The percentage option (if any) that was used to select the evaluated value. | ||
pub matched_percentage_option: Option<Arc<PercentageOption>>, | ||
} | ||
|
||
impl<T: Default> EvaluationDetails<T> { | ||
pub(crate) fn from_err(val: T, key: &str, user: Option<User>, err: ClientError) -> Self { | ||
Self { | ||
value: val, | ||
key: key.to_owned(), | ||
is_default_value: true, | ||
user, | ||
error: Some(err), | ||
..EvaluationDetails::default() | ||
} | ||
} | ||
|
||
pub(crate) fn from_results(eval_result: EvalResult, config_result: &ConfigResult) -> Self { | ||
Self { | ||
variation_id: eval_result.variation_id, | ||
fetch_time: Some(*config_result.fetch_time()), | ||
matched_targeting_rule: eval_result.rule, | ||
matched_percentage_option: eval_result.option, | ||
..EvaluationDetails::default() | ||
} | ||
} | ||
} |
Oops, something went wrong.