open_feature/api/
client.rs

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/// The metadata of OpenFeature client.
15#[derive(Clone, Default, PartialEq, Debug)]
16pub struct ClientMetadata {
17    /// The name of client.
18    pub name: String,
19}
20
21/// The OpenFeature client.
22/// Create it through the [`OpenFeature`] struct.
23pub 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    /// Create a new [`Client`] instance.
35    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    /// Return the metadata of current client.
52    pub fn metadata(&self) -> &ClientMetadata {
53        &self.metadata
54    }
55
56    /// Set evaluation context to the client.
57    pub fn set_evaluation_context(&mut self, evaluation_context: EvaluationContext) {
58        self.evaluation_context = evaluation_context;
59    }
60
61    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
62    /// as a bool value.
63    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
75    /// as an int (i64) value.
76    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
88    /// as a float (f64) value.
89    /// If the resolution fails, the `default_value` is returned.
90    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
102    /// as a string value.
103    /// If the resolution fails, the `default_value` is returned.
104    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
116    /// as a struct.
117    /// If the resolution fails, the `default_value` is returned.
118    /// The required type should implement [`From<StructValue>`] trait.
119    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
139    /// `evaluation_options`.
140    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
158    /// `evaluation_options`.
159    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
177    /// `evaluation_options`.
178    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
196    /// `evaluation_options`.
197    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
215    /// `evaluation_options`.
216    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    /// Merge provided `flag_evaluation_context` (that is passed when evaluating a flag) with
253    /// client and global evaluation context.
254    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    /// Add a hook to the client.
275    #[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    /// Add logging hook to the client.
282    #[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>, // INFO: Invocation
294        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        // INFO: API(global), Client, Invocation, Provider
328        // https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#requirement-442
329        let before_hooks = global_hooks
330            .iter()
331            .chain(client_hooks.iter())
332            .chain(invocation_hooks.iter())
333            .chain(provider_hooks.iter());
334
335        // INFO: Hooks called after the resolution are in reverse order
336        // Provider, Invocation, Client, API(global)
337        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        // INFO: Result of the resolution or error reason with default value
345        // This bind is defined here to minimize cloning of the `Value`
346        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        // INFO: Run the resolution
364        let result = resolve(&*provider, flag_key, &context)
365            .await
366            .map(|details| details.into_evaluation_details(flag_key));
367
368        // INFO: Run the after hooks
369        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) => { /* INFO: just continue execution */ }
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        // Test bool.
591        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}