open_feature_flagd/resolver/
rest.rs

1//! # REST Flag Resolver
2//!
3//! Evaluates feature flags using the OpenFeature Remote Evaluation Protocol (OFREP) over HTTP.
4//!
5//! ## Features
6//!
7//! * HTTP-based flag evaluation
8//! * OFREP protocol support
9//! * Type-safe evaluation
10//! * Structured error handling
11//! * Comprehensive logging
12//!
13//! ## Supported Types
14//!
15//! * Boolean flags
16//! * String flags
17//! * Integer flags
18//! * Float flags
19//! * Structured flags
20//!
21//! ## Example
22//!
23//! ```rust,no_run
24//! use open_feature_flagd::resolver::rest::RestResolver;
25//! use open_feature_flagd::FlagdOptions;
26//! use open_feature::provider::FeatureProvider;
27//! use open_feature::EvaluationContext;
28//!
29//! #[tokio::main]
30//! async fn main() {
31//!     let options = FlagdOptions {
32//!         host: "localhost".to_string(),
33//!         port: 8016,
34//!         ..Default::default()
35//!     };
36//!     let resolver = RestResolver::new(&options);
37//!     let context = EvaluationContext::default();
38//!     
39//!     let result = resolver.resolve_bool_value("my-flag", &context).await.unwrap();
40//!     println!("Flag value: {}", result.value);
41//! }
42//! ```
43
44/// REST-based resolver implementing the OpenFeature Remote Evaluation Protocol (OFREP).
45use async_trait::async_trait;
46use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
47use open_feature::{
48    EvaluationContext, EvaluationContextFieldValue, EvaluationError, EvaluationErrorCode,
49    EvaluationResult, StructValue, Value,
50};
51use reqwest::Client;
52use reqwest::StatusCode;
53use serde_json;
54use tracing::{debug, error, instrument};
55
56use crate::FlagdOptions;
57
58/// REST-based resolver implementing the OpenFeature Remote Evaluation Protocol
59#[derive(Debug)]
60pub struct RestResolver {
61    /// Base endpoint URL for the OFREP service
62    endpoint: String,
63    /// Provider metadata
64    metadata: ProviderMetadata,
65    /// HTTP client for making requests
66    client: Client,
67}
68
69impl RestResolver {
70    /// Creates a new REST resolver with the specified host and port
71    ///
72    /// # Arguments
73    ///
74    /// * `target` - The host and port of the OFREP service, in the format `host:port`
75    ///
76    /// # Returns
77    ///
78    /// A new instance of RestResolver configured to connect to the specified endpoint
79    pub fn new(options: &FlagdOptions) -> Self {
80        let endpoint = if let Some(uri) = &options.target_uri {
81            format!("http://{}", uri)
82        } else {
83            format!("http://{}:{}", options.host, options.port)
84        };
85        Self {
86            endpoint,
87            metadata: ProviderMetadata::new("flagd-rest-provider"),
88            client: Client::new(),
89        }
90    }
91}
92
93#[async_trait]
94impl FeatureProvider for RestResolver {
95    fn metadata(&self) -> &ProviderMetadata {
96        &self.metadata
97    }
98
99    /// Resolves a boolean flag value
100    ///
101    /// # Arguments
102    ///
103    /// * `flag_key` - The unique identifier of the flag
104    /// * `evaluation_context` - Context data for flag evaluation
105    ///
106    /// # Returns
107    ///
108    /// The resolved boolean value with metadata, or an error if evaluation fails
109    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
110    async fn resolve_bool_value(
111        &self,
112        flag_key: &str,
113        evaluation_context: &EvaluationContext,
114    ) -> EvaluationResult<ResolutionDetails<bool>> {
115        debug!("Resolving boolean flag");
116
117        let payload = serde_json::json!({
118            "context": context_to_json(evaluation_context)
119        });
120
121        let response = self
122            .client
123            .post(format!(
124                "{}/ofrep/v1/evaluate/flags/{}",
125                self.endpoint, flag_key
126            ))
127            .header("Content-Type", "application/json")
128            .json(&payload)
129            .send()
130            .await
131            .map_err(|e| {
132                error!(error = %e, "Failed to resolve boolean value");
133                EvaluationError {
134                    code: EvaluationErrorCode::General(
135                        "Failed to resolve boolean value".to_string(),
136                    ),
137                    message: Some(e.to_string()),
138                }
139            })?;
140
141        debug!(status = response.status().as_u16(), "Received response");
142
143        match response.status() {
144            StatusCode::BAD_REQUEST => {
145                return Err(EvaluationError {
146                    code: EvaluationErrorCode::InvalidContext,
147                    message: Some("Invalid context".to_string()),
148                });
149            }
150            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
151                return Err(EvaluationError {
152                    code: EvaluationErrorCode::General(
153                        "authentication/authorization error".to_string(),
154                    ),
155                    message: Some("authentication/authorization error".to_string()),
156                });
157            }
158            StatusCode::NOT_FOUND => {
159                return Err(EvaluationError {
160                    code: EvaluationErrorCode::FlagNotFound,
161                    message: Some(format!("Flag: {flag_key} not found")),
162                });
163            }
164            _ => {
165                let result = response.json::<serde_json::Value>().await.map_err(|e| {
166                    error!(error = %e, "Failed to parse boolean response");
167                    EvaluationError {
168                        code: EvaluationErrorCode::ParseError,
169                        message: Some(e.to_string()),
170                    }
171                })?;
172
173                let value = result["value"].as_bool().ok_or_else(|| {
174                    error!("Invalid boolean value in response");
175                    EvaluationError {
176                        code: EvaluationErrorCode::ParseError,
177                        message: Some("Invalid boolean value".to_string()),
178                    }
179                })?;
180
181                debug!(value = value, variant = ?result["variant"], "Flag evaluated");
182                Ok(ResolutionDetails {
183                    value,
184                    variant: result["variant"].as_str().map(String::from),
185                    reason: Some(open_feature::EvaluationReason::Static),
186                    flag_metadata: Default::default(),
187                })
188            }
189        }
190    }
191
192    /// Resolves a string flag value
193    ///
194    /// # Arguments
195    ///
196    /// * `flag_key` - The unique identifier of the flag
197    /// * `evaluation_context` - Context data for flag evaluation
198    ///
199    /// # Returns
200    ///
201    /// The resolved string value with metadata, or an error if evaluation fails
202    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
203    async fn resolve_string_value(
204        &self,
205        flag_key: &str,
206        evaluation_context: &EvaluationContext,
207    ) -> EvaluationResult<ResolutionDetails<String>> {
208        debug!("Resolving string flag");
209
210        let payload = serde_json::json!({
211            "context": context_to_json(evaluation_context)
212        });
213
214        let response = self
215            .client
216            .post(format!(
217                "{}/ofrep/v1/evaluate/flags/{}",
218                self.endpoint, flag_key
219            ))
220            .header("Content-Type", "application/json")
221            .json(&payload)
222            .send()
223            .await
224            .map_err(|e| {
225                error!(error = %e, "Failed to resolve string value");
226                EvaluationError {
227                    code: EvaluationErrorCode::General(
228                        "Failed to resolve string value".to_string(),
229                    ),
230                    message: Some(e.to_string()),
231                }
232            })?;
233
234        debug!(status = response.status().as_u16(), "Received response");
235
236        match response.status() {
237            StatusCode::BAD_REQUEST => {
238                return Err(EvaluationError {
239                    code: EvaluationErrorCode::InvalidContext,
240                    message: Some("Invalid context".to_string()),
241                });
242            }
243            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
244                return Err(EvaluationError {
245                    code: EvaluationErrorCode::General(
246                        "authentication/authorization error".to_string(),
247                    ),
248                    message: Some("authentication/authorization error".to_string()),
249                });
250            }
251            StatusCode::NOT_FOUND => {
252                return Err(EvaluationError {
253                    code: EvaluationErrorCode::FlagNotFound,
254                    message: Some(format!("Flag: {flag_key} not found")),
255                });
256            }
257            _ => {
258                let result = response.json::<serde_json::Value>().await.map_err(|e| {
259                    error!(error = %e, "Failed to parse string response");
260                    EvaluationError {
261                        code: EvaluationErrorCode::ParseError,
262                        message: Some(e.to_string()),
263                    }
264                })?;
265
266                let value = result["value"]
267                    .as_str()
268                    .ok_or_else(|| {
269                        error!("Invalid string value in response");
270                        EvaluationError {
271                            code: EvaluationErrorCode::ParseError,
272                            message: Some("Invalid string value".to_string()),
273                        }
274                    })?
275                    .to_string();
276
277                debug!(value = %value, variant = ?result["variant"], "Flag evaluated");
278                Ok(ResolutionDetails {
279                    value,
280                    variant: result["variant"].as_str().map(String::from),
281                    reason: Some(open_feature::EvaluationReason::Static),
282                    flag_metadata: Default::default(),
283                })
284            }
285        }
286    }
287
288    /// Resolves a float flag value
289    ///
290    /// # Arguments
291    ///
292    /// * `flag_key` - The unique identifier of the flag
293    /// * `evaluation_context` - Context data for flag evaluation
294    ///
295    /// # Returns
296    ///
297    /// The resolved float value with metadata, or an error if evaluation fails
298    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
299    async fn resolve_float_value(
300        &self,
301        flag_key: &str,
302        evaluation_context: &EvaluationContext,
303    ) -> EvaluationResult<ResolutionDetails<f64>> {
304        debug!("Resolving float flag");
305
306        let payload = serde_json::json!({
307            "context": context_to_json(evaluation_context)
308        });
309
310        let response = self
311            .client
312            .post(format!(
313                "{}/ofrep/v1/evaluate/flags/{}",
314                self.endpoint, flag_key
315            ))
316            .header("Content-Type", "application/json")
317            .json(&payload)
318            .send()
319            .await
320            .map_err(|e| {
321                error!(error = %e, "Failed to resolve float value");
322                EvaluationError {
323                    code: EvaluationErrorCode::General("Failed to resolve float value".to_string()),
324                    message: Some(e.to_string()),
325                }
326            })?;
327
328        debug!(status = response.status().as_u16(), "Received response");
329
330        match response.status() {
331            StatusCode::BAD_REQUEST => {
332                return Err(EvaluationError {
333                    code: EvaluationErrorCode::InvalidContext,
334                    message: Some("Invalid context".to_string()),
335                });
336            }
337            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
338                return Err(EvaluationError {
339                    code: EvaluationErrorCode::General(
340                        "authentication/authorization error".to_string(),
341                    ),
342                    message: Some("authentication/authorization error".to_string()),
343                });
344            }
345            StatusCode::NOT_FOUND => {
346                return Err(EvaluationError {
347                    code: EvaluationErrorCode::FlagNotFound,
348                    message: Some(format!("Flag: {flag_key} not found")),
349                });
350            }
351            _ => {
352                let result = response.json::<serde_json::Value>().await.map_err(|e| {
353                    error!(error = %e, "Failed to parse float response");
354                    EvaluationError {
355                        code: EvaluationErrorCode::ParseError,
356                        message: Some(e.to_string()),
357                    }
358                })?;
359
360                let value = result["value"].as_f64().ok_or_else(|| {
361                    error!("Invalid float value in response");
362                    EvaluationError {
363                        code: EvaluationErrorCode::ParseError,
364                        message: Some("Invalid float value".to_string()),
365                    }
366                })?;
367
368                debug!(value = value, variant = ?result["variant"], "Flag evaluated");
369                Ok(ResolutionDetails {
370                    value,
371                    variant: result["variant"].as_str().map(String::from),
372                    reason: Some(open_feature::EvaluationReason::Static),
373                    flag_metadata: Default::default(),
374                })
375            }
376        }
377    }
378
379    /// Resolves an integer flag value
380    ///
381    /// # Arguments
382    ///
383    /// * `flag_key` - The unique identifier of the flag
384    /// * `evaluation_context` - Context data for flag evaluation
385    ///
386    /// # Returns
387    ///
388    /// The resolved integer value with metadata, or an error if evaluation fails
389    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
390    async fn resolve_int_value(
391        &self,
392        flag_key: &str,
393        evaluation_context: &EvaluationContext,
394    ) -> EvaluationResult<ResolutionDetails<i64>> {
395        debug!("Resolving integer flag");
396
397        let payload = serde_json::json!({
398            "context": context_to_json(evaluation_context)
399        });
400
401        let response = self
402            .client
403            .post(format!(
404                "{}/ofrep/v1/evaluate/flags/{}",
405                self.endpoint, flag_key
406            ))
407            .header("Content-Type", "application/json")
408            .json(&payload)
409            .send()
410            .await
411            .map_err(|e| {
412                error!(error = %e, "Failed to resolve integer value");
413                EvaluationError {
414                    code: EvaluationErrorCode::General(
415                        "Failed to resolve integer value".to_string(),
416                    ),
417                    message: Some(e.to_string()),
418                }
419            })?;
420
421        debug!(status = response.status().as_u16(), "Received response");
422
423        match response.status() {
424            StatusCode::BAD_REQUEST => {
425                return Err(EvaluationError {
426                    code: EvaluationErrorCode::InvalidContext,
427                    message: Some("Invalid context".to_string()),
428                });
429            }
430            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
431                return Err(EvaluationError {
432                    code: EvaluationErrorCode::General(
433                        "authentication/authorization error".to_string(),
434                    ),
435                    message: Some("authentication/authorization error".to_string()),
436                });
437            }
438            StatusCode::NOT_FOUND => {
439                return Err(EvaluationError {
440                    code: EvaluationErrorCode::FlagNotFound,
441                    message: Some(format!("Flag: {flag_key} not found")),
442                });
443            }
444            _ => {
445                let result = response.json::<serde_json::Value>().await.map_err(|e| {
446                    error!(error = %e, "Failed to parse integer response");
447                    EvaluationError {
448                        code: EvaluationErrorCode::ParseError,
449                        message: Some(e.to_string()),
450                    }
451                })?;
452
453                let value = result["value"].as_i64().ok_or_else(|| {
454                    error!("Invalid integer value in response");
455                    EvaluationError {
456                        code: EvaluationErrorCode::ParseError,
457                        message: Some("Invalid integer value".to_string()),
458                    }
459                })?;
460
461                debug!(value = value, variant = ?result["variant"], "Flag evaluated");
462                Ok(ResolutionDetails {
463                    value,
464                    variant: result["variant"].as_str().map(String::from),
465                    reason: Some(open_feature::EvaluationReason::Static),
466                    flag_metadata: Default::default(),
467                })
468            }
469        }
470    }
471
472    /// Resolves a structured flag value
473    ///
474    /// # Arguments
475    ///
476    /// * `flag_key` - The unique identifier of the flag
477    /// * `evaluation_context` - Context data for flag evaluation
478    ///
479    /// # Returns
480    ///
481    /// The resolved structured value with metadata, or an error if evaluation fails.
482    /// The structured value can contain nested objects, arrays, and primitive types.
483    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
484    async fn resolve_struct_value(
485        &self,
486        flag_key: &str,
487        evaluation_context: &EvaluationContext,
488    ) -> EvaluationResult<ResolutionDetails<StructValue>> {
489        debug!("Resolving struct flag");
490
491        let payload = serde_json::json!({
492            "context": context_to_json(evaluation_context)
493        });
494
495        let response = self
496            .client
497            .post(format!(
498                "{}/ofrep/v1/evaluate/flags/{}",
499                self.endpoint, flag_key
500            ))
501            .header("Content-Type", "application/json")
502            .json(&payload)
503            .send()
504            .await
505            .map_err(|e| {
506                error!(error = %e, "Failed to resolve struct value");
507                EvaluationError {
508                    code: EvaluationErrorCode::General(
509                        "Failed to resolve struct value".to_string(),
510                    ),
511                    message: Some(e.to_string()),
512                }
513            })?;
514
515        debug!(status = response.status().as_u16(), "Received response");
516
517        match response.status() {
518            StatusCode::BAD_REQUEST => {
519                return Err(EvaluationError {
520                    code: EvaluationErrorCode::InvalidContext,
521                    message: Some("Invalid context".to_string()),
522                });
523            }
524            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
525                return Err(EvaluationError {
526                    code: EvaluationErrorCode::General(
527                        "authentication/authorization error".to_string(),
528                    ),
529                    message: Some("authentication/authorization error".to_string()),
530                });
531            }
532            StatusCode::NOT_FOUND => {
533                return Err(EvaluationError {
534                    code: EvaluationErrorCode::FlagNotFound,
535                    message: Some(format!("Flag: {flag_key} not found")),
536                });
537            }
538            _ => {
539                let result = response.json::<serde_json::Value>().await.map_err(|e| {
540                    error!(error = %e, "Failed to parse struct response");
541                    EvaluationError {
542                        code: EvaluationErrorCode::ParseError,
543                        message: Some(e.to_string()),
544                    }
545                })?;
546
547                let value = result["value"]
548                    .clone()
549                    .into_feature_value()
550                    .as_struct()
551                    .ok_or_else(|| {
552                        error!("Invalid struct value in response");
553                        EvaluationError {
554                            code: EvaluationErrorCode::ParseError,
555                            message: Some("Invalid struct value".to_string()),
556                        }
557                    })?
558                    .clone();
559
560                debug!(variant = ?result["variant"], "Flag evaluated");
561                Ok(ResolutionDetails {
562                    value,
563                    variant: result["variant"].as_str().map(String::from),
564                    reason: Some(open_feature::EvaluationReason::Static),
565                    flag_metadata: Default::default(),
566                })
567            }
568        }
569    }
570}
571
572/// Converts an evaluation context into a JSON value for the OFREP protocol
573///
574/// # Arguments
575///
576/// * `context` - The evaluation context to convert
577///
578/// # Returns
579///
580/// A JSON representation of the context
581fn context_to_json(context: &EvaluationContext) -> serde_json::Value {
582    let mut fields = serde_json::Map::new();
583
584    if let Some(targeting_key) = &context.targeting_key {
585        fields.insert(
586            "targetingKey".to_string(),
587            serde_json::Value::String(targeting_key.clone()),
588        );
589    }
590
591    for (key, value) in &context.custom_fields {
592        let json_value = match value {
593            EvaluationContextFieldValue::String(s) => serde_json::Value::String(s.clone()),
594            EvaluationContextFieldValue::Bool(b) => serde_json::Value::Bool(*b),
595            EvaluationContextFieldValue::Int(i) => serde_json::Value::Number((*i).into()),
596            EvaluationContextFieldValue::Float(f) => {
597                if let Some(n) = serde_json::Number::from_f64(*f) {
598                    serde_json::Value::Number(n)
599                } else {
600                    serde_json::Value::Null
601                }
602            }
603            EvaluationContextFieldValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
604            EvaluationContextFieldValue::Struct(s) => serde_json::Value::String(format!("{:?}", s)),
605        };
606        fields.insert(key.clone(), json_value);
607    }
608
609    serde_json::Value::Object(fields)
610}
611
612/// Trait for converting JSON values into OpenFeature values
613trait IntoFeatureValue {
614    /// Converts a JSON value into an OpenFeature value
615    fn into_feature_value(self) -> Value;
616}
617
618impl IntoFeatureValue for serde_json::Value {
619    fn into_feature_value(self) -> Value {
620        match self {
621            serde_json::Value::Bool(b) => Value::Bool(b),
622            serde_json::Value::Number(n) => {
623                if let Some(i) = n.as_i64() {
624                    Value::Int(i)
625                } else if let Some(f) = n.as_f64() {
626                    Value::Float(f)
627                } else {
628                    Value::Int(0)
629                }
630            }
631            serde_json::Value::String(s) => Value::String(s),
632            serde_json::Value::Array(arr) => {
633                Value::Array(arr.into_iter().map(|v| v.into_feature_value()).collect())
634            }
635            serde_json::Value::Object(obj) => {
636                let mut struct_value = StructValue::default();
637                for (k, v) in obj {
638                    struct_value.add_field(k, v.into_feature_value());
639                }
640                Value::Struct(struct_value)
641            }
642            serde_json::Value::Null => Value::String("".to_string()),
643        }
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use serde_json::json;
651    use test_log::test;
652    use wiremock::matchers::{method, path};
653    use wiremock::{Mock, MockServer, ResponseTemplate};
654
655    async fn setup_mock_server() -> (MockServer, RestResolver) {
656        let mock_server = MockServer::start().await;
657        let options = FlagdOptions {
658            host: mock_server.address().ip().to_string(),
659            port: mock_server.address().port(),
660            target_uri: None,
661            ..Default::default()
662        };
663        let resolver = RestResolver::new(&options);
664        (mock_server, resolver)
665    }
666
667    #[test(tokio::test)]
668    async fn test_resolve_bool_value() {
669        let (mock_server, resolver) = setup_mock_server().await;
670
671        Mock::given(method("POST"))
672            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
673            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
674                "value": true,
675                "variant": "on",
676                "reason": "STATIC"
677            })))
678            .mount(&mock_server)
679            .await;
680
681        let context = EvaluationContext::default().with_targeting_key("test-user");
682        let result = resolver
683            .resolve_bool_value("test-flag", &context)
684            .await
685            .unwrap();
686
687        assert_eq!(result.value, true);
688        assert_eq!(result.variant, Some("on".to_string()));
689        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
690    }
691
692    #[test(tokio::test)]
693    async fn test_resolve_string_value() {
694        let (mock_server, resolver) = setup_mock_server().await;
695
696        Mock::given(method("POST"))
697            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
698            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
699                "value": "test-value",
700                "variant": "key1",
701                "reason": "STATIC"
702            })))
703            .mount(&mock_server)
704            .await;
705
706        let context = EvaluationContext::default().with_targeting_key("test-user");
707        let result = resolver
708            .resolve_string_value("test-flag", &context)
709            .await
710            .unwrap();
711
712        assert_eq!(result.value, "test-value");
713        assert_eq!(result.variant, Some("key1".to_string()));
714        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
715    }
716
717    #[test(tokio::test)]
718    async fn test_resolve_float_value() {
719        let (mock_server, resolver) = setup_mock_server().await;
720
721        Mock::given(method("POST"))
722            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
723            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
724                "value": 1.23,
725                "variant": "one",
726                "reason": "STATIC"
727            })))
728            .mount(&mock_server)
729            .await;
730
731        let context = EvaluationContext::default().with_targeting_key("test-user");
732        let result = resolver
733            .resolve_float_value("test-flag", &context)
734            .await
735            .unwrap();
736
737        assert_eq!(result.value, 1.23);
738        assert_eq!(result.variant, Some("one".to_string()));
739        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
740    }
741
742    #[test(tokio::test)]
743    async fn test_resolve_int_value() {
744        let (mock_server, resolver) = setup_mock_server().await;
745
746        Mock::given(method("POST"))
747            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
748            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
749                "value": 42,
750                "variant": "one",
751                "reason": "STATIC"
752            })))
753            .mount(&mock_server)
754            .await;
755
756        let context = EvaluationContext::default().with_targeting_key("test-user");
757        let result = resolver
758            .resolve_int_value("test-flag", &context)
759            .await
760            .unwrap();
761
762        assert_eq!(result.value, 42);
763        assert_eq!(result.variant, Some("one".to_string()));
764        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
765    }
766
767    #[test(tokio::test)]
768    async fn test_resolve_struct_value() {
769        let (mock_server, resolver) = setup_mock_server().await;
770
771        Mock::given(method("POST"))
772            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
773            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
774                "value": {
775                    "key": "val",
776                    "number": 42,
777                    "boolean": true,
778                    "nested": {
779                        "inner": "value"
780                    }
781                },
782                "variant": "object1",
783                "reason": "STATIC"
784            })))
785            .mount(&mock_server)
786            .await;
787
788        let context = EvaluationContext::default().with_targeting_key("test-user");
789        let result = resolver
790            .resolve_struct_value("test-flag", &context)
791            .await
792            .unwrap();
793
794        let value = &result.value;
795        assert_eq!(value.fields.get("key").unwrap().as_str().unwrap(), "val");
796        assert_eq!(value.fields.get("number").unwrap().as_i64().unwrap(), 42);
797        assert_eq!(
798            value.fields.get("boolean").unwrap().as_bool().unwrap(),
799            true
800        );
801
802        let nested = value.fields.get("nested").unwrap().as_struct().unwrap();
803        assert_eq!(
804            nested.fields.get("inner").unwrap().as_str().unwrap(),
805            "value"
806        );
807
808        assert_eq!(result.variant, Some("object1".to_string()));
809        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
810    }
811
812    #[test(tokio::test)]
813    async fn test_error_400() {
814        let (mock_server, resolver) = setup_mock_server().await;
815
816        Mock::given(method("POST"))
817            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
818            .respond_with(ResponseTemplate::new(400).set_body_json(json!({})))
819            .mount(&mock_server)
820            .await;
821
822        let context = EvaluationContext::default();
823        let result_bool = resolver.resolve_bool_value("test-flag", &context).await;
824        let result_int = resolver.resolve_int_value("test-flag", &context).await;
825        let result_float = resolver.resolve_float_value("test-flag", &context).await;
826        let result_string = resolver.resolve_string_value("test-flag", &context).await;
827        let result_struct = resolver.resolve_struct_value("test-flag", &context).await;
828
829        assert!(result_bool.is_err());
830        assert!(result_int.is_err());
831        assert!(result_float.is_err());
832        assert!(result_string.is_err());
833        assert!(result_struct.is_err());
834
835        assert_eq!(
836            result_bool.unwrap_err().code,
837            EvaluationErrorCode::InvalidContext
838        );
839        assert_eq!(
840            result_int.unwrap_err().code,
841            EvaluationErrorCode::InvalidContext
842        );
843        assert_eq!(
844            result_float.unwrap_err().code,
845            EvaluationErrorCode::InvalidContext
846        );
847        assert_eq!(
848            result_string.unwrap_err().code,
849            EvaluationErrorCode::InvalidContext
850        );
851        assert_eq!(
852            result_struct.unwrap_err().code,
853            EvaluationErrorCode::InvalidContext
854        );
855    }
856
857    #[test(tokio::test)]
858    async fn test_error_401() {
859        let (mock_server, resolver) = setup_mock_server().await;
860
861        Mock::given(method("POST"))
862            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
863            .respond_with(ResponseTemplate::new(401).set_body_json(json!({})))
864            .mount(&mock_server)
865            .await;
866
867        let context = EvaluationContext::default();
868
869        let result_bool = resolver.resolve_bool_value("test-flag", &context).await;
870        let result_int = resolver.resolve_int_value("test-flag", &context).await;
871        let result_float = resolver.resolve_float_value("test-flag", &context).await;
872        let result_string = resolver.resolve_string_value("test-flag", &context).await;
873        let result_struct = resolver.resolve_struct_value("test-flag", &context).await;
874
875        assert!(result_bool.is_err());
876        assert!(result_int.is_err());
877        assert!(result_float.is_err());
878        assert!(result_string.is_err());
879        assert!(result_struct.is_err());
880
881        assert_eq!(
882            result_bool.unwrap_err().code,
883            EvaluationErrorCode::General("authentication/authorization error".to_string())
884        );
885        assert_eq!(
886            result_int.unwrap_err().code,
887            EvaluationErrorCode::General("authentication/authorization error".to_string())
888        );
889        assert_eq!(
890            result_float.unwrap_err().code,
891            EvaluationErrorCode::General("authentication/authorization error".to_string())
892        );
893        assert_eq!(
894            result_string.unwrap_err().code,
895            EvaluationErrorCode::General("authentication/authorization error".to_string())
896        );
897        assert_eq!(
898            result_struct.unwrap_err().code,
899            EvaluationErrorCode::General("authentication/authorization error".to_string())
900        );
901    }
902
903    #[test(tokio::test)]
904    async fn test_error_403() {
905        let (mock_server, resolver) = setup_mock_server().await;
906
907        Mock::given(method("POST"))
908            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
909            .respond_with(ResponseTemplate::new(403).set_body_json(json!({})))
910            .mount(&mock_server)
911            .await;
912
913        let context = EvaluationContext::default();
914
915        let result_bool = resolver.resolve_bool_value("test-flag", &context).await;
916        let result_int = resolver.resolve_int_value("test-flag", &context).await;
917        let result_float = resolver.resolve_float_value("test-flag", &context).await;
918        let result_string = resolver.resolve_string_value("test-flag", &context).await;
919        let result_struct = resolver.resolve_struct_value("test-flag", &context).await;
920
921        assert!(result_bool.is_err());
922        assert!(result_int.is_err());
923        assert!(result_float.is_err());
924        assert!(result_string.is_err());
925        assert!(result_struct.is_err());
926
927        assert_eq!(
928            result_bool.unwrap_err().code,
929            EvaluationErrorCode::General("authentication/authorization error".to_string())
930        );
931        assert_eq!(
932            result_int.unwrap_err().code,
933            EvaluationErrorCode::General("authentication/authorization error".to_string())
934        );
935        assert_eq!(
936            result_float.unwrap_err().code,
937            EvaluationErrorCode::General("authentication/authorization error".to_string())
938        );
939        assert_eq!(
940            result_string.unwrap_err().code,
941            EvaluationErrorCode::General("authentication/authorization error".to_string())
942        );
943        assert_eq!(
944            result_struct.unwrap_err().code,
945            EvaluationErrorCode::General("authentication/authorization error".to_string())
946        );
947    }
948
949    #[test(tokio::test)]
950    async fn test_error_404() {
951        let (mock_server, resolver) = setup_mock_server().await;
952
953        Mock::given(method("POST"))
954            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
955            .respond_with(ResponseTemplate::new(404).set_body_json(json!({})))
956            .mount(&mock_server)
957            .await;
958
959        let context = EvaluationContext::default();
960
961        let result_bool = resolver.resolve_bool_value("test-flag", &context).await;
962        let result_int = resolver.resolve_int_value("test-flag", &context).await;
963        let result_float = resolver.resolve_float_value("test-flag", &context).await;
964        let result_string = resolver.resolve_string_value("test-flag", &context).await;
965        let result_struct = resolver.resolve_struct_value("test-flag", &context).await;
966
967        assert!(result_bool.is_err());
968        assert!(result_int.is_err());
969        assert!(result_float.is_err());
970        assert!(result_string.is_err());
971        assert!(result_struct.is_err());
972
973        let result_bool_error = result_bool.unwrap_err();
974        assert_eq!(result_bool_error.code, EvaluationErrorCode::FlagNotFound);
975        assert_eq!(
976            result_bool_error.message.unwrap(),
977            "Flag: test-flag not found"
978        );
979
980        let result_int_error = result_int.unwrap_err();
981        assert_eq!(result_int_error.code, EvaluationErrorCode::FlagNotFound);
982        assert_eq!(
983            result_int_error.message.unwrap(),
984            "Flag: test-flag not found"
985        );
986
987        let result_float_error = result_float.unwrap_err();
988        assert_eq!(result_float_error.code, EvaluationErrorCode::FlagNotFound);
989        assert_eq!(
990            result_float_error.message.unwrap(),
991            "Flag: test-flag not found"
992        );
993
994        let result_string_error = result_string.unwrap_err();
995        assert_eq!(result_string_error.code, EvaluationErrorCode::FlagNotFound);
996        assert_eq!(
997            result_string_error.message.unwrap(),
998            "Flag: test-flag not found"
999        );
1000
1001        let result_struct_error = result_struct.unwrap_err();
1002        assert_eq!(result_struct_error.code, EvaluationErrorCode::FlagNotFound);
1003        assert_eq!(
1004            result_struct_error.message.unwrap(),
1005            "Flag: test-flag not found"
1006        );
1007    }
1008}