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