open_feature_flagd/lib.rs
1//! [Generated by cargo-readme: `cargo readme --no-title --no-license > README.md`]::
2//! # flagd Provider for OpenFeature
3//!
4//! A Rust implementation of the OpenFeature provider for flagd, enabling dynamic
5//! feature flag evaluation in your applications.
6//!
7//! This provider supports multiple evaluation modes, advanced targeting rules, caching strategies,
8//! and connection management. It is designed to work seamlessly with the OpenFeature SDK and the flagd service.
9//!
10//! ## Core Features
11//!
12//! - **Multiple Evaluation Modes**
13//! - **RPC Resolver (Remote Evaluation):** Uses gRPC to perform flag evaluations remotely at a flagd instance. Supports bi-directional streaming, retry backoff, and custom name resolution (including Envoy support).
14//! - **REST Resolver:** Uses the OpenFeature Remote Evaluation Protocol (OFREP) over HTTP to evaluate flags.
15//! - **In-Process Resolver:** Performs evaluations locally using an embedded evaluation engine. Flag configurations can be retrieved via gRPC (sync mode).
16//! - **File Resolver:** Operates entirely from a flag definition file, updating on file changes in a best-effort manner.
17//!
18//! - **Advanced Targeting**
19//! - **Fractional Rollouts:** Uses consistent hashing (implemented via murmurhash3) to split traffic between flag variants in configurable proportions.
20//! - **Semantic Versioning:** Compare values using common operators such as '=', '!=', '<', '<=', '>', '>=', '^', and '~'.
21//! - **String Operations:** Custom operators for performing “starts_with” and “ends_with” comparisons.
22//! - **Complex Targeting Rules:** Leverages JSONLogic and custom operators to support nested conditions and dynamic evaluation.
23//!
24//! - **Caching Strategies**
25//! - Built-in support for LRU caching as well as an in-memory alternative. Flag evaluation results can be cached and later returned with a “CACHED” reason until the configuration updates.
26//!
27//! - **Connection Management**
28//! - Automatic connection establishment with configurable retries, timeout settings, and custom TLS or Unix-socket options.
29//! - Support for upstream name resolution including a custom resolver for Envoy proxy integration.
30//!
31//! ## Installation
32//! Add the dependency in your `Cargo.toml`:
33//! ```bash
34//! cargo add open-feature-flagd
35//! cargo add open-feature
36//! ```
37//!
38//! ## Cargo Features
39//!
40//! This crate uses cargo features to allow clients to include only the evaluation modes they need,
41//! keeping the dependency footprint minimal. By default, all features are enabled.
42//!
43//! | Feature | Description | Enabled by Default |
44//! |---------|-------------|-------------------|
45//! | `rpc` | gRPC-based remote evaluation via flagd service | ✅ |
46//! | `rest` | HTTP/OFREP-based remote evaluation | ✅ |
47//! | `in-process` | Local evaluation with embedded engine (includes File mode) | ✅ |
48//!
49//! ### Using Specific Features
50//!
51//! To include only specific evaluation modes:
52//!
53//! ```toml
54//! # Only RPC evaluation
55//! open-feature-flagd = { version = "0.0.8", default-features = false, features = ["rpc"] }
56//!
57//! # Only REST evaluation (lightweight, no gRPC dependencies)
58//! open-feature-flagd = { version = "0.0.8", default-features = false, features = ["rest"] }
59//!
60//! # Only in-process/file evaluation
61//! open-feature-flagd = { version = "0.0.8", default-features = false, features = ["in-process"] }
62//!
63//! # RPC and REST (no local evaluation engine)
64//! open-feature-flagd = { version = "0.0.8", default-features = false, features = ["rpc", "rest"] }
65//! ```
66//!
67//! Then integrate it into your application:
68//!
69//! ```rust,no_run
70//! use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
71//! use open_feature::provider::FeatureProvider;
72//! use open_feature::EvaluationContext;
73//!
74//! #[tokio::main]
75//! async fn main() {
76//! // Example using the REST resolver mode.
77//! let provider = FlagdProvider::new(FlagdOptions {
78//! host: "localhost".to_string(),
79//! port: 8016,
80//! resolver_type: ResolverType::Rest,
81//! ..Default::default()
82//! }).await.unwrap();
83//!
84//! let context = EvaluationContext::default().with_targeting_key("user-123");
85//! let result = provider.resolve_bool_value("bool-flag", &context).await.unwrap();
86//! println!("Flag value: {}", result.value);
87//! }
88//! ```
89//!
90//! ## Evaluation Modes
91//! ### Remote Resolver (RPC)
92//! In RPC mode, the provider communicates with flagd via gRPC. It supports features like streaming updates, retry mechanisms, and name resolution (including Envoy).
93//!
94//! ```rust,no_run
95//! use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
96//! use open_feature::provider::FeatureProvider;
97//! use open_feature::EvaluationContext;
98//!
99//! #[tokio::main]
100//! async fn main() {
101//! let provider = FlagdProvider::new(FlagdOptions {
102//! host: "localhost".to_string(),
103//! port: 8013,
104//! resolver_type: ResolverType::Rpc,
105//! ..Default::default()
106//! }).await.unwrap();
107//!
108//! let context = EvaluationContext::default().with_targeting_key("user-123");
109//! let bool_result = provider.resolve_bool_value("feature-enabled", &context).await.unwrap();
110//! println!("Feature enabled: {}", bool_result.value);
111//! }
112//! ```
113//!
114//! ### REST Resolver
115//! In REST mode the provider uses the OpenFeature Remote Evaluation Protocol (OFREP) over HTTP.
116//! It is useful when gRPC is not an option.
117//! ```rust,no_run
118//! use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
119//! use open_feature::provider::FeatureProvider;
120//! use open_feature::EvaluationContext;
121//!
122//! #[tokio::main]
123//! async fn main() {
124//! let provider = FlagdProvider::new(FlagdOptions {
125//! host: "localhost".to_string(),
126//! port: 8016,
127//! resolver_type: ResolverType::Rest,
128//! ..Default::default()
129//! }).await.unwrap();
130//!
131//! let context = EvaluationContext::default().with_targeting_key("user-456");
132//! let result = provider.resolve_string_value("feature-variant", &context).await.unwrap();
133//! println!("Variant: {}", result.value);
134//! }
135//! ```
136//!
137//! ### In-Process Resolver
138//! In-process evaluation is performed locally. Flag configurations are sourced via gRPC sync stream.
139//! This mode supports advanced targeting operators (fractional, semver, string comparisons)
140//! using the built-in evaluation engine.
141//! ```rust,no_run
142//! use open_feature_flagd::{CacheSettings, FlagdOptions, FlagdProvider, ResolverType};
143//! use open_feature::provider::FeatureProvider;
144//! use open_feature::EvaluationContext;
145//!
146//! #[tokio::main]
147//! async fn main() {
148//! let provider = FlagdProvider::new(FlagdOptions {
149//! host: "localhost".to_string(),
150//! port: 8015,
151//! resolver_type: ResolverType::InProcess,
152//! selector: Some("my-service".to_string()),
153//! cache_settings: Some(CacheSettings::default()),
154//! ..Default::default()
155//! }).await.unwrap();
156//!
157//! let context = EvaluationContext::default()
158//! .with_targeting_key("user-abc")
159//! .with_custom_field("environment", "production")
160//! .with_custom_field("semver", "2.1.0");
161//!
162//! let dark_mode = provider.resolve_bool_value("dark-mode", &context).await.unwrap();
163//! println!("Dark mode enabled: {}", dark_mode.value);
164//! }
165//! ```
166//!
167//! ### File Mode
168//! File mode is an in-process variant where flag configurations are read from a file.
169//! This is useful for development or environments without network access.
170//! ```rust,no_run
171//! use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
172//! use open_feature::provider::FeatureProvider;
173//! use open_feature::EvaluationContext;
174//!
175//! #[tokio::main]
176//! async fn main() {
177//! let file_path = "./path/to/flagd-config.json".to_string();
178//! let provider = FlagdProvider::new(FlagdOptions {
179//! host: "localhost".to_string(),
180//! resolver_type: ResolverType::File,
181//! source_configuration: Some(file_path),
182//! ..Default::default()
183//! }).await.unwrap();
184//!
185//! let context = EvaluationContext::default();
186//! let result = provider.resolve_int_value("rollout-percentage", &context).await.unwrap();
187//! println!("Rollout percentage: {}", result.value);
188//! }
189//! ```
190//!
191//! ## Configuration Options
192//! Configurations can be provided as constructor options or via environment variables (with constructor options taking priority). The following options are supported:
193//!
194//! | Option | Env Variable | Type / Supported Value | Default | Compatible Resolver |
195//! |-----------------------------------------|-----------------------------------------|-----------------------------------|-------------------------------------|--------------------------------|
196//! | Host | FLAGD_HOST | string | "localhost" | RPC, REST, In-Process, File |
197//! | Port | FLAGD_PORT | number | 8013 (RPC), 8016 (REST) | RPC, REST, In-Process, File |
198//! | Target URI | FLAGD_TARGET_URI | string | "" | RPC, In-Process |
199//! | TLS | FLAGD_TLS | boolean | false | RPC, In-Process |
200//! | Socket Path | FLAGD_SOCKET_PATH | string | "" | RPC |
201//! | Certificate Path | FLAGD_SERVER_CERT_PATH | string | "" | RPC, In-Process |
202//! | Cache Type (LRU / In-Memory / Disabled) | FLAGD_CACHE | string ("lru", "mem", "disabled") | In-Process: disabled, others: lru | RPC, In-Process, File |
203//! | Cache TTL (Seconds) | FLAGD_CACHE_TTL | number | 60 | RPC, In-Process, File |
204//! | Max Cache Size | FLAGD_MAX_CACHE_SIZE | number | 1000 | RPC, In-Process, File |
205//! | Offline File Path | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | "" | File |
206//! | Retry Backoff (ms) | FLAGD_RETRY_BACKOFF_MS | number | 1000 | RPC, In-Process |
207//! | Retry Backoff Maximum (ms) | FLAGD_RETRY_BACKOFF_MAX_MS | number | 120000 | RPC, In-Process |
208//! | Retry Grace Period | FLAGD_RETRY_GRACE_PERIOD | number | 5 | RPC, In-Process |
209//! | Event Stream Deadline (ms) | FLAGD_STREAM_DEADLINE_MS | number | 600000 | RPC |
210//! | Offline Poll Interval (ms) | FLAGD_OFFLINE_POLL_MS | number | 5000 | File |
211//! | Source Selector | FLAGD_SOURCE_SELECTOR | string | "" | In-Process |
212//!
213//! ## License
214//! Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.
215//!
216
217pub mod cache;
218pub mod error;
219pub mod resolver;
220
221use crate::error::FlagdError;
222#[cfg(feature = "in-process")]
223use crate::resolver::in_process::resolver::{FileResolver, InProcessResolver};
224use async_trait::async_trait;
225#[cfg(feature = "rpc")]
226use open_feature::EvaluationContextFieldValue;
227use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
228use open_feature::{EvaluationContext, EvaluationError, StructValue, Value};
229#[cfg(feature = "rest")]
230use resolver::rest::RestResolver;
231use tracing::debug;
232use tracing::instrument;
233
234#[cfg(feature = "rpc")]
235use std::collections::BTreeMap;
236use std::sync::Arc;
237
238pub use cache::{CacheService, CacheSettings, CacheType};
239#[cfg(feature = "rpc")]
240pub use resolver::rpc::RpcResolver;
241
242// Include the generated protobuf code
243#[cfg(any(feature = "rpc", feature = "in-process"))]
244pub mod flagd {
245 #[cfg(feature = "rpc")]
246 pub mod evaluation {
247 pub mod v1 {
248 include!(concat!(env!("OUT_DIR"), "/flagd.evaluation.v1.rs"));
249 }
250 }
251 #[cfg(feature = "in-process")]
252 pub mod sync {
253 pub mod v1 {
254 include!(concat!(env!("OUT_DIR"), "/flagd.sync.v1.rs"));
255 }
256 }
257}
258
259/// Configuration options for the flagd provider
260#[derive(Debug, Clone)]
261pub struct FlagdOptions {
262 /// Host address for the service
263 pub host: String,
264 /// Port number for the service
265 pub port: u16,
266 /// Target URI for custom name resolution (e.g. "envoy://service/flagd")
267 pub target_uri: Option<String>,
268 /// Type of resolver to use
269 pub resolver_type: ResolverType,
270 /// Whether to use TLS for connections (uses HTTPS/gRPCS)
271 /// When enabled, connections will use TLS with system/webpki root certificates by default.
272 /// For self-signed or custom CA certificates, also set `cert_path`.
273 pub tls: bool,
274 /// Path to a PEM-encoded CA certificate file for TLS connections.
275 /// Use this to connect to flagd servers using self-signed or custom CA certificates.
276 /// When provided, this certificate is used as the trusted CA instead of system roots.
277 /// Can also be set via the `FLAGD_SERVER_CERT_PATH` environment variable.
278 /// Example: "/path/to/ca-cert.pem"
279 pub cert_path: Option<String>,
280 /// Request timeout in milliseconds
281 pub deadline_ms: u32,
282 /// Cache configuration settings
283 pub cache_settings: Option<CacheSettings>,
284 /// Initial backoff duration in milliseconds for retry attempts (default: 1000ms)
285 /// Not supported in OFREP (REST) evaluation
286 pub retry_backoff_ms: u32,
287 /// Maximum backoff duration in milliseconds for retry attempts, prevents exponential backoff from growing indefinitely (default: 120000ms)
288 /// Not supported in OFREP (REST) evaluation
289 pub retry_backoff_max_ms: u32,
290 /// Maximum number of retry attempts before giving up (default: 5)
291 /// Not supported in OFREP (REST) evaluation
292 pub retry_grace_period: u32,
293 /// Source selector for filtering flag configurations
294 /// Used to scope flag sync requests in in-process evaluation
295 pub selector: Option<String>,
296 /// Unix domain socket path for connecting to flagd
297 /// When provided, this takes precedence over host:port configuration
298 /// Example: "/var/run/flagd.sock"
299 /// Only works with GRPC resolver
300 pub socket_path: Option<String>,
301 /// Source configuration for file-based resolver
302 pub source_configuration: Option<String>,
303 /// The deadline in milliseconds for event streaming operations. Set to 0 to disable.
304 /// Recommended to prevent infrastructure from killing idle connections.
305 pub stream_deadline_ms: u32,
306 /// HTTP/2 keepalive time in milliseconds. Sends pings to keep connections alive during
307 /// idle periods, allowing RPCs to start quickly without delay. Set to 0 to disable.
308 pub keep_alive_time_ms: u64,
309 /// Offline polling interval in milliseconds
310 pub offline_poll_interval_ms: Option<u32>,
311 /// Provider ID for identifying this provider instance to flagd
312 /// Used in in-process resolver for sync requests
313 pub provider_id: Option<String>,
314}
315/// Type of resolver to use for flag evaluation
316#[derive(Debug, Clone, PartialEq)]
317pub enum ResolverType {
318 /// Remote evaluation using gRPC connection to flagd service
319 #[cfg(feature = "rpc")]
320 Rpc,
321 /// Remote evaluation using REST connection to flagd service
322 #[cfg(feature = "rest")]
323 Rest,
324 /// Local evaluation with embedded flag engine using gRPC connection
325 #[cfg(feature = "in-process")]
326 InProcess,
327 /// Local evaluation with no external dependencies
328 #[cfg(feature = "in-process")]
329 File,
330}
331impl Default for FlagdOptions {
332 fn default() -> Self {
333 let resolver_type = Self::default_resolver_type();
334
335 let port = Self::default_port(&resolver_type);
336
337 #[allow(unused_mut)]
338 let mut options = Self {
339 host: std::env::var("FLAGD_HOST").unwrap_or_else(|_| "localhost".to_string()),
340 port: std::env::var("FLAGD_PORT")
341 .ok()
342 .and_then(|p| p.parse().ok())
343 .unwrap_or(port),
344 target_uri: std::env::var("FLAGD_TARGET_URI").ok(),
345 resolver_type,
346 tls: std::env::var("FLAGD_TLS")
347 .map(|v| v.to_lowercase() == "true")
348 .unwrap_or(false),
349 cert_path: std::env::var("FLAGD_SERVER_CERT_PATH").ok(),
350 deadline_ms: std::env::var("FLAGD_DEADLINE_MS")
351 .ok()
352 .and_then(|v| v.parse().ok())
353 .unwrap_or(500),
354 retry_backoff_ms: std::env::var("FLAGD_RETRY_BACKOFF_MS")
355 .ok()
356 .and_then(|v| v.parse().ok())
357 .unwrap_or(1000),
358 retry_backoff_max_ms: std::env::var("FLAGD_RETRY_BACKOFF_MAX_MS")
359 .ok()
360 .and_then(|v| v.parse().ok())
361 .unwrap_or(120000),
362 retry_grace_period: std::env::var("FLAGD_RETRY_GRACE_PERIOD")
363 .ok()
364 .and_then(|v| v.parse().ok())
365 .unwrap_or(5),
366 stream_deadline_ms: std::env::var("FLAGD_STREAM_DEADLINE_MS")
367 .ok()
368 .and_then(|v| v.parse().ok())
369 .unwrap_or(600000),
370 keep_alive_time_ms: std::env::var("FLAGD_KEEP_ALIVE_TIME_MS")
371 .ok()
372 .and_then(|v| v.parse().ok())
373 .unwrap_or(0), // Disabled by default, per gherkin spec
374 socket_path: std::env::var("FLAGD_SOCKET_PATH").ok(),
375 selector: std::env::var("FLAGD_SOURCE_SELECTOR").ok(),
376 cache_settings: Some(CacheSettings::default()),
377 source_configuration: std::env::var("FLAGD_OFFLINE_FLAG_SOURCE_PATH").ok(),
378 offline_poll_interval_ms: Some(
379 std::env::var("FLAGD_OFFLINE_POLL_MS")
380 .ok()
381 .and_then(|s| s.parse().ok())
382 .unwrap_or(5000),
383 ),
384 provider_id: std::env::var("FLAGD_PROVIDER_ID").ok(),
385 };
386
387 #[cfg(feature = "in-process")]
388 {
389 let resolver_env_set = std::env::var("FLAGD_RESOLVER").is_ok();
390 if options.source_configuration.is_some() && !resolver_env_set {
391 // Only override to File if FLAGD_RESOLVER wasn't explicitly set
392 options.resolver_type = ResolverType::File;
393 }
394 // Disable caching for in-process/file modes per spec (caching is RPC-only)
395 if matches!(
396 options.resolver_type,
397 ResolverType::InProcess | ResolverType::File
398 ) {
399 options.cache_settings = None;
400 }
401 }
402
403 options
404 }
405}
406
407impl FlagdOptions {
408 fn default_resolver_type() -> ResolverType {
409 if let Ok(r) = std::env::var("FLAGD_RESOLVER") {
410 match r.to_uppercase().as_str() {
411 #[cfg(feature = "rpc")]
412 "RPC" => return ResolverType::Rpc,
413 #[cfg(feature = "rest")]
414 "REST" => return ResolverType::Rest,
415 #[cfg(feature = "in-process")]
416 "IN-PROCESS" | "INPROCESS" => return ResolverType::InProcess,
417 #[cfg(feature = "in-process")]
418 "FILE" | "OFFLINE" => return ResolverType::File,
419 _ => {}
420 }
421 }
422 // Return first available resolver type as default
423 #[cfg(feature = "rpc")]
424 return ResolverType::Rpc;
425 #[cfg(all(feature = "rest", not(feature = "rpc")))]
426 return ResolverType::Rest;
427 #[cfg(all(feature = "in-process", not(feature = "rpc"), not(feature = "rest")))]
428 return ResolverType::InProcess;
429 #[cfg(not(any(feature = "rpc", feature = "rest", feature = "in-process")))]
430 compile_error!("At least one resolver feature must be enabled: rpc, rest, or in-process");
431 }
432
433 fn default_port(resolver_type: &ResolverType) -> u16 {
434 match resolver_type {
435 #[cfg(feature = "rpc")]
436 ResolverType::Rpc => 8013,
437 #[cfg(feature = "in-process")]
438 ResolverType::InProcess => 8015,
439 #[cfg(feature = "rest")]
440 ResolverType::Rest => 8016,
441 #[allow(unreachable_patterns)]
442 _ => 8013,
443 }
444 }
445}
446
447/// Main provider implementation for flagd
448#[derive(Clone)]
449pub struct FlagdProvider {
450 /// The underlying feature flag resolver
451 provider: Arc<dyn FeatureProvider + Send + Sync>,
452 /// Optional caching layer
453 cache: Option<Arc<CacheService<Value>>>,
454}
455
456impl FlagdProvider {
457 #[instrument(skip(options))]
458 pub async fn new(options: FlagdOptions) -> Result<Self, FlagdError> {
459 debug!("Initializing FlagdProvider with options: {:?}", options);
460
461 // Validate File resolver configuration
462 #[cfg(feature = "in-process")]
463 if options.resolver_type == ResolverType::File && options.source_configuration.is_none() {
464 return Err(FlagdError::Config(
465 "File resolver requires 'source_configuration' (FLAGD_OFFLINE_FLAG_SOURCE_PATH) to be set".to_string()
466 ));
467 }
468
469 let provider: Arc<dyn FeatureProvider + Send + Sync> = match options.resolver_type {
470 #[cfg(feature = "rpc")]
471 ResolverType::Rpc => {
472 debug!("Using RPC resolver");
473 Arc::new(RpcResolver::new(&options).await?)
474 }
475 #[cfg(feature = "rest")]
476 ResolverType::Rest => {
477 debug!("Using REST resolver");
478 Arc::new(RestResolver::new(&options).await?)
479 }
480 #[cfg(feature = "in-process")]
481 ResolverType::InProcess => {
482 debug!("Using in-process resolver");
483 Arc::new(InProcessResolver::new(&options).await?)
484 }
485 #[cfg(feature = "in-process")]
486 ResolverType::File => {
487 debug!("Using file resolver");
488 Arc::new(
489 FileResolver::new(
490 options
491 .source_configuration
492 .expect("source_configuration validated above"),
493 options.cache_settings.clone(),
494 )
495 .await?,
496 )
497 }
498 };
499
500 Ok(Self {
501 provider,
502 cache: options
503 .cache_settings
504 .map(|settings| Arc::new(CacheService::new(settings))),
505 })
506 }
507}
508
509impl std::fmt::Debug for FlagdProvider {
510 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511 f.debug_struct("FlagdProvider")
512 .field("cache", &self.cache)
513 .finish()
514 }
515}
516
517#[cfg(feature = "rpc")]
518pub(crate) fn convert_context(context: &EvaluationContext) -> Option<prost_types::Struct> {
519 let mut fields = BTreeMap::new();
520
521 if let Some(targeting_key) = &context.targeting_key {
522 fields.insert(
523 "targetingKey".to_string(),
524 prost_types::Value {
525 kind: Some(prost_types::value::Kind::StringValue(targeting_key.clone())),
526 },
527 );
528 }
529
530 for (key, value) in &context.custom_fields {
531 let prost_value = match value {
532 EvaluationContextFieldValue::String(s) => prost_types::Value {
533 kind: Some(prost_types::value::Kind::StringValue(s.clone())),
534 },
535 EvaluationContextFieldValue::Bool(b) => prost_types::Value {
536 kind: Some(prost_types::value::Kind::BoolValue(*b)),
537 },
538 EvaluationContextFieldValue::Int(i) => prost_types::Value {
539 kind: Some(prost_types::value::Kind::NumberValue(*i as f64)),
540 },
541 EvaluationContextFieldValue::Float(f) => prost_types::Value {
542 kind: Some(prost_types::value::Kind::NumberValue(*f)),
543 },
544 EvaluationContextFieldValue::DateTime(dt) => prost_types::Value {
545 kind: Some(prost_types::value::Kind::StringValue(dt.to_string())),
546 },
547 EvaluationContextFieldValue::Struct(s) => prost_types::Value {
548 kind: Some(prost_types::value::Kind::StringValue(format!("{:?}", s))),
549 },
550 };
551 fields.insert(key.clone(), prost_value);
552 }
553
554 Some(prost_types::Struct { fields })
555}
556
557#[cfg(feature = "rpc")]
558pub(crate) fn convert_proto_struct_to_struct_value(
559 proto_struct: prost_types::Struct,
560) -> StructValue {
561 let fields = proto_struct
562 .fields
563 .into_iter()
564 .map(|(key, value)| {
565 (
566 key,
567 match value.kind.unwrap() {
568 prost_types::value::Kind::NullValue(_) => Value::String(String::new()),
569 prost_types::value::Kind::NumberValue(n) => Value::Float(n),
570 prost_types::value::Kind::StringValue(s) => Value::String(s),
571 prost_types::value::Kind::BoolValue(b) => Value::Bool(b),
572 prost_types::value::Kind::StructValue(s) => Value::String(format!("{:?}", s)),
573 prost_types::value::Kind::ListValue(l) => Value::String(format!("{:?}", l)),
574 },
575 )
576 })
577 .collect();
578
579 StructValue { fields }
580}
581
582impl FlagdProvider {
583 async fn get_cached_value<T>(
584 &self,
585 flag_key: &str,
586 context: &EvaluationContext,
587 value_converter: impl Fn(Value) -> Option<T>,
588 ) -> Option<T> {
589 if let Some(cache) = &self.cache
590 && let Some(cached_value) = cache.get(flag_key, context).await
591 {
592 return value_converter(cached_value);
593 }
594 None
595 }
596}
597
598#[async_trait]
599impl FeatureProvider for FlagdProvider {
600 fn metadata(&self) -> &ProviderMetadata {
601 self.provider.metadata()
602 }
603
604 async fn resolve_bool_value(
605 &self,
606 flag_key: &str,
607 context: &EvaluationContext,
608 ) -> Result<ResolutionDetails<bool>, EvaluationError> {
609 if let Some(value) = self
610 .get_cached_value(flag_key, context, |v| match v {
611 Value::Bool(b) => Some(b),
612 _ => None,
613 })
614 .await
615 {
616 return Ok(ResolutionDetails::new(value));
617 }
618
619 let result = self.provider.resolve_bool_value(flag_key, context).await?;
620
621 if let Some(cache) = &self.cache {
622 cache
623 .add(flag_key, context, Value::Bool(result.value))
624 .await;
625 }
626
627 Ok(result)
628 }
629
630 async fn resolve_int_value(
631 &self,
632 flag_key: &str,
633 context: &EvaluationContext,
634 ) -> Result<ResolutionDetails<i64>, EvaluationError> {
635 if let Some(value) = self
636 .get_cached_value(flag_key, context, |v| match v {
637 Value::Int(i) => Some(i),
638 _ => None,
639 })
640 .await
641 {
642 return Ok(ResolutionDetails::new(value));
643 }
644
645 let result = self.provider.resolve_int_value(flag_key, context).await?;
646
647 if let Some(cache) = &self.cache {
648 cache.add(flag_key, context, Value::Int(result.value)).await;
649 }
650
651 Ok(result)
652 }
653
654 async fn resolve_float_value(
655 &self,
656 flag_key: &str,
657 context: &EvaluationContext,
658 ) -> Result<ResolutionDetails<f64>, EvaluationError> {
659 if let Some(value) = self
660 .get_cached_value(flag_key, context, |v| match v {
661 Value::Float(f) => Some(f),
662 _ => None,
663 })
664 .await
665 {
666 return Ok(ResolutionDetails::new(value));
667 }
668
669 let result = self.provider.resolve_float_value(flag_key, context).await?;
670
671 if let Some(cache) = &self.cache {
672 cache
673 .add(flag_key, context, Value::Float(result.value))
674 .await;
675 }
676
677 Ok(result)
678 }
679
680 async fn resolve_string_value(
681 &self,
682 flag_key: &str,
683 context: &EvaluationContext,
684 ) -> Result<ResolutionDetails<String>, EvaluationError> {
685 if let Some(value) = self
686 .get_cached_value(flag_key, context, |v| match v {
687 Value::String(s) => Some(s),
688 _ => None,
689 })
690 .await
691 {
692 return Ok(ResolutionDetails::new(value));
693 }
694
695 let result = self
696 .provider
697 .resolve_string_value(flag_key, context)
698 .await?;
699
700 if let Some(cache) = &self.cache {
701 cache
702 .add(flag_key, context, Value::String(result.value.clone()))
703 .await;
704 }
705
706 Ok(result)
707 }
708
709 async fn resolve_struct_value(
710 &self,
711 flag_key: &str,
712 context: &EvaluationContext,
713 ) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
714 if let Some(value) = self
715 .get_cached_value(flag_key, context, |v| match v {
716 Value::Struct(s) => Some(s),
717 _ => None,
718 })
719 .await
720 {
721 return Ok(ResolutionDetails::new(value));
722 }
723
724 let result = self
725 .provider
726 .resolve_struct_value(flag_key, context)
727 .await?;
728
729 if let Some(cache) = &self.cache {
730 cache
731 .add(flag_key, context, Value::Struct(result.value.clone()))
732 .await;
733 }
734
735 Ok(result)
736 }
737}