-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add-flagsmith-provider-crate #82
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
Merged
toddbaert
merged 23 commits into
open-feature:main
from
Zaimwa9:feat/add-flagsmith-provider
Dec 12, 2025
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
b156c5c
feat: add-flagsmith-crates
Zaimwa9 8d86acc
feat: updated-context
Zaimwa9 545882a
Update crates/flagsmith/README.md
Zaimwa9 e727d9f
feat: run-linter
Zaimwa9 1e62f7c
Merge branch 'feat/add-flagsmith-provider' of github.com:Zaimwa9/rust…
Zaimwa9 cff5d47
feat: removed-context-file
Zaimwa9 ec1d504
Update crates/flagsmith/Cargo.toml
Zaimwa9 f2e89bb
feat: deduplicated-code-to-get-flags
Zaimwa9 c596d3c
Merge branch 'feat/add-flagsmith-provider' of github.com:Zaimwa9/rust…
Zaimwa9 4e9180d
feat: added-tests-to-catch-error-sdk-changes
Zaimwa9 027636a
feat: handle-non-supported-traits-in-context
Zaimwa9 88dead0
feat: restructured-tests
Zaimwa9 a3fa60b
feat: linter
Zaimwa9 6b1b75f
feat: use-spawn-blocking-for-client-init
Zaimwa9 16e9e61
Update crates/flagsmith/README.md
Zaimwa9 7621b0b
Update crates/flagsmith/Cargo.toml
Zaimwa9 1769943
feat: check-struct-type
Zaimwa9 af6eebf
feat: linter
Zaimwa9 8d8a128
feat: fixed-null-versus-empty-strings
Zaimwa9 2803f6e
Merge branch 'main' of github.com:Zaimwa9/rust-sdk-contrib into feat/…
Zaimwa9 8ff1949
feat: added-component-owner
Zaimwa9 b2eee75
feat: added-component-owner
Zaimwa9 9ce227f
feat: bumped-version-to-flagsmith-sdk-2.1
Zaimwa9 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to this project will be documented in this file. | ||
|
|
||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [0.1.0] - 2025-11-19 | ||
|
|
||
| ### Added | ||
|
|
||
| - Initial release of the Flagsmith OpenFeature provider | ||
| - Support for all five OpenFeature flag types: | ||
| - Boolean flags via `resolve_bool_value` | ||
| - String flags via `resolve_string_value` | ||
| - Integer flags via `resolve_int_value` | ||
| - Float flags via `resolve_float_value` | ||
| - Structured (JSON) flags via `resolve_struct_value` | ||
| - Environment-level flag evaluation (without targeting) | ||
| - Identity-specific flag evaluation (with targeting key and traits) | ||
| - Automatic conversion of OpenFeature context to Flagsmith traits | ||
| - Local evaluation mode support (requires server-side key) | ||
| - Comprehensive error handling and mapping: | ||
| - `FlagNotFound` for missing flags | ||
| - `ProviderNotReady` for API/network errors | ||
| - `TypeMismatch` for type conversion errors | ||
| - `ParseError` for JSON/value parsing errors | ||
| - OpenFeature reason code support: | ||
| - `Static` for environment-level evaluation | ||
| - `TargetingMatch` for identity-specific evaluation | ||
| - `Disabled` for disabled flags | ||
| - Configuration options: | ||
| - Custom API URL | ||
| - Request timeout | ||
| - Local evaluation mode | ||
| - Analytics tracking | ||
| - Custom HTTP headers | ||
| - Comprehensive unit tests | ||
| - Full documentation and examples | ||
|
|
||
| ### Dependencies | ||
|
|
||
| - `open-feature` 0.2.x | ||
| - `flagsmith` (local path to Rust SDK) | ||
| - `tokio` 1.x | ||
| - `async-trait` 0.1.x | ||
| - `thiserror` 2.0.x | ||
| - `serde_json` 1.0.x | ||
| - `tracing` 0.1.x | ||
|
|
||
| [0.1.0]: https://github.com/open-feature/rust-sdk-contrib/releases/tag/open-feature-flagsmith-v0.1.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| [package] | ||
| name = "open-feature-flagsmith" | ||
| version = "0.1.0" | ||
| authors = ["OpenFeature Maintainers"] | ||
| edition = "2024" | ||
| license = "Apache-2.0" | ||
| description = "Flagsmith provider for OpenFeature" | ||
| homepage = "https://openfeature.dev" | ||
| repository = "https://github.com/open-feature/rust-sdk-contrib" | ||
| readme = "README.md" | ||
| categories = ["config", "api-bindings"] | ||
| keywords = ["openfeature", "feature-flags", "flagsmith"] | ||
|
|
||
| [dependencies] | ||
| # OpenFeature SDK | ||
| open-feature = "0.2" | ||
|
|
||
| # Flagsmith SDK | ||
| flagsmith = "2.1" | ||
|
|
||
| # Async runtime | ||
| tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } | ||
| async-trait = "0.1" | ||
|
|
||
| # Error handling | ||
| thiserror = "1.0" | ||
|
|
||
| # Serialization | ||
| serde = { version = "1.0", features = ["derive"] } | ||
| serde_json = "1.0" | ||
|
|
||
| # Logging | ||
| tracing = "0.1" | ||
|
|
||
| # URL validation | ||
| url = "2.0" | ||
|
|
||
| # HTTP client (for HeaderMap type) | ||
| reqwest = { version = "0.11", default-features = false } | ||
|
|
||
| # Flagsmith flag engine types (must match flagsmith version) | ||
| flagsmith-flag-engine = "0.5" | ||
|
|
||
| [dev-dependencies] | ||
| # Testing | ||
| test-log = "0.2" | ||
| tracing-subscriber = { version = "0.3", features = ["env-filter"] } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| # Flagsmith Provider for OpenFeature | ||
|
|
||
| A Rust implementation of the OpenFeature provider for Flagsmith, enabling dynamic feature flag evaluation using the Flagsmith platform. | ||
|
|
||
| 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. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Environment-level evaluation**: Evaluate flags at the environment level without user context | ||
| - **Identity-specific evaluation**: Target users with personalized flag values based on traits | ||
| - **Type safety**: Full support for boolean, string, integer, float, and structured (JSON) flag types | ||
| - **Local evaluation**: Optional local evaluation mode for improved performance and offline support | ||
| - **Async support**: Built on Tokio with non-blocking flag evaluations | ||
|
|
||
| ## Installation | ||
|
|
||
| Add the dependency in your `Cargo.toml`: | ||
| ```bash | ||
| cargo add open-feature-flagsmith | ||
| cargo add open-feature | ||
| ``` | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ```rust | ||
| use open_feature::OpenFeature; | ||
| use open_feature::EvaluationContext; | ||
| use open_feature_flagsmith::{FlagsmithProvider, FlagsmithOptions}; | ||
|
|
||
| #[tokio::main] | ||
| async fn main() { | ||
| // Initialize the provider | ||
| let provider = FlagsmithProvider::new( | ||
| "your-environment-key".to_string(), | ||
| FlagsmithOptions::default() | ||
| ).await.unwrap(); | ||
|
|
||
| // Set up OpenFeature API | ||
| let mut api = OpenFeature::singleton_mut().await; | ||
| api.set_provider(provider).await; | ||
| let client = api.create_client(); | ||
|
|
||
| // Evaluate a flag | ||
| let context = EvaluationContext::default(); | ||
| let enabled = client | ||
| .get_bool_value("my-feature", &context, None) | ||
| .await | ||
| .unwrap_or(false); | ||
|
|
||
| println!("Feature enabled: {}", enabled); | ||
| } | ||
| ``` | ||
|
|
||
| ## Identity-Specific Evaluation | ||
|
|
||
| ```rust | ||
| use open_feature::EvaluationContext; | ||
|
|
||
| // Create context with targeting key and user traits | ||
| let context = EvaluationContext::default() | ||
| .with_targeting_key("user-123") | ||
| .with_custom_field("email", "user@example.com") | ||
| .with_custom_field("plan", "premium") | ||
| .with_custom_field("age", 25); | ||
|
|
||
| let enabled = client | ||
| .get_bool_value("premium-feature", &context, None) | ||
| .await | ||
| .unwrap_or(false); | ||
| ``` | ||
|
|
||
| ## Flag Types | ||
|
|
||
| ```rust | ||
Zaimwa9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Assuming you have set up the client as shown in the Basic Usage section | ||
| let context = EvaluationContext::default(); | ||
|
|
||
| // Boolean flags | ||
| let enabled = client.get_bool_value("feature-toggle", &context, None).await.unwrap(); | ||
|
|
||
| // String flags | ||
| let theme = client.get_string_value("theme", &context, None).await.unwrap(); | ||
|
|
||
| // Integer flags | ||
| let max_items = client.get_int_value("max-items", &context, None).await.unwrap(); | ||
|
|
||
| // Float flags | ||
| let multiplier = client.get_float_value("price-multiplier", &context, None).await.unwrap(); | ||
|
|
||
| // Structured flags (JSON objects) | ||
| let config = client.get_object_value("config", &context, None).await.unwrap(); | ||
| ``` | ||
|
|
||
| ## Local Evaluation | ||
|
|
||
| Local evaluation mode downloads the environment configuration and evaluates flags locally for better performance: | ||
|
|
||
| ```rust | ||
| use open_feature_flagsmith::FlagsmithOptions; | ||
|
|
||
| // Requires a server-side environment key (starts with "ser.") | ||
| let provider = FlagsmithProvider::new( | ||
| "ser.your-server-key".to_string(), | ||
| FlagsmithOptions::default() | ||
| .with_local_evaluation(true) | ||
| ).await.unwrap(); | ||
| ``` | ||
|
|
||
| **Benefits:** | ||
| - Lower latency (no API calls per evaluation) | ||
| - Works offline (uses cached environment) | ||
| - Reduced API load | ||
|
|
||
| **Requirements:** | ||
| - Server-side environment key (starts with `ser.`) | ||
| - Initial API call to fetch environment | ||
| - Periodic polling to refresh (default: 60s) | ||
|
|
||
| ## Configuration Options | ||
|
|
||
| Configurations can be provided as constructor options: | ||
|
|
||
| | Option | Type | Default | Description | | ||
| |--------|------|---------|-------------| | ||
| | `api_url` | `Option<String>` | Flagsmith Edge API | Custom Flagsmith API endpoint | | ||
| | `request_timeout_seconds` | `Option<u64>` | 10 | Request timeout in seconds | | ||
| | `enable_local_evaluation` | `bool` | `false` | Enable local evaluation mode | | ||
| | `environment_refresh_interval_mills` | `Option<u64>` | 60000 | Polling interval for local mode (ms) | | ||
| | `enable_analytics` | `bool` | `false` | Enable analytics tracking | | ||
| | `custom_headers` | `Option<HeaderMap>` | None | Custom HTTP headers | | ||
|
|
||
| ### Example Configuration | ||
|
|
||
| ```rust | ||
| use open_feature_flagsmith::FlagsmithOptions; | ||
|
|
||
| let options = FlagsmithOptions::default() | ||
| .with_local_evaluation(true) | ||
| .with_analytics(true) | ||
| .with_timeout(15); | ||
|
|
||
| let provider = FlagsmithProvider::new( | ||
| "ser.your-key".to_string(), | ||
| options | ||
| ).await.unwrap(); | ||
| ``` | ||
|
|
||
| ## Evaluation Context Transformation | ||
|
|
||
| OpenFeature standardizes the evaluation context with a `targeting_key` and arbitrary custom fields. For Flagsmith: | ||
|
|
||
| - **`targeting_key`** → Flagsmith identity identifier | ||
| - **`custom_fields`** → Flagsmith traits for segmentation | ||
|
|
||
| When a `targeting_key` is present, the provider performs identity-specific evaluation. Otherwise, it evaluates at the environment level. | ||
|
|
||
| ## License | ||
|
|
||
| Apache 2.0 - See [LICENSE](./../../LICENSE) for more information. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| use thiserror::Error; | ||
|
|
||
| /// Error message returned by Flagsmith SDK when a flag is not found. | ||
Zaimwa9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// | ||
| /// This constant matches the hardcoded error message in the Flagsmith Rust SDK v2.0 | ||
| /// (flagsmith/src/flagsmith/models.rs, Flags::get_flag method). | ||
| /// When a flag key doesn't exist in the flags HashMap and no default_flag_handler | ||
| /// is configured, the SDK returns a FlagsmithAPIError with this exact message. | ||
| /// | ||
| /// Note: This is a known limitation of the current SDK error reporting. A more robust | ||
| /// approach would be for the SDK to provide a structured error variant (e.g., | ||
| /// ErrorKind::FlagNotFound), but until that's available, we must rely on string matching. | ||
| /// This matching approach is used by other Flagsmith provider implementations as well. | ||
| const FLAGSMITH_FLAG_NOT_FOUND_MSG: &str = "API returned invalid response"; | ||
|
|
||
| /// Custom error types for the Flagsmith provider. | ||
| #[derive(Error, Debug, PartialEq)] | ||
| pub enum FlagsmithError { | ||
| /// Configuration error (invalid options during initialization) | ||
| #[error("Configuration error: {0}")] | ||
| Config(String), | ||
|
|
||
| /// API or network error (connection issues, timeouts, etc.) | ||
| #[error("API error: {0}")] | ||
| Api(String), | ||
|
|
||
| /// Flag evaluation error (flag not found, type mismatch, etc.) | ||
| #[error("Evaluation error: {0}")] | ||
| Evaluation(String), | ||
|
|
||
| /// Flag not found error | ||
| #[error("Flag not found: {0}")] | ||
| FlagNotFound(String), | ||
| } | ||
|
|
||
| /// Convert Flagsmith SDK errors to FlagsmithError | ||
| impl From<flagsmith::error::Error> for FlagsmithError { | ||
| fn from(error: flagsmith::error::Error) -> Self { | ||
| match error.kind { | ||
| flagsmith::error::ErrorKind::FlagsmithAPIError => { | ||
| // Check if this is a "flag not found" error by matching the SDK's error message | ||
| if error.msg == FLAGSMITH_FLAG_NOT_FOUND_MSG { | ||
| FlagsmithError::FlagNotFound(error.msg) | ||
| } else { | ||
| FlagsmithError::Api(error.msg) | ||
| } | ||
| } | ||
| flagsmith::error::ErrorKind::FlagsmithClientError => { | ||
| FlagsmithError::Evaluation(error.msg) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Convert URL parse errors to FlagsmithError | ||
| impl From<url::ParseError> for FlagsmithError { | ||
| fn from(error: url::ParseError) -> Self { | ||
| FlagsmithError::Config(format!("Invalid URL: {}", error)) | ||
| } | ||
| } | ||
|
|
||
| /// Map FlagsmithError to OpenFeature EvaluationError | ||
| impl From<FlagsmithError> for open_feature::EvaluationError { | ||
| fn from(error: FlagsmithError) -> Self { | ||
| use open_feature::EvaluationErrorCode; | ||
|
|
||
| match error { | ||
| FlagsmithError::Config(msg) => open_feature::EvaluationError { | ||
| code: EvaluationErrorCode::General("Configuration error".to_string()), | ||
| message: Some(msg), | ||
| }, | ||
| FlagsmithError::Api(msg) => open_feature::EvaluationError { | ||
| code: EvaluationErrorCode::ProviderNotReady, | ||
| message: Some(msg), | ||
| }, | ||
| FlagsmithError::Evaluation(msg) => open_feature::EvaluationError { | ||
| code: EvaluationErrorCode::General("Evaluation error".to_string()), | ||
| message: Some(msg), | ||
| }, | ||
| FlagsmithError::FlagNotFound(msg) => open_feature::EvaluationError { | ||
| code: EvaluationErrorCode::FlagNotFound, | ||
| message: Some(msg), | ||
| }, | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.