open_feature_flagd/resolver/
rest.rs1use async_trait::async_trait;
30use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
31use open_feature::{EvaluationContext, EvaluationError, StructValue};
32use open_feature_ofrep::{OfrepOptions, OfrepProvider};
33use std::time::Duration;
34use tracing::instrument;
35
36use crate::FlagdOptions;
37use crate::error::FlagdError;
38
39#[derive(Debug)]
42pub struct RestResolver {
43 provider: OfrepProvider,
44}
45
46impl RestResolver {
47 pub async fn new(options: &FlagdOptions) -> Result<Self, FlagdError> {
57 let scheme = if options.tls { "https" } else { "http" };
58 let base_url = if let Some(uri) = &options.target_uri {
59 if uri.starts_with("http://") || uri.starts_with("https://") {
61 uri.clone()
62 } else {
63 format!("{}://{}", scheme, uri)
64 }
65 } else {
66 format!("{}://{}:{}", scheme, options.host, options.port)
67 };
68
69 let connect_timeout = Duration::from_millis(options.deadline_ms as u64);
70
71 let ofrep_options = OfrepOptions {
72 base_url,
73 headers: Default::default(),
74 connect_timeout,
75 cert_path: options.cert_path.clone(),
76 };
77
78 OfrepProvider::new(ofrep_options)
79 .await
80 .map(|provider| Self { provider })
81 .map_err(|e| FlagdError::Config(e.to_string()))
82 }
83}
84
85#[async_trait]
86impl FeatureProvider for RestResolver {
87 fn metadata(&self) -> &ProviderMetadata {
88 self.provider.metadata()
89 }
90
91 #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
93 async fn resolve_bool_value(
94 &self,
95 flag_key: &str,
96 evaluation_context: &EvaluationContext,
97 ) -> Result<ResolutionDetails<bool>, EvaluationError> {
98 self.provider
99 .resolve_bool_value(flag_key, evaluation_context)
100 .await
101 }
102
103 #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
105 async fn resolve_string_value(
106 &self,
107 flag_key: &str,
108 evaluation_context: &EvaluationContext,
109 ) -> Result<ResolutionDetails<String>, EvaluationError> {
110 self.provider
111 .resolve_string_value(flag_key, evaluation_context)
112 .await
113 }
114
115 #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
117 async fn resolve_float_value(
118 &self,
119 flag_key: &str,
120 evaluation_context: &EvaluationContext,
121 ) -> Result<ResolutionDetails<f64>, EvaluationError> {
122 self.provider
123 .resolve_float_value(flag_key, evaluation_context)
124 .await
125 }
126
127 #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
129 async fn resolve_int_value(
130 &self,
131 flag_key: &str,
132 evaluation_context: &EvaluationContext,
133 ) -> Result<ResolutionDetails<i64>, EvaluationError> {
134 self.provider
135 .resolve_int_value(flag_key, evaluation_context)
136 .await
137 }
138
139 #[instrument(skip(self, evaluation_context), fields(flag_key = %flag_key))]
141 async fn resolve_struct_value(
142 &self,
143 flag_key: &str,
144 evaluation_context: &EvaluationContext,
145 ) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
146 self.provider
147 .resolve_struct_value(flag_key, evaluation_context)
148 .await
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use open_feature::EvaluationContext;
156 use serde_json::json;
157 use test_log::test;
158 use wiremock::matchers::{method, path};
159 use wiremock::{Mock, MockServer, ResponseTemplate};
160
161 async fn setup_mock_server() -> (MockServer, RestResolver) {
162 let mock_server = MockServer::start().await;
163 let options = FlagdOptions {
164 host: mock_server.address().ip().to_string(),
165 port: mock_server.address().port(),
166 target_uri: None,
167 deadline_ms: 5000,
168 ..Default::default()
169 };
170 let resolver = RestResolver::new(&options)
171 .await
172 .expect("Failed to create RestResolver");
173 (mock_server, resolver)
174 }
175
176 #[test(tokio::test)]
177 async fn test_resolve_bool_value() {
178 let (mock_server, resolver) = setup_mock_server().await;
179
180 Mock::given(method("POST"))
181 .and(path("/ofrep/v1/evaluate/flags/test-flag"))
182 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
183 "value": true,
184 "variant": "on",
185 "reason": "STATIC"
186 })))
187 .mount(&mock_server)
188 .await;
189
190 let context = EvaluationContext::default().with_targeting_key("test-user");
191 let result = resolver
192 .resolve_bool_value("test-flag", &context)
193 .await
194 .unwrap();
195
196 assert_eq!(result.value, true);
197 assert_eq!(result.variant, Some("on".to_string()));
198 assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
199 }
200
201 #[test(tokio::test)]
202 async fn test_resolve_string_value() {
203 let (mock_server, resolver) = setup_mock_server().await;
204
205 Mock::given(method("POST"))
206 .and(path("/ofrep/v1/evaluate/flags/test-flag"))
207 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
208 "value": "test-value",
209 "variant": "key1",
210 "reason": "STATIC"
211 })))
212 .mount(&mock_server)
213 .await;
214
215 let context = EvaluationContext::default().with_targeting_key("test-user");
216 let result = resolver
217 .resolve_string_value("test-flag", &context)
218 .await
219 .unwrap();
220
221 assert_eq!(result.value, "test-value");
222 assert_eq!(result.variant, Some("key1".to_string()));
223 assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
224 }
225
226 #[test(tokio::test)]
227 async fn test_resolve_float_value() {
228 let (mock_server, resolver) = setup_mock_server().await;
229
230 Mock::given(method("POST"))
231 .and(path("/ofrep/v1/evaluate/flags/test-flag"))
232 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
233 "value": 1.23,
234 "variant": "one",
235 "reason": "STATIC"
236 })))
237 .mount(&mock_server)
238 .await;
239
240 let context = EvaluationContext::default().with_targeting_key("test-user");
241 let result = resolver
242 .resolve_float_value("test-flag", &context)
243 .await
244 .unwrap();
245
246 assert_eq!(result.value, 1.23);
247 assert_eq!(result.variant, Some("one".to_string()));
248 assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
249 }
250
251 #[test(tokio::test)]
252 async fn test_resolve_int_value() {
253 let (mock_server, resolver) = setup_mock_server().await;
254
255 Mock::given(method("POST"))
256 .and(path("/ofrep/v1/evaluate/flags/test-flag"))
257 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
258 "value": 42,
259 "variant": "one",
260 "reason": "STATIC"
261 })))
262 .mount(&mock_server)
263 .await;
264
265 let context = EvaluationContext::default().with_targeting_key("test-user");
266 let result = resolver
267 .resolve_int_value("test-flag", &context)
268 .await
269 .unwrap();
270
271 assert_eq!(result.value, 42);
272 assert_eq!(result.variant, Some("one".to_string()));
273 assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
274 }
275
276 #[test(tokio::test)]
277 async fn test_resolve_struct_value() {
278 let (mock_server, resolver) = setup_mock_server().await;
279
280 Mock::given(method("POST"))
281 .and(path("/ofrep/v1/evaluate/flags/test-flag"))
282 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
283 "value": {
284 "key": "val",
285 "number": 42,
286 "boolean": true,
287 "nested": {
288 "inner": "value"
289 }
290 },
291 "variant": "object1",
292 "reason": "STATIC"
293 })))
294 .mount(&mock_server)
295 .await;
296
297 let context = EvaluationContext::default().with_targeting_key("test-user");
298 let result = resolver
299 .resolve_struct_value("test-flag", &context)
300 .await
301 .unwrap();
302
303 let value = &result.value;
304 assert_eq!(value.fields.get("key").unwrap().as_str().unwrap(), "val");
305 assert_eq!(value.fields.get("number").unwrap().as_i64().unwrap(), 42);
306 assert_eq!(
307 value.fields.get("boolean").unwrap().as_bool().unwrap(),
308 true
309 );
310
311 let nested = value.fields.get("nested").unwrap().as_struct().unwrap();
312 assert_eq!(
313 nested.fields.get("inner").unwrap().as_str().unwrap(),
314 "value"
315 );
316
317 assert_eq!(result.variant, Some("object1".to_string()));
318 assert_eq!(result.reason, Some(open_feature::EvaluationReason::Static));
319 }
320}