diff --git a/examples/simple/main.rs b/examples/simple/main.rs
index 8696f493..d12a3751 100644
--- a/examples/simple/main.rs
+++ b/examples/simple/main.rs
@@ -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);
diff --git a/src/client.rs b/src/client.rs
index 2bf7f730..8002adcb 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -9,6 +9,7 @@ use crate::{
configuration_store::ConfigurationStore,
poller::{PollerThread, PollerThreadConfig},
sharder::Md5Sharder,
+ ufc::VariationType,
ClientConfig, Result,
};
@@ -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:
@@ -103,6 +117,268 @@ impl<'a> Client<'a> {
subject_key: &str,
subject_attributes: &SubjectAttributes,
) -> Result> {
+ 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 > {
+ 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 > {
+ 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 > {
+ 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 > {
+ 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 > {
+ 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(
+ &self,
+ flag_key: &str,
+ subject_key: &str,
+ subject_attributes: &SubjectAttributes,
+ expected_type: Option,
+ convert: impl FnOnce(AssignmentValue) -> T,
+ ) -> Result> {
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
@@ -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,
@@ -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.
diff --git a/src/error.rs b/src/error.rs
index e15e8c3c..d93ff9c9 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -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
@@ -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")]
diff --git a/src/eval.rs b/src/eval.rs
index 8b0b40dc..40f0b924 100644
--- a/src/eval.rs
+++ b/src/eval.rs
@@ -5,7 +5,9 @@ use chrono::Utc;
use crate::{
client::AssignmentValue,
sharder::Sharder,
- ufc::{Allocation, Flag, Shard, Split, Timestamp, TryParse, UniversalFlagConfig},
+ ufc::{
+ Allocation, Flag, Shard, Split, Timestamp, TryParse, UniversalFlagConfig, VariationType,
+ },
AssignmentEvent, Error, Result, SubjectAttributes,
};
@@ -16,17 +18,39 @@ impl UniversalFlagConfig {
subject_key: &str,
subject_attributes: &SubjectAttributes,
sharder: &impl Sharder,
+ expected_type: Option,
) -> Result)>> {
+ let flag = self.get_flag(flag_key)?;
+
+ if let Some(ty) = expected_type {
+ flag.verify_type(ty)?;
+ }
+
+ flag.eval(subject_key, subject_attributes, sharder)
+ }
+
+ pub fn get_flag<'a>(&'a self, flag_key: &str) -> Result<&'a Flag> {
let flag = self.flags.get(flag_key).ok_or(Error::FlagNotFound)?;
match flag {
- TryParse::Parsed(flag) => flag.eval(subject_key, subject_attributes, sharder),
+ TryParse::Parsed(flag) => Ok(flag),
TryParse::ParseFailed(_) => Err(Error::ConfigurationParseError),
}
}
}
impl Flag {
+ pub fn verify_type(&self, ty: VariationType) -> Result<()> {
+ if self.variation_type == ty {
+ Ok(())
+ } else {
+ Err(Error::InvalidType {
+ expected: ty,
+ found: self.variation_type,
+ })
+ }
+ }
+
pub fn eval(
&self,
subject_key: &str,
@@ -224,6 +248,7 @@ mod tests {
&subject.subject_key,
&subject.subject_attributes,
&Md5Sharder,
+ Some(test_file.variation_type),
)
.unwrap_or(None);
diff --git a/src/lib.rs b/src/lib.rs
index cd288f37..15852551 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,14 +2,37 @@
//!
//! # Overview
//!
-//! The SDK revolves around a [`Client`] that evaluates feature flag values for `subjects`, where each
+//! The SDK revolves around a [`Client`] that evaluates feature flag values for "subjects", where each
//! subject has a unique key and key-value attributes associated with it. Feature flag evaluation
//! results in an [`AssignmentValue`] being returned, representing a specific feature flag value assigned
//! to the subject.
//!
+//! # Typed assignments
+//!
+//! Every Eppo flag has a return type that is set once on creation in the dashboard. Once a flag is
+//! created, assignments in code should be made using the corresponding typed function:
+//! - [`Client::get_string_assignment()`]
+//! - [`Client::get_integer_assignment()`]
+//! - [`Client::get_numeric_assignment()`]
+//! - [`Client::get_boolean_assignment()`]
+//! - [`Client::get_json_assignment()`]
+//!
+//! These functions provide additional type safety over [`Client::get_assignment()`] as they can
+//! detect type mismatch even before evaluating the feature, so the error is returned even if
+//! subject is otherwise uneligible (`get_assignment()` return `Ok(None)` in that case).
+//!
+//! # Assignment logger
+//!
//! An [`AssignmentLogger`] should be provided to save assignment events to your storage,
//! facilitating tracking of which user received which feature flag values.
//!
+//! ```
+//! # use eppo::ClientConfig;
+//! let config = ClientConfig::from_api_key("api-key").assignment_logger(|assignment| {
+//! println!("{:?}", assignment);
+//! });
+//! ```
+//!
//! # Error Handling
//!
//! Errors are represented by the [`Error`] enum.
diff --git a/src/ufc.rs b/src/ufc.rs
index 4ca4d643..8652a575 100644
--- a/src/ufc.rs
+++ b/src/ufc.rs
@@ -63,7 +63,7 @@ fn default_total_shards() -> u64 {
10_000
}
-#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum VariationType {
String,