Skip to content

Commit

Permalink
feat: add typed versions of get_assignment()
Browse files Browse the repository at this point in the history
  • Loading branch information
rasendubi committed May 15, 2024
1 parent 2cc10b7 commit 41c5fd0
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 8 deletions.
3 changes: 1 addition & 2 deletions examples/simple/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ pub fn main() -> eppo::Result<()> {

// Get assignment for test-subject.
let assignment = client
.get_assignment("a-boolean-flag", "test-subject", &HashMap::new())
.get_boolean_assignment("a-boolean-flag", "test-subject", &HashMap::new())
.unwrap_or_default()
.and_then(|x| x.as_boolean())
// default assignment
.unwrap_or(false);

Expand Down
286 changes: 284 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
configuration_store::ConfigurationStore,
poller::{PollerThread, PollerThreadConfig},
sharder::Md5Sharder,
ufc::VariationType,
ClientConfig, Result,
};

Expand Down Expand Up @@ -72,6 +73,19 @@ impl<'a> Client<'a> {
/// It is recommended to wait for the Eppo configuration to get fetched with
/// [`PollerThread::wait_for_configuration()`].
///
/// # Typed versions
///
/// There are typed versions of this function:
/// - [`Client::get_string_assignment()`]
/// - [`Client::get_integer_assignment()`]
/// - [`Client::get_numeric_assignment()`]
/// - [`Client::get_boolean_assignment()`]
/// - [`Client::get_json_assignment()`]
///
/// It is recommended to use typed versions of this function as they provide additional type
/// safety. They can catch type errors even _before_ evaluating the assignment, which helps to
/// detect errors if subject is not eligible for the flag allocation.
///
/// # Errors
///
/// Returns an error in the following cases:
Expand Down Expand Up @@ -103,6 +117,268 @@ impl<'a> Client<'a> {
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result<Option<AssignmentValue>> {
self.get_assignment_inner(flag_key, subject_key, subject_attributes, None, |x| x)
}

/// Retrieves the assignment value for a given feature flag and subject.
///
/// If the subject is not eligible for any allocation, returns `Ok(None)`.
///
/// If the configuration has not been fetched yet, returns `Ok(None)`.
/// You should call [`Client::start_poller_thread`] before any call to
/// `get_string_assignment()`. Otherwise, the client will always return `None`.
///
/// It is recommended to wait for the Eppo configuration to get fetched with
/// [`PollerThread::wait_for_configuration()`].
///
/// # Errors
///
/// Returns an error in the following cases:
/// - [`Error::FlagNotFound`] if the requested flag configuration was not found.
/// - [`Error::InvalidType`] if the requested flag has an invalid type or type conversion fails.
/// - [`Error::ConfigurationParseError`] or [`Error::ConfigurationError`] if the configuration
/// received from the server is invalid.
///
/// # Examples
///
/// ```
/// # fn test(client: &eppo::Client) {
/// let assignment = client
/// .get_string_assignment("a-string-flag", "user-id", &[
/// ("language".into(), "en".into())
/// ].into_iter().collect())
/// .unwrap_or_default()
/// .unwrap_or("default_value".to_owned());
/// # }
/// ```
pub fn get_string_assignment(
&self,
flag_key: &str,
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result<Option<String>> {
self.get_assignment_inner(
flag_key,
subject_key,
subject_attributes,
Some(VariationType::String),
|x| {
x.to_string()
// The unwrap cannot fail because the type is checked during evaluation.
.unwrap()
},
)
}

/// Retrieves the assignment value for a given feature flag and subject as an integer value.
///
/// If the subject is not eligible for any allocation, returns `Ok(None)`.
///
/// If the configuration has not been fetched yet, returns `Ok(None)`.
/// You should call [`Client::start_poller_thread`] before any call to
/// `get_integer_assignment()`. Otherwise, the client will always return `None`.
///
/// It is recommended to wait for the Eppo configuration to get fetched with
/// [`PollerThread::wait_for_configuration()`].
///
/// # Errors
///
/// Returns an error in the following cases:
/// - [`Error::FlagNotFound`] if the requested flag configuration was not found.
/// - [`Error::InvalidType`] if the requested flag has an invalid type or type conversion fails.
/// - [`Error::ConfigurationParseError`] or [`Error::ConfigurationError`] if the configuration
/// received from the server is invalid.
///
/// # Examples
///
/// ```
/// # fn test(client: &eppo::Client) {
/// let assignment = client
/// .get_integer_assignment("an-int-flag", "user-id", &[
/// ("age".to_owned(), 42.0.into())
/// ].into_iter().collect())
/// .unwrap_or_default()
/// .unwrap_or(0);
/// # }
/// ```
pub fn get_integer_assignment(
&self,
flag_key: &str,
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result<Option<i64>> {
self.get_assignment_inner(
flag_key,
subject_key,
subject_attributes,
Some(VariationType::Integer),
|x| {
x.as_integer()
// The unwrap cannot fail because the type is checked during evaluation.
.unwrap()
},
)
}

/// Retrieves the assignment value for a given feature flag and subject as a numeric value.
///
/// If the subject is not eligible for any allocation, returns `Ok(None)`.
///
/// If the configuration has not been fetched yet, returns `Ok(None)`.
/// You should call [`Client::start_poller_thread`] before any call to
/// `get_numeric_assignment()`. Otherwise, the client will always return `None`.
///
/// It is recommended to wait for the Eppo configuration to get fetched with
/// [`PollerThread::wait_for_configuration()`].
///
/// # Errors
///
/// Returns an error in the following cases:
/// - [`Error::FlagNotFound`] if the requested flag configuration was not found.
/// - [`Error::InvalidType`] if the requested flag has an invalid type or type conversion fails.
/// - [`Error::ConfigurationParseError`] or [`Error::ConfigurationError`] if the configuration
/// received from the server is invalid.
///
/// # Examples
///
/// ```
/// # fn test(client: &eppo::Client) {
/// let assignment = client
/// .get_numeric_assignment("a-num-flag", "user-id", &[
/// ("age".to_owned(), 42.0.into())
/// ].iter().cloned().collect())
/// .unwrap_or_default()
/// .unwrap_or(0.0);
/// # }
/// ```
pub fn get_numeric_assignment(
&self,
flag_key: &str,
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result<Option<f64>> {
self.get_assignment_inner(
flag_key,
subject_key,
subject_attributes,
Some(VariationType::Numeric),
|x| {
x.as_numeric()
// The unwrap cannot fail because the type is checked during evaluation.
.unwrap()
},
)
}

/// Retrieves the assignment value for a given feature flag and subject as a boolean value.
///
/// If the subject is not eligible for any allocation, returns `Ok(None)`.
///
/// If the configuration has not been fetched yet, returns `Ok(None)`.
/// You should call [`Client::start_poller_thread`] before any call to
/// `get_boolean_assignment()`. Otherwise, the client will always return `None`.
///
/// It is recommended to wait for the Eppo configuration to get fetched with
/// [`PollerThread::wait_for_configuration()`].
///
/// # Errors
///
/// Returns an error in the following cases:
/// - [`Error::FlagNotFound`] if the requested flag configuration was not found.
/// - [`Error::InvalidType`] if the requested flag has an invalid type or type conversion fails.
/// - [`Error::ConfigurationParseError`] or [`Error::ConfigurationError`] if the configuration
/// received from the server is invalid.
///
/// # Examples
///
/// ```
/// # fn test(client: &eppo::Client) {
/// let assignment = client
/// .get_boolean_assignment("a-bool-flag", "user-id", &[
/// ("age".to_owned(), 42.0.into())
/// ].into_iter().collect())
/// .unwrap_or_default()
/// .unwrap_or(false);
/// # }
/// ```
pub fn get_boolean_assignment(
&self,
flag_key: &str,
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result<Option<bool>> {
self.get_assignment_inner(
flag_key,
subject_key,
subject_attributes,
Some(VariationType::Boolean),
|x| {
x.as_boolean()
// The unwrap cannot fail because the type is checked during evaluation.
.unwrap()
},
)
}

/// Retrieves the assignment value for a given feature flag and subject as a JSON value.
///
/// If the subject is not eligible for any allocation, returns `Ok(None)`.
///
/// If the configuration has not been fetched yet, returns `Ok(None)`.
/// You should call [`Client::start_poller_thread`] before any call to
/// `get_json_assignment()`. Otherwise, the client will always return `None`.
///
/// It is recommended to wait for the Eppo configuration to get fetched with
/// [`PollerThread::wait_for_configuration()`].
///
/// # Errors
///
/// Returns an error in the following cases:
/// - [`Error::FlagNotFound`] if the requested flag configuration was not found.
/// - [`Error::InvalidType`] if the requested flag has an invalid type or type conversion fails.
/// - [`Error::ConfigurationParseError`] or [`Error::ConfigurationError`] if the configuration
/// received from the server is invalid.
///
/// # Examples
///
/// ```
/// # use serde_json::json;
/// # fn test(client: &eppo::Client) {
/// let assignment = client
/// .get_json_assignment("a-json-flag", "user-id", &[
/// ("language".into(), "en".into())
/// ].into_iter().collect())
/// .unwrap_or_default()
/// .unwrap_or(json!({}));
/// # }
/// ```
pub fn get_json_assignment(
&self,
flag_key: &str,
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result<Option<serde_json::Value>> {
self.get_assignment_inner(
flag_key,
subject_key,
subject_attributes,
Some(VariationType::Json),
|x| {
x.to_json()
// The unwrap cannot fail because the type is checked during evaluation.
.unwrap()
},
)
}

fn get_assignment_inner<T>(
&self,
flag_key: &str,
subject_key: &str,
subject_attributes: &SubjectAttributes,
expected_type: Option<VariationType>,
convert: impl FnOnce(AssignmentValue) -> T,
) -> Result<Option<T>> {
let Some(configuration) = self.configuration_store.get_configuration() else {
log::warn!(target: "eppo", flag_key, subject_key; "evaluating a flag before Eppo configuration has been fetched");
// We treat missing configuration (the poller has not fetched config) as a normal
Expand All @@ -111,7 +387,13 @@ impl<'a> Client<'a> {
};

let evaluation = configuration
.eval_flag(flag_key, subject_key, subject_attributes, &Md5Sharder)
.eval_flag(
flag_key,
subject_key,
subject_attributes,
&Md5Sharder,
expected_type,
)
.inspect_err(|err| {
log::warn!(target: "eppo",
flag_key,
Expand Down Expand Up @@ -139,7 +421,7 @@ impl<'a> Client<'a> {
self.config.assignment_logger.log_assignment(event);
}

Ok(Some(value))
Ok(Some(convert(value)))
}

/// Start a poller thread to fetch configuration from the server.
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::sync::Arc;

use crate::ufc::VariationType;

/// Represents a result type for operations in the Eppo SDK.
///
/// This type alias is used throughout the SDK to indicate the result of operations that may return
Expand All @@ -17,6 +19,15 @@ pub enum Error {
#[error("flag not found")]
FlagNotFound,

/// Requested flag has invalid type.
#[error("invalid flag type (expected: {expected:?}, found: {found:?})")]
InvalidType {
/// Expected type of the flag.
expected: VariationType,
/// Actual type of the flag.
found: VariationType,
},

/// An error occurred while parsing the configuration (server sent unexpected response). It is
/// recommended to upgrade the Eppo SDK.
#[error("error parsing configuration, try upgrading Eppo SDK")]
Expand Down
Loading

0 comments on commit 41c5fd0

Please sign in to comment.