diff --git a/README.md b/README.md index 15f3873..8637df3 100644 --- a/README.md +++ b/README.md @@ -97,23 +97,20 @@ async fn example() { .unwrap_or(false); // Let's get evaluation details. - // Note that we will inject `300` as the int value via evaluation context. - // It is not a feature mentioned in the standard but rather implemented for the - // convenience. let result = client .get_int_details( "key", - Some(&EvaluationContext::default().with_custom_field("Value", 300)), + Some(&EvaluationContext::default().with_custom_field("some_key", "some_value")), None, ) .await; match result { Ok(details) => { - assert_eq!(details.value, 300); + assert_eq!(details.value, 100); assert_eq!(details.reason, Some(EvaluationReason::Static)); assert_eq!(details.variant, Some("Static".to_string())); - assert_eq!(details.flag_metadata.values.iter().count(), 1); + assert_eq!(details.flag_metadata.values.iter().count(), 2); } Err(error) => { println!( diff --git a/src/api/api.rs b/src/api/api.rs index c9e3771..0fa13c7 100644 --- a/src/api/api.rs +++ b/src/api/api.rs @@ -102,7 +102,9 @@ mod tests { use std::sync::Arc; use super::*; - use crate::{provider::NoOpProvider, EvaluationContextFieldValue, EvaluationReason}; + use crate::{ + provider::NoOpProvider, EvaluationContextFieldValue, EvaluationReason, FlagMetadataValue, + }; use spec::spec; #[spec( @@ -295,48 +297,69 @@ mod tests { )] #[tokio::test] async fn evaluation_context() { - // Ensure the value set into provider is picked up. + // Set global client context and ensure its values are picked up. + let evaluation_context = EvaluationContext::builder() + .targeting_key("global_targeting_key") + .build() + .with_custom_field("key", "global_value"); + let mut api = OpenFeature::default(); - api.set_provider(NoOpProvider::builder().int_value(100).build()) - .await; + api.set_evaluation_context(evaluation_context).await; let mut client = api.create_client(); - assert_eq!(client.get_int_value("", None, None).await.unwrap(), 100); + let result = client.get_int_details("", None, None).await.unwrap(); - // Ensure the value set into global context is picked up. - api.set_evaluation_context( - EvaluationContext::default() - .with_custom_field("Value", EvaluationContextFieldValue::Int(200)), - ) - .await; + assert_eq!( + *result.flag_metadata.values.get("TargetingKey").unwrap(), + FlagMetadataValue::String("global_targeting_key".to_string()) + ); + + assert_eq!( + *result.flag_metadata.values.get("key").unwrap(), + FlagMetadataValue::String("global_value".to_string()) + ); - assert_eq!(client.get_int_value("", None, None).await.unwrap(), 200); + // Set client evaluation context and ensure its values overwrite the global ones. + let evaluation_context = EvaluationContext::builder() + .targeting_key("client_targeting_key") + .build() + .with_custom_field("key", "client_value"); - // Set another provider to the API and ensure its value is picked up. - api.set_evaluation_context( - EvaluationContext::default() - .with_custom_field("Value", EvaluationContextFieldValue::Int(150)), - ) - .await; + client.set_evaluation_context(evaluation_context); + + let result = client.get_bool_details("", None, None).await.unwrap(); - assert_eq!(client.get_int_value("", None, None).await.unwrap(), 150); + assert_eq!( + *result.flag_metadata.values.get("TargetingKey").unwrap(), + FlagMetadataValue::String("client_targeting_key".to_string()) + ); - // Ensure the value set into client context is picked up. - client.set_evaluation_context( - EvaluationContext::default() - .with_custom_field("Value", EvaluationContextFieldValue::Int(300)), + assert_eq!( + *result.flag_metadata.values.get("key").unwrap(), + FlagMetadataValue::String("client_value".to_string()) ); - assert_eq!(client.get_int_value("", None, None).await.unwrap(), 300); + // Use invocation level evaluation context and ensure its values are used. + let evaluation_context = EvaluationContext::builder() + .targeting_key("invocation_targeting_key") + .build() + .with_custom_field("key", "invocation_value"); + + let result = client + .get_string_details("", Some(&evaluation_context), None) + .await + .unwrap(); - // Ensure the value set into invocation evaluation context is picked up. - client.set_evaluation_context( - EvaluationContext::default() - .with_custom_field("Value", EvaluationContextFieldValue::Int(400)), + assert_eq!( + *result.flag_metadata.values.get("TargetingKey").unwrap(), + FlagMetadataValue::String("invocation_targeting_key".to_string()) ); - assert_eq!(client.get_int_value("", None, None).await.unwrap(), 400); + assert_eq!( + *result.flag_metadata.values.get("key").unwrap(), + FlagMetadataValue::String("invocation_value".to_string()) + ); } #[spec( @@ -363,26 +386,13 @@ mod tests { // Note the `await` call here because asynchronous lock is used to guarantee thread safety. let mut api = OpenFeature::singleton_mut().await; - // Createa a global evaluation context and set it into the API. - // Note that this is optional. By default it uses an empty one. - let global_evaluation_context = EvaluationContext::default(); - api.set_evaluation_context(global_evaluation_context).await; - - // Set the default feature provider. - // If you do not do that, [`NoOpProvider`] will be used by default. - // - // By default, [`NoOpProvider`] will simply return the default value of each type. - // You can inject value you want via its builder or evaluation context. See its document - // for more details. - // - // If you set a new provider after creating some clients, the existing clients will pick up - // the new provider you just set. - api.set_provider(NoOpProvider::default()).await; + api.set_provider(NoOpProvider::builder().int_value(100).build()) + .await; // Create an unnamed client. let client = api.create_client(); - // Createa an evaluation context. + // Create an evaluation context. // It supports types mentioned in the specification. // // You have multiple ways to add a custom field. @@ -404,41 +414,31 @@ mod tests { EvaluationContextFieldValue::new_struct(MyStruct::default()), ); - assert_eq!( - client - .get_bool_value("key", Some(&evaluation_context), None) - .await - .unwrap(), - bool::default() - ); - - // Create a named provider and bind it. - api.set_named_provider("named", NoOpProvider::builder().int_value(42).build()) - .await; - - // This named client will use the feature provider bound to this name. - let client = api.create_named_client("named"); + // This function returns a `Result`. You can process it with functions provided by std. + let is_feature_enabled = client + .get_bool_value("SomeFlagEnabled", Some(&evaluation_context), None) + .await + .unwrap_or(false); - assert_eq!(42, client.get_int_value("key", None, None).await.unwrap()); + if is_feature_enabled { + // Do something. + } // Let's get evaluation details. - // Note that we will inject `300` as the int value via evaluation context. - // It is not a feature mentioned in the standard but rather implemented for the - // convenience. let result = client .get_int_details( "key", - Some(&EvaluationContext::default().with_custom_field("Value", 300)), + Some(&EvaluationContext::default().with_custom_field("some_key", "some_value")), None, ) .await; match result { Ok(details) => { - assert_eq!(details.value, 300); + assert_eq!(details.value, 100); assert_eq!(details.reason, Some(EvaluationReason::Static)); assert_eq!(details.variant, Some("Static".to_string())); - assert_eq!(details.flag_metadata.values.iter().count(), 1); + assert_eq!(details.flag_metadata.values.iter().count(), 2); } Err(error) => { println!( diff --git a/src/provider/no_op_provider.rs b/src/provider/no_op_provider.rs index ea76ad3..37cce89 100644 --- a/src/provider/no_op_provider.rs +++ b/src/provider/no_op_provider.rs @@ -5,7 +5,7 @@ use typed_builder::TypedBuilder; use crate::{ EvaluationContext, EvaluationContextFieldValue, EvaluationError, EvaluationReason, - FlagMetadata, StructValue, + FlagMetadata, FlagMetadataValue, StructValue, }; use super::{FeatureProvider, ProviderMetadata, ResolutionDetails}; @@ -17,17 +17,17 @@ use super::{FeatureProvider, ProviderMetadata, ResolutionDetails}; /// The default provider that does nothing. /// /// By default, it returns the default value of each supported type. You can inject values (for -/// testing purpose or simply providing some fixed value) by the following ways: -/// 1. When creating an instance, inject values through the builder. -/// 2. Supply a value with key "Value" in the evaluation context. Because the evaluation context is -/// merged, you can achieve this by providing value in API level, client level or invocation -/// level. Note that the type must be compliant for it to work. +/// testing purpose or simply providing some fixed value) when creating an instance. /// /// Other tips: /// * It will return reason `Default` when the value equals to the default, `Static` otherwise. /// * It will return variant `"Default"` or `"Static"` respectively. /// * It will return flag metadata with key `"Type"` and a string value that corresponds to the /// real type, EXCEPT for `resolve_struct_value`. +/// * It will return flag metadata with keys "TargetingKey" and value of targeting key extracted +/// from the evaluation context, if the value is not `None`. +/// * It will return flag metadata with keys/values extracted from the evaluation context, as long +/// as the value is a bool, number or string. #[derive(TypedBuilder, Debug)] pub struct NoOpProvider { #[builder(default)] @@ -57,6 +57,34 @@ impl NoOpProvider { (EvaluationReason::Static, "Static".to_string()) } } + + fn populate_evaluation_context_values( + flag_metadata: &mut FlagMetadata, + evaluation_context: &EvaluationContext, + ) { + if let Some(value) = &evaluation_context.targeting_key { + flag_metadata.add_value("TargetingKey", value.clone()); + } + + evaluation_context + .custom_fields + .iter() + .for_each(|(key, value)| match value { + EvaluationContextFieldValue::Bool(value) => { + flag_metadata.add_value(key, FlagMetadataValue::Bool(*value)) + } + EvaluationContextFieldValue::Int(value) => { + flag_metadata.add_value(key, FlagMetadataValue::Int(*value)) + } + EvaluationContextFieldValue::Float(value) => { + flag_metadata.add_value(key, FlagMetadataValue::Float(*value)) + } + EvaluationContextFieldValue::String(value) => { + flag_metadata.add_value(key, FlagMetadataValue::String(value.clone())) + } + _ => (), + }) + } } impl Default for NoOpProvider { @@ -87,21 +115,16 @@ impl FeatureProvider for NoOpProvider { _flag_key: &str, evaluation_context: &EvaluationContext, ) -> Result, EvaluationError> { - let value = match evaluation_context.custom_fields.get("Value") { - Some(v) => match *v { - EvaluationContextFieldValue::Bool(value) => value, - _ => self.bool_value, - }, - None => self.bool_value, - }; + let (reason, variant) = Self::create_reason_variant(self.bool_value == Default::default()); - let (reason, variant) = Self::create_reason_variant(value == Default::default()); + let mut flag_metadata = FlagMetadata::default().with_value("Type", "Bool"); + Self::populate_evaluation_context_values(&mut flag_metadata, &evaluation_context); Ok(ResolutionDetails::builder() - .value(value) + .value(self.bool_value) .reason(reason) .variant(variant) - .flag_metadata(FlagMetadata::default().with_value("Type", "Bool")) + .flag_metadata(flag_metadata) .build()) } @@ -110,21 +133,16 @@ impl FeatureProvider for NoOpProvider { _flag_key: &str, evaluation_context: &EvaluationContext, ) -> Result, EvaluationError> { - let value = match evaluation_context.custom_fields.get("Value") { - Some(v) => match *v { - EvaluationContextFieldValue::Int(value) => value, - _ => self.int_value, - }, - None => self.int_value, - }; + let (reason, variant) = Self::create_reason_variant(self.int_value == Default::default()); - let (reason, variant) = Self::create_reason_variant(value == Default::default()); + let mut flag_metadata = FlagMetadata::default().with_value("Type", "Int"); + Self::populate_evaluation_context_values(&mut flag_metadata, &evaluation_context); Ok(ResolutionDetails::builder() - .value(value) + .value(self.int_value) .reason(reason) .variant(variant) - .flag_metadata(FlagMetadata::default().with_value("Type", "Int")) + .flag_metadata(flag_metadata) .build()) } @@ -133,21 +151,16 @@ impl FeatureProvider for NoOpProvider { _flag_key: &str, evaluation_context: &EvaluationContext, ) -> Result, EvaluationError> { - let value = match evaluation_context.custom_fields.get("Value") { - Some(v) => match *v { - EvaluationContextFieldValue::Float(value) => value, - _ => self.float_value, - }, - None => self.float_value, - }; + let (reason, variant) = Self::create_reason_variant(self.float_value == Default::default()); - let (reason, variant) = Self::create_reason_variant(value == Default::default()); + let mut flag_metadata = FlagMetadata::default().with_value("Type", "Float"); + Self::populate_evaluation_context_values(&mut flag_metadata, &evaluation_context); Ok(ResolutionDetails::builder() - .value(value) + .value(self.float_value) .reason(reason) .variant(variant) - .flag_metadata(FlagMetadata::default().with_value("Type", "Float")) + .flag_metadata(flag_metadata) .build()) } @@ -156,33 +169,30 @@ impl FeatureProvider for NoOpProvider { _flag_key: &str, evaluation_context: &EvaluationContext, ) -> Result, EvaluationError> { - let value = match evaluation_context.custom_fields.get("Value") { - Some(v) => match v { - EvaluationContextFieldValue::String(value) => value.to_string(), - _ => self.string_value.clone(), - }, - - None => self.string_value.clone(), - }; + let (reason, variant) = Self::create_reason_variant(self.string_value == String::default()); - let (reason, variant) = Self::create_reason_variant(value == String::default()); + let mut flag_metadata = FlagMetadata::default().with_value("Type", "String"); + Self::populate_evaluation_context_values(&mut flag_metadata, &evaluation_context); Ok(ResolutionDetails::builder() - .value(value) + .value(self.string_value.clone()) .reason(reason) .variant(variant) - .flag_metadata(FlagMetadata::default().with_value("Type", "String")) + .flag_metadata(flag_metadata) .build()) } async fn resolve_struct_value( &self, _flag_key: &str, - _evaluation_context: &EvaluationContext, + evaluation_context: &EvaluationContext, ) -> Result, EvaluationError> { let (reason, variant) = Self::create_reason_variant(self.struct_value == Default::default()); + let mut flag_metadata = FlagMetadata::default().with_value("Type", "Struct"); + Self::populate_evaluation_context_values(&mut flag_metadata, &evaluation_context); + Ok(ResolutionDetails::builder() .value((*self.struct_value).clone()) .reason(reason)