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