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 serde_json;
53use tracing::{debug, error, instrument};
54
55use crate::FlagdOptions;
56
57/// REST-based resolver implementing the OpenFeature Remote Evaluation Protocol
58#[derive(Debug)]
59pub struct RestResolver {
60    /// Base endpoint URL for the OFREP service
61    endpoint: String,
62    /// Provider metadata
63    metadata: ProviderMetadata,
64    /// HTTP client for making requests
65    client: Client,
66}
67
68impl RestResolver {
69    /// Creates a new REST resolver with the specified host and port
70    ///
71    /// # Arguments
72    ///
73    /// * `target` - The host and port of the OFREP service, in the format `host:port`
74    ///
75    /// # Returns
76    ///
77    /// A new instance of RestResolver configured to connect to the specified endpoint
78    pub fn new(options: &FlagdOptions) -> Self {
79        let endpoint = if let Some(uri) = &options.target_uri {
80            format!("http://{}", uri)
81        } else {
82            format!("http://{}:{}", options.host, options.port)
83        };
84        Self {
85            endpoint,
86            metadata: ProviderMetadata::new("flagd-rest-provider"),
87            client: Client::new(),
88        }
89    }
90}
91
92#[async_trait]
93impl FeatureProvider for RestResolver {
94    fn metadata(&self) -> &ProviderMetadata {
95        &self.metadata
96    }
97
98    /// Resolves a boolean flag value
99    ///
100    /// # Arguments
101    ///
102    /// * `flag_key` - The unique identifier of the flag
103    /// * `evaluation_context` - Context data for flag evaluation
104    ///
105    /// # Returns
106    ///
107    /// The resolved boolean value with metadata, or an error if evaluation fails
108    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
109    async fn resolve_bool_value(
110        &self,
111        flag_key: &str,
112        evaluation_context: &EvaluationContext,
113    ) -> EvaluationResult<ResolutionDetails<bool>> {
114        debug!("Resolving boolean flag");
115
116        let payload = serde_json::json!({
117            "context": context_to_json(evaluation_context)
118        });
119
120        let response = self
121            .client
122            .post(format!(
123                "{}/ofrep/v1/evaluate/flags/{}",
124                self.endpoint, flag_key
125            ))
126            .header("Content-Type", "application/json")
127            .json(&payload)
128            .send()
129            .await
130            .map_err(|e| {
131                error!(error = %e, "Failed to resolve boolean value");
132                EvaluationError {
133                    code: EvaluationErrorCode::General(
134                        "Failed to resolve boolean value".to_string(),
135                    ),
136                    message: Some(e.to_string()),
137                }
138            })?;
139
140        debug!(status = response.status().as_u16(), "Received response");
141
142        let result = response.json::<serde_json::Value>().await.map_err(|e| {
143            error!(error = %e, "Failed to parse boolean response");
144            EvaluationError {
145                code: EvaluationErrorCode::ParseError,
146                message: Some(e.to_string()),
147            }
148        })?;
149
150        let value = result["value"].as_bool().ok_or_else(|| {
151            error!("Invalid boolean value in response");
152            EvaluationError {
153                code: EvaluationErrorCode::ParseError,
154                message: Some("Invalid boolean value".to_string()),
155            }
156        })?;
157
158        debug!(value = value, variant = ?result["variant"], "Flag evaluated");
159        Ok(ResolutionDetails {
160            value,
161            variant: result["variant"].as_str().map(String::from),
162            reason: Some(open_feature::EvaluationReason::Static),
163            flag_metadata: Default::default(),
164        })
165    }
166
167    /// Resolves a string flag value
168    ///
169    /// # Arguments
170    ///
171    /// * `flag_key` - The unique identifier of the flag
172    /// * `evaluation_context` - Context data for flag evaluation
173    ///
174    /// # Returns
175    ///
176    /// The resolved string value with metadata, or an error if evaluation fails
177    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
178    async fn resolve_string_value(
179        &self,
180        flag_key: &str,
181        evaluation_context: &EvaluationContext,
182    ) -> EvaluationResult<ResolutionDetails<String>> {
183        debug!("Resolving string flag");
184
185        let payload = serde_json::json!({
186            "context": context_to_json(evaluation_context)
187        });
188
189        let response = self
190            .client
191            .post(format!(
192                "{}/ofrep/v1/evaluate/flags/{}",
193                self.endpoint, flag_key
194            ))
195            .header("Content-Type", "application/json")
196            .json(&payload)
197            .send()
198            .await
199            .map_err(|e| {
200                error!(error = %e, "Failed to resolve string value");
201                EvaluationError {
202                    code: EvaluationErrorCode::General(
203                        "Failed to resolve string value".to_string(),
204                    ),
205                    message: Some(e.to_string()),
206                }
207            })?;
208
209        debug!(status = response.status().as_u16(), "Received response");
210
211        let result = response.json::<serde_json::Value>().await.map_err(|e| {
212            error!(error = %e, "Failed to parse string response");
213            EvaluationError {
214                code: EvaluationErrorCode::ParseError,
215                message: Some(e.to_string()),
216            }
217        })?;
218
219        let value = result["value"]
220            .as_str()
221            .ok_or_else(|| {
222                error!("Invalid string value in response");
223                EvaluationError {
224                    code: EvaluationErrorCode::ParseError,
225                    message: Some("Invalid string value".to_string()),
226                }
227            })?
228            .to_string();
229
230        debug!(value = %value, variant = ?result["variant"], "Flag evaluated");
231        Ok(ResolutionDetails {
232            value,
233            variant: result["variant"].as_str().map(String::from),
234            reason: Some(open_feature::EvaluationReason::Static),
235            flag_metadata: Default::default(),
236        })
237    }
238
239    /// Resolves a float flag value
240    ///
241    /// # Arguments
242    ///
243    /// * `flag_key` - The unique identifier of the flag
244    /// * `evaluation_context` - Context data for flag evaluation
245    ///
246    /// # Returns
247    ///
248    /// The resolved float value with metadata, or an error if evaluation fails
249    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
250    async fn resolve_float_value(
251        &self,
252        flag_key: &str,
253        evaluation_context: &EvaluationContext,
254    ) -> EvaluationResult<ResolutionDetails<f64>> {
255        debug!("Resolving float flag");
256
257        let payload = serde_json::json!({
258            "context": context_to_json(evaluation_context)
259        });
260
261        let response = self
262            .client
263            .post(format!(
264                "{}/ofrep/v1/evaluate/flags/{}",
265                self.endpoint, flag_key
266            ))
267            .header("Content-Type", "application/json")
268            .json(&payload)
269            .send()
270            .await
271            .map_err(|e| {
272                error!(error = %e, "Failed to resolve float value");
273                EvaluationError {
274                    code: EvaluationErrorCode::General("Failed to resolve float value".to_string()),
275                    message: Some(e.to_string()),
276                }
277            })?;
278
279        debug!(status = response.status().as_u16(), "Received response");
280
281        let result = response.json::<serde_json::Value>().await.map_err(|e| {
282            error!(error = %e, "Failed to parse float response");
283            EvaluationError {
284                code: EvaluationErrorCode::ParseError,
285                message: Some(e.to_string()),
286            }
287        })?;
288
289        let value = result["value"].as_f64().ok_or_else(|| {
290            error!("Invalid float value in response");
291            EvaluationError {
292                code: EvaluationErrorCode::ParseError,
293                message: Some("Invalid float value".to_string()),
294            }
295        })?;
296
297        debug!(value = value, variant = ?result["variant"], "Flag evaluated");
298        Ok(ResolutionDetails {
299            value,
300            variant: result["variant"].as_str().map(String::from),
301            reason: Some(open_feature::EvaluationReason::Static),
302            flag_metadata: Default::default(),
303        })
304    }
305
306    /// Resolves an integer flag value
307    ///
308    /// # Arguments
309    ///
310    /// * `flag_key` - The unique identifier of the flag
311    /// * `evaluation_context` - Context data for flag evaluation
312    ///
313    /// # Returns
314    ///
315    /// The resolved integer value with metadata, or an error if evaluation fails
316    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
317    async fn resolve_int_value(
318        &self,
319        flag_key: &str,
320        evaluation_context: &EvaluationContext,
321    ) -> EvaluationResult<ResolutionDetails<i64>> {
322        debug!("Resolving integer flag");
323
324        let payload = serde_json::json!({
325            "context": context_to_json(evaluation_context)
326        });
327
328        let response = self
329            .client
330            .post(format!(
331                "{}/ofrep/v1/evaluate/flags/{}",
332                self.endpoint, flag_key
333            ))
334            .header("Content-Type", "application/json")
335            .json(&payload)
336            .send()
337            .await
338            .map_err(|e| {
339                error!(error = %e, "Failed to resolve integer value");
340                EvaluationError {
341                    code: EvaluationErrorCode::General(
342                        "Failed to resolve integer value".to_string(),
343                    ),
344                    message: Some(e.to_string()),
345                }
346            })?;
347
348        debug!(status = response.status().as_u16(), "Received response");
349
350        let result = response.json::<serde_json::Value>().await.map_err(|e| {
351            error!(error = %e, "Failed to parse integer response");
352            EvaluationError {
353                code: EvaluationErrorCode::ParseError,
354                message: Some(e.to_string()),
355            }
356        })?;
357
358        let value = result["value"].as_i64().ok_or_else(|| {
359            error!("Invalid integer value in response");
360            EvaluationError {
361                code: EvaluationErrorCode::ParseError,
362                message: Some("Invalid integer value".to_string()),
363            }
364        })?;
365
366        debug!(value = value, variant = ?result["variant"], "Flag evaluated");
367        Ok(ResolutionDetails {
368            value,
369            variant: result["variant"].as_str().map(String::from),
370            reason: Some(open_feature::EvaluationReason::Static),
371            flag_metadata: Default::default(),
372        })
373    }
374
375    /// Resolves a structured flag value
376    ///
377    /// # Arguments
378    ///
379    /// * `flag_key` - The unique identifier of the flag
380    /// * `evaluation_context` - Context data for flag evaluation
381    ///
382    /// # Returns
383    ///
384    /// The resolved structured value with metadata, or an error if evaluation fails.
385    /// The structured value can contain nested objects, arrays, and primitive types.
386    #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
387    async fn resolve_struct_value(
388        &self,
389        flag_key: &str,
390        evaluation_context: &EvaluationContext,
391    ) -> EvaluationResult<ResolutionDetails<StructValue>> {
392        debug!("Resolving struct flag");
393
394        let payload = serde_json::json!({
395            "context": context_to_json(evaluation_context)
396        });
397
398        let response = self
399            .client
400            .post(format!(
401                "{}/ofrep/v1/evaluate/flags/{}",
402                self.endpoint, flag_key
403            ))
404            .header("Content-Type", "application/json")
405            .json(&payload)
406            .send()
407            .await
408            .map_err(|e| {
409                error!(error = %e, "Failed to resolve struct value");
410                EvaluationError {
411                    code: EvaluationErrorCode::General(
412                        "Failed to resolve struct value".to_string(),
413                    ),
414                    message: Some(e.to_string()),
415                }
416            })?;
417
418        debug!(status = response.status().as_u16(), "Received response");
419
420        let result = response.json::<serde_json::Value>().await.map_err(|e| {
421            error!(error = %e, "Failed to parse struct response");
422            EvaluationError {
423                code: EvaluationErrorCode::ParseError,
424                message: Some(e.to_string()),
425            }
426        })?;
427
428        let value = result["value"]
429            .clone()
430            .into_feature_value()
431            .as_struct()
432            .ok_or_else(|| {
433                error!("Invalid struct value in response");
434                EvaluationError {
435                    code: EvaluationErrorCode::ParseError,
436                    message: Some("Invalid struct value".to_string()),
437                }
438            })?
439            .clone();
440
441        debug!(variant = ?result["variant"], "Flag evaluated");
442        Ok(ResolutionDetails {
443            value,
444            variant: result["variant"].as_str().map(String::from),
445            reason: Some(open_feature::EvaluationReason::Static),
446            flag_metadata: Default::default(),
447        })
448    }
449}
450
451/// Converts an evaluation context into a JSON value for the OFREP protocol
452///
453/// # Arguments
454///
455/// * `context` - The evaluation context to convert
456///
457/// # Returns
458///
459/// A JSON representation of the context
460fn context_to_json(context: &EvaluationContext) -> serde_json::Value {
461    let mut fields = serde_json::Map::new();
462
463    if let Some(targeting_key) = &context.targeting_key {
464        fields.insert(
465            "targetingKey".to_string(),
466            serde_json::Value::String(targeting_key.clone()),
467        );
468    }
469
470    for (key, value) in &context.custom_fields {
471        let json_value = match value {
472            EvaluationContextFieldValue::String(s) => serde_json::Value::String(s.clone()),
473            EvaluationContextFieldValue::Bool(b) => serde_json::Value::Bool(*b),
474            EvaluationContextFieldValue::Int(i) => serde_json::Value::Number((*i).into()),
475            EvaluationContextFieldValue::Float(f) => {
476                if let Some(n) = serde_json::Number::from_f64(*f) {
477                    serde_json::Value::Number(n)
478                } else {
479                    serde_json::Value::Null
480                }
481            }
482            EvaluationContextFieldValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
483            EvaluationContextFieldValue::Struct(s) => serde_json::Value::String(format!("{:?}", s)),
484        };
485        fields.insert(key.clone(), json_value);
486    }
487
488    serde_json::Value::Object(fields)
489}
490
491/// Trait for converting JSON values into OpenFeature values
492trait IntoFeatureValue {
493    /// Converts a JSON value into an OpenFeature value
494    fn into_feature_value(self) -> Value;
495}
496
497impl IntoFeatureValue for serde_json::Value {
498    fn into_feature_value(self) -> Value {
499        match self {
500            serde_json::Value::Bool(b) => Value::Bool(b),
501            serde_json::Value::Number(n) => {
502                if let Some(i) = n.as_i64() {
503                    Value::Int(i)
504                } else if let Some(f) = n.as_f64() {
505                    Value::Float(f)
506                } else {
507                    Value::Int(0)
508                }
509            }
510            serde_json::Value::String(s) => Value::String(s),
511            serde_json::Value::Array(arr) => {
512                Value::Array(arr.into_iter().map(|v| v.into_feature_value()).collect())
513            }
514            serde_json::Value::Object(obj) => {
515                let mut struct_value = StructValue::default();
516                for (k, v) in obj {
517                    struct_value.add_field(k, v.into_feature_value());
518                }
519                Value::Struct(struct_value)
520            }
521            serde_json::Value::Null => Value::String("".to_string()),
522        }
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use serde_json::json;
530    use test_log::test;
531    use wiremock::matchers::{method, path};
532    use wiremock::{Mock, MockServer, ResponseTemplate};
533
534    async fn setup_mock_server() -> (MockServer, RestResolver) {
535        let mock_server = MockServer::start().await;
536        let options = FlagdOptions {
537            host: mock_server.address().ip().to_string(),
538            port: mock_server.address().port(),
539            target_uri: None,
540            ..Default::default()
541        };
542        let resolver = RestResolver::new(&options);
543        (mock_server, resolver)
544    }
545
546    #[test(tokio::test)]
547    async fn test_resolve_bool_value() {
548        let (mock_server, resolver) = setup_mock_server().await;
549
550        Mock::given(method("POST"))
551            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
552            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
553                "value": true,
554                "variant": "on",
555                "reason": "STATIC"
556            })))
557            .mount(&mock_server)
558            .await;
559
560        let context = EvaluationContext::default().with_targeting_key("test-user");
561        let result = resolver
562            .resolve_bool_value("test-flag", &context)
563            .await
564            .unwrap();
565
566        assert_eq!(result.value, true);
567        assert_eq!(result.variant, Some("on".to_string()));
568        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
569    }
570
571    #[test(tokio::test)]
572    async fn test_resolve_string_value() {
573        let (mock_server, resolver) = setup_mock_server().await;
574
575        Mock::given(method("POST"))
576            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
577            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
578                "value": "test-value",
579                "variant": "key1",
580                "reason": "STATIC"
581            })))
582            .mount(&mock_server)
583            .await;
584
585        let context = EvaluationContext::default().with_targeting_key("test-user");
586        let result = resolver
587            .resolve_string_value("test-flag", &context)
588            .await
589            .unwrap();
590
591        assert_eq!(result.value, "test-value");
592        assert_eq!(result.variant, Some("key1".to_string()));
593        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
594    }
595
596    #[test(tokio::test)]
597    async fn test_resolve_float_value() {
598        let (mock_server, resolver) = setup_mock_server().await;
599
600        Mock::given(method("POST"))
601            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
602            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
603                "value": 1.23,
604                "variant": "one",
605                "reason": "STATIC"
606            })))
607            .mount(&mock_server)
608            .await;
609
610        let context = EvaluationContext::default().with_targeting_key("test-user");
611        let result = resolver
612            .resolve_float_value("test-flag", &context)
613            .await
614            .unwrap();
615
616        assert_eq!(result.value, 1.23);
617        assert_eq!(result.variant, Some("one".to_string()));
618        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
619    }
620
621    #[test(tokio::test)]
622    async fn test_resolve_int_value() {
623        let (mock_server, resolver) = setup_mock_server().await;
624
625        Mock::given(method("POST"))
626            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
627            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
628                "value": 42,
629                "variant": "one",
630                "reason": "STATIC"
631            })))
632            .mount(&mock_server)
633            .await;
634
635        let context = EvaluationContext::default().with_targeting_key("test-user");
636        let result = resolver
637            .resolve_int_value("test-flag", &context)
638            .await
639            .unwrap();
640
641        assert_eq!(result.value, 42);
642        assert_eq!(result.variant, Some("one".to_string()));
643        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
644    }
645
646    #[test(tokio::test)]
647    async fn test_resolve_struct_value() {
648        let (mock_server, resolver) = setup_mock_server().await;
649
650        Mock::given(method("POST"))
651            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
652            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
653                "value": {
654                    "key": "val",
655                    "number": 42,
656                    "boolean": true,
657                    "nested": {
658                        "inner": "value"
659                    }
660                },
661                "variant": "object1",
662                "reason": "STATIC"
663            })))
664            .mount(&mock_server)
665            .await;
666
667        let context = EvaluationContext::default().with_targeting_key("test-user");
668        let result = resolver
669            .resolve_struct_value("test-flag", &context)
670            .await
671            .unwrap();
672
673        let value = &result.value;
674        assert_eq!(value.fields.get("key").unwrap().as_str().unwrap(), "val");
675        assert_eq!(value.fields.get("number").unwrap().as_i64().unwrap(), 42);
676        assert_eq!(
677            value.fields.get("boolean").unwrap().as_bool().unwrap(),
678            true
679        );
680
681        let nested = value.fields.get("nested").unwrap().as_struct().unwrap();
682        assert_eq!(
683            nested.fields.get("inner").unwrap().as_str().unwrap(),
684            "value"
685        );
686
687        assert_eq!(result.variant, Some("object1".to_string()));
688        assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
689    }
690
691    #[test(tokio::test)]
692    async fn test_error_handling() {
693        let (mock_server, resolver) = setup_mock_server().await;
694
695        Mock::given(method("POST"))
696            .and(path("/ofrep/v1/evaluate/flags/test-flag"))
697            .respond_with(ResponseTemplate::new(404).set_body_json(json!({
698                "errorCode": "FLAG_NOT_FOUND",
699                "errorDetails": "Flag not found"
700            })))
701            .mount(&mock_server)
702            .await;
703
704        let context = EvaluationContext::default();
705        let result = resolver.resolve_bool_value("test-flag", &context).await;
706        assert!(result.is_err());
707    }
708}