Skip to content

Commit

Permalink
Finish evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
z4kn4fein committed May 10, 2024
1 parent f1f8ba2 commit 6abc6fd
Show file tree
Hide file tree
Showing 18 changed files with 960 additions and 314 deletions.
140 changes: 129 additions & 11 deletions src/client.rs
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})"),
)),
}
}
}
}
}
2 changes: 0 additions & 2 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
include!(concat!(env!("OUT_DIR"), "/built.rs"));

pub const SDK_KEY_PROXY_PREFIX: &str = "configcat-proxy/";
pub const SDK_KEY_PREFIX: &str = "configcat-sdk-1";
pub const CONFIG_FILE_NAME: &str = "config_v6.json";
pub const SERIALIZATION_FORMAT_VERSION: &str = "v2";
pub const SDK_KEY_SECTION_LENGTH: i64 = 22;

#[cfg(test)]
pub mod test_constants {
Expand Down
71 changes: 58 additions & 13 deletions src/errors.rs
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 {}
50 changes: 50 additions & 0 deletions src/eval/details.rs
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()
}
}
}
Loading

0 comments on commit 6abc6fd

Please sign in to comment.