Skip to content

feat: ofrep provider #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ edition = "2021"
members = [
"crates/env-var",
"crates/flagd",
"crates/flipt"
]
"crates/flipt",
"crates/ofrep"
]
19 changes: 19 additions & 0 deletions crates/ofrep/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "open-feature-ofrep"
version = "0.0.1"
edition = "2024"

[dev-dependencies]
wiremock = "0.6.3"
test-log = { version = "0.2", features = ["trace"] }

[dependencies]
async-trait = "0.1.88"
open-feature = "0.2.5"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
serde = "1.0.219"
serde_json = "1.0.140"
tracing = "0.1.41"
thiserror = "2.0"
anyhow = "1.0.98"
tokio = { version = "1.45", features = ["full"] }
56 changes: 56 additions & 0 deletions crates/ofrep/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[Generated by cargo-readme: `cargo readme --no-title --no-license > README.md`]::
# OFREP Provider for OpenFeature

A Rust implementation of the OpenFeature OFREP provider, enabling dynamic
feature flag evaluation in your applications.

This provider allows to connect to any feature flag management system that supports OFREP.

### Installation
Add the dependency in your `Cargo.toml`:
```bash
cargo add open-feature-ofrep
cargo add open-feature
```
Then integrate it into your application:

```rust
use std::time::Duration;
use open_feature::provider::FeatureProvider;
use open_feature::EvaluationContext;
use open_feature_ofrep::{OfrepProvider, OfrepOptions};
use reqwest::header::{HeaderMap, HeaderValue};

#[tokio::main]
async fn main() {
let mut headers = HeaderMap::new();
headers.insert("color", HeaderValue::from_static("yellow"));

let provider = OfrepProvider::new(OfrepOptions {
base_url: "http://localhost:8016".to_string(),
headers: headers.clone(),
connect_timeout: Duration::from_secs(4),
..Default::default()
}).await.unwrap();

let context = EvaluationContext::default()
.with_targeting_key("user-123")
.with_custom_field("color", "yellow");

let result = provider.resolve_bool_value("isColorYellow", &context).await.unwrap();
println!("Flag value: {}", result.value);
}
```

### Configuration Options
Configurations can be provided as constructor options. The following options are supported:

| Option | Type / Supported Value | Default |
|-----------------------------------------|-----------------------------------|-------------------------------------|
| base_url | string | http://localhost:8016 |
| headers | HeaderMap | Empty Map |
| connect_timeout | Duration | 10 seconds |

### License
Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.

30 changes: 30 additions & 0 deletions crates/ofrep/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use thiserror::Error;

#[derive(Error, Debug)]
pub enum OfrepError {
#[error("Provider error: {0}")]
Provider(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Invalid configuration: {0}")]
Config(String),
}

// Add implementations for error conversion
impl From<Box<dyn std::error::Error>> for OfrepError {
fn from(error: Box<dyn std::error::Error>) -> Self {
OfrepError::Provider(error.to_string())
}
}

impl From<Box<dyn std::error::Error + Send + Sync>> for OfrepError {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
OfrepError::Provider(error.to_string())
}
}

impl From<anyhow::Error> for OfrepError {
fn from(error: anyhow::Error) -> Self {
OfrepError::Provider(error.to_string())
}
}
106 changes: 106 additions & 0 deletions crates/ofrep/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
mod error;
mod resolver;

use error::OfrepError;
use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
use open_feature::{EvaluationContext, EvaluationError, StructValue};
use reqwest::header::HeaderMap;
use resolver::Resolver;
use std::sync::Arc;
use std::time::Duration;
use tracing::debug;
use tracing::instrument;

use async_trait::async_trait;

const DEFAULT_BASE_URL: &str = "http://localhost:8016";
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);

#[derive(Debug, Clone)]
pub struct OfrepOptions {
pub base_url: String,
pub headers: HeaderMap,
pub connect_timeout: Duration,
}

impl Default for OfrepOptions {
fn default() -> Self {
OfrepOptions {
base_url: DEFAULT_BASE_URL.to_string(),
headers: HeaderMap::new(),
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
}
}
}

pub struct OfrepProvider {
provider: Arc<dyn FeatureProvider + Send + Sync>,
}

impl OfrepProvider {
#[instrument(skip(options))]
pub async fn new(options: OfrepOptions) -> Result<Self, OfrepError> {
debug!("Initializing OfrepProvider with options: {:?}", options);
Ok(Self {
provider: Arc::new(Resolver::new(&options)),
})
}
}

#[async_trait]
impl FeatureProvider for OfrepProvider {
fn metadata(&self) -> &ProviderMetadata {
self.provider.metadata()
}

async fn resolve_bool_value(
&self,
flag_key: &str,
context: &EvaluationContext,
) -> Result<ResolutionDetails<bool>, EvaluationError> {
let result = self.provider.resolve_bool_value(flag_key, context).await?;
Ok(result)
}

async fn resolve_int_value(
&self,
flag_key: &str,
context: &EvaluationContext,
) -> Result<ResolutionDetails<i64>, EvaluationError> {
let result = self.provider.resolve_int_value(flag_key, context).await?;
Ok(result)
}

async fn resolve_float_value(
&self,
flag_key: &str,
context: &EvaluationContext,
) -> Result<ResolutionDetails<f64>, EvaluationError> {
let result = self.provider.resolve_float_value(flag_key, context).await?;
Ok(result)
}

async fn resolve_string_value(
&self,
flag_key: &str,
context: &EvaluationContext,
) -> Result<ResolutionDetails<String>, EvaluationError> {
let result = self
.provider
.resolve_string_value(flag_key, context)
.await?;
Ok(result)
}

async fn resolve_struct_value(
&self,
flag_key: &str,
context: &EvaluationContext,
) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
let result = self
.provider
.resolve_struct_value(flag_key, context)
.await?;
Ok(result)
}
}
Loading