-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add typed versions of get_assignment() #7
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,26 +117,293 @@ 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a |
||
) -> 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could happen if someone adds the What I want to happen, from the client's perspective, is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's almost exactly what happens in the example. client.get_boolean_assignment("non-existent", ...) // Err(Error::FlagNotFound)
// log error (or skip next line and rely on logger)
.inspect_err(|err| eprintln!("error: {:?}", err)
.unwrap_or_default() // ignoring errors. Err(_) -> None
.unwrap_or(false); // default value. None -> false The functions doesn't "throw" and error, it returns |
||
/// - [`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 | ||
// scenario (at least for now). | ||
return Ok(None); | ||
}; | ||
|
||
let evaluation = match configuration | ||
.eval_flag(flag_key, subject_key, subject_attributes, &Md5Sharder) { | ||
Ok(result) => result, | ||
Err(err) => { | ||
log::warn!(target: "eppo", | ||
flag_key, | ||
subject_key, | ||
subject_attributes:serde; | ||
"error occurred while evaluating a flag: {:?}", err, | ||
); | ||
return Err(err); | ||
}, | ||
}; | ||
let evaluation = match configuration.eval_flag( | ||
flag_key, | ||
subject_key, | ||
subject_attributes, | ||
&Md5Sharder, | ||
expected_type, | ||
) { | ||
Ok(result) => result, | ||
Err(err) => { | ||
log::warn!(target: "eppo", | ||
flag_key, | ||
subject_key, | ||
subject_attributes:serde; | ||
"error occurred while evaluating a flag: {:?}", err, | ||
); | ||
return Err(err); | ||
} | ||
}; | ||
|
||
log::trace!(target: "eppo", | ||
flag_key, | ||
|
@@ -142,7 +423,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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