Skip to main content

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, EvaluationReason, 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<CachedResolution>>>,
454}
455
456#[derive(Clone, Debug)]
457struct CachedResolution {
458    value: Value,
459    variant: Option<String>,
460    flag_metadata: Option<open_feature::FlagMetadata>,
461}
462
463impl CachedResolution {
464    fn from_resolution<T>(value: Value, result: &ResolutionDetails<T>) -> Self {
465        Self {
466            value,
467            variant: result.variant.clone(),
468            flag_metadata: result.flag_metadata.clone(),
469        }
470    }
471
472    fn into_resolution<T>(
473        self,
474        value_converter: impl Fn(Value) -> Option<T>,
475    ) -> Option<ResolutionDetails<T>> {
476        value_converter(self.value).map(|value| ResolutionDetails {
477            value,
478            variant: self.variant,
479            reason: Some(EvaluationReason::Cached),
480            flag_metadata: self.flag_metadata,
481        })
482    }
483}
484
485fn should_cache<T>(result: &ResolutionDetails<T>) -> bool {
486    match result.reason.as_ref() {
487        Some(EvaluationReason::Static) => true,
488        Some(EvaluationReason::Other(reason)) => reason.eq_ignore_ascii_case("STATIC"),
489        _ => false,
490    }
491}
492
493impl FlagdProvider {
494    #[instrument(skip(options))]
495    pub async fn new(options: FlagdOptions) -> Result<Self, FlagdError> {
496        debug!("Initializing FlagdProvider with options: {:?}", options);
497
498        // Validate File resolver configuration
499        #[cfg(feature = "in-process")]
500        if options.resolver_type == ResolverType::File && options.source_configuration.is_none() {
501            return Err(FlagdError::Config(
502                "File resolver requires 'source_configuration' (FLAGD_OFFLINE_FLAG_SOURCE_PATH) to be set".to_string()
503            ));
504        }
505
506        let provider: Arc<dyn FeatureProvider + Send + Sync> = match options.resolver_type {
507            #[cfg(feature = "rpc")]
508            ResolverType::Rpc => {
509                debug!("Using RPC resolver");
510                Arc::new(RpcResolver::new(&options).await?)
511            }
512            #[cfg(feature = "rest")]
513            ResolverType::Rest => {
514                debug!("Using REST resolver");
515                Arc::new(RestResolver::new(&options).await?)
516            }
517            #[cfg(feature = "in-process")]
518            ResolverType::InProcess => {
519                debug!("Using in-process resolver");
520                Arc::new(InProcessResolver::new(&options).await?)
521            }
522            #[cfg(feature = "in-process")]
523            ResolverType::File => {
524                debug!("Using file resolver");
525                Arc::new(
526                    FileResolver::new(
527                        options
528                            .source_configuration
529                            .expect("source_configuration validated above"),
530                        options.cache_settings.clone(),
531                    )
532                    .await?,
533                )
534            }
535        };
536
537        Ok(Self {
538            provider,
539            cache: options
540                .cache_settings
541                .map(|settings| Arc::new(CacheService::new(settings))),
542        })
543    }
544}
545
546impl std::fmt::Debug for FlagdProvider {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        f.debug_struct("FlagdProvider")
549            .field("cache", &self.cache)
550            .finish()
551    }
552}
553
554#[cfg(feature = "rpc")]
555pub(crate) fn convert_context(context: &EvaluationContext) -> Option<prost_types::Struct> {
556    let mut fields = BTreeMap::new();
557
558    if let Some(targeting_key) = &context.targeting_key {
559        fields.insert(
560            "targetingKey".to_string(),
561            prost_types::Value {
562                kind: Some(prost_types::value::Kind::StringValue(targeting_key.clone())),
563            },
564        );
565    }
566
567    for (key, value) in &context.custom_fields {
568        let prost_value = match value {
569            EvaluationContextFieldValue::String(s) => prost_types::Value {
570                kind: Some(prost_types::value::Kind::StringValue(s.clone())),
571            },
572            EvaluationContextFieldValue::Bool(b) => prost_types::Value {
573                kind: Some(prost_types::value::Kind::BoolValue(*b)),
574            },
575            EvaluationContextFieldValue::Int(i) => prost_types::Value {
576                kind: Some(prost_types::value::Kind::NumberValue(*i as f64)),
577            },
578            EvaluationContextFieldValue::Float(f) => prost_types::Value {
579                kind: Some(prost_types::value::Kind::NumberValue(*f)),
580            },
581            EvaluationContextFieldValue::DateTime(dt) => prost_types::Value {
582                kind: Some(prost_types::value::Kind::StringValue(dt.to_string())),
583            },
584            EvaluationContextFieldValue::Struct(s) => prost_types::Value {
585                kind: Some(prost_types::value::Kind::StringValue(format!("{:?}", s))),
586            },
587        };
588        fields.insert(key.clone(), prost_value);
589    }
590
591    Some(prost_types::Struct { fields })
592}
593
594#[cfg(feature = "rpc")]
595pub(crate) fn convert_proto_struct_to_struct_value(
596    proto_struct: prost_types::Struct,
597) -> StructValue {
598    let fields = proto_struct
599        .fields
600        .into_iter()
601        .map(|(key, value)| {
602            (
603                key,
604                match value.kind.unwrap() {
605                    prost_types::value::Kind::NullValue(_) => Value::String(String::new()),
606                    prost_types::value::Kind::NumberValue(n) => Value::Float(n),
607                    prost_types::value::Kind::StringValue(s) => Value::String(s),
608                    prost_types::value::Kind::BoolValue(b) => Value::Bool(b),
609                    prost_types::value::Kind::StructValue(s) => Value::String(format!("{:?}", s)),
610                    prost_types::value::Kind::ListValue(l) => Value::String(format!("{:?}", l)),
611                },
612            )
613        })
614        .collect();
615
616    StructValue { fields }
617}
618
619impl FlagdProvider {
620    async fn get_cached_value<T>(
621        &self,
622        flag_key: &str,
623        context: &EvaluationContext,
624        value_converter: impl Fn(Value) -> Option<T>,
625    ) -> Option<ResolutionDetails<T>> {
626        if let Some(cache) = &self.cache
627            && let Some(cached_value) = cache.get(flag_key, context).await
628        {
629            return cached_value.into_resolution(value_converter);
630        }
631        None
632    }
633
634    async fn cache_value<T>(
635        &self,
636        flag_key: &str,
637        context: &EvaluationContext,
638        value: Value,
639        result: &ResolutionDetails<T>,
640    ) {
641        if let Some(cache) = &self.cache
642            && should_cache(result)
643        {
644            cache
645                .add(
646                    flag_key,
647                    context,
648                    CachedResolution::from_resolution(value, result),
649                )
650                .await;
651        }
652    }
653}
654
655#[async_trait]
656impl FeatureProvider for FlagdProvider {
657    fn metadata(&self) -> &ProviderMetadata {
658        self.provider.metadata()
659    }
660
661    async fn resolve_bool_value(
662        &self,
663        flag_key: &str,
664        context: &EvaluationContext,
665    ) -> Result<ResolutionDetails<bool>, EvaluationError> {
666        if let Some(result) = self
667            .get_cached_value(flag_key, context, |v| match v {
668                Value::Bool(b) => Some(b),
669                _ => None,
670            })
671            .await
672        {
673            return Ok(result);
674        }
675
676        let result = self.provider.resolve_bool_value(flag_key, context).await?;
677        self.cache_value(flag_key, context, Value::Bool(result.value), &result)
678            .await;
679
680        Ok(result)
681    }
682
683    async fn resolve_int_value(
684        &self,
685        flag_key: &str,
686        context: &EvaluationContext,
687    ) -> Result<ResolutionDetails<i64>, EvaluationError> {
688        if let Some(result) = self
689            .get_cached_value(flag_key, context, |v| match v {
690                Value::Int(i) => Some(i),
691                _ => None,
692            })
693            .await
694        {
695            return Ok(result);
696        }
697
698        let result = self.provider.resolve_int_value(flag_key, context).await?;
699        self.cache_value(flag_key, context, Value::Int(result.value), &result)
700            .await;
701
702        Ok(result)
703    }
704
705    async fn resolve_float_value(
706        &self,
707        flag_key: &str,
708        context: &EvaluationContext,
709    ) -> Result<ResolutionDetails<f64>, EvaluationError> {
710        if let Some(result) = self
711            .get_cached_value(flag_key, context, |v| match v {
712                Value::Float(f) => Some(f),
713                _ => None,
714            })
715            .await
716        {
717            return Ok(result);
718        }
719
720        let result = self.provider.resolve_float_value(flag_key, context).await?;
721        self.cache_value(flag_key, context, Value::Float(result.value), &result)
722            .await;
723
724        Ok(result)
725    }
726
727    async fn resolve_string_value(
728        &self,
729        flag_key: &str,
730        context: &EvaluationContext,
731    ) -> Result<ResolutionDetails<String>, EvaluationError> {
732        if let Some(result) = self
733            .get_cached_value(flag_key, context, |v| match v {
734                Value::String(s) => Some(s),
735                _ => None,
736            })
737            .await
738        {
739            return Ok(result);
740        }
741
742        let result = self
743            .provider
744            .resolve_string_value(flag_key, context)
745            .await?;
746        self.cache_value(
747            flag_key,
748            context,
749            Value::String(result.value.clone()),
750            &result,
751        )
752        .await;
753
754        Ok(result)
755    }
756
757    async fn resolve_struct_value(
758        &self,
759        flag_key: &str,
760        context: &EvaluationContext,
761    ) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
762        if let Some(result) = self
763            .get_cached_value(flag_key, context, |v| match v {
764                Value::Struct(s) => Some(s),
765                _ => None,
766            })
767            .await
768        {
769            return Ok(result);
770        }
771
772        let result = self
773            .provider
774            .resolve_struct_value(flag_key, context)
775            .await?;
776        self.cache_value(
777            flag_key,
778            context,
779            Value::Struct(result.value.clone()),
780            &result,
781        )
782        .await;
783
784        Ok(result)
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn cache_policy_only_allows_static_results() {
794        let cases = [
795            ("static enum", Some(EvaluationReason::Static), true),
796            (
797                "static extension",
798                Some(EvaluationReason::Other("STATIC".to_string())),
799                true,
800            ),
801            (
802                "static extension lowercase",
803                Some(EvaluationReason::Other("static".to_string())),
804                true,
805            ),
806            (
807                "targeting match",
808                Some(EvaluationReason::TargetingMatch),
809                false,
810            ),
811            ("default", Some(EvaluationReason::Default), false),
812            ("error", Some(EvaluationReason::Error), false),
813            ("cached", Some(EvaluationReason::Cached), false),
814            ("missing reason", None, false),
815        ];
816
817        for (name, reason, expected) in cases {
818            let result = ResolutionDetails {
819                value: true,
820                variant: None,
821                reason,
822                flag_metadata: None,
823            };
824
825            assert_eq!(should_cache(&result), expected, "{name}");
826        }
827    }
828}