open_feature_ofrep/
lib.rs

1//! [Generated by cargo-readme: `cargo readme --no-title --no-license > README.md`]::
2//!  # OFREP Provider for OpenFeature
3//!
4//! A Rust implementation of the OpenFeature OFREP provider, enabling dynamic
5//! feature flag evaluation in your applications.
6//!
7//! This provider allows to connect to any feature flag management system that supports OFREP.
8//!
9//! ### Installation
10//! Add the dependency in your `Cargo.toml`:
11//! ```bash
12//! cargo add open-feature-ofrep
13//! cargo add open-feature
14//! ```
15//! Then integrate it into your application:
16//!
17//! ```rust,no_run
18//! use std::time::Duration;
19//! use open_feature::provider::FeatureProvider;
20//! use open_feature::EvaluationContext;
21//! use open_feature_ofrep::{OfrepProvider, OfrepOptions};
22//! use reqwest::header::{HeaderMap, HeaderValue};
23//!
24//! #[tokio::main]
25//! async fn main() {
26//!     let mut headers = HeaderMap::new();
27//!     headers.insert("color", HeaderValue::from_static("yellow"));
28//!
29//!     let provider = OfrepProvider::new(OfrepOptions {
30//!         base_url: "http://localhost:8016".to_string(),
31//!         headers: headers.clone(),
32//!         connect_timeout: Duration::from_secs(4),
33//!         ..Default::default()
34//!     }).await.unwrap();
35//!
36//!     let context = EvaluationContext::default()
37//!                     .with_targeting_key("user-123")
38//!                     .with_custom_field("color", "yellow");
39//!
40//!     let result = provider.resolve_bool_value("isColorYellow", &context).await.unwrap();
41//!     println!("Flag value: {}", result.value);
42//! }
43//! ```
44//!
45//! ### Configuration Options
46//! Configurations can be provided as constructor options. The following options are supported:
47//!
48//! | Option                                  | Type / Supported Value            | Default                             |
49//! |-----------------------------------------|-----------------------------------|-------------------------------------|
50//! | base_url                                | string                            | http://localhost:8016               |
51//! | headers                                 | HeaderMap                         | Empty Map                           |
52//! | connect_timeout                         | Duration                          | 10 seconds                          |
53//!
54//! ### License
55//! Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.
56
57mod error;
58mod resolver;
59
60use error::OfrepError;
61use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
62use open_feature::{EvaluationContext, EvaluationError, StructValue};
63use reqwest::header::HeaderMap;
64use resolver::Resolver;
65use std::fmt;
66use std::sync::Arc;
67use std::time::Duration;
68use tracing::debug;
69use tracing::instrument;
70use url::Url;
71
72use async_trait::async_trait;
73
74const DEFAULT_BASE_URL: &str = "http://localhost:8016";
75const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
76
77#[derive(Debug, Clone)]
78pub struct OfrepOptions {
79    pub base_url: String,
80    pub headers: HeaderMap,
81    pub connect_timeout: Duration,
82}
83
84impl Default for OfrepOptions {
85    fn default() -> Self {
86        OfrepOptions {
87            base_url: DEFAULT_BASE_URL.to_string(),
88            headers: HeaderMap::new(),
89            connect_timeout: DEFAULT_CONNECT_TIMEOUT,
90        }
91    }
92}
93
94pub struct OfrepProvider {
95    provider: Arc<dyn FeatureProvider + Send + Sync>,
96}
97
98impl fmt::Debug for OfrepProvider {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.debug_struct("OfrepProvider")
101            .field("provider", &"<FeatureProvider>")
102            .finish()
103    }
104}
105
106impl OfrepProvider {
107    #[instrument(skip(options))]
108    pub async fn new(options: OfrepOptions) -> Result<Self, OfrepError> {
109        debug!("Initializing OfrepProvider with options: {:?}", options);
110
111        let url = Url::parse(&options.base_url).map_err(|e| {
112            OfrepError::Config(format!("Invalid base url: '{}' ({})", options.base_url, e))
113        })?;
114
115        if !matches!(url.scheme(), "http" | "https") {
116            return Err(OfrepError::Config(format!(
117                "Invalid base url: '{}' (unsupported scheme)",
118                url.scheme()
119            )));
120        }
121
122        Ok(Self {
123            provider: Arc::new(Resolver::new(&options)),
124        })
125    }
126}
127
128#[async_trait]
129impl FeatureProvider for OfrepProvider {
130    fn metadata(&self) -> &ProviderMetadata {
131        self.provider.metadata()
132    }
133
134    async fn resolve_bool_value(
135        &self,
136        flag_key: &str,
137        context: &EvaluationContext,
138    ) -> Result<ResolutionDetails<bool>, EvaluationError> {
139        self.provider.resolve_bool_value(flag_key, context).await
140    }
141
142    async fn resolve_int_value(
143        &self,
144        flag_key: &str,
145        context: &EvaluationContext,
146    ) -> Result<ResolutionDetails<i64>, EvaluationError> {
147        self.provider.resolve_int_value(flag_key, context).await
148    }
149
150    async fn resolve_float_value(
151        &self,
152        flag_key: &str,
153        context: &EvaluationContext,
154    ) -> Result<ResolutionDetails<f64>, EvaluationError> {
155        self.provider.resolve_float_value(flag_key, context).await
156    }
157
158    async fn resolve_string_value(
159        &self,
160        flag_key: &str,
161        context: &EvaluationContext,
162    ) -> Result<ResolutionDetails<String>, EvaluationError> {
163        self.provider.resolve_string_value(flag_key, context).await
164    }
165
166    async fn resolve_struct_value(
167        &self,
168        flag_key: &str,
169        context: &EvaluationContext,
170    ) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
171        self.provider.resolve_struct_value(flag_key, context).await
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use test_log::test;
179
180    #[test(tokio::test)]
181    async fn test_ofrep_options_validation() {
182        let provider_with_empty_host = OfrepProvider::new(OfrepOptions {
183            base_url: "http://".to_string(),
184            ..Default::default()
185        })
186        .await;
187
188        let provider_with_invalid_scheme = OfrepProvider::new(OfrepOptions {
189            base_url: "invalid://".to_string(),
190            ..Default::default()
191        })
192        .await;
193
194        assert!(provider_with_empty_host.is_err());
195        assert!(provider_with_invalid_scheme.is_err());
196
197        assert_eq!(
198            provider_with_empty_host.unwrap_err(),
199            OfrepError::Config("Invalid base url: 'http://' (empty host)".to_string())
200        );
201        assert_eq!(
202            provider_with_invalid_scheme.unwrap_err(),
203            OfrepError::Config("Invalid base url: 'invalid' (unsupported scheme)".to_string())
204        );
205    }
206}