Skip to content

Commit d0b9d76

Browse files
feat: ofrep provider (#53)
Signed-off-by: Rahul Baradol <[email protected]>
1 parent b265717 commit d0b9d76

File tree

6 files changed

+1017
-2
lines changed

6 files changed

+1017
-2
lines changed

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ edition = "2021"
1212
members = [
1313
"crates/env-var",
1414
"crates/flagd",
15-
"crates/flipt"
16-
]
15+
"crates/flipt",
16+
"crates/ofrep"
17+
]

crates/ofrep/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "open-feature-ofrep"
3+
version = "0.0.1"
4+
edition = "2024"
5+
6+
[dev-dependencies]
7+
wiremock = "0.6.3"
8+
test-log = { version = "0.2", features = ["trace"] }
9+
serial_test = "3.2.0"
10+
11+
[dependencies]
12+
async-trait = "0.1.88"
13+
open-feature = "0.2.5"
14+
reqwest = { version = "0.12", default-features = false, features = [
15+
"json",
16+
"stream",
17+
"rustls-tls",
18+
] }
19+
serde_json = "1.0.140"
20+
tracing = "0.1.41"
21+
thiserror = "2.0"
22+
anyhow = "1.0.98"
23+
chrono = "0.4"
24+
once_cell = "1.18"
25+
tokio = { version = "1.45", features = ["full"] }
26+
url = "2.5.4"

crates/ofrep/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[Generated by cargo-readme: `cargo readme --no-title --no-license > README.md`]::
2+
# OFREP Provider for OpenFeature
3+
4+
A Rust implementation of the OpenFeature OFREP provider, enabling dynamic
5+
feature flag evaluation in your applications.
6+
7+
This provider allows to connect to any feature flag management system that supports OFREP.
8+
9+
### Installation
10+
Add the dependency in your `Cargo.toml`:
11+
```bash
12+
cargo add open-feature-ofrep
13+
cargo add open-feature
14+
```
15+
Then integrate it into your application:
16+
17+
```rust
18+
use std::time::Duration;
19+
use open_feature::provider::FeatureProvider;
20+
use open_feature::EvaluationContext;
21+
use open_feature_ofrep::{OfrepProvider, OfrepOptions};
22+
use reqwest::header::{HeaderMap, HeaderValue};
23+
24+
#[tokio::main]
25+
async fn main() {
26+
let mut headers = HeaderMap::new();
27+
headers.insert("color", HeaderValue::from_static("yellow"));
28+
29+
let provider = OfrepProvider::new(OfrepOptions {
30+
base_url: "http://localhost:8016".to_string(),
31+
headers: headers.clone(),
32+
connect_timeout: Duration::from_secs(4),
33+
..Default::default()
34+
}).await.unwrap();
35+
36+
let context = EvaluationContext::default()
37+
.with_targeting_key("user-123")
38+
.with_custom_field("color", "yellow");
39+
40+
let result = provider.resolve_bool_value("isColorYellow", &context).await.unwrap();
41+
println!("Flag value: {}", result.value);
42+
}
43+
```
44+
45+
### Configuration Options
46+
Configurations can be provided as constructor options. The following options are supported:
47+
48+
| Option | Type / Supported Value | Default |
49+
|-----------------------------------------|-----------------------------------|-------------------------------------|
50+
| base_url | string | http://localhost:8016 |
51+
| headers | HeaderMap | Empty Map |
52+
| connect_timeout | Duration | 10 seconds |
53+
54+
### License
55+
Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.
56+

crates/ofrep/src/error.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug, PartialEq)]
4+
pub enum OfrepError {
5+
#[error("Provider error: {0}")]
6+
Provider(String),
7+
#[error("Connection error: {0}")]
8+
Connection(String),
9+
#[error("Invalid configuration: {0}")]
10+
Config(String),
11+
}
12+
13+
// Add implementations for error conversion
14+
impl From<Box<dyn std::error::Error>> for OfrepError {
15+
fn from(error: Box<dyn std::error::Error>) -> Self {
16+
OfrepError::Provider(error.to_string())
17+
}
18+
}
19+
20+
impl From<Box<dyn std::error::Error + Send + Sync>> for OfrepError {
21+
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
22+
OfrepError::Provider(error.to_string())
23+
}
24+
}
25+
26+
impl From<anyhow::Error> for OfrepError {
27+
fn from(error: anyhow::Error) -> Self {
28+
OfrepError::Provider(error.to_string())
29+
}
30+
}

