Skip to content

Commit 25a3a3e

Browse files
feat: add-flagsmith-provider-crate (#82)
Signed-off-by: wadii <wadii.zaim@flagsmith.com> Signed-off-by: Zaimwa9 <wadii.zaim@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent b6b60f2 commit 25a3a3e

10 files changed

Lines changed: 1701 additions & 0 deletions

File tree

.github/component_owners.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ components:
1111
crates/ofrep:
1212
- erenatas
1313
- Rahul-Baradol
14+
crates/flagsmith:
15+
- matthewelwell
16+
- zaimwa9
1417

1518
ignored-authors:
1619
- renovate-bot

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ edition = "2024"
1212
members = [
1313
"crates/env-var",
1414
"crates/flagd",
15+
"crates/flagsmith",
1516
"crates/flipt",
1617
"crates/ofrep"
1718
]

crates/flagsmith/CHANGELOG.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2025-11-19
9+
10+
### Added
11+
12+
- Initial release of the Flagsmith OpenFeature provider
13+
- Support for all five OpenFeature flag types:
14+
- Boolean flags via `resolve_bool_value`
15+
- String flags via `resolve_string_value`
16+
- Integer flags via `resolve_int_value`
17+
- Float flags via `resolve_float_value`
18+
- Structured (JSON) flags via `resolve_struct_value`
19+
- Environment-level flag evaluation (without targeting)
20+
- Identity-specific flag evaluation (with targeting key and traits)
21+
- Automatic conversion of OpenFeature context to Flagsmith traits
22+
- Local evaluation mode support (requires server-side key)
23+
- Comprehensive error handling and mapping:
24+
- `FlagNotFound` for missing flags
25+
- `ProviderNotReady` for API/network errors
26+
- `TypeMismatch` for type conversion errors
27+
- `ParseError` for JSON/value parsing errors
28+
- OpenFeature reason code support:
29+
- `Static` for environment-level evaluation
30+
- `TargetingMatch` for identity-specific evaluation
31+
- `Disabled` for disabled flags
32+
- Configuration options:
33+
- Custom API URL
34+
- Request timeout
35+
- Local evaluation mode
36+
- Analytics tracking
37+
- Custom HTTP headers
38+
- Comprehensive unit tests
39+
- Full documentation and examples
40+
41+
### Dependencies
42+
43+
- `open-feature` 0.2.x
44+
- `flagsmith` (local path to Rust SDK)
45+
- `tokio` 1.x
46+
- `async-trait` 0.1.x
47+
- `thiserror` 2.0.x
48+
- `serde_json` 1.0.x
49+
- `tracing` 0.1.x
50+
51+
[0.1.0]: https://github.com/open-feature/rust-sdk-contrib/releases/tag/open-feature-flagsmith-v0.1.0

crates/flagsmith/Cargo.toml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[package]
2+
name = "open-feature-flagsmith"
3+
version = "0.1.0"
4+
authors = ["OpenFeature Maintainers"]
5+
edition = "2024"
6+
license = "Apache-2.0"
7+
description = "Flagsmith provider for OpenFeature"
8+
homepage = "https://openfeature.dev"
9+
repository = "https://github.com/open-feature/rust-sdk-contrib"
10+
readme = "README.md"
11+
categories = ["config", "api-bindings"]
12+
keywords = ["openfeature", "feature-flags", "flagsmith"]
13+
14+
[dependencies]
15+
# OpenFeature SDK
16+
open-feature = "0.2"
17+
18+
# Flagsmith SDK
19+
flagsmith = "2.1"
20+
21+
# Async runtime
22+
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
23+
async-trait = "0.1"
24+
25+
# Error handling
26+
thiserror = "1.0"
27+
28+
# Serialization
29+
serde = { version = "1.0", features = ["derive"] }
30+
serde_json = "1.0"
31+
32+
# Logging
33+
tracing = "0.1"
34+
35+
# URL validation
36+
url = "2.0"
37+
38+
# HTTP client (for HeaderMap type)
39+
reqwest = { version = "0.11", default-features = false }
40+
41+
# Flagsmith flag engine types (must match flagsmith version)
42+
flagsmith-flag-engine = "0.5"
43+
44+
[dev-dependencies]
45+
# Testing
46+
test-log = "0.2"
47+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

crates/flagsmith/README.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Flagsmith Provider for OpenFeature
2+
3+
A Rust implementation of the OpenFeature provider for Flagsmith, enabling dynamic feature flag evaluation using the Flagsmith platform.
4+
5+
This provider integrates the [Flagsmith Rust SDK](https://github.com/Flagsmith/flagsmith-rust-client) with [OpenFeature](https://openfeature.dev/), supporting both environment-level and identity-specific flag evaluation with trait-based targeting.
6+
7+
## Features
8+
9+
- **Environment-level evaluation**: Evaluate flags at the environment level without user context
10+
- **Identity-specific evaluation**: Target users with personalized flag values based on traits
11+
- **Type safety**: Full support for boolean, string, integer, float, and structured (JSON) flag types
12+
- **Local evaluation**: Optional local evaluation mode for improved performance and offline support
13+
- **Async support**: Built on Tokio with non-blocking flag evaluations
14+
15+
## Installation
16+
17+
Add the dependency in your `Cargo.toml`:
18+
```bash
19+
cargo add open-feature-flagsmith
20+
cargo add open-feature
21+
```
22+
23+
## Basic Usage
24+
25+
```rust
26+
use open_feature::OpenFeature;
27+
use open_feature::EvaluationContext;
28+
use open_feature_flagsmith::{FlagsmithProvider, FlagsmithOptions};
29+
30+
#[tokio::main]
31+
async fn main() {
32+
// Initialize the provider
33+
let provider = FlagsmithProvider::new(
34+
"your-environment-key".to_string(),
35+
FlagsmithOptions::default()
36+
).await.unwrap();
37+
38+
// Set up OpenFeature API
39+
let mut api = OpenFeature::singleton_mut().await;
40+
api.set_provider(provider).await;
41+
let client = api.create_client();
42+
43+
// Evaluate a flag
44+
let context = EvaluationContext::default();
45+
let enabled = client
46+
.get_bool_value("my-feature", &context, None)
47+
.await
48+
.unwrap_or(false);
49+
50+
println!("Feature enabled: {}", enabled);
51+
}
52+
```
53+
54+
## Identity-Specific Evaluation
55+
56+
```rust
57+
use open_feature::EvaluationContext;
58+
59+
// Create context with targeting key and user traits
60+
let context = EvaluationContext::default()
61+
.with_targeting_key("user-123")
62+
.with_custom_field("email", "user@example.com")
63+
.with_custom_field("plan", "premium")
64+
.with_custom_field("age", 25);
65+
66+
let enabled = client
67+
.get_bool_value("premium-feature", &context, None)
68+
.await
69+
.unwrap_or(false);
70+
```
71+
72+
## Flag Types
73+
74+
```rust
75+
// Assuming you have set up the client as shown in the Basic Usage section
76+
let context = EvaluationContext::default();
77+
78+
// Boolean flags
79+
let enabled = client.get_bool_value("feature-toggle", &context, None).await.unwrap();
80+
81+
// String flags
82+
let theme = client.get_string_value("theme", &context, None).await.unwrap();
83+
84+
// Integer flags
85+
let max_items = client.get_int_value("max-items", &context, None).await.unwrap();
86+
87+
// Float flags
88+
let multiplier = client.get_float_value("price-multiplier", &context, None).await.unwrap();
89+
90+
// Structured flags (JSON objects)
91+
let config = client.get_object_value("config", &context, None).await.unwrap();
92+
```
93+
94+
## Local Evaluation
95+
96+
Local evaluation mode downloads the environment configuration and evaluates flags locally for better performance:
97+
98+
```rust
99+
use open_feature_flagsmith::FlagsmithOptions;
100+
101+
// Requires a server-side environment key (starts with "ser.")
102+
let provider = FlagsmithProvider::new(
103+
"ser.your-server-key".to_string(),
104+
FlagsmithOptions::default()
105+
.with_local_evaluation(true)
106+
).await.unwrap();
107+
```
108+
109+
**Benefits:**
110+
- Lower latency (no API calls per evaluation)
111+
- Works offline (uses cached environment)
112+
- Reduced API load
113+
114+
**Requirements:**
115+
- Server-side environment key (starts with `ser.`)
116+
- Initial API call to fetch environment
117+
- Periodic polling to refresh (default: 60s)
118+
119+
## Configuration Options
120+
121+
Configurations can be provided as constructor options:
122+
123+
| Option | Type | Default | Description |
124+
|--------|------|---------|-------------|
125+
| `api_url` | `Option<String>` | Flagsmith Edge API | Custom Flagsmith API endpoint |
126+
| `request_timeout_seconds` | `Option<u64>` | 10 | Request timeout in seconds |
127+
| `enable_local_evaluation` | `bool` | `false` | Enable local evaluation mode |
128+
| `environment_refresh_interval_mills` | `Option<u64>` | 60000 | Polling interval for local mode (ms) |
129+
| `enable_analytics` | `bool` | `false` | Enable analytics tracking |
130+
| `custom_headers` | `Option<HeaderMap>` | None | Custom HTTP headers |
131+
132+
### Example Configuration
133+
134+
```rust
135+
use open_feature_flagsmith::FlagsmithOptions;
136+
137+
let options = FlagsmithOptions::default()
138+
.with_local_evaluation(true)
139+
.with_analytics(true)
140+
.with_timeout(15);
141+
142+
let provider = FlagsmithProvider::new(
143+
"ser.your-key".to_string(),
144+
options
145+
).await.unwrap();
146+
```
147+
148+
## Evaluation Context Transformation
149+
150+
OpenFeature standardizes the evaluation context with a `targeting_key` and arbitrary custom fields. For Flagsmith:
151+
152+
- **`targeting_key`** → Flagsmith identity identifier
153+
- **`custom_fields`** → Flagsmith traits for segmentation
154+
155+
When a `targeting_key` is present, the provider performs identity-specific evaluation. Otherwise, it evaluates at the environment level.
156+
157+
## License
158+
159+
Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.

crates/flagsmith/src/error.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use thiserror::Error;
2+
3+
/// Error message returned by Flagsmith SDK when a flag is not found.
4+
///
5+
/// This constant matches the hardcoded error message in the Flagsmith Rust SDK v2.0
6+
/// (flagsmith/src/flagsmith/models.rs, Flags::get_flag method).
7+
/// When a flag key doesn't exist in the flags HashMap and no default_flag_handler
8+
/// is configured, the SDK returns a FlagsmithAPIError with this exact message.
9+
///
10+
/// Note: This is a known limitation of the current SDK error reporting. A more robust
11+
/// approach would be for the SDK to provide a structured error variant (e.g.,
12+
/// ErrorKind::FlagNotFound), but until that's available, we must rely on string matching.
13+
/// This matching approach is used by other Flagsmith provider implementations as well.
14+
const FLAGSMITH_FLAG_NOT_FOUND_MSG: &str = "API returned invalid response";
15+
16+
/// Custom error types for the Flagsmith provider.
17+
#[derive(Error, Debug, PartialEq)]
18+
pub enum FlagsmithError {
19+
/// Configuration error (invalid options during initialization)
20+
#[error("Configuration error: {0}")]
21+
Config(String),
22+
23+
/// API or network error (connection issues, timeouts, etc.)
24+
#[error("API error: {0}")]
25+
Api(String),
26+
27+
/// Flag evaluation error (flag not found, type mismatch, etc.)
28+
#[error("Evaluation error: {0}")]
29+
Evaluation(String),
30+
31+
/// Flag not found error
32+
#[error("Flag not found: {0}")]
33+
FlagNotFound(String),
34+
}
35+
36+
/// Convert Flagsmith SDK errors to FlagsmithError
37+
impl From<flagsmith::error::Error> for FlagsmithError {
38+
fn from(error: flagsmith::error::Error) -> Self {
39+
match error.kind {
40+
flagsmith::error::ErrorKind::FlagsmithAPIError => {
41+
// Check if this is a "flag not found" error by matching the SDK's error message
42+
if error.msg == FLAGSMITH_FLAG_NOT_FOUND_MSG {
43+
FlagsmithError::FlagNotFound(error.msg)
44+
} else {
45+
FlagsmithError::Api(error.msg)
46+
}
47+
}
48+
flagsmith::error::ErrorKind::FlagsmithClientError => {
49+
FlagsmithError::Evaluation(error.msg)
50+
}
51+
}
52+
}
53+
}
54+
55+
/// Convert URL parse errors to FlagsmithError
56+
impl From<url::ParseError> for FlagsmithError {
57+
fn from(error: url::ParseError) -> Self {
58+
FlagsmithError::Config(format!("Invalid URL: {}", error))
59+
}
60+
}
61+
62+
/// Map FlagsmithError to OpenFeature EvaluationError
63+
impl From<FlagsmithError> for open_feature::EvaluationError {
64+
fn from(error: FlagsmithError) -> Self {
65+
use open_feature::EvaluationErrorCode;
66+
67+
match error {
68+
FlagsmithError::Config(msg) => open_feature::EvaluationError {
69+
code: EvaluationErrorCode::General("Configuration error".to_string()),
70+
message: Some(msg),
71+
},
72+
FlagsmithError::Api(msg) => open_feature::EvaluationError {
73+
code: EvaluationErrorCode::ProviderNotReady,
74+
message: Some(msg),
75+
},
76+
FlagsmithError::Evaluation(msg) => open_feature::EvaluationError {
77+
code: EvaluationErrorCode::General("Evaluation error".to_string()),
78+
message: Some(msg),
79+
},
80+
FlagsmithError::FlagNotFound(msg) => open_feature::EvaluationError {
81+
code: EvaluationErrorCode::FlagNotFound,
82+
message: Some(msg),
83+
},
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)