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.
23#[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    /// Create a new [`Client`] instance.
36    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    /// Return the metadata of current client.
53    pub fn metadata(&self) -> &ClientMetadata {
54        &self.metadata
55    }
56
57    /// Set evaluation context to the client.
58    pub fn set_evaluation_context(&mut self, evaluation_context: EvaluationContext) {
59        self.evaluation_context = evaluation_context;
60    }
61
62    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
63    /// as a bool value.
64    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
76    /// as an int (i64) value.
77    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
89    /// as a float (f64) value.
90    /// If the resolution fails, the `default_value` is returned.
91    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
103    /// as a string value.
104    /// If the resolution fails, the `default_value` is returned.
105    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    /// Evaluate given `flag_key` with corresponding `evaluation_context` and `evaluation_options`
117    /// as a struct.
118    /// If the resolution fails, the `default_value` is returned.
119    /// The required type should implement [`From<StructValue>`] trait.
120    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
140    /// `evaluation_options`.
141    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
159    /// `evaluation_options`.
160    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
178    /// `evaluation_options`.
179    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
197    /// `evaluation_options`.
198    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    /// Return the [`EvaluationDetails`] with given `flag_key`, `evaluation_context` and
216    /// `evaluation_options`.
217    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    /// Merge provided `flag_evaluation_context` (that is passed when evaluating a flag) with
254    /// client and global evaluation context.
255    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    /// Add a hook to the client.
276    #[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    /// Add logging hook to the client.
283    #[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>, // INFO: Invocation
295        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        // INFO: API(global), Client, Invocation, Provider
329        // https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#requirement-442
330        let before_hooks = global_hooks
331            .iter()
332            .chain(client_hooks.iter())
333            .chain(invocation_hooks.iter())
334            .chain(provider_hooks.iter());
335
336        // INFO: Hooks called after the resolution are in reverse order
337        // Provider, Invocation, Client, API(global)
338        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        // INFO: Result of the resolution or error reason with default value
346        // This bind is defined here to minimize cloning of the `Value`
347        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        // INFO: Run the resolution
365        let result = resolve(&*provider, flag_key, &context)
366            .await
367            .map(|details| details.into_evaluation_details(flag_key));
368
369        // INFO: Run the after hooks
370        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) => { /* INFO: just continue execution */ }
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        // Test bool.
592        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}