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