1use std::{future::Future, pin::Pin, sync::Arc};
2
3use crate::{
4 provider::{FeatureProvider, ResolutionDetails},
5 EvaluationContext, EvaluationDetails, EvaluationError, EvaluationErrorCode, EvaluationOptions,
6 EvaluationResult, Hook, HookContext, HookHints, HookWrapper, StructValue, Value,
7};
8
9use super::{
10 global_evaluation_context::GlobalEvaluationContext, global_hooks::GlobalHooks,
11 provider_registry::ProviderRegistry,
12};
13
14#[derive(Clone, Default, PartialEq, Debug)]
16pub struct ClientMetadata {
17 pub name: String,
19}
20
21#[allow(clippy::struct_field_names)]
24pub struct Client {
25 metadata: ClientMetadata,
26 provider_registry: ProviderRegistry,
27 evaluation_context: EvaluationContext,
28 global_evaluation_context: GlobalEvaluationContext,
29 global_hooks: GlobalHooks,
30
31 client_hooks: Vec<HookWrapper>,
32}
33
34impl Client {
35 pub fn new(
37 name: impl Into<String>,
38 global_evaluation_context: GlobalEvaluationContext,
39 global_hooks: GlobalHooks,
40 provider_registry: ProviderRegistry,
41 ) -> Self {
42 Self {
43 metadata: ClientMetadata { name: name.into() },
44 global_evaluation_context,
45 global_hooks,
46 provider_registry,
47 evaluation_context: EvaluationContext::default(),
48 client_hooks: Vec::new(),
49 }
50 }
51
52 pub fn metadata(&self) -> &ClientMetadata {
54 &self.metadata
55 }
56
57 pub fn set_evaluation_context(&mut self, evaluation_context: EvaluationContext) {
59 self.evaluation_context = evaluation_context;
60 }
61
62 pub async fn get_bool_value(
65 &self,
66 flag_key: &str,
67 evaluation_context: Option<&EvaluationContext>,
68 evaluation_options: Option<&EvaluationOptions>,
69 ) -> EvaluationResult<bool> {
70 self.get_bool_details(flag_key, evaluation_context, evaluation_options)
71 .await
72 .map(|details| details.value)
73 }
74
75 pub async fn get_int_value(
78 &self,
79 flag_key: &str,
80 evaluation_context: Option<&EvaluationContext>,
81 evaluation_options: Option<&EvaluationOptions>,
82 ) -> EvaluationResult<i64> {
83 self.get_int_details(flag_key, evaluation_context, evaluation_options)
84 .await
85 .map(|details| details.value)
86 }
87
88 pub async fn get_float_value(
92 &self,
93 flag_key: &str,
94 evaluation_context: Option<&EvaluationContext>,
95 evaluation_options: Option<&EvaluationOptions>,
96 ) -> EvaluationResult<f64> {
97 self.get_float_details(flag_key, evaluation_context, evaluation_options)
98 .await
99 .map(|details| details.value)
100 }
101
102 pub async fn get_string_value(
106 &self,
107 flag_key: &str,
108 evaluation_context: Option<&EvaluationContext>,
109 evaluation_options: Option<&EvaluationOptions>,
110 ) -> EvaluationResult<String> {
111 self.get_string_details(flag_key, evaluation_context, evaluation_options)
112 .await
113 .map(|details| details.value)
114 }
115
116 pub async fn get_struct_value<T: TryFrom<StructValue>>(
121 &self,
122 flag_key: &str,
123 evaluation_context: Option<&EvaluationContext>,
124 evaluation_options: Option<&EvaluationOptions>,
125 ) -> EvaluationResult<T> {
126 let result = self
127 .get_struct_details(flag_key, evaluation_context, evaluation_options)
128 .await?;
129
130 match T::try_from(result.value) {
131 Ok(t) => Ok(t),
132 Err(_) => Err(EvaluationError {
133 code: EvaluationErrorCode::TypeMismatch,
134 message: Some("Unable to cast value to required type".to_string()),
135 }),
136 }
137 }
138
139 pub async fn get_bool_details(
142 &self,
143 flag_key: &str,
144 evaluation_context: Option<&EvaluationContext>,
145 evaluation_options: Option<&EvaluationOptions>,
146 ) -> EvaluationResult<EvaluationDetails<bool>> {
147 let context = self.merge_evaluation_context(evaluation_context).await;
148
149 self.evaluate(
150 flag_key,
151 &context,
152 evaluation_options,
153 call_resolve_bool_value,
154 )
155 .await
156 }
157
158 pub async fn get_int_details(
161 &self,
162 flag_key: &str,
163 evaluation_context: Option<&EvaluationContext>,
164 evaluation_options: Option<&EvaluationOptions>,
165 ) -> EvaluationResult<EvaluationDetails<i64>> {
166 let context = self.merge_evaluation_context(evaluation_context).await;
167
168 self.evaluate(
169 flag_key,
170 &context,
171 evaluation_options,
172 call_resolve_int_value,
173 )
174 .await
175 }
176
177 pub async fn get_float_details(
180 &self,
181 flag_key: &str,
182 evaluation_context: Option<&EvaluationContext>,
183 evaluation_options: Option<&EvaluationOptions>,
184 ) -> EvaluationResult<EvaluationDetails<f64>> {
185 let context = self.merge_evaluation_context(evaluation_context).await;
186
187 self.evaluate(
188 flag_key,
189 &context,
190 evaluation_options,
191 call_resolve_float_value,
192 )
193 .await
194 }
195
196 pub async fn get_string_details(
199 &self,
200 flag_key: &str,
201 evaluation_context: Option<&EvaluationContext>,
202 evaluation_options: Option<&EvaluationOptions>,
203 ) -> EvaluationResult<EvaluationDetails<String>> {
204 let context = self.merge_evaluation_context(evaluation_context).await;
205
206 self.evaluate(
207 flag_key,
208 &context,
209 evaluation_options,
210 call_resolve_string_value,
211 )
212 .await
213 }
214
215 pub async fn get_struct_details<T: TryFrom<StructValue>>(
218 &self,
219 flag_key: &str,
220 evaluation_context: Option<&EvaluationContext>,
221 evaluation_options: Option<&EvaluationOptions>,
222 ) -> EvaluationResult<EvaluationDetails<T>> {
223 let context = self.merge_evaluation_context(evaluation_context).await;
224
225 let result = self
226 .evaluate(
227 flag_key,
228 &context,
229 evaluation_options,
230 call_resolve_struct_value,
231 )
232 .await?;
233
234 match T::try_from(result.value) {
235 Ok(value) => Ok(EvaluationDetails {
236 flag_key: flag_key.to_string(),
237 value,
238 reason: result.reason,
239 variant: result.variant,
240 flag_metadata: result.flag_metadata,
241 }),
242 Err(_) => Err(EvaluationError {
243 code: EvaluationErrorCode::TypeMismatch,
244 message: Some("Unable to cast value to required type".to_string()),
245 }),
246 }
247 }
248
249 async fn get_provider(&self) -> Arc<dyn FeatureProvider> {
250 self.provider_registry.get(&self.metadata.name).await.get()
251 }
252
253 async fn merge_evaluation_context(
256 &self,
257 flag_evaluation_context: Option<&EvaluationContext>,
258 ) -> EvaluationContext {
259 let mut context = match flag_evaluation_context {
260 Some(c) => c.clone(),
261 None => EvaluationContext::default(),
262 };
263
264 context.merge_missing(&self.evaluation_context);
265
266 let global_evaluation_context = self.global_evaluation_context.get().await;
267
268 context.merge_missing(&global_evaluation_context);
269
270 context
271 }
272}
273
274impl Client {
275 #[must_use]
277 pub fn with_hook<T: Hook>(mut self, hook: T) -> Self {
278 self.client_hooks.push(HookWrapper::new(hook));
279 self
280 }
281
282 #[must_use]
284 pub fn with_logging_hook(self, include_evaluation_context: bool) -> Self {
285 self.with_hook(crate::LoggingHook {
286 include_evaluation_context,
287 })
288 }
289
290 async fn evaluate<T>(
291 &self,
292 flag_key: &str,
293 context: &EvaluationContext,
294 evaluation_options: Option<&EvaluationOptions>, resolve: impl for<'a> FnOnce(
296 &'a dyn FeatureProvider,
297 &'a str,
298 &'a EvaluationContext,
299 ) -> Pin<
300 Box<dyn Future<Output = EvaluationResult<ResolutionDetails<T>>> + Send + 'a>,
301 >,
302 ) -> EvaluationResult<EvaluationDetails<T>>
303 where
304 T: Into<Value> + Clone + Default,
305 {
306 let provider = self.get_provider().await;
307 let hints = evaluation_options.map(|options| &options.hints);
308
309 let default: Value = T::default().into();
310
311 let mut hook_context = HookContext {
312 flag_key,
313 flag_type: default.get_type(),
314 client_metadata: self.metadata.clone(),
315 provider_metadata: provider.metadata().clone(),
316 evaluation_context: context,
317
318 default_value: Some(default),
319 };
320
321 let global_hooks = self.global_hooks.get().await;
322 let client_hooks = &self.client_hooks[..];
323 let invocation_hooks: &[HookWrapper] = evaluation_options
324 .map(|options| options.hooks.as_ref())
325 .unwrap_or_default();
326 let provider_hooks = provider.hooks();
327
328 let before_hooks = global_hooks
331 .iter()
332 .chain(client_hooks.iter())
333 .chain(invocation_hooks.iter())
334 .chain(provider_hooks.iter());
335
336 let after_hooks = before_hooks.clone().rev();
339
340 let (context, result) = self
341 .before_hooks(before_hooks.into_iter(), &hook_context, hints)
342 .await;
343 hook_context.evaluation_context = &context;
344
345 let evaluation_details;
348
349 if let Err(error) = result {
350 self.error_hooks(after_hooks.clone(), &hook_context, &error, hints)
351 .await;
352 evaluation_details = EvaluationDetails::error_reason(flag_key, T::default());
353 self.finally_hooks(
354 after_hooks.into_iter(),
355 &hook_context,
356 &evaluation_details,
357 hints,
358 )
359 .await;
360
361 return Err(error);
362 }
363
364 let result = resolve(&*provider, flag_key, &context)
366 .await
367 .map(|details| details.into_evaluation_details(flag_key));
368
369 match result {
371 Ok(ref details) => {
372 let details = details.clone().into_value();
373 if let Err(error) = self
374 .after_hooks(after_hooks.clone(), &hook_context, &details, hints)
375 .await
376 {
377 evaluation_details = EvaluationDetails::error_reason(flag_key, T::default());
378 self.error_hooks(after_hooks.clone(), &hook_context, &error, hints)
379 .await;
380 } else {
381 evaluation_details = details;
382 }
383 }
384 Err(ref error) => {
385 evaluation_details = EvaluationDetails::error_reason(flag_key, T::default());
386 self.error_hooks(after_hooks.clone(), &hook_context, error, hints)
387 .await;
388 }
389 }
390
391 self.finally_hooks(
392 after_hooks.into_iter(),
393 &hook_context,
394 &evaluation_details,
395 hints,
396 )
397 .await;
398
399 result
400 }
401
402 async fn before_hooks<'a, I>(
403 &self,
404 hooks: I,
405 hook_context: &HookContext<'_>,
406 hints: Option<&HookHints>,
407 ) -> (EvaluationContext, EvaluationResult<()>)
408 where
409 I: Iterator<Item = &'a HookWrapper>,
410 {
411 let mut context = hook_context.evaluation_context.clone();
412 for hook in hooks {
413 let invoke_hook_context = HookContext {
414 evaluation_context: &context,
415 ..hook_context.clone()
416 };
417 match hook.before(&invoke_hook_context, hints).await {
418 Ok(Some(output)) => context = output,
419 Ok(None) => { }
420 Err(error) => {
421 drop(invoke_hook_context);
422 context.merge_missing(hook_context.evaluation_context);
423 return (context, Err(error));
424 }
425 }
426 }
427
428 context.merge_missing(hook_context.evaluation_context);
429 (context, Ok(()))
430 }
431
432 async fn after_hooks<'a, I>(
433 &self,
434 hooks: I,
435 hook_context: &HookContext<'_>,
436 details: &EvaluationDetails<Value>,
437 hints: Option<&HookHints>,
438 ) -> EvaluationResult<()>
439 where
440 I: Iterator<Item = &'a HookWrapper>,
441 {
442 for hook in hooks {
443 hook.after(hook_context, details, hints).await?;
444 }
445
446 Ok(())
447 }
448
449 async fn error_hooks<'a, I>(
450 &self,
451 hooks: I,
452 hook_context: &HookContext<'_>,
453 error: &EvaluationError,
454 hints: Option<&HookHints>,
455 ) where
456 I: Iterator<Item = &'a HookWrapper>,
457 {
458 for hook in hooks {
459 hook.error(hook_context, error, hints).await;
460 }
461 }
462
463 async fn finally_hooks<'a, I>(
464 &self,
465 hooks: I,
466 hook_context: &HookContext<'_>,
467 evaluation_details: &EvaluationDetails<Value>,
468 hints: Option<&HookHints>,
469 ) where
470 I: Iterator<Item = &'a HookWrapper>,
471 {
472 for hook in hooks {
473 hook.finally(hook_context, evaluation_details, hints).await;
474 }
475 }
476}
477
478impl<T> ResolutionDetails<T> {
479 fn into_evaluation_details(self, flag_key: impl Into<String>) -> EvaluationDetails<T> {
480 EvaluationDetails {
481 flag_key: flag_key.into(),
482 value: self.value,
483 reason: self.reason,
484 variant: self.variant,
485 flag_metadata: self.flag_metadata.unwrap_or_default(),
486 }
487 }
488}
489
490fn call_resolve_bool_value<'a>(
491 provider: &'a dyn FeatureProvider,
492 flag_key: &'a str,
493 context: &'a EvaluationContext,
494) -> Pin<Box<dyn Future<Output = EvaluationResult<ResolutionDetails<bool>>> + Send + 'a>> {
495 Box::pin(async move { provider.resolve_bool_value(flag_key, context).await })
496}
497
498fn call_resolve_int_value<'a>(
499 provider: &'a dyn FeatureProvider,
500 flag_key: &'a str,
501 context: &'a EvaluationContext,
502) -> Pin<Box<dyn Future<Output = EvaluationResult<ResolutionDetails<i64>>> + Send + 'a>> {
503 Box::pin(async move { provider.resolve_int_value(flag_key, context).await })
504}
505
506fn call_resolve_float_value<'a>(
507 provider: &'a dyn FeatureProvider,
508 flag_key: &'a str,
509 context: &'a EvaluationContext,
510) -> Pin<Box<dyn Future<Output = EvaluationResult<ResolutionDetails<f64>>> + Send + 'a>> {
511 Box::pin(async move { provider.resolve_float_value(flag_key, context).await })
512}
513
514fn call_resolve_string_value<'a>(
515 provider: &'a dyn FeatureProvider,
516 flag_key: &'a str,
517 context: &'a EvaluationContext,
518) -> Pin<Box<dyn Future<Output = EvaluationResult<ResolutionDetails<String>>> + Send + 'a>> {
519 Box::pin(async move { provider.resolve_string_value(flag_key, context).await })
520}
521
522fn call_resolve_struct_value<'a>(
523 provider: &'a dyn FeatureProvider,
524 flag_key: &'a str,
525 context: &'a EvaluationContext,
526) -> Pin<Box<dyn Future<Output = EvaluationResult<ResolutionDetails<StructValue>>> + Send + 'a>> {
527 Box::pin(async move { provider.resolve_struct_value(flag_key, context).await })
528}
529
530#[cfg(test)]
531mod tests {
532
533 use spec::spec;
534
535 use crate::{
536 api::{
537 global_evaluation_context::GlobalEvaluationContext, global_hooks::GlobalHooks,
538 provider_registry::ProviderRegistry,
539 },
540 provider::{FeatureProvider, MockFeatureProvider, ProviderMetadata, ResolutionDetails},
541 Client, EvaluationReason, FlagMetadata, StructValue, Value,
542 };
543
544 #[spec(
545 number = "1.2.2",
546 text = "The client interface MUST define a metadata member or accessor, containing an immutable name field or accessor of type string, which corresponds to the name value supplied during client creation."
547 )]
548 #[test]
549 fn get_metadata_name() {
550 assert_eq!(create_default_client().metadata().name, "no_op");
551 }
552
553 #[derive(PartialEq, Debug)]
554 struct Student {
555 id: i64,
556 name: String,
557 }
558
559 impl TryFrom<StructValue> for Student {
560 type Error = String;
561
562 fn try_from(value: StructValue) -> Result<Self, Self::Error> {
563 Ok(Student {
564 id: value
565 .fields
566 .get("id")
567 .ok_or("id not provided")?
568 .as_i64()
569 .ok_or("id is not a valid number")?,
570 name: value
571 .fields
572 .get("name")
573 .ok_or("name not provided")?
574 .as_str()
575 .ok_or("name is not a valid string")?
576 .to_string(),
577 })
578 }
579 }
580
581 #[spec(
582 number = "1.3.1.1",
583 text = "The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value."
584 )]
585 #[spec(
586 number = "1.3.3.1",
587 text = "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms."
588 )]
589 #[tokio::test]
590 async fn get_value() {
591 let mut provider = MockFeatureProvider::new();
593 provider.expect_initialize().returning(|_| {});
594 provider.expect_hooks().return_const(vec![]);
595 provider
596 .expect_metadata()
597 .return_const(ProviderMetadata::default());
598
599 provider
600 .expect_resolve_bool_value()
601 .return_const(Ok(ResolutionDetails::new(true)));
602
603 provider
604 .expect_resolve_int_value()
605 .return_const(Ok(ResolutionDetails::new(123)));
606
607 provider
608 .expect_resolve_float_value()
609 .return_const(Ok(ResolutionDetails::new(12.34)));
610
611 provider
612 .expect_resolve_string_value()
613 .return_const(Ok(ResolutionDetails::new("Hello")));
614
615 provider
616 .expect_resolve_struct_value()
617 .return_const(Ok(ResolutionDetails::new(
618 StructValue::default()
619 .with_field("id", 100)
620 .with_field("name", "Alex"),
621 )));
622
623 let client = create_client(provider).await;
624
625 assert_eq!(
626 client.get_bool_value("key", None, None).await.unwrap(),
627 true
628 );
629
630 assert_eq!(client.get_int_value("key", None, None).await.unwrap(), 123);
631
632 assert_eq!(
633 client.get_float_value("key", None, None).await.unwrap(),
634 12.34
635 );
636
637 assert_eq!(
638 client.get_string_value("", None, None).await.unwrap(),
639 "Hello"
640 );
641
642 println!(
643 "Result: {:?}",
644 client.get_struct_value::<Value>("", None, None).await
645 );
646
647 assert_eq!(
648 client
649 .get_struct_value::<Student>("", None, None)
650 .await
651 .unwrap(),
652 Student {
653 id: 100,
654 name: "Alex".to_string()
655 }
656 );
657 }
658
659 #[spec(
660 number = "1.3.4",
661 text = "The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned."
662 )]
663 #[test]
664 fn get_value_return_right_type_checked_by_type_system() {}
665
666 #[spec(
667 number = "1.4.1.1",
668 text = "The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure."
669 )]
670 #[spec(
671 number = "1.4.3",
672 text = "The evaluation details structure's value field MUST contain the evaluated flag value."
673 )]
674 #[spec(
675 number = "1.4.4.1",
676 text = "The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field."
677 )]
678 #[spec(
679 number = "1.4.5",
680 text = "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method."
681 )]
682 #[spec(
683 number = "1.4.6",
684 text = "In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set."
685 )]
686 #[spec(
687 number = "1.4.7",
688 text = "In cases of normal execution, the evaluation details structure's reason field MUST contain the value of the reason field in the flag resolution structure returned by the configured provider, if the field is set."
689 )]
690 #[spec(
691 number = "1.4.12",
692 text = "The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation."
693 )]
694 #[tokio::test]
695 async fn get_details() {
696 let mut provider = MockFeatureProvider::new();
697 provider.expect_initialize().returning(|_| {});
698 provider.expect_hooks().return_const(vec![]);
699 provider
700 .expect_metadata()
701 .return_const(ProviderMetadata::default());
702 provider
703 .expect_resolve_int_value()
704 .return_const(Ok(ResolutionDetails::builder()
705 .value(123)
706 .variant("Static")
707 .reason(EvaluationReason::Static)
708 .build()));
709
710 let client = create_client(provider).await;
711
712 let result = client.get_int_details("key", None, None).await.unwrap();
713
714 assert_eq!(result.value, 123);
715 assert_eq!(result.flag_key, "key");
716 assert_eq!(result.reason, Some(EvaluationReason::Static));
717 assert_eq!(result.variant, Some("Static".to_string()));
718 }
719
720 #[spec(
721 number = "1.4.8",
722 text = "In cases of abnormal execution, the evaluation details structure's error code field MUST contain an error code."
723 )]
724 #[spec(
725 number = "1.4.9",
726 text = "In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error."
727 )]
728 #[spec(
729 number = "1.4.13",
730 text = "In cases of abnormal execution, the evaluation details structure's error message field MAY contain a string containing additional details about the nature of the error."
731 )]
732 #[test]
733 fn evaluation_details_contains_error_checked_by_type_system() {}
734
735 #[spec(
736 number = "1.4.10",
737 text = "Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the default value in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup."
738 )]
739 #[test]
740 fn evaluation_return_default_value_covered_by_result() {}
741
742 #[spec(
743 number = "1.4.14",
744 text = "If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record."
745 )]
746 #[spec(
747 number = "1.4.14.1",
748 text = "Condition: Flag metadata MUST be immutable."
749 )]
750 #[tokio::test]
751 async fn get_details_flag_metadata() {
752 let mut provider = MockFeatureProvider::new();
753 provider.expect_initialize().returning(|_| {});
754 provider.expect_hooks().return_const(vec![]);
755 provider
756 .expect_metadata()
757 .return_const(ProviderMetadata::default());
758 provider
759 .expect_resolve_bool_value()
760 .return_const(Ok(ResolutionDetails::builder()
761 .value(true)
762 .flag_metadata(FlagMetadata::default().with_value("Type", "Bool"))
763 .build()));
764
765 let client = create_client(provider).await;
766
767 let result = client.get_bool_details("", None, None).await.unwrap();
768
769 assert_eq!(
770 *result.flag_metadata.values.get("Type").unwrap(),
771 "Bool".into()
772 );
773 }
774
775 #[spec(
776 number = "1.3.2.1",
777 text = "The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value."
778 )]
779 #[spec(
780 number = "1.4.2.1",
781 text = "The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns an evaluation details structure."
782 )]
783 #[test]
784 fn static_context_not_applicable() {}
785
786 #[tokio::test]
787 async fn with_hook() {
788 let mut provider = MockFeatureProvider::new();
789 provider.expect_initialize().returning(|_| {});
790
791 let client = create_client(provider).await;
792
793 let client = client.with_hook(crate::LoggingHook::default());
794
795 assert_eq!(client.client_hooks.len(), 1);
796 }
797
798 #[tokio::test]
799 async fn with_logging_hook() {
800 let mut provider = MockFeatureProvider::new();
801 provider.expect_initialize().returning(|_| {});
802
803 let client = create_client(provider).await;
804
805 let client = client.with_logging_hook(false);
806
807 assert_eq!(client.client_hooks.len(), 1);
808 }
809
810 fn create_default_client() -> Client {
811 Client::new(
812 "no_op",
813 GlobalEvaluationContext::default(),
814 GlobalHooks::default(),
815 ProviderRegistry::default(),
816 )
817 }
818
819 async fn create_client(provider: impl FeatureProvider) -> Client {
820 let provider_registry = ProviderRegistry::default();
821 provider_registry.set_named("custom", provider).await;
822
823 Client::new(
824 "custom",
825 GlobalEvaluationContext::default(),
826 GlobalHooks::default(),
827 provider_registry,
828 )
829 }
830}