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