Skip to content
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 hooks implementation #95

Merged
merged 9 commits into from
Jan 13, 2025
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ lazy_static = "1.5"
mockall = { version = "0.13.0", optional = true }
serde_json = { version = "1.0.116", optional = true }
time = "0.3.36"
tokio = { version = "1.40", features = [ "full" ] }
tokio = { version = "1.40", features = ["full"] }
typed-builder = "0.20.0"
log = "0.4.14"

[dev-dependencies]
env_logger = "0.11.5"
spec = { path = "spec" }

[features]
default = [ "test-util" ]
test-util = [ "dep:mockall" ]
serde_json = [ "dep:serde_json" ]
default = ["test-util"]
test-util = ["dep:mockall"]
serde_json = ["dep:serde_json"]
155 changes: 155 additions & 0 deletions examples/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use open_feature::{
provider::{FeatureProvider, ProviderMetadata, ProviderStatus, ResolutionDetails},
EvaluationContext, EvaluationDetails, EvaluationError, EvaluationOptions, EvaluationResult,
Hook, HookContext, HookHints, OpenFeature, StructValue, Value,
};

struct DummyProvider(ProviderMetadata);

impl Default for DummyProvider {
fn default() -> Self {
Self(ProviderMetadata::new("Dummy Provider"))
}
}

#[async_trait::async_trait]
impl FeatureProvider for DummyProvider {
fn metadata(&self) -> &ProviderMetadata {
&self.0
}

fn status(&self) -> ProviderStatus {
ProviderStatus::Ready
}

async fn resolve_bool_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<bool>> {
Ok(ResolutionDetails::new(true))
}

async fn resolve_int_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<i64>> {
unimplemented!()
}

async fn resolve_float_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<f64>> {
unimplemented!()
}

async fn resolve_string_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<String>> {
unimplemented!()
}

async fn resolve_struct_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
unimplemented!()
}
}

struct DummyLoggingHook(String);

#[async_trait::async_trait]
impl Hook for DummyLoggingHook {
async fn before<'a>(
&self,
context: &HookContext<'a>,
_hints: Option<&'a HookHints>,
) -> Result<Option<EvaluationContext>, EvaluationError> {
log::info!(
"Evaluating({}) flag {} of type {}",
self.0,
context.flag_key,
context.flag_type
);

Ok(None)
}

async fn after<'a>(
&self,
context: &HookContext<'a>,
details: &EvaluationDetails<Value>,
_hints: Option<&'a HookHints>,
) -> Result<(), EvaluationError> {
log::info!(
"Flag({}) {} of type {} evaluated to {:?}",
self.0,
context.flag_key,
context.flag_type,
details.value
);

Ok(())
}

async fn error<'a>(
&self,
context: &HookContext<'a>,
error: &EvaluationError,
_hints: Option<&'a HookHints>,
) {
log::error!(
"Error({}) evaluating flag {} of type {}: {:?}",
self.0,
context.flag_key,
context.flag_type,
error
);
}

async fn finally<'a>(
&self,
context: &HookContext<'a>,
_: &EvaluationDetails<Value>,
_hints: Option<&'a HookHints>,
) {
log::info!(
"Finally({}) evaluating flag {} of type {}",
self.0,
context.flag_key,
context.flag_type
);
}
}

#[tokio::main]
async fn main() {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();

let mut api = OpenFeature::singleton_mut().await;
api.set_provider(DummyProvider::default()).await;
api.add_hook(DummyLoggingHook("global".to_string())).await;
drop(api);

let client = OpenFeature::singleton()
.await
.create_client()
.with_hook(DummyLoggingHook("client".to_string())); // Add a client-level hook

let eval = EvaluationOptions::default().with_hook(DummyLoggingHook("eval".to_string()));
let feature = client
.get_bool_details("my_feature", None, Some(&eval))
.await
.unwrap();

println!("Feature value: {}", feature.value);
}
19 changes: 17 additions & 2 deletions src/api/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};

use crate::{
provider::{FeatureProvider, ProviderMetadata},
Client, EvaluationContext,
Client, EvaluationContext, Hook, HookWrapper,
};

use super::{
global_evaluation_context::GlobalEvaluationContext, provider_registry::ProviderRegistry,
global_evaluation_context::GlobalEvaluationContext, global_hooks::GlobalHooks,
provider_registry::ProviderRegistry,
};

lazy_static! {
Expand All @@ -21,6 +22,7 @@ lazy_static! {
#[derive(Default)]
pub struct OpenFeature {
evaluation_context: GlobalEvaluationContext,
hooks: GlobalHooks,

provider_registry: ProviderRegistry,
}
Expand Down Expand Up @@ -54,6 +56,12 @@ impl OpenFeature {
self.provider_registry.set_named(name, provider).await;
}

/// Add a new hook to the global list of hooks.
pub async fn add_hook<T: Hook>(&mut self, hook: T) {
let mut lock = self.hooks.get_mut().await;
lock.push(HookWrapper::new(hook));
}

/// Return the metadata of default (unnamed) provider.
pub async fn provider_metadata(&self) -> ProviderMetadata {
self.provider_registry
Expand All @@ -77,6 +85,7 @@ impl OpenFeature {
Client::new(
String::default(),
self.evaluation_context.clone(),
self.hooks.clone(),
self.provider_registry.clone(),
)
}
Expand All @@ -87,6 +96,7 @@ impl OpenFeature {
Client::new(
name.to_string(),
self.evaluation_context.clone(),
self.hooks.clone(),
self.provider_registry.clone(),
)
}
Expand Down Expand Up @@ -156,6 +166,7 @@ mod tests {
// Set the new provider and ensure the value comes from it.
let mut provider = MockFeatureProvider::new();
provider.expect_initialize().returning(|_| {});
provider.expect_hooks().return_const(vec![]);
provider
.expect_resolve_int_value()
.return_const(Ok(ResolutionDetails::new(200)));
Expand Down Expand Up @@ -203,6 +214,7 @@ mod tests {
// Bind provider to the same name.
let mut provider = MockFeatureProvider::new();
provider.expect_initialize().returning(|_| {});
provider.expect_hooks().return_const(vec![]);
provider
.expect_resolve_int_value()
.return_const(Ok(ResolutionDetails::new(30)));
Expand Down Expand Up @@ -246,12 +258,14 @@ mod tests {

let mut default_provider = MockFeatureProvider::new();
default_provider.expect_initialize().returning(|_| {});
default_provider.expect_hooks().return_const(vec![]);
default_provider
.expect_resolve_int_value()
.return_const(Ok(ResolutionDetails::new(100)));

let mut named_provider = MockFeatureProvider::new();
named_provider.expect_initialize().returning(|_| {});
named_provider.expect_hooks().return_const(vec![]);
named_provider
.expect_resolve_int_value()
.return_const(Ok(ResolutionDetails::new(200)));
Expand Down Expand Up @@ -314,6 +328,7 @@ mod tests {
// Setup expectations for different evaluation contexts.
let mut provider = MockFeatureProvider::new();
provider.expect_initialize().returning(|_| {});
provider.expect_hooks().return_const(vec![]);

provider
.expect_resolve_int_value()
Expand Down
Loading
Loading