crates/ofrep/src/lib.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
mod error;
2+
mod resolver;
3+
4+
use error::OfrepError;
5+
use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
6+
use open_feature::{EvaluationContext, EvaluationError, StructValue};
7+
use reqwest::header::HeaderMap;
8+
use resolver::Resolver;
9+
use std::fmt;
10+
use std::sync::Arc;
11+
use std::time::Duration;
12+
use tracing::debug;
13+
use tracing::instrument;
14+
use url::Url;
15+
16+
use async_trait::async_trait;
17+
18+
const DEFAULT_BASE_URL: &str = "http://localhost:8016";
19+
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
20+
21+
#[derive(Debug, Clone)]
22+
pub struct OfrepOptions {
23+
pub base_url: String,
24+
pub headers: HeaderMap,
25+
pub connect_timeout: Duration,
26+
}
27+
28+
impl Default for OfrepOptions {
29+
fn default() -> Self {
30+
OfrepOptions {
31+
base_url: DEFAULT_BASE_URL.to_string(),
32+
headers: HeaderMap::new(),
33+
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
34+
}
35+
}
36+
}
37+
38+
pub struct OfrepProvider {
39+
provider: Arc<dyn FeatureProvider + Send + Sync>,
40+
}
41+
42+
impl fmt::Debug for OfrepProvider {
43+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44+
f.debug_struct("OfrepProvider")
45+
.field("provider", &"<FeatureProvider>")
46+
.finish()
47+
}
48+
}
49+
50+
impl OfrepProvider {
51+
#[instrument(skip(options))]
52+
pub async fn new(options: OfrepOptions) -> Result<Self, OfrepError> {
53+
debug!("Initializing OfrepProvider with options: {:?}", options);
54+
55+
let url = Url::parse(&options.base_url).map_err(|e| {
56+
OfrepError::Config(format!("Invalid base url: '{}' ({})", options.base_url, e))
57+
})?;
58+
59+
if !matches!(url.scheme(), "http" | "https") {
60+
return Err(OfrepError::Config(format!(
61+
"Invalid base url: '{}' (unsupported scheme)",
62+
url.scheme()
63+
)));
64+
}
65+
66+
Ok(Self {
67+
provider: Arc::new(Resolver::new(&options)),
68+
})
69+
}
70+
}
71+
72+
#[async_trait]
73+
impl FeatureProvider for OfrepProvider {
74+
fn metadata(&self) -> &ProviderMetadata {
75+
self.provider.metadata()
76+
}
77+
78+
async fn resolve_bool_value(
79+
&self,
80+
flag_key: &str,
81+
context: &EvaluationContext,
82+
) -> Result<ResolutionDetails<bool>, EvaluationError> {
83+
self.provider.resolve_bool_value(flag_key, context).await
84+
}
85+
86+
async fn resolve_int_value(
87+
&self,
88+
flag_key: &str,
89+
context: &EvaluationContext,
90+
) -> Result<ResolutionDetails<i64>, EvaluationError> {
91+
self.provider.resolve_int_value(flag_key, context).await
92+
}
93+
94+
async fn resolve_float_value(
95+
&self,
96+
flag_key: &str,
97+
context: &EvaluationContext,
98+
) -> Result<ResolutionDetails<f64>, EvaluationError> {
99+
self.provider.resolve_float_value(flag_key, context).await
100+
}
101+
102+
async fn resolve_string_value(
103+
&self,
104+
flag_key: &str,
105+
context: &EvaluationContext,
106+
) -> Result<ResolutionDetails<String>, EvaluationError> {
107+
self.provider.resolve_string_value(flag_key, context).await
108+
}
109+
110+
async fn resolve_struct_value(
111+
&self,
112+
flag_key: &str,
113+
context: &EvaluationContext,
114+
) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
115+
self.provider.resolve_struct_value(flag_key, context).await
116+
}
117+
}
118+
119+
#[cfg(test)]
120+
mod tests {
121+
use super::*;
122+
use test_log::test;
123+
124+
#[test(tokio::test)]
125+
async fn test_ofrep_options_validation() {
126+
let provider_with_empty_host = OfrepProvider::new(OfrepOptions {
127+
base_url: "http://".to_string(),
128+
..Default::default()
129+
})
130+
.await;
131+
132+
let provider_with_invalid_scheme = OfrepProvider::new(OfrepOptions {
133+
base_url: "invalid://".to_string(),
134+
..Default::default()
135+
})
136+
.await;
137+
138+
assert!(provider_with_empty_host.is_err());
139+
assert!(provider_with_invalid_scheme.is_err());
140+
141+
assert_eq!(
142+
provider_with_empty_host.unwrap_err(),
143+
OfrepError::Config("Invalid base url: 'http://' (empty host)".to_string())
144+
);
145+
assert_eq!(
146+
provider_with_invalid_scheme.unwrap_err(),
147+
OfrepError::Config("Invalid base url: 'invalid' (unsupported scheme)".to_string())
148+
);
149+
}
150+
}

0 commit comments

Comments
 (0)