open_feature_ofrep/
lib.rs1mod 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}