open_feature_ofrep/
lib.rs1mod error;
57mod resolver;
58
59use error::OfrepError;
60use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
61use open_feature::{EvaluationContext, EvaluationError, StructValue};
62use reqwest::header::HeaderMap;
63use resolver::Resolver;
64use std::fmt;
65use std::sync::Arc;
66use std::time::Duration;
67use tracing::debug;
68use tracing::instrument;
69use url::Url;
70
71use async_trait::async_trait;
72
73const DEFAULT_BASE_URL: &str = "http://localhost:8016";
74const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
75
76#[derive(Debug, Clone)]
77pub struct OfrepOptions {
78 pub base_url: String,
79 pub headers: HeaderMap,
80 pub connect_timeout: Duration,
81 pub cert_path: Option<String>,
84}
85
86impl Default for OfrepOptions {
87 fn default() -> Self {
88 OfrepOptions {
89 base_url: DEFAULT_BASE_URL.to_string(),
90 headers: HeaderMap::new(),
91 connect_timeout: DEFAULT_CONNECT_TIMEOUT,
92 cert_path: None,
93 }
94 }
95}
96
97pub struct OfrepProvider {
98 provider: Arc<dyn FeatureProvider + Send + Sync>,
99}
100
101impl fmt::Debug for OfrepProvider {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.debug_struct("OfrepProvider")
104 .field("provider", &"<FeatureProvider>")
105 .finish()
106 }
107}
108
109impl OfrepProvider {
110 #[instrument(skip(options))]
111 pub async fn new(options: OfrepOptions) -> Result<Self, OfrepError> {
112 debug!("Initializing OfrepProvider with options: {:?}", options);
113
114 let url = Url::parse(&options.base_url).map_err(|e| {
115 OfrepError::Config(format!("Invalid base url: '{}' ({})", options.base_url, e))
116 })?;
117
118 if !matches!(url.scheme(), "http" | "https") {
119 return Err(OfrepError::Config(format!(
120 "Invalid base url: '{}' (unsupported scheme)",
121 url.scheme()
122 )));
123 }
124
125 Ok(Self {
126 provider: Arc::new(Resolver::new(&options)?),
127 })
128 }
129}
130
131#[async_trait]
132impl FeatureProvider for OfrepProvider {
133 fn metadata(&self) -> &ProviderMetadata {
134 self.provider.metadata()
135 }
136
137 async fn resolve_bool_value(
138 &self,
139 flag_key: &str,
140 context: &EvaluationContext,
141 ) -> Result<ResolutionDetails<bool>, EvaluationError> {
142 self.provider.resolve_bool_value(flag_key, context).await
143 }
144
145 async fn resolve_int_value(
146 &self,
147 flag_key: &str,
148 context: &EvaluationContext,
149 ) -> Result<ResolutionDetails<i64>, EvaluationError> {
150 self.provider.resolve_int_value(flag_key, context).await
151 }
152
153 async fn resolve_float_value(
154 &self,
155 flag_key: &str,
156 context: &EvaluationContext,
157 ) -> Result<ResolutionDetails<f64>, EvaluationError> {
158 self.provider.resolve_float_value(flag_key, context).await
159 }
160
161 async fn resolve_string_value(
162 &self,
163 flag_key: &str,
164 context: &EvaluationContext,
165 ) -> Result<ResolutionDetails<String>, EvaluationError> {
166 self.provider.resolve_string_value(flag_key, context).await
167 }
168
169 async fn resolve_struct_value(
170 &self,
171 flag_key: &str,
172 context: &EvaluationContext,
173 ) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
174 self.provider.resolve_struct_value(flag_key, context).await
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use test_log::test;
182
183 #[test(tokio::test)]
184 async fn test_ofrep_options_validation() {
185 let provider_with_empty_host = OfrepProvider::new(OfrepOptions {
186 base_url: "http://".to_string(),
187 ..Default::default()
188 })
189 .await;
190
191 let provider_with_invalid_scheme = OfrepProvider::new(OfrepOptions {
192 base_url: "invalid://".to_string(),
193 ..Default::default()
194 })
195 .await;
196
197 assert!(provider_with_empty_host.is_err());
198 assert!(provider_with_invalid_scheme.is_err());
199
200 assert_eq!(
201 provider_with_empty_host.unwrap_err(),
202 OfrepError::Config("Invalid base url: 'http://' (empty host)".to_string())
203 );
204 assert_eq!(
205 provider_with_invalid_scheme.unwrap_err(),
206 OfrepError::Config("Invalid base url: 'invalid' (unsupported scheme)".to_string())
207 );
208 }
209
210 #[test(tokio::test)]
211 async fn test_ofrep_provider_with_https_missing_cert_fails() {
212 let provider = OfrepProvider::new(OfrepOptions {
213 base_url: "https://localhost:8016".to_string(),
214 cert_path: Some("/nonexistent/path/to/cert.pem".to_string()),
215 ..Default::default()
216 })
217 .await;
218
219 assert_eq!(
220 provider.unwrap_err(),
221 OfrepError::Config(
222 "Failed to read certificate file '/nonexistent/path/to/cert.pem': No such file or directory (os error 2)"
223 .to_string()
224 )
225 );
226 }
227}