diff --git a/README.md b/README.md index 1a624cf10..31993cfe8 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Superposition ships two integration surfaces: | Rust | [![Crates.io Version](https://img.shields.io/crates/v/superposition_sdk?color=green&label=superposition_sdk)](https://crates.io/crates/superposition_sdk) | [![Crates.io Version](https://img.shields.io/crates/v/superposition_provider?color=green&label=superposition_provider)](https://crates.io/crates/superposition_provider) | | JavaScript | [![NPM Version](https://img.shields.io/npm/v/superposition-sdk?color=green&label=superposition-sdk)](https://www.npmjs.com/package/superposition-sdk) | [![NPM Version](https://img.shields.io/npm/v/superposition-provider?color=green&label=superposition-provider)](https://www.npmjs.com/package/superposition-provider) | | Python | [![PyPI - Version](https://img.shields.io/pypi/v/superposition_sdk?color=green&label=superposition_sdk)](https://pypi.org/project/superposition-sdk/) | [![PyPI - Version](https://img.shields.io/pypi/v/superposition_provider?color=green&label=superposition_provider)](https://pypi.org/project/superposition-provider/) | -| Java | [![Maven Central Version](https://img.shields.io/maven-central/v/io.juspay.superposition/sdk?label=io.juspay.superposition.sdk&color=green)](https://central.sonatype.com/artifact/io.juspay.superposition/sdk) | [![Maven Central Version](https://img.shields.io/maven-central/v/io.juspay.superposition/openfeature-provider?label=io.juspay.superposition.openfeature-provider&color=green)](https://central.sonatype.com/artifact/io.juspay.superposition/openfeature-provider) | +| Java | [![Maven Central Version](https://img.shields.io/maven-central/v/io.juspay.superposition/sdk?label=io.juspay.superposition.sdk&color=green)](https://central.sonatype.com/artifact/io.juspay.superposition/sdk) | [`io.juspay.superposition.openfeature:superposition-provider`](https://juspay.io/superposition/docs/providers/openfeature/java) | | Haskell | [![Haskell SDK](https://img.shields.io/badge/source-SuperpositionSDK-green)](https://github.com/juspay/superposition/tree/main/clients/haskell/sdk) | [![Haskell Provider](https://img.shields.io/badge/source-superposition--open--feature--provider-green)](https://github.com/juspay/superposition/tree/main/clients/haskell/open-feature-provider) | | Go | TBD | TBD | diff --git a/docs/docs/applications/cac-redis-module.md b/docs/docs/applications/cac-redis-module.md index 1d8191060..29390be2b 100644 --- a/docs/docs/applications/cac-redis-module.md +++ b/docs/docs/applications/cac-redis-module.md @@ -8,10 +8,10 @@ The `cac_redis_module` extends Redis with custom commands that allow clients to ## Features -- **Context-Aware Configuration**: Resolve configurations based on dynamic context conditions using JSONLogic +- **Context-Aware Configuration**: Resolve configurations based on dynamic context conditions using Superposition's JSONLogic-compatible evaluator - **Redis Integration**: Access configuration resolution through Redis commands - **Real-time Configuration**: Load and evaluate configurations from local JSON files -- **Multiple Merge Strategies**: Support for MERGE and REPLACE strategies when applying overrides +- **MERGE Resolution**: `CAC.EVAL` applies selected overrides with the `MERGE` strategy. `REPLACE` exists in the internal evaluator, but is not exposed by the Redis command. - **Error Handling**: Comprehensive error reporting with timestamps and detailed messages ## Architecture @@ -38,12 +38,14 @@ git clone https://github.com/juspay/superposition.git cd superposition/examples/cac_redis_module ``` -2. Build the Redis module: +2. As checked in today, this example is under the repository workspace but is not listed in the root `workspace.members`. Before running Cargo commands from this directory, either add `examples/cac_redis_module` to the root workspace members or make the example an independent workspace by adding an empty `[workspace]` table to `examples/cac_redis_module/Cargo.toml`. + +3. Build the Redis module: ```bash cargo build --release ``` -3. The compiled module will be available at `target/release/libredis_module_cac.so` (Linux) or `target/release/libredis_module_cac.dylib` (macOS). +4. The compiled module is emitted to Cargo's target directory. In a root workspace build, that is typically `/target/release/libredis_module_cac.so` on Linux or `/target/release/libredis_module_cac.dylib` on macOS. In a standalone example workspace, the same files are under `target/release/` in the example directory. ### Loading the Module @@ -223,7 +225,7 @@ The module provides detailed error messages for various scenarios: ## Performance Considerations - **File I/O**: Configuration is loaded from disk on each evaluation. For production use, consider caching mechanisms -- **JSON Parsing**: JSONLogic evaluation is performed for each context condition +- **Condition Evaluation**: The Superposition logic helper evaluates each context condition - **Memory Usage**: Module keeps minimal state; most data is processed per request ## Development @@ -234,8 +236,7 @@ The module uses these key dependencies: - `redis-module`: Redis module SDK for Rust - `serde_json`: JSON serialization/deserialization -- `jsonlogic`: JSONLogic rule evaluation engine -- `superposition_types`: Superposition's type definitions +- `superposition_types`: Superposition's shared types and condition evaluation helpers ### Extending the Module @@ -304,8 +305,8 @@ redis-cli> CAC.EVAL '{"test": "value"}' - The module reads configuration files from the local filesystem - No authentication is performed on Redis commands (use Redis AUTH if needed) -- JSONLogic evaluation should be considered when exposing user-controllable context data +- Condition evaluation should be considered when exposing user-controllable context data ## Source Code -The complete source code for this example is available in the [Superposition repository](https://github.com/juspay/superposition/tree/main/examples/cac_redis_module). \ No newline at end of file +The complete source code for this example is available in the [Superposition repository](https://github.com/juspay/superposition/tree/main/examples/cac_redis_module). diff --git a/docs/docs/applications/k8s-staggered-releaser.md b/docs/docs/applications/k8s-staggered-releaser.md index d2fd1c2f4..2e7225e95 100644 --- a/docs/docs/applications/k8s-staggered-releaser.md +++ b/docs/docs/applications/k8s-staggered-releaser.md @@ -1,6 +1,6 @@ # Kubernetes Staggered Releaser (Mayday) -A Kubernetes deployment automation tool that leverages Superposition's experimentation features to perform safe, gradual rollouts with automatic traffic management and rollback capabilities. +A Kubernetes deployment automation example that leverages Superposition's experimentation events to perform gradual rollouts with NGINX Ingress traffic management. ## Overview @@ -13,7 +13,7 @@ The `k8s-staggered-releaser` (internally named "Mayday") is a webhook-driven dep - **Automatic Resource Management**: Creates and manages Kubernetes Deployments, Services, and Ingresses - **Multi-Namespace Support**: Deploy to different Kubernetes namespaces based on context - **Traffic Management**: Uses NGINX Ingress Controller for weighted traffic distribution -- **Rollback Capability**: Automatic cleanup and rollback when experiments conclude +- **Conclusion Cleanup**: Updates the main Service for the chosen variant and deletes experimental Services and Ingresses when experiments conclude. Deployment deletion is not implemented in the checked-in example. - **Configuration Integration**: Fetches deployment configurations from Superposition ## Architecture @@ -31,7 +31,7 @@ The `k8s-staggered-releaser` (internally named "Mayday") is a webhook-driven dep 1. **Experiment Started**: Creates new Deployment, Service, and Ingress resources 2. **Experiment In Progress**: Updates traffic weights for gradual rollout -3. **Experiment Concluded**: Promotes winning variant and cleans up resources +3. **Experiment Concluded**: Promotes the winning variant by updating the main Service, then deletes experimental Services and Ingresses ## Installation @@ -56,12 +56,14 @@ pub const K8S_API_SERVER: &str = "https://your-k8s-api-server:6443"; pub const TOKEN: &str = "your-k8s-api-token"; ``` -3. Build the application: +3. As checked in today, this example is under the repository workspace but is not listed in the root `workspace.members`. Before running Cargo commands from this directory, either add `examples/k8s-staggered-releaser` to the root workspace members or make the example an independent workspace by adding an empty `[workspace]` table to `examples/k8s-staggered-releaser/Cargo.toml`. + +4. Build the application: ```bash cargo build --release ``` -4. Run the webhook server: +5. Run the webhook server: ```bash cargo run ``` @@ -70,13 +72,13 @@ The server will start on `127.0.0.1:8090` and listen for webhook events at `/hi` ## Configuration -### Environment Variables +### Source Constants -You can configure the following constants in `src/utils.rs`: +The checked-in example reads these values as constants in `src/utils.rs`, not as environment variables: - `K8S_API_SERVER`: Kubernetes API server URL - `TOKEN`: Kubernetes API authentication token -- `NAMESPACE`: Default namespace for deployments +- `NAMESPACE`: Declared as `"default"`, but the webhook handlers derive the active namespace from `payload.context` through `get_namespace` ### Superposition Integration @@ -186,17 +188,17 @@ Triggered when an experiment ends with a chosen variant: **Actions Performed:** - Updates main service to point to winning variant deployment - Removes experimental Ingress resources -- Cleans up unused Services and Deployments +- Deletes experimental Ingresses and Services. Deployment cleanup is not implemented. ### Configuration Resolution The system fetches deployment configurations from Superposition using the `/config/resolve` endpoint: ``` -GET /config/resolve?namespace={namespace}&variantIds={variant_id} +GET http://localhost:8080/config/resolve?dimension[namespace]={namespace}&dimension[variantIds]={variant_id} Headers: x-org-id: localorg - x-tenant: {service_name} + x-tenant: {workspace_id} ``` Expected configuration format: @@ -235,11 +237,13 @@ metadata: nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "25" spec: + ingressClassName: nginx rules: - host: nginxservice.mumbai http: paths: - path: / + pathType: Prefix backend: service: name: nginxservice-service-variant-123 @@ -344,12 +348,7 @@ Common error scenarios and troubleshooting: ### Health Checks -Monitor the webhook endpoint: - -```bash -# Basic health check -curl http://127.0.0.1:8090/hi -``` +There is no separate health endpoint. The `/hi` route is a GET webhook receiver that expects a JSON request body matching the Superposition event shape. ## Advanced Configuration @@ -364,6 +363,8 @@ let app_state = Data::new(AppState { }); ``` +The checked-in handler stores this `AppState`, but does not currently validate webhook namespaces or tenants against those lists. + ### Custom Resource Templates Modify resource generation in respective modules: @@ -451,4 +452,4 @@ When contributing to this example: ## Source Code -The complete source code for this example is available in the [Superposition repository](https://github.com/juspay/superposition/tree/main/examples/k8s-staggered-releaser). \ No newline at end of file +The complete source code for this example is available in the [Superposition repository](https://github.com/juspay/superposition/tree/main/examples/k8s-staggered-releaser). diff --git a/docs/docs/basic-concepts/context-aware-config/default-config.mdx b/docs/docs/basic-concepts/context-aware-config/default-config.mdx index befe01113..f61942c58 100644 --- a/docs/docs/basic-concepts/context-aware-config/default-config.mdx +++ b/docs/docs/basic-concepts/context-aware-config/default-config.mdx @@ -38,4 +38,4 @@ Two functions that can be linked with default configs to ensure correctness: - Value Validation - Value Compute -To learn more about these, check out [functions](./basic-concepts/safety/functions.mdx) \ No newline at end of file +To learn more about these, check out [functions](../safety/functions.mdx) diff --git a/docs/docs/basic-concepts/context-aware-config/dimensions.mdx b/docs/docs/basic-concepts/context-aware-config/dimensions.mdx index 482e63490..45e20a509 100644 --- a/docs/docs/basic-concepts/context-aware-config/dimensions.mdx +++ b/docs/docs/basic-concepts/context-aware-config/dimensions.mdx @@ -84,7 +84,7 @@ Two functions that can be linked with dimensions to ensure correctness: - Value Validation - Value Compute -To learn more about these, check out [functions](./basic-concepts/safety/functions.mdx) +To learn more about these, check out [functions](../safety/functions.mdx) ## Cohort Dimensions @@ -209,4 +209,4 @@ Use a **Local Cohort** when: Use a **Remote Cohort** when: - The grouping logic requires **database lookups** or **external API calls**. - The logic is too complex for JSONLogic expressions. -- You need the cohort value to be computed fresh on each server-side resolution. \ No newline at end of file +- You need the cohort value to be computed fresh on each server-side resolution. diff --git a/docs/docs/basic-concepts/context-aware-config/how-all-this-works.mdx b/docs/docs/basic-concepts/context-aware-config/how-all-this-works.mdx index 6d85f61a2..07d152c56 100644 --- a/docs/docs/basic-concepts/context-aware-config/how-all-this-works.mdx +++ b/docs/docs/basic-concepts/context-aware-config/how-all-this-works.mdx @@ -91,4 +91,4 @@ _context_ = { city = "Bangalore" } per_km_rate = 22.0 ``` -Interested in writing better config files? Check out [SuperTOML](./docs/superposition-config-file/intro.md) \ No newline at end of file +Interested in writing better config files? Check out [SuperTOML](../../superposition-config-file/intro.md) diff --git a/docs/docs/basic-concepts/experimentation/experiment-groups.md b/docs/docs/basic-concepts/experimentation/experiment-groups.md index 93edb027a..a8b9755e3 100644 --- a/docs/docs/basic-concepts/experimentation/experiment-groups.md +++ b/docs/docs/basic-concepts/experimentation/experiment-groups.md @@ -6,7 +6,7 @@ description: Group experiments to prevent overlap and ensure isolated traffic al ## Introduction -When running multiple experiments, they can overlap with each other due to the cascading nature of [Context-Aware Configs](/docs/basic-concepts/how-all-this-works.mdx). This overlap makes it difficult to predict the final configuration a user receives, as multiple experiments may simultaneously modify the same config keys. +When running multiple experiments, they can overlap with each other due to the cascading nature of [Context-Aware Configs](../context-aware-config/how-all-this-works.mdx). This overlap makes it difficult to predict the final configuration a user receives, as multiple experiments may simultaneously modify the same config keys. **Experiment Groups** solve this by ensuring experiments within a group never overlap with each other. Each user is assigned to exactly one experiment (or none) within the group, providing predictable and isolated traffic allocation. @@ -124,4 +124,4 @@ Group B: context = [environment IS "production"] ## Related Concepts - [Experiments](/docs/basic-concepts/experimentation/experiments) - Understanding individual experiments -- [Context-Aware Configs](/docs/basic-concepts/context-aware-configs) - The foundation for experimentation \ No newline at end of file +- [Context-Aware Configs](../context-aware-config/how-all-this-works.mdx) - The foundation for experimentation diff --git a/docs/docs/basic-concepts/safety/type-templates.md b/docs/docs/basic-concepts/safety/type-templates.md index b7ef14398..70faa6024 100644 --- a/docs/docs/basic-concepts/safety/type-templates.md +++ b/docs/docs/basic-concepts/safety/type-templates.md @@ -90,9 +90,19 @@ Type templates are used when defining: **Example Usage in Default Config:** ```toml -[default-config] -per_km_rate = { "value" = 20.0, "schema" = { "$ref": "#/types/Decimal" } } -user_location = { "value" = {"lat": 0.0, "lng": 0.0}, "schema" = { "$ref": "#/types/GeographicCoordinate" } } +[default-configs] +per_km_rate = { value = 20.0, schema = { type = "number" } } +user_location = { + value = { lat = 0.0, lng = 0.0 }, + schema = { + type = "object", + properties = { + lat = { type = "number" }, + lng = { type = "number" } + }, + required = ["lat", "lng"] + } +} ``` #### Type Template Management @@ -103,4 +113,4 @@ Type templates support full lifecycle management: - **Versioning**: Track changes with audit trail (created_by, last_modified_by, change_reason) - **Updates**: Modify existing templates with automatic validation - **Dependencies**: Templates can reference other templates for composition -- **Deletion**: Remove unused templates with dependency checking \ No newline at end of file +- **Deletion**: Remove unused templates with dependency checking diff --git a/docs/docs/intro.mdx b/docs/docs/intro.mdx index e8ef09a29..ded5c2418 100644 --- a/docs/docs/intro.mdx +++ b/docs/docs/intro.mdx @@ -124,7 +124,7 @@ The following matrix contains the languages in which the above client libraries | Rust | [![Crates.io Version](https://img.shields.io/crates/v/superposition_sdk?color=green&label=superposition_sdk)](https://crates.io/crates/superposition_sdk) | [![Crates.io Version](https://img.shields.io/crates/v/superposition_provider?color=green&label=superposition_provider)](https://crates.io/crates/superposition_provider) | | Javascript | [![NPM Version](https://img.shields.io/npm/v/superposition-sdk?color=green&label=superposition-sdk)](https://www.npmjs.com/package/superposition-sdk) | [![NPM Version](https://img.shields.io/npm/v/superposition-provider?color=green&label=superposition-provider)](https://www.npmjs.com/package/superposition-provider) | | Python | [![PyPI - Version](https://img.shields.io/pypi/v/superposition_sdk?color=green&label=superposition_sdk)](https://pypi.org/project/superposition-sdk/) | [![PyPI - Version](https://img.shields.io/pypi/v/superposition_provider?color=green&label=superposition_provider)](https://pypi.org/project/superposition-provider/) | -| Java | [![Maven Central Version](https://img.shields.io/maven-central/v/io.juspay.superposition/sdk?label=io.juspay.superposition.sdk&color=green)](https://central.sonatype.com/artifact/io.juspay.superposition/sdk) | [![Maven Central Version](https://img.shields.io/maven-central/v/io.juspay.superposition/openfeature-provider?label=io.juspay.superposition.openfeature-provider&color=green)](https://central.sonatype.com/artifact/io.juspay.superposition/openfeature-provider)| +| Java | [![Maven Central Version](https://img.shields.io/maven-central/v/io.juspay.superposition/sdk?label=io.juspay.superposition.sdk&color=green)](https://central.sonatype.com/artifact/io.juspay.superposition/sdk) | [`io.juspay.superposition.openfeature:superposition-provider`](./providers/openfeature/java.md) | | Haskell | ![Static Badge](https://img.shields.io/badge/SDK-Github-8A2BE2?style=flat&logoColor=purple&label=SDK&color=8A2BE2&cacheSeconds=30&link=https%3A%2F%2Fgithub.com%2Fjuspay%2Fsuperposition%2Ftree%2Fmain%2Fclients%2Fhaskell%2Fsdk) | ![Static Badge](https://img.shields.io/badge/SDK-Github-8A2BE2?style=flat&logoColor=purple&label=Provider&color=8A2BE2&cacheSeconds=30&link=https%3A%2F%2Fgithub.com%2Fjuspay%2Fsuperposition%2Ftree%2Fmain%2Fclients%2Fhaskell%2Fopen-feature-provider) | | Go | TBD | TBD | @@ -160,4 +160,3 @@ Superposition comes as a shot in the arm for any application that needs safe and ## Email us * [superposition@juspay.in](mailto:superposition@juspay.in) - diff --git a/docs/docs/providers/legacy/client_context_aware_configuration.md b/docs/docs/providers/legacy/client_context_aware_configuration.md index 9aab16d2f..f9fbb0bfd 100644 --- a/docs/docs/providers/legacy/client_context_aware_configuration.md +++ b/docs/docs/providers/legacy/client_context_aware_configuration.md @@ -67,11 +67,26 @@ Create a client in the factory. You can chose to use the result to check for err ##### Function definition ``` pub async fn create_client( + &self, tenant: String, polling_interval: Duration, hostname: String, ) -> Result, String> ``` + +To configure the local resolution cache explicitly, use: + +``` +pub async fn create_client_with_cache_properties( + &self, + tenant: String, + polling_interval: Duration, + hostname: String, + cache_max_capacity: u64, + cache_ttl: u64, + cache_tti: u64, + ) -> Result, String> +``` ##### Params | Param | type | description | Example value | | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | @@ -100,23 +115,23 @@ Below is the rust implementation to instantiate CAC client using the client fact ```rust use cac_client as cc; +use std::time::Duration; -let tenants: Vec = ["dev", "test"]; -//You can create a clientFactory +let tenants = ["dev", "test"]; for tenant in tenants { - cc::CLIENT_FACTORY + let client = cc::CLIENT_FACTORY .create_client( tenant.to_string(), - update_cac_periodically,//flag for if you want to update cac config periodically - polling_interval,//polling interval in secs, default is 60 - cac_hostname.to_string(),// superposition service host + Duration::from_secs(10), + cac_hostname.to_string(), ) .await .expect(format!("{}: Failed to acquire cac_client", tenant).as_str()); + tokio::spawn(client.clone().run_polling_updates()); }; -//You can extract an individual tenant's client from clientFactory + let tenant = "dev".to_owned(); -let cac_client = cc::CLIENT_FACTORY.get_client(tenant.clone()).map_err(|e| { +let cac_client = cc::CLIENT_FACTORY.get_client(tenant.clone()).await.map_err(|e| { log::error!("{}: {}", tenant.clone(), e); ErrorType::IgnoreError(format!("{}: Failed to get cac client", tenant)) })?; @@ -151,7 +166,11 @@ pub struct Config { ##### Funtion Definition ``` -pub fn get_full_config_state_with_filter(query_data: Option>) -> Result +pub async fn get_full_config_state_with_filter( + &self, + query_data: Option>, + prefix: Option>, + ) -> Result ``` #### Get the last modified Time @@ -161,24 +180,30 @@ CAC client lets you get the last modified time of your configs, in case you want ##### Function Definition ``` -pub fn get_last_modified() -> Result, String> +pub async fn get_last_modified(&self) -> DateTime ``` #### Evaluate Context to derive configs -Given a context, get overrides for a specific set of keys, if provided. If None is provided for `filter_keys`, all configs are returned. +Given a context, get overrides for a specific set of keys, if provided. If None is provided for `filter_keys`, all configs are returned. The Rust client also requires a `MergeStrategy`. ##### Function Definition ``` -pub fn get_resolved_config(context: Map, filter_keys: Option>) -> Result, String> +pub async fn get_resolved_config( + &self, + query_data: Map, + filter_keys: Option>, + merge_strategy: MergeStrategy, + ) -> Result, String> ``` ##### Params -| Param | type | description | Example value | -| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | -| `context` | `Map` | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | -| `filter_keys` | `Option>` | The keys for which you want the values. If empty, all configuration keys are returned | `Some([payment, network, color])` | +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `query_data` | `Map` | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `filter_keys` | `Option>` | The keys for which you want the values. If empty, all configuration keys are returned | `Some(vec!["payment".into(), "network".into()])` | +| `merge_strategy` | `MergeStrategy` | Merge mode to use while applying overrides | `MergeStrategy::MERGE` | #### Get Default Config @@ -187,7 +212,10 @@ The default config for a specific set of keys, if provided. If None is provided ##### Function Definition ``` -pub fn get_default_config(filter_keys: Option>) -> Result, String> +pub async fn get_default_config( + &self, + filter_keys: Option>, + ) -> ExtendedMap ``` ##### Param | Param | type | description | Example value | @@ -239,6 +267,12 @@ Create a new client in the Client Factory createCacClient:: Tenant -> Interval -> Hostname -> IO (Either Error ()) ``` +To configure cache properties explicitly: + +``` +createCacClientWithCacheProperties :: Tenant -> Interval -> Hostname -> Integer -> Integer -> Integer -> IO (Either Error ()) +``` + ##### Param | Param | type | description | Example value | @@ -316,12 +350,13 @@ getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) #### Evaluate Context to derive configs -Given a context, get overrides for a specific set of keys, if provided. If Nothing is provided for `filter_keys`, all configs are returned. +Given a context, get overrides for a specific set of keys, if provided. If Nothing is provided for `filter_keys`, all configs are returned. `getResolvedConfig` uses `MERGE`; use `getResolvedConfigWithStrategy` to pass `MERGE` or `REPLACE`. ##### Function Definition ``` -getResolvedConfig :: ForeignPtr CacClient -> String -> Maybe [String] -> IO (Either Error Value) +getResolvedConfig :: FromJSON a => ForeignPtr CacClient -> String -> Maybe [String] -> IO (Either Error a) +getResolvedConfigWithStrategy :: FromJSON a => ForeignPtr CacClient -> String -> Maybe [String] -> MergeStrategy -> IO (Either Error a) ``` ##### Params diff --git a/docs/docs/providers/legacy/client_experimentation.md b/docs/docs/providers/legacy/client_experimentation.md index a53c82db3..586aad5e2 100644 --- a/docs/docs/providers/legacy/client_experimentation.md +++ b/docs/docs/providers/legacy/client_experimentation.md @@ -61,16 +61,17 @@ Create a client in the factory. You can chose to use the result to check for err ##### Function definition ``` pub async fn create_client( + &self, tenant: String, - polling_interval: Duration, + poll_frequency: u64, hostname: String, ) -> Result, String> ``` ##### Params | Param | type | description | Example value | | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -| `tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | -| `polling_interval` | Duration | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `tenant` | String | specifies the tenant whose experiments will be loaded into the client from `hostname` | mjos | +| `poll_frequency` | u64 | specifies the time experimentation client waits, in seconds, before checking with the server for updates | 5 | | `hostname` | String | The URL of the superposition server | https://superposition.example.com | #### Get Client @@ -93,26 +94,23 @@ pub async fn get_client( Below is the rust implementation to instantiate Experimentation client . ```rust -use superpostion_client as sp; +use experimentation_client as exp; -let tenants: Vec = ["dev", "test"]; -//You can create a clientFactory +let tenants = ["dev", "test"]; for tenant in tenants { - rt::spawn( - sp::CLIENT_FACTORY - .create_client(tenant.to_string(), - poll_frequency,//How frequently you want to update config in secs - hostname.to_string()// superposition hostname - ) - .await - .expect(format!("{}: Failed to acquire experimentation_client", tenant).as_str()) - .clone() - .run_polling_updates(), - ); + let client = exp::CLIENT_FACTORY + .create_client( + tenant.to_string(), + 10, + hostname.to_string(), + ) + .await + .expect(format!("{}: Failed to acquire experimentation_client", tenant).as_str()); + tokio::spawn(client.clone().run_polling_updates()); }; -//You can extract an individual tenant's client from clientFactory + let tenant = "dev".to_owned(); -let sp_client = sp::CLIENT_FACTORY +let sp_client = exp::CLIENT_FACTORY .get_client(tenant.clone()) .await .map_err(|e| { @@ -127,7 +125,7 @@ let sp_client = sp::CLIENT_FACTORY #### Run polling for updates from Superposition Service -the Experimentation client polls for updates from the superposition service and loads any changes done on the server. This means that experiments changed in superposition are reflected on the client in the duration of `polling_interval`. `run_polling_updates()` should be run in a separate thread, as it does not terminate. +the Experimentation client polls for updates from the superposition service and loads any changes done on the server. This means that experiments changed in superposition are reflected on the client within the configured `poll_frequency`. `run_polling_updates()` should be run in a separate thread, as it does not terminate. ##### Function definition @@ -137,20 +135,28 @@ the Experimentation client polls for updates from the superposition service and #### Get an applicable variant -When experiments are running, you can get different variants of the experiment based on the `toss` value you provide. Superposition decides which bucket your request falls into based on this value, and returns an ID called the `variantId`. You can then include this in your CAC client request. +When experiments are running, you can get different variants of the experiment based on an identifier. The client hashes the identifier with each experiment group id to select a bucket, and returns the matching variant IDs. You can then include these in your CAC client request. -The toss can be a random number between -1 to 100. You can log the variantId so that your metrics can help you decide on a variant +The dimensions map is used to evaluate local cohorts before matching experiment contexts. The optional prefix filters config keys on satisfied experiments before variant selection. ##### Function Definition ``` -pub async fn get_applicable_variant(context: &Value, toss: i8) -> Vec +pub async fn get_applicable_variant( + &self, + dimensions_info: &HashMap, + context: &Map, + identifier: &str, + prefix: Option>, + ) -> Result, String> ``` ##### Params -| Param | type | description | Example value | -| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | -| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | -| `toss` | i8 | an integer that assigns your request to a variant | `4` | +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `dimensions_info` | `HashMap` | Dimension metadata used for local cohort evaluation | `HashMap::new()` | +| `context` | `Map` | The context under which you want to resolve variants | `{"os": "android", "merchant": "juspay"}` | +| `identifier` | `&str` | Stable identifier used for bucketing | `"user-123"` | +| `prefix` | `Option>` | Optional config-key prefix filter | `Some(vec!["payment".into()])` | #### Get satisfied experiments @@ -158,13 +164,31 @@ Rather than just getting the variant ID, you can get the whole experiment(s) tha ##### Function Definition ``` -pub async fn get_satisfied_experiments(context: &Value) -> Experiments +pub async fn get_satisfied_experiments( + &self, + context: &Map, + prefix: Option>, + ) -> Result ``` ##### Params -| Param | type | description | Example value | -| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | -| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | `Map` | The context under which you want to resolve experiments | `{"os": "android", "merchant": "juspay"}` | +| `prefix` | `Option>` | Optional config-key prefix filter | `None` | + +#### Get filtered satisfied experiments + +Evaluate experiments with unresolved local cohorts skipped, then return experiments that satisfy the context. + +##### Function Definition +``` +pub async fn get_filtered_satisfied_experiments( + &self, + context: &Map, + prefix: Option>, + ) -> Result +``` #### Get all running experiments @@ -172,7 +196,7 @@ Get all running experiments, why would you want to do this? We don't know. But y ##### Function Definition ``` -pub async fn get_running_experiments() -> Experiments +pub async fn get_running_experiments(&self) -> Result ``` ## Haskell @@ -190,8 +214,8 @@ createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) ##### Params | Param | type | description | Example value | | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | -| `Interval` | Integer | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `Tenant` | String | specifies the tenant whose experiments will be loaded into the client from `hostname` | mjos | +| `Interval` | Integer | specifies the time experimentation client waits, in seconds, before checking with the server for updates | 5 | | `Hostname` | String | The URL of the superposition server | https://superposition.example.com | #### Get Client @@ -219,20 +243,22 @@ the Experimentation client polls for updates from the superposition service and #### Get an applicable variant -When experiments are running, you can get different variants of the experiment based on the `toss` value you provide. Superposition decides which bucket your request falls into based on this value, and returns an ID called the `variantId`. You can then include this in your CAC client request. +When experiments are running, you can get different variants of the experiment based on a stable identifier. The dimensions JSON is used for local cohort evaluation, the context JSON is matched against experiments, and the optional prefix filters config keys. -The toss can be a random number between -1 to 100. You can log the variantId so that your metrics can help you decide on a variant +The function returns a JSON string containing the matching variant IDs. ##### Function Definition ``` -getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) +getApplicableVariants :: ForeignPtr ExpClient -> String -> String -> String -> Maybe String -> IO (Either Error String) ``` ##### Params -| Param | type | description | Example value | -| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | -| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | -| `toss` | Integer | an integer that assigns your request to a variant | `4` | +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `dimensions` | String | JSON object containing dimension metadata | `{}` | +| `context` | String | JSON object containing request context | `{"os": "android", "merchant": "juspay"}` | +| `identifier` | String | Stable identifier used for bucketing | `"user-123"` | +| `prefix` | Maybe String | Optional comma-separated config-key prefix filter | `Just "payment,network"` | #### Get satisfied experiments @@ -240,13 +266,22 @@ Rather than just getting the variant ID, you can get the whole experiment(s) tha ##### Function Definition ``` -getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) +getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> String -> Maybe String -> IO (Either Error Value) ``` ##### Params -| Param | type | description | Example value | -| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | -| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `dimensions` | String | JSON object containing dimension metadata | `{}` | +| `context` | String | JSON object containing request context | `{"os": "android", "merchant": "juspay"}` | +| `prefix` | Maybe String | Optional comma-separated config-key prefix filter | `Nothing` | + +#### Get filtered satisfied experiments + +##### Function Definition +``` +getFilteredSatisfiedExperiments :: ForeignPtr ExpClient -> String -> Maybe String -> Maybe String -> IO (Either Error Value) +``` #### Get all running experiments @@ -284,8 +319,8 @@ main = do where loop client = do runningExperiments <- getRunningExperiments client - satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" - variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 + satisfiedExperiments <- getSatisfiedExperiments client "{}" "{\"os\": \"android\", \"client\": \"1mg\"}" Nothing + variants <- getApplicableVariants client "{}" "{\"os\": \"android\", \"client\": \"1mg\"}" "1mg-android" Nothing print "Running experiments" print runningExperiments print "experiments that satisfy context" @@ -295,4 +330,4 @@ main = do -- threadDelay 10000000 loop client -``` \ No newline at end of file +``` diff --git a/docs/docs/providers/legacy/creating_client.md b/docs/docs/providers/legacy/creating_client.md index c2a77ba06..9a204f321 100644 --- a/docs/docs/providers/legacy/creating_client.md +++ b/docs/docs/providers/legacy/creating_client.md @@ -13,6 +13,7 @@ title: Creating a Client - [const char \*cac\_last\_error\_message(void)](#const-char-cac_last_error_messagevoid) - [void cac\_free\_string(char \*s)](#void-cac_free_stringchar-s) - [int cac\_new\_client(const char \*tenant, unsigned long update\_frequency, const char \*hostname)](#int-cac_new_clientconst-char-tenant-unsigned-long-update_frequency-const-char-hostname) + - [int cac\_new\_client\_with\_cache\_properties(const char \*tenant, unsigned long update\_frequency, const char \*hostname, unsigned long cache\_max\_capacity, unsigned long cache\_ttl, unsigned long cache\_tti)](#int-cac_new_client_with_cache_propertiesconst-char-tenant-unsigned-long-update_frequency-const-char-hostname-unsigned-long-cache_max_capacity-unsigned-long-cache_ttl-unsigned-long-cache_tti) - [void cac\_start\_polling\_update(const char \*tenant)](#void-cac_start_polling_updateconst-char-tenant) - [void cac\_free\_client(struct Arc\_Client \*ptr)](#void-cac_free_clientstruct-arc_client-ptr) - [struct Arc\_Client \*cac\_get\_client(const char \*tenant)](#struct-arc_client-cac_get_clientconst-char-tenant) @@ -31,9 +32,9 @@ title: Creating a Client - [void expt\_start\_polling\_update(const char \*tenant)](#void-expt_start_polling_updateconst-char-tenant) - [void expt\_free\_client(struct Arc\_Client \*ptr)](#void-expt_free_clientstruct-arc_client-ptr) - [struct Arc\_Client \*expt\_get\_client(const char \*tenant)](#struct-arc_client-expt_get_clientconst-char-tenant) - - [char \*expt\_get\_applicable\_variant(struct Arc\_Client \*client, const char \*c\_context, short toss)](#char-expt_get_applicable_variantstruct-arc_client-client-const-char-c_context-short-toss) - - [char \*expt\_get\_satisfied\_experiments(struct Arc\_Client \*client, const char \*c\_context, const char \*filter\_prefix)](#char-expt_get_satisfied_experimentsstruct-arc_client-client-const-char-c_context-const-char-filter_prefix) - - [char \*expt\_get\_filtered\_satisfied\_experiments(struct Arc\_Client \*client, const char \*c\_context, const char \*filter\_prefix)](#char-expt_get_filtered_satisfied_experimentsstruct-arc_client-client-const-char-c_context-const-char-filter_prefix) + - [char \*expt\_get\_applicable\_variant(struct Arc\_Client \*client, const char \*c\_dimensions, const char \*c\_context, const char \*identifier, const char \*filter\_prefix)](#char-expt_get_applicable_variantstruct-arc_client-client-const-char-c_dimensions-const-char-c_context-const-char-identifier-const-char-filter_prefix) + - [char \*expt\_get\_satisfied\_experiments(struct Arc\_Client \*client, const char \*c\_dimensions, const char \*c\_context, const char \*filter\_prefix)](#char-expt_get_satisfied_experimentsstruct-arc_client-client-const-char-c_dimensions-const-char-c_context-const-char-filter_prefix) + - [char \*expt\_get\_filtered\_satisfied\_experiments(struct Arc\_Client \*client, const char \*c\_dimensions, const char \*c\_context, const char \*filter\_prefix)](#char-expt_get_filtered_satisfied_experimentsstruct-arc_client-client-const-char-c_dimensions-const-char-c_context-const-char-filter_prefix) - [char \*expt\_get\_running\_experiments(struct Arc\_Client \*client)](#char-expt_get_running_experimentsstruct-arc_client-client) - [Testing with an Example](#testing-with-an-example-1) @@ -61,7 +62,7 @@ Thank you for considering adding support for a new language in superposition cli All C structs and functions are generated automatically when the `cac-client` written in rust is compiled. Superposition uses the crate `cbindgen` to do this. The compiler generates a `.h` header file and an object file specific to the target platform, for example a `.dll` for windows or `.so` for linux. Some tips for writing an FFI/ABI to these files: - All memory operations should be done by the rust segment of the code. You should always free memory for strings and clients using the inbuilt `free_*` functions -- You don't need to store the client on your implementation's end. Always use `get_cac_client` to get a pointer to a client to pass to other functions +- You don't need to store the client on your implementation's end. Always use `cac_get_client` to get a pointer to a client to pass to other functions - Remember, a client is created for a particular tenant - Check the header files in the `headers` directory for further documentation - Your client implementation should expect the object file that it dynamically links to be present in the same directory @@ -95,6 +96,13 @@ A function that takes a tenant name as string, the update frequency and the host Returns 0 if client was successfully initialized Returns 1 if an error occurred, use `cac_last_error_message` to get the error +### int cac_new_client_with_cache_properties(const char *tenant, unsigned long update_frequency, const char *hostname, unsigned long cache_max_capacity, unsigned long cache_ttl, unsigned long cache_tti) + +Creates a CAC client with explicit cache sizing and expiry settings. `cache_max_capacity` is interpreted as megabytes, while `cache_ttl` and `cache_tti` are interpreted as minutes. + +Returns 0 if client was successfully initialized +Returns 1 if an error occurred, use `cac_last_error_message` to get the error + ### void cac_start_polling_update(const char *tenant) Start polling the superposition server for updates for the given tenant @@ -129,7 +137,7 @@ returns a string that represents the config of your tenant based on your client ### const char *cac_get_resolved_config(struct Arc_Client *client, const char *query, const char *filter_keys, const char *merge_strategy) -Does the same thing as `cac_get_config` but does not return the entire config, rather the config filtered on the keys provided as arguments +Resolves configuration for `query`, filters by `filter_keys` when provided, and applies the requested `merge_strategy`. `filter_keys` is a pipe-separated string such as `payment|network`. `merge_strategy` is parsed as `merge` or `replace`, defaulting to merge for unknown values. returns a null pointer if an error occurred. Use `cac_last_error_message` to get the error @@ -154,7 +162,7 @@ Checkout the examples directory to understand how to create examples for `cac_cl All C structs and functions are generated automatically when the `exp-client` written in rust is compiled. Superposition uses the crate `cbindgen` to do this. The compiler generates a `.h` header file and an object file specific to the target platform, for example a `.dll` for windows or `.so` for linux. Some tips for writing an FFI/ABI to these files: - All memory operations should be done by the rust segment of the code. You should always free memory for strings and clients using the inbuilt `free_*` functions -- You don't need to store the client on your implementation's end. Always use `get_exp_client` to get a pointer to a client to pass to other functions +- You don't need to store the client on your implementation's end. Always use `expt_get_client` to get a pointer to a client to pass to other functions - Remember, a client is created for a particular tenant - Check the header files in the `headers` directory for further documentation - Your client implementation should expect the object file that it dynamically links to be present in the same directory @@ -194,7 +202,7 @@ Start polling the superposition server for updates for the given tenant ### void expt_free_client(struct Arc_Client *ptr) -Free the memory allocated to the cac client. Always call this function instead of the implementing language's `free` memory function +Free the memory allocated to the experimentation client. Always call this function instead of the implementing language's `free` memory function ### struct Arc_Client *expt_get_client(const char *tenant) @@ -204,23 +212,23 @@ returns a null pointer if an error occurred. Use `expt_last_error_message` to ge returns a pointer to Arc_Client that can be used to perform other client operations -### char *expt_get_applicable_variant(struct Arc_Client *client, const char *c_context, short toss) +### char *expt_get_applicable_variant(struct Arc_Client *client, const char *c_dimensions, const char *c_context, const char *identifier, const char *filter_prefix) -get the experiments that apply to a given context `c_context`. It also takes a number toss between 0 - 100 that is used to assign a variant IDs +Get the variant IDs that apply to `c_context`. `c_dimensions` is a JSON object of dimension metadata used to evaluate local cohorts, `identifier` is a stable bucketing key, and `filter_prefix` is an optional comma-separated config-key prefix filter. returns null pointer if no variant is found returns a string formatted array of variant IDs that match the parameters passed -### char *expt_get_satisfied_experiments(struct Arc_Client *client, const char *c_context, const char *filter_prefix) +### char *expt_get_satisfied_experiments(struct Arc_Client *client, const char *c_dimensions, const char *c_context, const char *filter_prefix) -get the experiments that apply to a given context `c_context`. It also filters on config key prefix +Get the experiments that apply to `c_context` after evaluating local cohorts from `c_dimensions`. It also filters on config key prefix. returns null pointer if no variant is found returns a string formatted array of experiments that match the parameters passed -### char *expt_get_filtered_satisfied_experiments(struct Arc_Client *client, const char *c_context, const char *filter_prefix) +### char *expt_get_filtered_satisfied_experiments(struct Arc_Client *client, const char *c_dimensions, const char *c_context, const char *filter_prefix) -get the experiments that apply to a given context `c_context`. It also filters on config key prefix +Get the experiments that apply to `c_context`, skipping unresolved local cohorts while evaluating dimensions. It also filters on config key prefix. returns null pointer if no variant is found returns a string formatted array of experiments that match the parameters passed diff --git a/docs/docs/providers/openfeature/haskell.md b/docs/docs/providers/openfeature/haskell.md index 13208e84d..108abbce3 100644 --- a/docs/docs/providers/openfeature/haskell.md +++ b/docs/docs/providers/openfeature/haskell.md @@ -3,401 +3,212 @@ sidebar_position: 6 title: Haskell --- -# Haskell — Superposition OpenFeature Provider +# Haskell - Superposition OpenFeature Provider -The Haskell provider is the native implementation of the Superposition OpenFeature provider. It offers two provider variants: +The Haskell package currently exposes one OpenFeature provider: +`SuperpositionProvider`. -- **`LocalResolutionProvider`** — Fetches config from a data source (HTTP server or local file), caches it locally, and evaluates flags in-process. Supports polling, on-demand, file-watch, and manual refresh strategies. This is the **recommended provider** for most use cases. -- **`SuperpositionAPIProvider`** — A stateless remote provider that makes an HTTP API call to the Superposition server on every evaluation. No local caching — useful for serverless or low-traffic scenarios. +It fetches configuration from the Superposition HTTP API, stores it in a native +provider cache through `superposition-bindings`, and evaluates OpenFeature +values from that local cache. -**Hackage:** [`superposition-open-feature-provider`](https://hackage.haskell.org/package/superposition-open-feature-provider) +The Haskell provider supports: + +- OpenFeature evaluation for boolean, string, integer, double, and object values +- Full resolved config lookup through `resolveAllConfig` +- Polling and on-demand refresh for configuration +- Optional polling or on-demand refresh for experimentation metadata +- Provider cleanup through `closeSuperpositionProvider` + +It does not currently expose the newer data-source provider API used by some +other language providers. In particular, there is no Haskell +`LocalResolutionProvider`, `SuperpositionAPIProvider`, `HttpDataSource`, +`FileDataSource`, fallback data source, file-watch refresh, or manual refresh +API in the current code. + +**Package source:** [`clients/haskell/open-feature-provider`](https://github.com/juspay/superposition/tree/main/clients/haskell/open-feature-provider) ## Installation -Add the following to your `.cabal` file: +Add the provider and OpenFeature packages to your `.cabal` file: ```cabal build-depends: base, superposition-open-feature-provider, open-feature, - text, network-uri ``` ## Quick Start -This is the most common usage — the provider connects to a Superposition server via HTTP, polls for config updates, and evaluates flags locally. - ```haskell {-# LANGUAGE OverloadedStrings #-} module Main where -import Data.OpenFeature (EvaluationContext, OpenFeature) -import Data.OpenFeature.SuperpositionProvider - ( HttpDataSource(..), LocalResolutionProvider(..) - , PollingStrategy(..), RefreshStrategy(..), SuperpositionOptions(..) - ) -import Control.Concurrent (threadDelay) +import Data.Aeson (Value (String)) +import Data.OpenFeature.EvaluationContext qualified as Context +import Data.OpenFeature.FeatureProvider qualified as OF +import Data.OpenFeature.SuperpositionProvider qualified as SP +import Network.URI qualified as URI + +expectRight :: Show e => Either e a -> IO a +expectRight (Right value) = pure value +expectRight (Left err) = fail (show err) main :: IO () main = do - -- 1. Create an HTTP data source pointing to your Superposition server - let httpSource = HttpDataSource $ SuperpositionOptions - { endpoint = "http://localhost:8080" - , token = "your_token_here" - , orgId = "localorg" - , workspaceId = "test" - } - - -- 2. Create the provider with a polling refresh strategy - let provider = LocalResolutionProvider - { dataSource = httpSource - , fallbackSource = Nothing -- no fallback data source - , refreshStrategy = Polling $ PollingStrategy - { interval = 60 -- seconds between polls - , timeout = Just 30 -- HTTP request timeout in seconds - } - } - - -- 3. Register with OpenFeature and create a client - api <- openFeatureSingleton - setProvider api provider - let client = createClient api - - -- Allow time for the provider to initialize - threadDelay 2000000 - - -- 4. Evaluate feature flags - let context = defaultEvaluationContext - & withTargetingKey "user-42" - & withCustomField "city" "Berlin" - - stringVal <- getStringValue client "currency" (Just context) Nothing - putStrLn $ "currency = " <> stringVal - - intVal <- getIntValue client "price" (Just context) Nothing - putStrLn $ "price = " <> show intVal - - boolVal <- getBoolValue client "dark_mode" (Just context) Nothing - putStrLn $ "dark_mode = " <> show boolVal -``` - -## Configuration Options - -### `SuperpositionOptions` - -Connection options shared by `HttpDataSource` and `SuperpositionAPIProvider`: - -| Field | Type | Required | Description | -| -------------- | -------- | -------- | ------------------------------ | -| `endpoint` | `Text` | Yes | Superposition server URL | -| `token` | `Text` | Yes | Authentication token (bearer) | -| `orgId` | `Text` | Yes | Organisation ID | -| `workspaceId` | `Text` | Yes | Workspace ID | - -```haskell -let options = SuperpositionOptions - { endpoint = "http://localhost:8080" - , token = "your_token" - , orgId = "localorg" - , workspaceId = "test" + endpoint <- + maybe (fail "Invalid Superposition endpoint") pure $ + URI.parseURI "http://localhost:8080" + + provider <- + SP.newSuperpositionProvider + SP.defaultProviderOptions + { SP.orgId = "localorg", + SP.workspaceId = "dev", + SP.endpoint = endpoint, + SP.token = "your_token_here", + SP.refreshOptions = SP.Poll 60, + SP.experimentationRefreshOptions = Nothing, + SP.logLevel = SP.LevelInfo } -``` + >>= expectRight -### Refresh Strategies + let context = + Context.withCustomField "city" (String "Berlin") $ + Context.withTargetingKey "user-42" Context.defaultContext -The `RefreshStrategy` type supports four variants: + OF.initialize provider context -```haskell --- Polling — periodically fetches updates from the server -Polling $ PollingStrategy - { interval = 60 -- seconds between polls (default: 60) - , timeout = Just 30 -- HTTP request timeout in seconds (default: 30) - } - --- On-Demand — fetches on first access, then caches with a TTL -OnDemand $ OnDemandStrategy - { ttl = 300 -- cache TTL in seconds (default: 300) - , useStaleOnError = Just True -- serve stale data on fetch error (default: Just True) - , timeout = Just 30 -- HTTP timeout in seconds (default: Just 30) - } - --- Watch — uses file-system notifications (for FileDataSource only) -Watch $ WatchStrategy - { debounceMs = Just 500 -- debounce interval in milliseconds (default: Just 500) - } - --- Manual — no automatic refresh; user triggers refresh -Manual -``` - -### `ExperimentationOptions` + resolvedConfig <- SP.resolveAllConfig provider context + case resolvedConfig of + Right json -> putStrLn json + Left err -> fail err -| Field | Type | Required | Description | -| ------------------ | -------------------------------- | -------- | ----------------------------------- | -| `refreshStrategy` | `RefreshStrategy` | Yes | How experiment data is refreshed | -| `evaluationCache` | `Maybe EvaluationCacheOptions` | No | Cache for experiment evaluations | -| `defaultToss` | `Maybe Int` | No | Default toss value for experiments | + boolResult <- OF.resolveBooleanValue provider "dark_mode" context + case boolResult of + Right details -> print (OF.value details) + Left err -> print err -`ExperimentationOptions` supports a builder pattern: - -```haskell -let expOptions = defaultExperimentationOptions - (Polling $ PollingStrategy { interval = 5, timeout = Just 3 }) - & withEvaluationCache defaultEvaluationCacheOptions - & withDefaultToss 50 + SP.closeSuperpositionProvider provider ``` -### `EvaluationCacheOptions` - -| Field | Type | Default | Description | -| ------ | ------------- | ---------- | ------------------------------- | -| `ttl` | `Maybe Int` | `Just 60` | Cache time-to-live in seconds | -| `size` | `Maybe Int` | `Just 500` | Maximum number of cache entries | - -## Provider Variants - -### 1. `LocalResolutionProvider` (Recommended) - -Fetches config from a pluggable data source (HTTP or file), caches locally, and evaluates flags in-process. Supports all four refresh strategies. Accepts an optional fallback data source. - -```haskell -import Data.OpenFeature (EvaluationContext(..)) -import Data.OpenFeature.SuperpositionProvider - ( HttpDataSource(..), LocalResolutionProvider(..) - , AllFeatureProvider(..), FeatureExperimentMeta(..) - , PollingStrategy(..), RefreshStrategy(..), SuperpositionOptions(..) - ) - -let httpSource = HttpDataSource $ SuperpositionOptions - { endpoint = "http://localhost:8080" - , token = "token" - , orgId = "localorg" - , workspaceId = "dev" - } - -let provider = LocalResolutionProvider - { dataSource = httpSource - , fallbackSource = Nothing -- optional fallback data source - , refreshStrategy = Polling $ PollingStrategy { interval = 30, timeout = Just 10 } - } - --- Initialize the provider (fetches initial config) -initProvider provider defaultEvaluationContext - --- Resolve all features -let context = defaultEvaluationContext - & withTargetingKey "user-1234" - & withCustomField "dimension" "d2" - -allConfig <- resolveAllFeatures provider context -putStrLn $ "All config: " <> show allConfig - --- Get applicable experiment variants -variants <- getApplicableVariants provider context Nothing -putStrLn $ "Variants: " <> show variants - --- Cleanup -closeProvider provider -``` - -**Key capabilities:** - -- **Pluggable data sources** — use `HttpDataSource` for server-backed, `FileDataSource` for local TOML files, or implement the `SuperpositionDataSource` typeclass for custom sources -- **Optional fallback** — provide a secondary data source (e.g. a local file) that is used when the primary source fails -- **Manual refresh** — call `refresh provider` to trigger a config refresh on demand -- **Chainable** — `LocalResolutionProvider` itself implements `SuperpositionDataSource`, so it can be used as a data source for another provider +## Provider Options -### 2. `SuperpositionAPIProvider` (Remote / Stateless) +Create a provider with `newSuperpositionProvider` and +`SuperpositionProviderOptions`. The easiest path is to start from +`defaultProviderOptions` and override the fields for your workspace. -A stateless provider that calls the Superposition server on every evaluation. No local caching — each flag evaluation makes an HTTP request. Best for serverless, low-traffic, or scenarios where you always want the latest config. +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `orgId` | `Text` | Yes | Organisation ID | +| `workspaceId` | `Text` | Yes | Workspace ID | +| `endpoint` | `Network.URI.URI` | Yes | Superposition server URL | +| `token` | `Text` | Yes | Authentication bearer token | +| `refreshOptions` | `RefreshOptions` | Yes | Config refresh strategy | +| `experimentationRefreshOptions` | `Maybe RefreshOptions` | No | Enables experiment and experiment-group refresh when set | +| `logLevel` | `LogLevel` | No | Minimum log level emitted by the provider | +| `fallbackConfig` | `()` | No | Placeholder field only; fallback config is not implemented today | ```haskell -import Data.OpenFeature (EvaluationContext, OpenFeature) -import Data.OpenFeature.SuperpositionProvider - ( SuperpositionAPIProvider(..), SuperpositionOptions(..) ) - -let provider = SuperpositionAPIProvider $ SuperpositionOptions - { endpoint = "http://localhost:8080" - , token = "token" - , orgId = "localorg" - , workspaceId = "dev" +let options = + SP.defaultProviderOptions + { SP.orgId = "localorg", + SP.workspaceId = "dev", + SP.endpoint = endpoint, + SP.token = "your_token_here", + SP.refreshOptions = SP.Poll 60, + SP.logLevel = SP.LevelInfo } - --- Use with OpenFeature -api <- openFeatureSingleton -setProvider api provider -let client = createClient api - -let context = defaultEvaluationContext - & withCustomField "city" "Berlin" - -value <- getStringValue client "currency" (Just context) Nothing -putStrLn $ "currency = " <> value ``` -## Local File Resolution (SuperTOML) - -Resolve configs from a local `.toml` file without needing a server: - -```haskell -{-# LANGUAGE OverloadedStrings #-} - -module Main where - -import Data.OpenFeature (EvaluationContext) -import Data.OpenFeature.SuperpositionProvider - ( FileDataSource(..), LocalResolutionProvider(..) - , AllFeatureProvider(..) - , OnDemandStrategy(..), RefreshStrategy(..) - ) - -main :: IO () -main = do - let fileSource = FileDataSource "config.toml" - - let provider = LocalResolutionProvider - { dataSource = fileSource - , fallbackSource = Nothing - , refreshStrategy = OnDemand $ OnDemandStrategy - { ttl = 60 - , useStaleOnError = Nothing - , timeout = Nothing - } - } +## Refresh Options - initProvider provider defaultEvaluationContext - - let context = defaultEvaluationContext - & withCustomField "os" "linux" - & withCustomField "city" "Boston" - - config <- resolveAllFeatures provider context - putStrLn $ "Config: " <> show config - - closeProvider provider -``` +The current Haskell API has two refresh modes: -:::note -The `FileDataSource` supports the `Watch` refresh strategy for automatic reloading on file changes. It does **not** support experimentation. -::: - -## Local HTTP with File Fallback - -Use the server as the primary data source with a local TOML file as fallback. If the HTTP source fails during initialization, the provider loads config from the fallback file instead — ensuring your application always starts with a valid configuration. +| Constructor | Description | +| ----------- | ----------- | +| `Poll Int` | Starts a background refresh task. The integer is the polling interval in seconds. | +| `OnDemand Int64` | Fetches lazily and caches the value for the given TTL in seconds. If a refresh fails, the previous cached value is reused when available. | ```haskell -{-# LANGUAGE OverloadedStrings #-} - -module Main where +-- Poll every 60 seconds +SP.Poll 60 -import Data.OpenFeature (EvaluationContext, OpenFeature) -import Data.OpenFeature.SuperpositionProvider - ( FileDataSource(..), HttpDataSource(..), LocalResolutionProvider(..) - , PollingStrategy(..), RefreshStrategy(..), SuperpositionOptions(..) - ) -import Control.Concurrent (threadDelay) - -main :: IO () -main = do - -- Primary: HTTP data source (Superposition server) - let httpSource = HttpDataSource $ SuperpositionOptions - { endpoint = "http://localhost:8080" - , token = "token" - , orgId = "localorg" - , workspaceId = "dev" - } - - -- Fallback: local TOML config file - let fileSource = FileDataSource "config.toml" - - let provider = LocalResolutionProvider - { dataSource = httpSource - , fallbackSource = Just fileSource -- used when HTTP source is unavailable - , refreshStrategy = Polling $ PollingStrategy - { interval = 10 - , timeout = Just 10 - } - } - - -- Register with OpenFeature - api <- openFeatureSingleton - setProvider api provider - let client = createClient api - - threadDelay 2000000 - - let context = defaultEvaluationContext - & withTargetingKey "user-456" - & withCustomField "os" "linux" - & withCustomField "city" "Berlin" - - currency <- getStringValue client "currency" (Just context) Nothing - putStrLn $ "currency = " <> currency - - price <- getIntValue client "price" (Just context) Nothing - putStrLn $ "price = " <> show price +-- Fetch on demand and cache for 300 seconds +SP.OnDemand 300 ``` -:::tip -The fallback is only consulted during initialization or when the primary source fails. Once the primary source succeeds, the provider uses its data exclusively. Polling continues to try the primary source on each interval. -::: - -## Evaluation Context +## Experimentation -Pass dimensions and a targeting key for experiment bucketing: +Experimentation refresh is enabled by setting `experimentationRefreshOptions`. +When enabled, the provider fetches active experiments and experiment groups from +the server and initializes the native experiment cache. ```haskell -import Data.OpenFeature (EvaluationContext) - -let context = defaultEvaluationContext - & withTargetingKey "user-42" - & withCustomField "city" "Berlin" - & withCustomField "os" "android" - -value <- getStringValue client "currency" (Just context) Nothing +let options = + SP.defaultProviderOptions + { SP.orgId = "localorg", + SP.workspaceId = "dev", + SP.endpoint = endpoint, + SP.token = "your_token_here", + SP.refreshOptions = SP.Poll 60, + SP.experimentationRefreshOptions = Just (SP.Poll 60) + } ``` -## Supported Value Types +Experimentation uses the OpenFeature targeting key for bucketing. If +experimentation refresh is configured and the evaluation context has no +targeting key, the provider logs a warning and evaluates without experiment +bucketing. -| Method | Return Type | -| ----------------- | -------------- | -| `getBoolValue` | `Bool` | -| `getIntValue` | `Int` | -| `getFloatValue` | `Double` | -| `getStringValue` | `Text` | -| `getStructValue` | `Value` | +The Haskell provider does not currently expose `ExperimentationOptions`, +evaluation-cache options, default toss configuration, or a +`getApplicableVariants` helper. -The `LocalResolutionProvider` and `SuperpositionAPIProvider` also implement the `AllFeatureProvider` typeclass: +## Resolving Values -```haskell --- Resolve all features -allConfig <- resolveAllFeatures provider context +The provider implements the OpenFeature `FeatureProvider` instance methods: --- Resolve with a prefix filter (e.g. only keys starting with "payment.") -filtered <- resolveAllFeaturesWithFilter provider context (Just ["payment."]) -``` +| Method | Value type | +| ------ | ---------- | +| `resolveBooleanValue` | `Bool` | +| `resolveStringValue` | `Text` | +| `resolveIntegerValue` | `Integer` | +| `resolveDoubleValue` | `Double` | +| `resolveObjectValue` | Object / JSON-like value | -And `FeatureExperimentMeta` for experiment metadata: +For the full resolved configuration, use `resolveAllConfig`: ```haskell --- Get applicable experiment variant IDs -variants <- getApplicableVariants provider context Nothing -- optional prefix filter +resolvedConfig <- SP.resolveAllConfig provider context +case resolvedConfig of + Right json -> putStrLn json + Left err -> fail err ``` ## Logging -The provider uses the `monad-logger` framework. Enable with `runStdoutLoggingT`: +Set `logLevel` on `SuperpositionProviderOptions` to control provider logs: ```haskell -import Control.Monad.Logger (runStdoutLoggingT, LogLevel(..)) - -runStdoutLoggingT $ do - -- provider operations here +let options = + SP.defaultProviderOptions + { SP.logLevel = SP.LevelDebug + } ``` -## Examples +## Current Limitations + +The following provider features are not implemented in Haskell yet: -- [`clients/haskell/open-feature-provider`](https://github.com/juspay/superposition/tree/main/clients/haskell/open-feature-provider) — Provider source and usage +- Local file resolution from SuperTOML or JSON +- Pluggable data sources +- HTTP primary plus local-file fallback +- Stateless remote provider API +- File-watch refresh +- Manual refresh +- Prefix-filtered all-feature resolution +- Direct applicable-variant lookup diff --git a/docs/docs/providers/openfeature/java.md b/docs/docs/providers/openfeature/java.md index 07af941e5..7aa78dcfc 100644 --- a/docs/docs/providers/openfeature/java.md +++ b/docs/docs/providers/openfeature/java.md @@ -3,401 +3,201 @@ sidebar_position: 5 title: Java / Kotlin --- -# Java / Kotlin — Superposition OpenFeature Provider +# Java / Kotlin - Superposition OpenFeature Provider -The Java provider is an OpenFeature-compatible provider for Superposition that works with both Java and Kotlin. It offers two provider variants: +The Java package currently documents one OpenFeature provider: +`SuperpositionOpenFeatureProvider`. -- **`LocalResolutionProvider`** — Fetches config from a data source (HTTP server), caches it locally, and evaluates flags in-process. Supports polling and on-demand refresh strategies. This is the **recommended provider** for most use cases. -- **`SuperpositionAPIProvider`** — A stateless remote provider that makes an HTTP API call to the Superposition server on every evaluation. No local caching — useful for serverless or low-traffic scenarios. +It is configured with `SuperpositionProviderOptions`, fetches data from the +Superposition HTTP API, and integrates with the OpenFeature Java SDK. -The provider includes an Android-compatible HTTP transport. +The Java provider documentation in this repository supports: -**Maven Central:** [`io.juspay.superposition:openfeature-provider`](https://central.sonatype.com/artifact/io.juspay.superposition/openfeature-provider) +- OpenFeature evaluation for boolean, string, number, and object values +- Full resolved config lookup through `evaluateConfig` +- Applicable experiment variant lookup through `applicableVariants` +- Polling refresh strategies for configuration and experimentation +- Optional experimentation configuration + +The current Java package in this checkout does not include the newer +`LocalResolutionProvider`, `SuperpositionAPIProvider`, `HttpDataSource`, or +`FileDataSource` provider API shown by some other language implementations. ## Installation -### Gradle (Kotlin DSL) +The current provider README uses the Juspay sandbox Maven repository: ```kotlin -implementation("dev.openfeature:sdk:") -implementation("io.juspay.superposition:openfeature-provider:") -``` - -### Gradle (Groovy) - -```groovy -implementation "dev.openfeature:sdk:" -implementation "io.juspay.superposition:openfeature-provider:" -``` +repositories { + mavenCentral() + maven(url = "https://sandbox.assets.juspay.in/m2") +} -### Maven - -```xml - - dev.openfeature - sdk - ${openfeature.version} - - - io.juspay.superposition - openfeature-provider - ${superposition.version} - +dependencies { + implementation("io.juspay.superposition.openfeature:superposition-provider:0.0.1-dev") + implementation("dev.openfeature:sdk:1.15.1") +} ``` :::note You need a running Superposition server. See [Quick Start](../../quick_start) for setup instructions. ::: -## Quick Start (Java) - -This is the most common usage — the provider connects to a Superposition server via HTTP, polls for config updates, and evaluates flags locally. +## Quick Start ```java import dev.openfeature.sdk.*; import io.juspay.superposition.openfeature.*; -import io.juspay.superposition.openfeature.options.*; +import io.juspay.superposition.openfeature.options.RefreshStrategy; import java.util.List; import java.util.Map; public class Example { public static void main(String[] args) { - // 1. Create an HTTP data source pointing to your Superposition server - HttpDataSource httpSource = new HttpDataSource(SuperpositionOptions.builder() - .endpoint("http://localhost:8080") - .token("your-api-token") - .orgId("localorg") - .workspaceId("test") - .build()); - - // 2. Configure experimentation options - ExperimentationOptions expOptions = ExperimentationOptions.builder() - .refreshStrategy(RefreshStrategy.Polling.of(5000, 2000)) - .build(); - - // 3. Create the provider with a polling refresh strategy - LocalResolutionProvider provider = LocalResolutionProvider.builder() - .dataSource(httpSource) - .refreshStrategy(RefreshStrategy.Polling.of(10000, 5000)) - .experimentationOptions(expOptions) - .build(); - - // 4. Initialize and register with OpenFeature + SuperpositionProviderOptions.ExperimentationOptions expOptions = + SuperpositionProviderOptions.ExperimentationOptions.builder() + .refreshStrategy(RefreshStrategy.Polling.of(5000, 2000)) + .build(); + + SuperpositionProviderOptions options = + SuperpositionProviderOptions.builder() + .orgId("your-org-id") + .workspaceId("your-workspace-id") + .endpoint("http://localhost:8080") + .token("your-api-token") + .refreshStrategy(RefreshStrategy.Polling.of(10000, 5000)) + .experimentationOptions(expOptions) + .build(); + + SuperpositionOpenFeatureProvider provider = + new SuperpositionOpenFeatureProvider(options); + EvaluationContext initCtx = new ImmutableContext(Map.of("city", new Value("Berlin"))); + provider.initialize(initCtx); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(provider); - Client client = api.getClient(); + OpenFeatureAPI.getInstance().setProvider(provider); + Client client = OpenFeatureAPI.getInstance().getClient(); - // 5. Define evaluation context - EvaluationContext ctx = new ImmutableContext( - "user-42", - Map.of( - "city", new Value("Berlin"), - "os", new Value("android") - ) - ); - - // 6. Evaluate feature flags - String currency = client.getStringValue("currency", "USD", ctx); - System.out.println("currency = " + currency); - - Integer price = client.getIntegerValue("price", 0, ctx); - System.out.println("price = " + price); + EvaluationContext ctx = + new ImmutableContext( + "user-42", + Map.of( + "city", new Value("Berlin"), + "os", new Value("android") + ) + ); boolean darkMode = client.getBooleanValue("dark_mode", false, ctx); System.out.println("dark_mode = " + darkMode); - // 7. Get full resolved config (provider-specific) - Map fullConfig = provider.evaluateConfig(ctx); - System.out.println("Full config: " + fullConfig); + String currency = client.getStringValue("currency", "USD", ctx); + System.out.println("currency = " + currency); + + System.out.println("Full config: " + provider.evaluateConfig(ctx)); - // 8. Get applicable experiment variants (provider-specific) - List variants = provider.getApplicableVariants(ctx); - System.out.println("Applicable variants: " + variants); + List variants = provider.applicableVariants(ctx); + System.out.println("Variants: " + variants); } } ``` -## Quick Start (Kotlin) +## Provider Options -```kotlin -import dev.openfeature.sdk.* -import io.juspay.superposition.openfeature.* -import io.juspay.superposition.openfeature.options.* +Create a provider with `SuperpositionProviderOptions.builder()`. -fun main() { - // 1. Create an HTTP data source - val httpSource = HttpDataSource(SuperpositionOptions.builder() +| Field | Required | Description | +| ----- | -------- | ----------- | +| `orgId` | Yes | Organisation ID | +| `workspaceId` | Yes | Workspace ID | +| `endpoint` | Yes | Superposition server URL | +| `token` | Yes | Authentication token | +| `refreshStrategy` | Yes | Configuration refresh strategy | +| `experimentationOptions` | No | Enables experiment refresh and variant resolution | + +```java +SuperpositionProviderOptions options = + SuperpositionProviderOptions.builder() + .orgId("your-org-id") + .workspaceId("your-workspace-id") .endpoint("http://localhost:8080") .token("your-api-token") - .orgId("localorg") - .workspaceId("test") - .build()) - - // 2. Configure experimentation - val expOptions = ExperimentationOptions.builder() - .refreshStrategy(RefreshStrategy.Polling.of(5000, 2000)) - .build() - - // 3. Create the provider - val provider = LocalResolutionProvider.builder() - .dataSource(httpSource) .refreshStrategy(RefreshStrategy.Polling.of(10000, 5000)) - .experimentationOptions(expOptions) - .build() - - // 4. Initialize and register - val initCtx = ImmutableContext(mapOf("city" to Value("Berlin"))) - provider.initialize(initCtx) - - OpenFeatureAPI.getInstance().setProvider(provider) - val client = OpenFeatureAPI.getInstance().client - - // 5. Evaluate feature flags - val ctx = ImmutableContext( - "user-42", - mapOf("city" to Value("Berlin"), "os" to Value("android")) - ) - - val currency = client.getStringValue("currency", "USD", ctx) - println("currency = $currency") - - val darkMode = client.getBooleanValue("dark_mode", false, ctx) - println("dark_mode = $darkMode") - - val price = client.getIntegerValue("price", 0, ctx) - println("price = $price") - - // 6. Get full config - val fullConfig = provider.evaluateConfig(ctx) - println("Full config: $fullConfig") -} -``` - -## Configuration Options - -### `SuperpositionOptions` - -Connection options shared by `HttpDataSource` and `SuperpositionAPIProvider`. Uses Lombok `@Builder`. - -| Field | Type | Required | Annotation | Description | -| ------------- | -------- | -------- | ---------- | ----------------------------- | -| `endpoint` | `String` | Yes | `@NonNull` | Superposition server URL | -| `token` | `String` | Yes | `@NonNull` | Authentication token (bearer) | -| `orgId` | `String` | Yes | `@NonNull` | Organisation ID | -| `workspaceId` | `String` | Yes | `@NonNull` | Workspace ID | - -```java -SuperpositionOptions options = SuperpositionOptions.builder() - .endpoint("http://localhost:8080") - .token("your-api-token") - .orgId("localorg") - .workspaceId("test") - .build(); + .build(); ``` -### Refresh Strategies +## Experimentation -The `RefreshStrategy` interface has two implementations: +Enable experimentation by adding nested experimentation options: ```java -// Polling — periodic background refresh -RefreshStrategy.Polling.of( - 10000, // interval in milliseconds - 5000 // timeout in milliseconds -) - -// OnDemand — fetch on access, cache with TTL -RefreshStrategy.OnDemand.of( - 300000, // TTL in milliseconds - 5000 // timeout in milliseconds -) -``` - -### `ExperimentationOptions` - -Uses Lombok `@Builder`: - -| Field | Type | Required | Description | -| ------------------------ | ------------------------ | -------- | ---------------------------------- | -| `refreshStrategy` | `RefreshStrategy` | Yes | How experiment data is refreshed | -| `evaluationCacheOptions` | `EvaluationCacheOptions` | No | Cache for experiment evaluations | -| `defaultToss` | `Integer` | No | Default toss value for experiments | - -```java -ExperimentationOptions expOptions = ExperimentationOptions.builder() - .refreshStrategy(RefreshStrategy.Polling.of(5000, 2000)) - .evaluationCacheOptions(EvaluationCacheOptions.builder() - .ttl(300) - .size(1000) - .build()) - .defaultToss(50) - .build(); -``` - -### `EvaluationCacheOptions` - -| Field | Type | Default | Description | -| ------ | ----- | ------- | ------------------------------- | -| `ttl` | `int` | `60` | Cache time-to-live in seconds | -| `size` | `int` | `500` | Maximum number of cache entries | - -## Provider Variants - -### 1. `LocalResolutionProvider` (Recommended) - -Fetches config from a pluggable data source (HTTP), caches locally, and evaluates flags in-process. Accepts an optional fallback data source. - -```java -HttpDataSource httpSource = new HttpDataSource(SuperpositionOptions.builder() - .endpoint("http://localhost:8080") - .token("token") - .orgId("localorg") - .workspaceId("dev") - .build()); - -LocalResolutionProvider provider = LocalResolutionProvider.builder() - .dataSource(httpSource) - .refreshStrategy(RefreshStrategy.Polling.of(30000, 10000)) - .build(); - -// Initialize -provider.initialize(new ImmutableContext(Map.of("city", new Value("Berlin")))); - -// Resolve all config -EvaluationContext ctx = new ImmutableContext( - "user-1234", - Map.of("city", new Value("Berlin"))); -Map allConfig = provider.evaluateConfig(ctx); - -// Get applicable experiment variants -List variants = provider.getApplicableVariants(ctx); -``` - -**Key capabilities:** - -- **Pluggable data sources** — use `HttpDataSource` for server-backed resolution -- **Optional fallback** — provide a `SuperpositionConfig` that is used when the primary source fails -- **Full config resolution** — resolve all features at once via `evaluateConfig()` -- **Experiment metadata** — get applicable variants via `getApplicableVariants()` - -### 2. `SuperpositionAPIProvider` (Remote / Stateless) - -A stateless provider that calls the Superposition server on every evaluation. No local caching — each flag evaluation makes an HTTP request. Best for serverless, low-traffic, or scenarios where you always want the latest config. +SuperpositionProviderOptions.ExperimentationOptions expOptions = + SuperpositionProviderOptions.ExperimentationOptions.builder() + .refreshStrategy(RefreshStrategy.Polling.of(5000, 2000)) + .build(); -```java -SuperpositionAPIProvider provider = new SuperpositionAPIProvider( - SuperpositionOptions.builder() +SuperpositionProviderOptions options = + SuperpositionProviderOptions.builder() + .orgId("your-org-id") + .workspaceId("your-workspace-id") .endpoint("http://localhost:8080") - .token("token") - .orgId("localorg") - .workspaceId("dev") - .build() -); - -OpenFeatureAPI api = OpenFeatureAPI.getInstance(); -api.setProvider(provider); -Client client = api.getClient(); - -EvaluationContext ctx = new ImmutableContext( - "user-42", - Map.of("city", new Value("Berlin"))); - -String currency = client.getStringValue("currency", "USD", ctx); -System.out.println("currency = " + currency); -``` - -### Fallback Configuration - -Provides default configuration when the server is unreachable: - -```java -SuperpositionConfig fallback = SuperpositionConfig.builder() - .contexts(contexts) - .defaultConfigs(defaults) - .overrides(overrides) - .build(); - -LocalResolutionProvider provider = LocalResolutionProvider.builder() - .dataSource(httpSource) - .fallbackConfig(fallback) - .refreshStrategy(RefreshStrategy.Polling.of(10000, 5000)) - .build(); + .token("your-api-token") + .refreshStrategy(RefreshStrategy.Polling.of(10000, 5000)) + .experimentationOptions(expOptions) + .build(); ``` -:::tip -The fallback is only consulted during initialization or when the primary source fails. Once the primary source succeeds, the provider uses its data exclusively. -::: - -## Evaluation Context +Use an OpenFeature targeting key when evaluating experiment-backed config: ```java -// Java — with targeting key for experiments -EvaluationContext ctx = new ImmutableContext( - "user-42", // targeting key - Map.of( - "city", new Value("Berlin"), - "os", new Value("android"), - "customers", new Value("platinum") - ) -); -``` +EvaluationContext experimentCtx = + new ImmutableContext( + "user-42", + Map.of("city", new Value("Berlin")) + ); -```kotlin -// Kotlin — equivalent -val ctx = ImmutableContext( - "user-42", - mapOf("city" to Value("Berlin"), "os" to Value("android")) -) +List variants = provider.applicableVariants(experimentCtx); ``` -- **`targeting_key`** — Used for experiment variant bucketing. Typically a user ID or session ID. -- All other keys map to your Superposition dimensions. - -## Supported Value Types - -| Method | Return Type | Description | -| ------------------ | ----------- | ----------------------- | -| `getBooleanValue` | `Boolean` | Boolean flag evaluation | -| `getStringValue` | `String` | String flag evaluation | -| `getIntegerValue` | `Integer` | Integer flag evaluation | -| `getDoubleValue` | `Double` | Double flag evaluation | -| `getObjectValue` | `Value` | Object/JSON evaluation | +## Evaluation Context -The `LocalResolutionProvider` also supports provider-specific methods: +Custom fields on the OpenFeature evaluation context map to Superposition +dimensions. ```java -// Resolve all features -Map fullConfig = provider.evaluateConfig(ctx); +EvaluationContext ctx = + new ImmutableContext( + "user-42", + Map.of( + "city", new Value("Berlin"), + "os", new Value("android") + ) + ); -// Get applicable experiment variant IDs -List variants = provider.getApplicableVariants(ctx); +String currency = client.getStringValue("currency", "USD", ctx); ``` -## Android Support - -The Java provider includes `URLConnectionTransport`, an Android-compatible HTTP transport that uses `java.net.HttpURLConnection` instead of the default transport: - -```java -import io.juspay.superposition.openfeature.transport.URLConnectionTransport; - -// The transport is used internally — no explicit configuration needed -// It's automatically compatible with Android's networking restrictions -``` +## Provider-Specific Methods -## Dependencies +The Java provider README documents these provider-specific methods: -The Java provider depends on: +| Method | Description | +| ------ | ----------- | +| `evaluateConfig(ctx)` | Resolve and return the full configuration for a context | +| `applicableVariants(ctx)` | Return applicable experiment variant IDs for a context | -- [`dev.openfeature:sdk`](https://central.sonatype.com/artifact/dev.openfeature/sdk) — OpenFeature Java SDK -- Smithy-generated Java SDK for Superposition API -- Native bindings via JNI for local config resolution -- Lombok for builder pattern generation +## Current Limitations -## Full Example +The following provider features are not present in the checked-in Java provider +documentation or source layout today: -See the integration test: [`clients/java/provider-sdk-tests/src/main/kotlin/io/juspay/superposition/providertests/Main.kt`](https://github.com/juspay/superposition/blob/main/clients/java/provider-sdk-tests/src/main/kotlin/io/juspay/superposition/providertests/Main.kt) +- `LocalResolutionProvider` +- `SuperpositionAPIProvider` +- `HttpDataSource` and `FileDataSource` +- Local SuperTOML or JSON file resolution +- Pluggable data sources +- Fallback data sources +- Watch refresh +- Manual refresh diff --git a/docs/docs/providers/openfeature/javascript.md b/docs/docs/providers/openfeature/javascript.md index 4733087af..a51e3377d 100644 --- a/docs/docs/providers/openfeature/javascript.md +++ b/docs/docs/providers/openfeature/javascript.md @@ -3,12 +3,26 @@ sidebar_position: 4 title: JavaScript --- -# JavaScript — Superposition OpenFeature Provider +# JavaScript - Superposition OpenFeature Provider -The JavaScript provider is an OpenFeature-compatible provider for Superposition that runs in Node.js. It offers two provider variants: +The JavaScript package currently exposes one Node.js OpenFeature provider: +`SuperpositionProvider`. -- **`LocalResolutionProvider`** — Fetches config from a data source (HTTP server), caches it locally, and evaluates flags in-process. Supports polling and on-demand refresh strategies. This is the **recommended provider** for most use cases. -- **`SuperpositionAPIProvider`** — A stateless remote provider that makes an HTTP API call to the Superposition server on every evaluation. No local caching — useful for serverless or low-traffic scenarios. +It fetches configuration from the Superposition HTTP API, initializes a native +resolver cache through `superposition-bindings`, and evaluates OpenFeature +values from that local cache. + +The JavaScript provider supports: + +- OpenFeature evaluation for boolean, string, number, and object values +- Full resolved config lookup through `resolveAllConfigDetails` +- Polling refresh for configuration +- Optional experimentation refresh +- Provider cleanup through `OpenFeature.close()` or provider close hooks + +It does not currently export `LocalResolutionProvider`, +`SuperpositionAPIProvider`, `HttpDataSource`, `FileDataSource`, local-file +resolution, watch refresh, manual refresh, or pluggable data sources. **npm:** [`superposition-provider`](https://www.npmjs.com/package/superposition-provider) @@ -24,224 +38,141 @@ You need a running Superposition server. See [Quick Start](../../quick_start) fo ## Quick Start -This is the most common usage — the provider connects to a Superposition server via HTTP, polls for config updates, and evaluates flags locally. - ```javascript import { OpenFeature } from "@openfeature/server-sdk"; -import { - LocalResolutionProvider, - HttpDataSource, - SuperpositionOptions, -} from "superposition-provider"; +import { SuperpositionProvider } from "superposition-provider"; async function main() { - // 1. Create an HTTP data source pointing to your Superposition server - const httpSource = new HttpDataSource( - new SuperpositionOptions({ - endpoint: "http://localhost:8080", - token: "your-api-token", - orgId: "localorg", - workspaceId: "test", - }) - ); - - // 2. Create the provider with a polling refresh strategy - const provider = new LocalResolutionProvider({ - dataSource: httpSource, + const provider = new SuperpositionProvider({ + endpoint: "http://localhost:8080", + token: "your-api-token", + org_id: "localorg", + workspace_id: "test", refreshStrategy: { - type: "polling", - interval: 60000, // milliseconds between polls - timeout: 30000, // HTTP request timeout in milliseconds + interval: 60000, + timeout: 30000, }, }); - // 3. Register with OpenFeature and wait for initialization await OpenFeature.setProviderAndWait(provider); - console.log("Provider initialized successfully"); - - // 4. Create a client const client = OpenFeature.getClient(); - // 5. Define evaluation context (dimensions) const context = { targetingKey: "user-42", city: "Berlin", os: "android", }; - // 6. Evaluate feature flags - const stringVal = await client.getStringValue("currency", "USD", context); - console.log("currency:", stringVal); + const currency = await client.getStringValue("currency", "USD", context); + console.log("currency:", currency); + + const price = await client.getNumberValue("price", 0, context); + console.log("price:", price); - const numberVal = await client.getNumberValue("price", 0, context); - console.log("price:", numberVal); + const darkMode = await client.getBooleanValue("dark_mode", false, context); + console.log("dark_mode:", darkMode); - const boolVal = await client.getBooleanValue("dark_mode", false, context); - console.log("dark_mode:", boolVal); + const allConfig = await provider.resolveAllConfigDetails({}, context); + console.log("All config:", allConfig); - // 7. Cleanup await OpenFeature.close(); } main().catch(console.error); ``` -## Configuration Options +## Provider Options -### `SuperpositionOptions` +Create a provider with `new SuperpositionProvider(options)`. -Connection options shared by `HttpDataSource` and `SuperpositionAPIProvider`: - -| Field | Type | Required | Description | -| ------------- | -------- | -------- | ----------------------------- | -| `endpoint` | `string` | Yes | Superposition server URL | -| `token` | `string` | Yes | Authentication token (bearer) | -| `orgId` | `string` | Yes | Organisation ID | -| `workspaceId` | `string` | Yes | Workspace ID | +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `endpoint` | `string` | Yes | Superposition server URL | +| `token` | `string` | Yes | Authentication token | +| `org_id` | `string` | Yes | Organisation ID | +| `workspace_id` | `string` | Yes | Workspace ID | +| `httpClient` | `any` | No | Custom Smithy request handler | +| `refreshStrategy` | `RefreshStrategy` | No | Configuration refresh behavior | +| `experimentationOptions` | `ExperimentationOptions` | No | Enables experiment and experiment-group refresh | +| `fallbackConfig` | `ConfigData` | No | Present on the TypeScript interface, but not automatically installed as fallback data by current provider initialization | +| `evaluationCache` | `EvaluationCacheOptions` | No | Present on the TypeScript interface; current configuration evaluation does not apply this cache option | ```javascript -const options = new SuperpositionOptions({ +const provider = new SuperpositionProvider({ endpoint: "http://localhost:8080", token: "your-api-token", - orgId: "localorg", - workspaceId: "test", + org_id: "localorg", + workspace_id: "test", + refreshStrategy: { interval: 60000, timeout: 30000 }, }); ``` -### Refresh Strategies - -```javascript -// Polling — periodically fetches updates from the server -{ - type: "polling", - interval: 60000, // milliseconds between polls (default: 60000) - timeout: 30000, // HTTP request timeout in milliseconds (default: 30000) -} - -// On-Demand — fetches on first access, then caches with a TTL -{ - type: "on-demand", - ttl: 300, // cache TTL in seconds (default: 300) - useStaleOnError: true, // serve stale data on fetch error (default: true) - timeout: 30000, // HTTP timeout in milliseconds (default: 30000) -} -``` +## Refresh Strategies -### `ExperimentationOptions` +The current TypeScript `RefreshStrategy` type is a union of two object shapes: -| Field | Type | Required | Description | -| ------------------ | ------------------------ | -------- | ---------------------------------- | -| `refreshStrategy` | `RefreshStrategy` | Yes | How experiment data is refreshed | -| `evaluationCache` | `EvaluationCacheOptions` | No | Cache for experiment evaluations | -| `defaultToss` | `number` | No | Default toss value for experiments | +```typescript +type PollingStrategy = { + interval: number; + timeout?: number; +}; -```javascript -const experimentationOptions = { - refreshStrategy: { - type: "polling", - interval: 5000, - timeout: 2000, - }, - evaluationCache: { - ttl: 300, - size: 1000, - }, - defaultToss: 50, +type OnDemandStrategy = { + ttl: number; + timeout?: number; + use_stale_on_error?: boolean; }; ``` -### `EvaluationCacheOptions` +For configuration refresh, the current `ConfigurationClient` starts background +polling when `refreshStrategy` has an `interval` field. Without an interval, it +fetches configuration lazily when the provider evaluates config. -| Field | Type | Default | Description | -| ------ | -------- | ------- | ------------------------------- | -| `ttl` | `number` | `60` | Cache time-to-live in seconds | -| `size` | `number` | `500` | Maximum number of cache entries | +```javascript +// Poll configuration every 60 seconds. +refreshStrategy: { + interval: 60000, + timeout: 30000, +} +``` -## Provider Variants +For experimentation refresh, both polling and TTL-based on-demand strategies are +handled by `ExperimentationClient`. -### 1. `LocalResolutionProvider` (Recommended) +## Experimentation -Fetches config from a pluggable data source (HTTP), caches locally, and evaluates flags in-process. Accepts an optional fallback data source. +Enable experimentation by passing `experimentationOptions`: ```javascript -import { - LocalResolutionProvider, - HttpDataSource, - SuperpositionOptions, -} from "superposition-provider"; - -const httpSource = new HttpDataSource( - new SuperpositionOptions({ - endpoint: "http://localhost:8080", - token: "token", - orgId: "localorg", - workspaceId: "dev", - }) -); - -const provider = new LocalResolutionProvider({ - dataSource: httpSource, - refreshStrategy: { - type: "polling", - interval: 30000, - timeout: 10000, - }, +const provider = new SuperpositionProvider({ + endpoint: "http://localhost:8080", + token: "your-api-token", + org_id: "localorg", + workspace_id: "test", + refreshStrategy: { interval: 60000, timeout: 30000 }, experimentationOptions: { refreshStrategy: { - type: "polling", - interval: 5000, - timeout: 2000, + interval: 60000, + timeout: 30000, }, - evaluationCache: { ttl: 300, size: 1000 }, - defaultToss: 50, }, }); - -await OpenFeature.setProviderAndWait(provider); -const client = OpenFeature.getClient(); - -// Resolve all config at once (calls provider directly) -const context = { targetingKey: "user-1234", city: "Berlin", os: "android" }; -const allConfig = await provider.resolveAllConfigDetails({}, context); -console.log("All config:", allConfig); ``` -**Key capabilities:** - -- **Pluggable data sources** — use `HttpDataSource` for server-backed resolution -- **Optional fallback** — provide a secondary data source that is used when the primary source fails -- **Full config resolution** — resolve all features at once via `resolveAllConfigDetails()` - -### 2. `SuperpositionAPIProvider` (Remote / Stateless) - -A stateless provider that calls the Superposition server on every evaluation. No local caching — each flag evaluation makes an HTTP request. Best for serverless, low-traffic, or scenarios where you always want the latest config. - -```javascript -import { OpenFeature } from "@openfeature/server-sdk"; -import { SuperpositionAPIProvider, SuperpositionOptions } from "superposition-provider"; - -const provider = new SuperpositionAPIProvider( - new SuperpositionOptions({ - endpoint: "http://localhost:8080", - token: "token", - orgId: "localorg", - workspaceId: "dev", - }) -); - -await OpenFeature.setProviderAndWait(provider); -const client = OpenFeature.getClient(); - -const context = { targetingKey: "user-42", city: "Berlin" }; -const value = await client.getStringValue("currency", "USD", context); -console.log("currency:", value); -``` +Experimentation uses `context.targetingKey` for bucketing. The current +`ExperimentationOptions` interface also includes `evaluationCache` and +`defaultIdentifier`, but those are provider-specific options rather than the +Rust/Python `default_toss` API. ## Evaluation Context +OpenFeature context properties other than `targetingKey`, `timestamp`, and +internal `__` fields are passed to Superposition as dimensions when they are +simple serializable values. + ```javascript const context = { - targetingKey: "user-42", // Used for experiment bucketing + targetingKey: "user-42", city: "Berlin", os: "ios", customers: "platinum", @@ -250,47 +181,30 @@ const context = { const value = await client.getStringValue("currency", "USD", context); ``` -- **`targetingKey`** — Used for experiment variant bucketing. Typically a user ID. -- All other keys map to your Superposition dimensions. - ## Supported Value Types -| Method | Return Type | Description | -| ----------------- | ----------- | ----------------------- | -| `getBooleanValue` | `boolean` | Boolean flag evaluation | -| `getStringValue` | `string` | String flag evaluation | -| `getNumberValue` | `number` | Number flag evaluation | -| `getObjectValue` | `object` | Object/JSON evaluation | +| Method | Return type | +| ------ | ----------- | +| `getBooleanValue` | `boolean` | +| `getStringValue` | `string` | +| `getNumberValue` | `number` | +| `getObjectValue` | object / JSON-like value | -The `LocalResolutionProvider` also supports resolving all features: +For the full resolved configuration, call the provider directly: ```javascript -// Resolve all config at once const allConfig = await provider.resolveAllConfigDetails({}, context); ``` -:::tip -Use `provider.resolveAllConfigDetails({}, context)` to get the entire resolved configuration in one call. This calls the provider directly, bypassing the OpenFeature client. -::: - -## ES Modules Setup - -Make sure your project uses ES modules. Add this to your `package.json`: - -```json -{ - "type": "module" -} -``` - -## Dependencies - -The JavaScript provider depends on: - -- [`@openfeature/server-sdk`](https://www.npmjs.com/package/@openfeature/server-sdk) — OpenFeature Server SDK -- `superposition-sdk` — Smithy-generated client for Superposition API -- Native bindings for local config resolution (bundled) +## Current Limitations -## Full Example +The following provider features are not implemented in JavaScript yet: -See the integration test: [`clients/javascript/provider-sdk-tests/index.js`](https://github.com/juspay/superposition/blob/main/clients/javascript/provider-sdk-tests/index.js) +- `LocalResolutionProvider` +- `SuperpositionAPIProvider` +- `HttpDataSource` and `FileDataSource` +- Local SuperTOML or JSON file resolution +- Pluggable data sources +- Watch refresh +- Manual refresh +- Automatic fallback data source initialization diff --git a/docs/docs/providers/openfeature/overview.md b/docs/docs/providers/openfeature/overview.md index 39d5fee81..fb3c5f0e5 100644 --- a/docs/docs/providers/openfeature/overview.md +++ b/docs/docs/providers/openfeature/overview.md @@ -12,27 +12,27 @@ OpenFeature provider implementations for [Superposition](https://github.com/jusp - 🚩 **Feature Flag Management** — Full OpenFeature compatibility for boolean, string, integer, float, and object flags - 🎯 **Context-Aware Configuration** — Dynamic configuration based on user context and dimensions -- ⚡ **Multiple Refresh Strategies** — Polling and on-demand configuration fetching +- ⚡ **Multiple Refresh Strategies** — Polling and on-demand configuration fetching across providers, with watch/manual support where implemented - 🔄 **Real-time Updates** — Automatic configuration refresh with configurable polling -- 🛡️ **Error Handling** — Graceful fallbacks and stale data usage on errors -- 🧪 **Experimentation** — Built-in support for A/B testing and feature experiments -- 🔌 **Pluggable Data Sources** — HTTP server, local files, or custom data sources +- 🛡️ **Error Handling** — Provider-specific fallback and stale-cache behavior where implemented +- 🧪 **Experimentation** — Support for A/B testing and feature experiments where enabled by the provider +- 🔌 **Provider-Specific Data Sources** — HTTP server, local files, or custom data sources depending on the language implementation ## Supported Languages | Language | Package | Docs | | ---------- | ----------------------------------------------------------------------------------------- | ------------------------------------- | -| Rust | [`superposition_provider`](https://crates.io/crates/superposition_provider) | [Rust Provider](./rust) | -| Python | [`superposition-provider`](https://pypi.org/project/superposition-provider/) | [Python Provider](./python) | -| JavaScript | [`superposition-provider`](https://www.npmjs.com/package/superposition-provider) | [JavaScript Provider](./javascript) | -| Java | [`io.juspay.superposition:openfeature-provider`](https://central.sonatype.com/artifact/io.juspay.superposition/openfeature-provider) | [Java Provider](./java) | -| Haskell | [`superposition-open-feature-provider`](https://hackage.haskell.org/package/superposition-open-feature-provider) | [Haskell Provider](./haskell) | +| Rust | [`superposition_provider`](https://crates.io/crates/superposition_provider) | [Rust Provider](/docs/providers/openfeature/rust) | +| Python | [`superposition-provider`](https://pypi.org/project/superposition-provider/) | [Python Provider](/docs/providers/openfeature/python) | +| JavaScript | [`superposition-provider`](https://www.npmjs.com/package/superposition-provider) | [JavaScript Provider](/docs/providers/openfeature/javascript) | +| Java | `io.juspay.superposition.openfeature:superposition-provider` | [Java Provider](/docs/providers/openfeature/java) | +| Haskell | [`superposition-open-feature-provider`](https://hackage.haskell.org/package/superposition-open-feature-provider) | [Haskell Provider](/docs/providers/openfeature/haskell) | ## Prerequisites Before using any provider, ensure that: -1. **Superposition server is running** — See [Quick Start](../../quick_start) +1. **Superposition server is running** — See [Quick Start](/docs/quick_start) 2. **Valid credentials** are configured — `token`, `org_id`, `workspace_id` 3. **Feature flags / default configs** are set up in your Superposition workspace @@ -40,14 +40,22 @@ Before using any provider, ensure that: ### Provider Architecture -All providers offer two approaches to flag evaluation: +Provider architecture varies by language. Rust and Python currently expose the +newer data-source provider API with `LocalResolutionProvider` and +`SuperpositionAPIProvider`. JavaScript, Java, and Haskell currently expose a +single language-specific provider API backed by the Superposition HTTP API and +local provider caches. | Variant | Description | When to Use | | ------- | ----------- | ----------- | -| **`LocalResolutionProvider`** | Fetches config from a data source (HTTP server or local file), caches locally, and evaluates flags in-process. **Recommended for most use cases.** | Production applications — low latency, supports offline fallback, supports all refresh strategies | -| **`SuperpositionAPIProvider`** | A stateless remote provider that calls the Superposition server on every evaluation. No local caching. | Serverless, low-traffic, or always-latest requirements | +| **`LocalResolutionProvider`** | Fetches config from a data source, caches locally, and evaluates flags in-process. Available in languages that have adopted the data-source provider API. | Production applications that need low-latency local evaluation | +| **`SuperpositionAPIProvider`** | A stateless remote provider that calls the Superposition server on every evaluation. No local caching. Available only in providers that implement it. | Serverless, low-traffic, or always-latest requirements | +| **`SuperpositionProvider` / `SuperpositionOpenFeatureProvider`** | Language-specific provider wrappers used by current JavaScript, Java, and Haskell providers. | Applications using the current single-provider APIs | -The `LocalResolutionProvider` uses pluggable **data sources** (e.g. `HttpDataSource`, `FileDataSource`) and supports an optional **fallback data source** for resilient initialization. +Where available, `LocalResolutionProvider` uses pluggable **data sources** +(for example `HttpDataSource` and `FileDataSource`) and may support an optional +**fallback data source** for resilient initialization. Check the language page +for the exact API surface. ### Evaluation Context @@ -62,17 +70,22 @@ Providers support multiple refresh strategies for keeping configuration up to da | Strategy | Description | Availability | | ------------- | -------------------------------------------------------------------- | ------------- | -| **Polling** | Periodically fetches config updates at a fixed interval | All languages | -| **On-Demand** | Fetches on first access, then caches with a configurable TTL | All languages | -| **Watch** | Uses file-system notifications to reload on file changes | Rust only | -| **Manual** | No automatic refresh; user triggers refresh explicitly | Rust only | +| **Polling** | Periodically fetches config updates at a fixed interval | All documented providers | +| **On-Demand / lazy fetch** | Fetches on first access, or refreshes from a TTL-backed cache where implemented | Language-specific; see each provider page | +| **Watch** | Uses file-system notifications to reload on file changes | Rust and Python | +| **Manual** | No automatic refresh; user triggers refresh explicitly | Rust and Python | ### Experimentation -When experimentation options are configured, the provider will: +When experimentation support is configured, providers generally: 1. Fetch running experiments from the server 2. Use the `targeting_key` to determine which variant a user belongs to 3. Apply experiment overrides to the resolved configuration -All providers support configurable `ExperimentationOptions` including refresh strategy, evaluation cache, and a default toss value. +The exact experimentation options vary by language. Rust and Python expose +`ExperimentationOptions` with refresh and cache settings. JavaScript uses an +`experimentationOptions` object, Java uses nested +`SuperpositionProviderOptions.ExperimentationOptions`, and Haskell currently +uses `experimentationRefreshOptions` without evaluation-cache or default-toss +options. diff --git a/docs/docs/providers/openfeature/python.md b/docs/docs/providers/openfeature/python.md index 96b74ba1a..634df90ae 100644 --- a/docs/docs/providers/openfeature/python.md +++ b/docs/docs/providers/openfeature/python.md @@ -3,12 +3,15 @@ sidebar_position: 3 title: Python --- -# Python — Superposition OpenFeature Provider +# Python - Superposition OpenFeature Provider -The Python provider is an OpenFeature-compatible provider for Superposition. It offers two provider variants: +The Python package currently exposes two OpenFeature-compatible providers: -- **`LocalResolutionProvider`** — Fetches config from a data source (HTTP server), caches it locally, and evaluates flags in-process. Supports polling and on-demand refresh strategies. This is the **recommended provider** for most use cases. -- **`SuperpositionAPIProvider`** — A stateless remote provider that makes an HTTP API call to the Superposition server on every evaluation. No local caching — useful for serverless or low-traffic scenarios. +- **`LocalResolutionProvider`** - Fetches config through a data source, caches it locally, and evaluates flags in-process through native bindings. +- **`SuperpositionAPIProvider`** - Calls the Superposition API directly for remote evaluation and does not keep a local config cache. + +The local provider supports HTTP and file data sources, optional fallback data +sources, polling, on-demand, watch, and manual refresh strategies. **PyPI:** [`superposition-provider`](https://pypi.org/project/superposition-provider/) @@ -24,67 +27,49 @@ You need a running Superposition server. See [Quick Start](../../quick_start) fo ## Quick Start -This is the most common usage — the provider connects to a Superposition server via HTTP, polls for config updates, and evaluates flags locally. - ```python import asyncio + from openfeature import api from openfeature.evaluation_context import EvaluationContext -from superposition_provider import ( - LocalResolutionProvider, - HttpDataSource, - SuperpositionOptions, - PollingStrategy, - RefreshStrategy, -) +from superposition_provider import LocalResolutionProvider, HttpDataSource +from superposition_provider.types import SuperpositionOptions, PollingStrategy async def main(): - # 1. Create an HTTP data source pointing to your Superposition server - http_source = HttpDataSource(SuperpositionOptions( + options = SuperpositionOptions( endpoint="http://localhost:8080", token="your-api-token", org_id="localorg", workspace_id="test", - )) + ) - # 2. Create the provider with a polling refresh strategy provider = LocalResolutionProvider( - data_source=http_source, - fallback_source=None, # no fallback data source - refresh_strategy=RefreshStrategy.Polling(PollingStrategy( - interval=60, # seconds between polls - timeout=30, # HTTP request timeout in seconds - )), + primary_source=HttpDataSource(options), + refresh_strategy=PollingStrategy(interval=60, timeout=30), ) - # 3. Set up evaluation context (dimensions + targeting key) - ctx = EvaluationContext( + context = EvaluationContext( targeting_key="user-42", attributes={"city": "Berlin", "os": "android"}, ) - # 4. Initialize the provider (async — starts polling, fetches initial config) - await provider.initialize(context=ctx) - - # 5. Register with OpenFeature + await provider.initialize(context) api.set_provider(provider) client = api.get_client() - # 6. Evaluate feature flags - string_val = client.get_string_details("currency", "USD", ctx) - print(f"currency = {string_val.value}") + currency = client.get_string_details("currency", "USD", context) + print(f"currency = {currency.value}") - int_val = client.get_integer_details("price", 0, ctx) - print(f"price = {int_val.value}") + price = client.get_integer_details("price", 0, context) + print(f"price = {price.value}") - bool_val = client.get_boolean_details("dark_mode", False, ctx) - print(f"dark_mode = {bool_val.value}") + dark_mode = client.get_boolean_details("dark_mode", False, context) + print(f"dark_mode = {dark_mode.value}") - float_val = client.get_float_details("discount_rate", 0.0, ctx) - print(f"discount_rate = {float_val.value}") + all_config = provider.resolve_all_features(context) + print(f"All config = {all_config}") - # 7. Cleanup — stops polling, releases resources await provider.shutdown() @@ -92,18 +77,17 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## Configuration Options - -### `SuperpositionOptions` +## Connection Options -Connection options shared by `HttpDataSource` and `SuperpositionAPIProvider`: +`SuperpositionOptions` is used by `HttpDataSource` and +`SuperpositionAPIProvider`. -| Field | Type | Required | Description | -| -------------- | ----- | -------- | ----------------------------- | -| `endpoint` | `str` | Yes | Superposition server URL | -| `token` | `str` | Yes | Authentication token (bearer) | -| `org_id` | `str` | Yes | Organisation ID | -| `workspace_id` | `str` | Yes | Workspace ID | +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `endpoint` | `str` | Yes | Superposition server URL | +| `token` | `str` | Yes | Authentication token | +| `org_id` | `str` | Yes | Organisation ID | +| `workspace_id` | `str` | Yes | Workspace ID | ```python options = SuperpositionOptions( @@ -114,109 +98,145 @@ options = SuperpositionOptions( ) ``` -### Refresh Strategies +## Refresh Strategies + +The current Python API uses dataclass strategy objects directly: ```python -# Polling — periodically fetches updates from the server -RefreshStrategy.Polling(PollingStrategy( - interval=60, # seconds between polls (default: 60) - timeout=30, # HTTP request timeout in seconds (default: 30) -)) +from superposition_provider.types import ( + PollingStrategy, + OnDemandStrategy, + WatchStrategy, + ManualStrategy, +) -# On-Demand — fetches on first access, then caches with a TTL -RefreshStrategy.OnDemand(OnDemandStrategy( - ttl=300, # cache TTL in seconds (default: 300) - use_stale_on_error=True, # serve stale data on fetch error (default: True) - timeout=30, # HTTP timeout in seconds (default: 30) -)) +# Poll every 60 seconds. +PollingStrategy(interval=60, timeout=30) + +# Refresh when cached data is older than the TTL. +OnDemandStrategy(ttl=300, use_stale_on_error=True, timeout=30) + +# Watch a file-backed data source. +WatchStrategy(debounce_ms=500) + +# No automatic refresh; call refresh() yourself. +ManualStrategy() ``` -### `ExperimentationOptions` +There is a `RefreshStrategy` type alias, but there are no +`RefreshStrategy.Polling(...)` or `RefreshStrategy.OnDemand(...)` constructors. + +## Local Provider -| Field | Type | Required | Description | -| ------------------ | ---------------------------------- | -------- | ---------------------------------- | -| `refresh_strategy` | `RefreshStrategy` | Yes | How experiment data is refreshed | -| `evaluation_cache` | `Optional[EvaluationCacheOptions]` | No | Cache for experiment evaluations | -| `default_toss` | `Optional[int]` | No | Default toss value for experiments | +`LocalResolutionProvider` takes a primary data source, an optional fallback data +source, and a refresh strategy. ```python -exp_options = ExperimentationOptions( - refresh_strategy=RefreshStrategy.Polling(PollingStrategy( - interval=5, - timeout=3, +from openfeature.evaluation_context import EvaluationContext +from superposition_provider import LocalResolutionProvider, HttpDataSource +from superposition_provider.types import SuperpositionOptions, PollingStrategy + +provider = LocalResolutionProvider( + primary_source=HttpDataSource(SuperpositionOptions( + endpoint="http://localhost:8080", + token="token", + org_id="localorg", + workspace_id="dev", )), - evaluation_cache=EvaluationCacheOptions(ttl=300, size=1000), - default_toss=50, + refresh_strategy=PollingStrategy(interval=30, timeout=10), ) -``` -### `EvaluationCacheOptions` +await provider.initialize(EvaluationContext()) + +context = EvaluationContext( + targeting_key="user-1234", + attributes={"dimension": "d2"}, +) + +all_config = provider.resolve_all_features(context) +variants = await provider.get_applicable_variants(context) + +await provider.shutdown() +``` -| Field | Type | Default | Description | -| ------ | --------------- | ------- | ------------------------------- | -| `ttl` | `Optional[int]` | `60` | Cache time-to-live in seconds | -| `size` | `Optional[int]` | `500` | Maximum number of cache entries | +**Key capabilities:** -## Provider Variants +- **HTTP data source** - `HttpDataSource(SuperpositionOptions(...))` +- **File data source** - `FileDataSource("config.toml")` +- **Optional fallback** - pass `fallback_source=...` +- **Manual refresh** - call `await provider.refresh()` +- **Async lifecycle** - `initialize()` and `shutdown()` are async -### 1. `LocalResolutionProvider` (Recommended) +## Local File Resolution -Fetches config from a pluggable data source (HTTP), caches locally, and evaluates flags in-process. Accepts an optional fallback data source. +Resolve config from a local TOML or JSON file without a server: ```python -from superposition_provider import ( - LocalResolutionProvider, - HttpDataSource, - SuperpositionOptions, - PollingStrategy, - RefreshStrategy, - ExperimentationOptions, - EvaluationCacheOptions, +from openfeature.evaluation_context import EvaluationContext +from superposition_provider import FileDataSource, LocalResolutionProvider +from superposition_provider.types import OnDemandStrategy + +provider = LocalResolutionProvider( + primary_source=FileDataSource("config.toml"), + refresh_strategy=OnDemandStrategy(ttl=60), ) + +await provider.initialize(EvaluationContext()) + +context = EvaluationContext( + attributes={"os": "linux", "city": "Boston"}, +) + +config = provider.resolve_all_features(context) +timeout = provider.resolve_integer_details("timeout", 0, context) + +await provider.shutdown() +``` + +:::note +`FileDataSource` does not support experiments. Use an HTTP-backed source when +experiment metadata is required. +::: + +## HTTP with File Fallback + +Use a Superposition server as the primary source and a local file as fallback: + +```python from openfeature.evaluation_context import EvaluationContext +from superposition_provider import FileDataSource, HttpDataSource, LocalResolutionProvider +from superposition_provider.types import SuperpositionOptions, PollingStrategy -http_source = HttpDataSource(SuperpositionOptions( +options = SuperpositionOptions( endpoint="http://localhost:8080", token="token", org_id="localorg", workspace_id="dev", -)) +) provider = LocalResolutionProvider( - data_source=http_source, - fallback_source=None, - refresh_strategy=RefreshStrategy.Polling(PollingStrategy(interval=30, timeout=10)), - experimentation_options=ExperimentationOptions( - refresh_strategy=RefreshStrategy.Polling(PollingStrategy(interval=5, timeout=3)), - evaluation_cache=EvaluationCacheOptions(ttl=300, size=1000), - default_toss=50, - ), + primary_source=HttpDataSource(options), + fallback_source=FileDataSource("config.toml"), + refresh_strategy=PollingStrategy(interval=10, timeout=10), ) -# Initialize the provider (fetches initial config) -await provider.initialize(context=EvaluationContext()) +await provider.initialize(EvaluationContext()) -# Resolve all config values at once -ctx = EvaluationContext( - targeting_key="user-1234", - attributes={"city": "Berlin", "os": "android"}, +context = EvaluationContext( + targeting_key="user-456", + attributes={"os": "linux", "city": "Berlin"}, ) -all_config = provider.resolve_all_config_details({}, ctx) -print(f"All config: {all_config}") -# Cleanup +config = provider.resolve_all_features(context) +currency = provider.resolve_string_details("currency", "No value", context) + await provider.shutdown() ``` -**Key capabilities:** - -- **Pluggable data sources** — use `HttpDataSource` for server-backed resolution -- **Optional fallback** — provide a secondary data source that is used when the primary source fails -- **Async lifecycle** — `initialize()` and `shutdown()` are async, supporting non-blocking I/O - -### 2. `SuperpositionAPIProvider` (Remote / Stateless) +## Remote Provider -A stateless provider that calls the Superposition server on every evaluation. No local caching — each flag evaluation makes an HTTP request. Best for serverless, low-traffic, or scenarios where you always want the latest config. +`SuperpositionAPIProvider` performs remote API evaluation and keeps no local +config cache. Its current lifecycle methods are synchronous. ```python from openfeature import api @@ -230,51 +250,24 @@ provider = SuperpositionAPIProvider(SuperpositionOptions( workspace_id="dev", )) -# Initialize and register with OpenFeature -await provider.initialize(context=EvaluationContext()) -api.set_provider(provider) -client = api.get_client() - -ctx = EvaluationContext( +context = EvaluationContext( targeting_key="user-42", attributes={"city": "Berlin"}, ) -currency = client.get_string_details("currency", "USD", ctx) -print(f"currency = {currency.value}") -``` - -## Provider Lifecycle - -```python -# Create -provider = LocalResolutionProvider( - data_source=http_source, - fallback_source=None, - refresh_strategy=RefreshStrategy.Polling(PollingStrategy(interval=60, timeout=30)), -) - -# Initialize (async — starts polling, fetches initial config) -await provider.initialize(context=ctx) - -# Register with OpenFeature +provider.initialize(context) api.set_provider(provider) client = api.get_client() -# ... evaluate flags ... +currency = client.get_string_details("currency", "USD", context) +print(f"currency = {currency.value}") -# Shutdown (stops polling, releases resources) -await provider.shutdown() +provider.shutdown() ``` -### Provider Status - -| Status | Meaning | -| ------------ | ------------------------------------------ | -| `NOT_READY` | Provider created but not yet initialized | -| `READY` | Provider initialized and ready for use | -| `ERROR` | Initialization failed | -| `FATAL` | Unrecoverable error during shutdown | +Use the async provider-specific methods such as +`resolve_all_features_with_filter_async` when you want direct access to remote +all-config resolution. ## Evaluation Context @@ -283,8 +276,8 @@ Pass dimensions and a targeting key for experiment bucketing: ```python from openfeature.evaluation_context import EvaluationContext -ctx = EvaluationContext( - targeting_key="user-42", # Used for experiment variant selection +context = EvaluationContext( + targeting_key="user-42", attributes={ "city": "Berlin", "os": "ios", @@ -293,55 +286,34 @@ ctx = EvaluationContext( ) ``` -- **`targeting_key`** — Maps to the toss value for experiment bucketing. Typically a user ID or session ID. -- **`attributes`** — Key-value pairs matching your Superposition dimensions. +- **`targeting_key`** - Used for experiment variant selection. +- **`attributes`** - Key-value pairs matching your Superposition dimensions. ## Supported Value Types -| Method | Return Type | Description | -| ---------------------------- | ------------------------------ | ----------------------- | -| `get_boolean_details` | `FlagResolutionDetails[bool]` | Boolean flag evaluation | -| `get_string_details` | `FlagResolutionDetails[str]` | String flag evaluation | -| `get_integer_details` | `FlagResolutionDetails[int]` | Integer flag evaluation | -| `get_float_details` | `FlagResolutionDetails[float]` | Float flag evaluation | -| `resolve_object_details` | `FlagResolutionDetails[Any]` | Object/JSON evaluation | +| Method | Return Type | +| ------ | ----------- | +| `resolve_boolean_details` / `get_boolean_details` | `FlagResolutionDetails[bool]` | +| `resolve_string_details` / `get_string_details` | `FlagResolutionDetails[str]` | +| `resolve_integer_details` / `get_integer_details` | `FlagResolutionDetails[int]` | +| `resolve_float_details` / `get_float_details` | `FlagResolutionDetails[float]` | +| `resolve_object_details` | `FlagResolutionDetails[Any]` | -The `LocalResolutionProvider` also supports resolving all features: +The provider-specific full-config API is: ```python -# Resolve all config values at once -all_config = provider.resolve_all_config_details({}, ctx) +all_config = provider.resolve_all_features(context) +filtered = provider.resolve_all_features_with_filter(context, ["payment."]) ``` -:::tip -Use `provider.resolve_all_config_details({}, ctx)` to get the entire resolved configuration in one call. This bypasses the OpenFeature client and calls the provider directly. -::: - -## Error Handling - -The provider handles errors gracefully: - -- If the server is unreachable during initialization, the provider status is set to `ERROR` -- If a fallback data source is provided, it will be used when the primary source fails -- Flag evaluation returns the `default_value` when a flag is not found +Async variants are also available: ```python -try: - await provider.initialize(context=ctx) -except Exception as e: - print(f"Failed to initialize: {e}") - # Provider status will be ERROR - # If fallback_source was provided, it will be used +all_config = await provider.resolve_all_features_async(context) ``` -## Dependencies - -The Python provider depends on: - -- [`openfeature-sdk`](https://pypi.org/project/openfeature-sdk/) — OpenFeature Python SDK -- `superposition_py_sdk` — Smithy-generated client for Superposition API -- `superposition_py_cac_client` — Native FFI bindings (via Rust) for local config resolution - -## Full Example +## Current Limitations -See the integration test: [`clients/python/provider-sdk-tests/main.py`](https://github.com/juspay/superposition/blob/main/clients/python/provider-sdk-tests/main.py) +- `LocalResolutionProvider` does not take an `experimentation_options` argument today. +- `ExperimentationOptions` is available in `types.py` for compatibility paths, but it is not wired into the `LocalResolutionProvider` constructor. +- File-backed sources do not support experiments. diff --git a/docs/docs/providers/openfeature/rust.md b/docs/docs/providers/openfeature/rust.md index 21b92298a..4431638bf 100644 --- a/docs/docs/providers/openfeature/rust.md +++ b/docs/docs/providers/openfeature/rust.md @@ -271,7 +271,7 @@ use superposition_provider::{ async fn main() { env_logger::init(); - let file_source = FileDataSource::new(PathBuf::from("config.toml")); + let file_source = FileDataSource::new(PathBuf::from("config.toml")).unwrap(); let provider = LocalResolutionProvider::new( Box::new(file_source), @@ -326,7 +326,7 @@ async fn main() { )); // Fallback: local TOML config file - let file_source = FileDataSource::new(PathBuf::from("config.toml")); + let file_source = FileDataSource::new(PathBuf::from("config.toml")).unwrap(); let provider = LocalResolutionProvider::new( Box::new(http_source), diff --git a/docs/docs/quick_start.mdx b/docs/docs/quick_start.mdx index 7635a5930..0f62a9b93 100644 --- a/docs/docs/quick_start.mdx +++ b/docs/docs/quick_start.mdx @@ -46,13 +46,13 @@ Install the provider for your language of choice. The provider will allow you to {`implementation "dev.openfeature:sdk:${VERSIONS.openfeature}" -implementation "io.juspay.superposition:openfeature-provider:${VERSIONS.superposition}"`} +implementation "io.juspay.superposition.openfeature:superposition-provider:0.0.1-dev"`} {`implementation("dev.openfeature:sdk:${VERSIONS.openfeature}") -implementation("io.juspay.superposition:openfeature-provider:${VERSIONS.superposition}")`} +implementation("io.juspay.superposition.openfeature:superposition-provider:0.0.1-dev")`} @@ -145,32 +145,25 @@ superposition_provider = "${VERSIONS.superposition}"`} ```python title="configuration.py" from openfeature import api - from superposition_provider.provider import SuperpositionProvider - from superposition_provider.types import ExperimentationOptions, SuperpositionProviderOptions, PollingStrategy - - config_options = SuperpositionProviderOptions( - endpoint="http://localhost:8080", - token="api-token", - org_id="localorg", - workspace_id="test", - refresh_strategy=PollingStrategy( - interval=5, # Poll every 5 seconds - timeout=3 # Timeout after 3 seconds - ), - fallback_config=None, - evaluation_cache_options=None, - experimentation_options=ExperimentationOptions( - refresh_strategy=PollingStrategy( - interval=5, # Poll every 5 seconds - timeout=3 # Timeout after 3 seconds - ) - ) - ) - - provider = SuperpositionProvider(provider_options=config_options) - # Initialize provider - await provider.initialize(context=ctx) - api.set_provider(provider) + from openfeature.evaluation_context import EvaluationContext + from superposition_provider import LocalResolutionProvider, HttpDataSource + from superposition_provider.types import SuperpositionOptions, PollingStrategy + + options = SuperpositionOptions( + endpoint="http://localhost:8080", + token="api-token", + org_id="localorg", + workspace_id="test", + ) + + provider = LocalResolutionProvider( + primary_source=HttpDataSource(options), + refresh_strategy=PollingStrategy(interval=5, timeout=3), + ) + + ctx = EvaluationContext(attributes={}) + await provider.initialize(ctx) + api.set_provider(provider) ``` @@ -282,7 +275,7 @@ Once the provider is initialized, you can evaluate feature flags and configurati evaluation_context=ctx ) # Note: If you want the whole config, you can directly use the provider itself - resp = provider.resolve_all_config_details({}, ctx) + resp = provider.resolve_all_features(ctx) print(f"Response for all config: {resp}") print("Successfully resolved boolean flag details:", bool_val) @@ -352,9 +345,9 @@ If you want to do more than just read configs, Superposition provides SDKs for v ``` - ```toml title="Cargo.toml" - [dependencies] - superposition_sdk = "0.85.0" - ``` + + {`[dependencies] +superposition_sdk = "${VERSIONS.superposition}"`} + diff --git a/docs/docs/superposition-config-file/cascading-model.md b/docs/docs/superposition-config-file/cascading-model.md index 4d17c5ca7..03b7a194f 100644 --- a/docs/docs/superposition-config-file/cascading-model.md +++ b/docs/docs/superposition-config-file/cascading-model.md @@ -355,30 +355,30 @@ _context_ = { city = "Bangalore", vehicle_type = "cab" } per_km_rate = 22.0 ``` -### 2. Conflicting Overrides at Same Priority +### 2. Conflicting Overrides for the Same Context -Avoid overrides that conflict at the same priority level: +Avoid duplicate overrides that set the same key for the same context: ```toml -# ❌ Bad: Same priority, different values +# Bad: Same context, different values [[overrides]] _context_ = { city = "Bangalore" } per_km_rate = 22.0 [[overrides]] -_context_ = { vehicle_type = "cab" } +_context_ = { city = "Bangalore" } per_km_rate = 25.0 -# For Bangalore cab: which one wins? -# Both have same priority (one dimension each) +# For Bangalore: which one wins? +# Both use the same dimension set and therefore the same computed weight. ``` -Resolution: Make one more specific: +Resolution: keep only one override, or make the later override intentionally more specific: ```toml -# ✅ Good: Clear priority +# Good: Clear priority [[overrides]] -_context_ = { vehicle_type = "cab" } +_context_ = { city = "Bangalore" } per_km_rate = 25.0 [[overrides]] diff --git a/docs/docs/superposition-config-file/context-expressions.md b/docs/docs/superposition-config-file/context-expressions.md index 452ae9719..19b8a9494 100644 --- a/docs/docs/superposition-config-file/context-expressions.md +++ b/docs/docs/superposition-config-file/context-expressions.md @@ -64,7 +64,7 @@ For these use cases, use **LOCAL_COHORT dimensions** with JSONLogic definitions. ## Complex Conditions with LOCAL_COHORT -When you need conditions beyond simple equality, create a LOCAL_COHORT dimension that derives its value using JSONLogic. +When you need conditions beyond simple equality on one source dimension, create a LOCAL_COHORT dimension that derives its value using JSONLogic. ### What is JSONLogic? @@ -134,7 +134,7 @@ time_period = { type = "LOCAL_COHORT:hour_of_day", schema = { type = "string", - enum = ["morning_rush", "evening_rush", "off_peak"], + enum = ["morning_rush", "evening_rush", "otherwise"], definitions = { morning_rush = { "and": [ { ">=": [{ "var": "hour_of_day" }, 7] }, @@ -174,7 +174,7 @@ is_not_delhi = { type = "LOCAL_COHORT:city", schema = { type = "string", - enum = ["yes", "no"], + enum = ["yes", "otherwise"], definitions = { yes = { "!=": [{ "var": "city" }, "Delhi"] } } @@ -220,42 +220,36 @@ _context_ = { region = "north" } per_km_rate = 22.0 ``` -### Complex Conditions: Peak Hours in Specific City +### Complex Conditions: Business Hour Bands -Combine multiple conditions: +Combine multiple JSONLogic operations against the cohort's source dimension: ```toml [dimensions] -city = { position = 4, schema = { type = "string" } } hour_of_day = { position = 3, schema = { type = "integer", minimum = 0, maximum = 23 } } -delhi_peak_hours = { +business_hour_band = { position = 1, - type = "LOCAL_COHORT:city", + type = "LOCAL_COHORT:hour_of_day", schema = { type = "string", - enum = ["yes", "no"], + enum = ["business_hours", "late_night", "otherwise"], definitions = { - yes = { "and": [ - { "==": [{ "var": "city" }, "Delhi"] }, - { "or": [ - { "and": [ - { ">=": [{ "var": "hour_of_day" }, 8] }, - { "<=": [{ "var": "hour_of_day" }, 10] } - ]}, - { "and": [ - { ">=": [{ "var": "hour_of_day" }, 18] }, - { "<=": [{ "var": "hour_of_day" }, 21] } - ]} - ]} + business_hours = { "and": [ + { ">=": [{ "var": "hour_of_day" }, 9] }, + { "<": [{ "var": "hour_of_day" }, 18] } + ]}, + late_night = { "or": [ + { "<": [{ "var": "hour_of_day" }, 5] }, + { ">=": [{ "var": "hour_of_day" }, 22] } ]} } } } [[overrides]] -_context_ = { delhi_peak_hours = "yes" } -surge_factor = 2.5 +_context_ = { business_hour_band = "late_night" } +support_mode = "async_only" ``` ### Range Check: Age Groups @@ -272,7 +266,7 @@ age_group = { type = "LOCAL_COHORT:user_age", schema = { type = "string", - enum = ["youth", "adult", "senior"], + enum = ["youth", "senior", "otherwise"], definitions = { youth = { "<": [{ "var": "user_age" }, 25] }, senior = { ">=": [{ "var": "user_age" }, 60] } @@ -312,7 +306,7 @@ is_peak_hour = { type = "LOCAL_COHORT:hour_of_day", schema = { type = "string", - enum = ["yes", "no"], + enum = ["yes", "otherwise"], definitions = { yes = { "or": [ { "and": [ @@ -346,16 +340,16 @@ condition_1 = { ... } check = { ... } ``` -### 4. Always Include a Default Value +### 4. Always Include `otherwise` -Cohorts should have a default value for unmatched cases: +Local cohorts must include `otherwise` in the enum for unmatched cases: ```toml schema = { type = "string", - enum = ["yes", "no"], # "no" is the default for unmatched + enum = ["yes", "otherwise"], definitions = { - yes = { ... } # Only define the "yes" condition + yes = { ... } # Do not define "otherwise"; it is the fallback } } ``` @@ -374,7 +368,7 @@ Context conditions are validated at parse time: 1. **Dimension existence**: All dimensions in `_context_` must be declared 2. **Value validity**: Values must match dimension schemas -3. **Cohort definitions**: JSONLogic must be valid and reference existing dimensions +3. **Cohort definitions**: `LOCAL_COHORT` schemas must include `otherwise`, exclude it from `definitions`, and define every other enum option ### Example Errors diff --git a/docs/docs/superposition-config-file/deterministic-resolution.md b/docs/docs/superposition-config-file/deterministic-resolution.md index 962172a91..c6ab96f30 100644 --- a/docs/docs/superposition-config-file/deterministic-resolution.md +++ b/docs/docs/superposition-config-file/deterministic-resolution.md @@ -164,7 +164,7 @@ When designing your dimensions, assign higher positions to more significant dime ```toml [dimensions] # Lower positions = less significant -variant = { position = 0, ... } # Experimentation (reserved) +variant = { position = 0, ... } # Conventionally used by experimentation tier = { position = 1, ... } # User tier vehicle_type = { position = 2, ... } # Vehicle category hour_of_day = { position = 3, ... } # Time-based @@ -183,7 +183,7 @@ With this hierarchy: ### When Contexts Have Equal Priority -If two matching contexts have the same priority, **file order determines the winner**. The later override wins: +With unique dimension positions, different dimension sets have different weights. Equal priority mainly happens when two matching overrides use the same dimension set. In that case, **file order determines the winner** and the later override wins: ```toml [[overrides]] @@ -191,12 +191,12 @@ _context_ = { city = "Bangalore" } per_km_rate = 21.0 [[overrides]] -_context_ = { vehicle_type = "cab" } # Same priority if positions are equal +_context_ = { city = "Bangalore" } # Same dimension set per_km_rate = 25.0 # This wins (appears later) ``` :::caution -Avoid equal-priority conflicts by designing distinct positions for your dimensions. +Avoid duplicate same-context overrides unless the later override is intentionally replacing an earlier one. ::: ### When Multiple Overrides Affect the Same Key @@ -368,7 +368,7 @@ If context A is a subset of context B, then B's priority is at least as high as ## Position Assignment Guidelines -1. **Reserve position 0** for `variantIds` (experimentation) +1. Leave position 0 for `variantIds` when using experimentation flows 2. **Use gaps** between positions to allow future dimensions: ```toml [dimensions] diff --git a/docs/docs/superposition-config-file/dimensions.md b/docs/docs/superposition-config-file/dimensions.md index e4b755a06..fb3bb52a9 100644 --- a/docs/docs/superposition-config-file/dimensions.md +++ b/docs/docs/superposition-config-file/dimensions.md @@ -37,7 +37,7 @@ The position determines how much weight this dimension contributes to context pr - Weight = 2^position - Higher positions = more influence on priority -- Position 0 is reserved for `variantIds` (experimentation) +- Position 0 is conventionally left for `variantIds` in experimentation flows; it is not rejected by the file parser - Positions must be unique :::info @@ -189,10 +189,10 @@ peak_hours = { ">=" = [{ var = "hour_of_day" }, 18] } Combine conditions: ```toml -premium_zone = { +after_hours = { and = [ - { in = [{ var = "city" }, ["Bangalore", "Delhi"]] }, - { ">=" = [{ var = "hour_of_day" }, 18] } + { ">=" = [{ var = "hour_of_day" }, 18] }, + { "<=" = [{ var = "hour_of_day" }, 23] } ] } ``` @@ -239,7 +239,7 @@ time_period = { type = "LOCAL_COHORT:hour_of_day", schema = { type = "string", - enum = ["morning_rush", "evening_rush", "off_peak"], + enum = ["morning_rush", "evening_rush", "otherwise"], definitions = { morning_rush = { and = [{ ">=" = [{ var = "hour_of_day" }, 7] }, { "<=" = [{ var = "hour_of_day" }, 9] }] }, evening_rush = { and = [{ ">=" = [{ var = "hour_of_day" }, 17] }, { "<=" = [{ var = "hour_of_day" }, 20] }] } @@ -269,7 +269,7 @@ tier = { type = "LOCAL_COHORT:user_segment", schema = { type = "string", - enum = ["premium", "regular", "new"], + enum = ["premium", "new", "otherwise"], definitions = { premium = { in = [{ var = "user_segment" }, ["gold", "platinum", "diamond"]] }, new = { in = [{ var = "user_segment" }, ["trial", "onboarding"]] } @@ -284,7 +284,7 @@ support_priority = "high" ### REMOTE_COHORT -A `REMOTE_COHORT` dimension derives its value from an external service: +A `REMOTE_COHORT` dimension declares that a cohort value is derived outside the local file resolver: ```toml [dimensions] @@ -300,10 +300,10 @@ user_cohort = { } ``` -The cohort value is fetched from an external service at resolution time, allowing for dynamic segmentation based on real-time data. +In local file resolution, remote cohort values are expected to be present in the runtime context. Server/API resolution paths can compute remote cohorts when `resolve_remote` is enabled and the dimension has a value-compute function configured. :::note -REMOTE_COHORT requires backend service integration. The cohort value is resolved by calling an external API with the base dimension value. +REMOTE_COHORT requires backend service integration. The standalone SuperTOML parser validates the reference and schema; it does not call external services. ::: ## Dimension Dependencies @@ -336,7 +336,7 @@ region = { type = "LOCAL_COHORT:city", schema = { type = "string", - enum = ["south", "north", "west"], + enum = ["south", "north", "otherwise"], definitions = { south = { in = [{ var = "city" }, ["Bangalore"]] }, north = { in = [{ var = "city" }, ["Delhi"]] } @@ -350,7 +350,7 @@ market_tier = { type = "LOCAL_COHORT:city", schema = { type = "string", - enum = ["tier1", "tier2"], + enum = ["tier1", "otherwise"], definitions = { tier1 = { in = [{ var = "city" }, ["Bangalore", "Delhi", "Mumbai"]] } } @@ -454,8 +454,8 @@ Avoid using cohorts for: ## Validation Rules 1. **Position uniqueness**: No two dimensions can have the same position -2. **Position 0 reserved**: Reserved for `variantIds` +2. **Position 0 convention**: Leave position 0 for `variantIds` when using experimentation flows 3. **Cohort reference**: Must reference an existing dimension 4. **Cohort position**: Must be ≤ referenced dimension's position 5. **Schema validity**: All schemas must be valid JSON Schema -6. **Cohort schema structure**: Must have `type`, `enum`, and `definitions` +6. **Cohort schema structure**: `LOCAL_COHORT` schemas must have `type`, `enum`, and `definitions`; `enum` must include `otherwise`, and every other enum value must have a definition diff --git a/docs/docs/superposition-config-file/examples.md b/docs/docs/superposition-config-file/examples.md index 9bfeb2b10..143ba36d2 100644 --- a/docs/docs/superposition-config-file/examples.md +++ b/docs/docs/superposition-config-file/examples.md @@ -25,25 +25,25 @@ waiting_charge = { value = 2.0, schema = { type = "number", minimum = 0, descrip [dimensions] city = { - position = 4, + position = 5, schema = { type = "string", enum = ["Bangalore", "Delhi", "Chennai", "Mumbai"] } } vehicle_type = { - position = 2, + position = 3, schema = { type = "string", enum = ["auto", "cab", "bike", "premium"] } } hour_of_day = { - position = 3, + position = 4, schema = { type = "integer", minimum = 0, maximum = 23 } } # Derived dimension for time periods time_period = { - position = 1, + position = 2, type = "LOCAL_COHORT:hour_of_day", schema = { type = "string", - enum = ["morning_rush", "evening_rush", "off_peak"], + enum = ["morning_rush", "evening_rush", "otherwise"], definitions = { morning_rush = { and = [ { ">=" = [{ var = "hour_of_day" }, 7] }, @@ -148,7 +148,7 @@ waiting_charge = 1.5 | `{ city: "Bangalore", vehicle_type: "cab", hour_of_day: 14 }` | 16.0 | 0.0 | 50.0 | | `{ city: "Delhi", vehicle_type: "cab", hour_of_day: 18 }` | 22.0 | 3.0 | 60.0 | | `{ city: "Chennai", vehicle_type: "bike", hour_of_day: 8 }` | 12.0 | 1.5 | 30.0 | -| `{ city: "Mumbai", vehicle_type: "premium", hour_of_day: 20 }` | 35.0 | 2.0 | 100.0 | +| `{ city: "Mumbai", vehicle_type: "premium", hour_of_day: 20 }` | 22.0 | 2.0 | 55.0 | --- diff --git a/docs/docs/superposition-config-file/format-specification.md b/docs/docs/superposition-config-file/format-specification.md index ecea401c9..f954125e6 100644 --- a/docs/docs/superposition-config-file/format-specification.md +++ b/docs/docs/superposition-config-file/format-specification.md @@ -161,7 +161,7 @@ The `position` parameter determines the weight of a dimension in priority calcul - Weight = 2^position - Higher positions contribute more to context priority -- Position 0 is reserved for `variantIds` (experimentation) +- Position 0 is conventionally left for `variantIds` in experimentation flows; the parser enforces uniqueness but does not reject position 0 - Positions must be unique across dimensions :::info @@ -442,9 +442,10 @@ The same configuration in JSON format: 1. Every dimension must have `position` and `schema` 2. Positions must be unique -3. Position 0 is reserved for `variantIds` +3. Avoid position 0 for user-defined dimensions when the same config will be used with experimentation/`variantIds` 4. Cohort dimensions must reference existing dimensions 5. Cohort dimensions must have position ≤ their referenced dimension +6. `LOCAL_COHORT` schemas must include `otherwise` in `enum`, must not define `otherwise` under `definitions`, and must define every other enum option ### Overrides Validation diff --git a/docs/docs/superposition-config-file/intro.md b/docs/docs/superposition-config-file/intro.md index 7b40b5add..396c6eef9 100644 --- a/docs/docs/superposition-config-file/intro.md +++ b/docs/docs/superposition-config-file/intro.md @@ -126,25 +126,23 @@ SuperTOML configurations can also be written in JSON - both formats are equivale ## Language Support -SuperTOML can be consumed from multiple programming languages: +SuperTOML is parsed by the shared `superposition_core` implementation. The checked-in Rust and Python providers expose local file/data-source flows around that resolver; the other OpenFeature providers consume Superposition service data through their current provider APIs. | Language | Library | Description | | ---------- | ------------------------ | ------------------------------- | | Rust | `superposition_provider` | Native Rust implementation | | JavaScript | `superposition-provider` | OpenFeature compatible provider | | Python | `superposition_provider` | OpenFeature compatible provider | -| Java | `openfeature-provider` | OpenFeature compatible provider | - -All languages support the same configuration format and resolution logic. +| Java | `io.juspay.superposition.openfeature:superposition-provider` | OpenFeature compatible provider | ## LSP Support -SuperTOML has full Language Server Protocol support, providing: +SuperTOML has a `supertoml-analyzer` Language Server Protocol implementation under `tooling/lsp`, providing: - **Autocomplete** - Suggestions for dimension names, config keys, and enum values - **Validation** - Real-time schema validation with error diagnostics - **Hover Information** - Documentation and type information on hover -- **Go to Definition** - Navigate to dimension or config definitions +- **Formatting** - Full-document formatting through the LSP server This makes editing SuperTOML files as comfortable as working with code in an IDE. diff --git a/docs/docs/superposition-config-file/lsp-support.md b/docs/docs/superposition-config-file/lsp-support.md index 33e8e9dbe..0d91d0579 100644 --- a/docs/docs/superposition-config-file/lsp-support.md +++ b/docs/docs/superposition-config-file/lsp-support.md @@ -6,7 +6,7 @@ description: Language Server Protocol support for SuperTOML # LSP Support -SuperTOML has full Language Server Protocol (LSP) support, providing a rich editing experience in IDEs and text editors. The LSP enables real-time validation, autocompletion, hover information, and more. +SuperTOML has a Language Server Protocol (LSP) server in this repository: `supertoml-analyzer`, built from the `supertoml_lsp` crate. Editor integrations live under `tooling/lsp/`. ## Features @@ -17,7 +17,7 @@ Get intelligent suggestions while typing: - **Dimension names**: Suggests available dimensions when editing `_context_` - **Config keys**: Suggests valid configuration keys in overrides - **Enum values**: Suggests valid enum values for dimensions and configs -- **Schema properties**: Suggests JSON Schema properties +- **Schema properties**: Suggests JSON Schema draft-7 properties **Example:** @@ -46,6 +46,9 @@ Real-time validation catches errors before you run your application: per_km_rate = { value = -5.0, schema = { type = "number", minimum = 0 } } # Error: Value -5.0 is less than minimum 0 +[dimensions] +city = { position = 1, schema = { type = "string", enum = ["Bangalore", "Delhi", "Chennai"] } } + [[overrides]] _context_ = { city = "Mumbai" } # Error: "Mumbai" is not in enum ["Bangalore", "Delhi", "Chennai"] per_km_rate = 25.0 @@ -73,6 +76,10 @@ per_km_rate = { value = 20.0, schema = { type = "number", minimum = 0 } } # --- ``` +### Formatting + +The server exposes document formatting and uses Taplo formatting for the full document. + ### Diagnostics Get detailed error messages and warnings: @@ -80,64 +87,58 @@ Get detailed error messages and warnings: - **Syntax errors**: Invalid TOML syntax - **Schema errors**: Values don't match schemas - **Reference errors**: Unknown dimensions or config keys -- **Logic errors**: Conflicting overrides, circular dependencies - -### Go to Definition - -Navigate to definitions: - -- From a context dimension name to its dimension definition -- From an override key to its default-config definition -- From a cohort reference to its base dimension - -### Code Actions - -Quick fixes for common issues: - -- **Add missing schema**: Add schema to config without one -- **Fix enum value**: Correct an invalid enum value -- **Add missing dimension**: Add an undeclared dimension +- **Structure errors**: Missing required sections, duplicate positions, invalid cohort references ## Editor Setup ### VS Code -1. Install the SuperTOML extension from the marketplace -2. Open any `.toml` file with SuperTOML content -3. The extension automatically activates for files with `[default-configs]` section +Build the language server and run the checked-in extension from source: + +```bash +cargo build -p supertoml_lsp +cd tooling/lsp/vscode-extension +npm install +npm run compile +``` + +Open `tooling/lsp/vscode-extension` in VS Code and start the extension development host, or package it with `npm run package` and install the generated VSIX. **Extension features:** - Syntax highlighting for SuperTOML -- Full LSP integration +- LSP integration with diagnostics, completions, hover, and formatting - Configuration snippets - File icons ### Neovim -Configure Neovim to use the SuperTOML language server: +Use the checked-in Neovim plugin after building the language server: ```lua --- Using nvim-lspconfig -local lspconfig = require('lspconfig') - -lspconfig.supertoml.setup { - filetypes = { 'toml' }, - root_dir = function(fname) - return lspconfig.util.find_git_ancestor(fname) +{ + "superposition/superposition", + dir = "/path/to/superposition", + rtp = "tooling/lsp/neovim-extension", + config = function() + require("supertoml-analyzer").setup({ + server = { + path = "/path/to/superposition/target/debug/supertoml-analyzer", + }, + }) end, } ``` ### Emacs -Use `lsp-mode` with the SuperTOML language server: +Use `lsp-mode` with the `supertoml-analyzer` binary: ```elisp (use-package lsp-mode :config (lsp-register-client - (make-lsp-client :new-connection (lsp-stdio-connection "supertoml-lsp") + (make-lsp-client :new-connection (lsp-stdio-connection "supertoml-analyzer") :major-modes '(toml-mode) :server-id 'supertoml)) (add-hook 'toml-mode-hook #'lsp)) @@ -147,7 +148,7 @@ Use `lsp-mode` with the SuperTOML language server: Any editor with LSP support can use the SuperTOML language server: -1. Install the `supertoml-lsp` binary +1. Build or install the `supertoml-analyzer` binary 2. Configure your editor to use it for TOML files 3. The server communicates via stdio @@ -160,20 +161,10 @@ Configure the language server during initialization: ```json { "settings": { - "supertoml": { - "validation": { - "enabled": true, - "strict": false - }, - "completion": { - "enabled": true, - "suggestValues": true - }, - "hover": { - "enabled": true, - "showSchema": true - } - } + "superTOML.diagnostics.enable": true, + "superTOML.completions.enable": true, + "superTOML.hover.enable": true, + "superTOML.server.path": "/path/to/superposition/target/debug/supertoml-analyzer" } } ``` @@ -182,12 +173,11 @@ Configure the language server during initialization: | Setting | Type | Default | Description | | -------------------------- | ------- | ------- | --------------------------- | -| `validation.enabled` | boolean | true | Enable real-time validation | -| `validation.strict` | boolean | false | Treat warnings as errors | -| `completion.enabled` | boolean | true | Enable autocomplete | -| `completion.suggestValues` | boolean | true | Suggest enum values | -| `hover.enabled` | boolean | true | Enable hover information | -| `hover.showSchema` | boolean | true | Show schema in hover | +| `superTOML.server.path` | string | `""` | Path to the `supertoml-analyzer` binary | +| `superTOML.trace.server` | string | `"off"` | VS Code language-client tracing | +| `superTOML.diagnostics.enable` | boolean | true | Enable diagnostics | +| `superTOML.completions.enable` | boolean | true | Enable completions | +| `superTOML.hover.enable` | boolean | true | Enable hover documentation | ## Validation Rules @@ -209,7 +199,7 @@ The LSP validates the following: - Every dimension has `position` and `schema` - Positions are unique -- Position 0 is not used (reserved) +- Position 0 is conventionally left for experimentation/`variantIds` workflows - Cohort references are valid ### Overrides Validation @@ -222,63 +212,38 @@ The LSP validates the following: ## Performance -The language server is optimized for large configuration files: - -- **Incremental parsing**: Only re-parses changed sections -- **Caching**: Schemas and validations are cached -- **Lazy validation**: Only validates visible ranges -- **Background processing**: Heavy operations don't block the UI - -### File Size Limits - -| File Size | Performance | -| ------------- | ------------------ | -| < 100 KB | Instant validation | -| 100 KB - 1 MB | < 100ms validation | -| 1 MB - 10 MB | < 500ms validation | -| > 10 MB | May require tuning | - -### Optimizing Large Files - -For very large configuration files: - -1. **Split into multiple files**: Use includes or imports -2. **Reduce schema complexity**: Simpler schemas validate faster -3. **Disable strict mode**: Use `validation.strict: false` -4. **Increase memory**: Allocate more memory to the language server +The current server uses full-document sync and validates the current document text. For very large files, expect the full file to be parsed again after edits. ## Troubleshooting ### Language Server Not Starting -1. Check that `supertoml-lsp` is installed and in PATH +1. Check that `supertoml-analyzer` is built and in PATH, or configure the editor with its full path 2. Check editor logs for error messages 3. Verify the file type is recognized as TOML ### No Autocomplete 1. Ensure the file has a `[default-configs]` section -2. Check that `completion.enabled` is true +2. Check that `superTOML.completions.enable` is true 3. Verify schemas are valid JSON Schema ### Validation Not Working -1. Check that `validation.enabled` is true +1. Check that `superTOML.diagnostics.enable` is true 2. Look for syntax errors that prevent parsing 3. Check the output panel for language server errors ### Slow Performance -1. Reduce file size by splitting -2. Disable features you don't need -3. Check for circular references in schemas -4. Increase language server memory +1. Reduce schema complexity where possible +2. Disable editor-side features you do not need +3. Check for syntax errors that force repeated failed parses ## Future Features The LSP is actively developed with planned features: -- **Code formatting**: Auto-format SuperTOML files - **Refactoring**: Rename dimensions and config keys - **Find references**: Find all uses of a dimension or config - **Code lenses**: Inline actions and information diff --git a/docs/docs/superposition-config-file/type-safety.md b/docs/docs/superposition-config-file/type-safety.md index bf65586ef..826f3aa01 100644 --- a/docs/docs/superposition-config-file/type-safety.md +++ b/docs/docs/superposition-config-file/type-safety.md @@ -53,8 +53,7 @@ rate = { value = 1.5, schema = { type = "number", minimum = 0 } } # Boolean enabled = { value = true, schema = { type = "boolean" } } -# Null -optional_value = { value = "null", schema = { type = "null" } } +# TOML has no null literal. Use JSON format if a config value must be null. ``` ### String Constraints @@ -336,32 +335,7 @@ address = { ## Type Templates -Superposition supports reusable type templates that can be referenced across configurations: - -```toml -# Type templates are defined at the workspace level -# and can be reused across multiple configs - -[default-configs] -user_email = { - value = "user@example.com", - schema = { "$ref" = "Email" } # References a type template -} - -admin_email = { - value = "admin@example.com", - schema = { "$ref" = "Email" } # Same template, different config -} -``` - -### Built-in Type Templates - -| Template | Schema | -| --------- | ----------------------- | -| `Number` | `{ "type": "integer" }` | -| `Decimal` | `{ "type": "number" }` | -| `Boolean` | `{ "type": "boolean" }` | -| `String` | `{ "type": "string" }` | +Superposition workspaces have type-template APIs and UI support for reusable schemas. Standalone SuperTOML parsing validates the JSON Schema embedded in each `schema` field; it does not look up workspace type-template names while parsing a file. To reuse schema fragments inside a SuperTOML file, use standard JSON Schema references within the same schema object, such as `"$ref" = "#/definitions/zipCode"`. ## Validation in Action @@ -476,16 +450,25 @@ api_timeout = { } ``` -### 4. Use Type Templates for Consistency +### 4. Reuse Local Schema Definitions for Consistency -Define common types once and reuse: +Use standard JSON Schema `definitions` and `"$ref"` when a schema fragment is reused inside the same schema: ```toml -# Define once [default-configs] -primary_email = { value = "primary@example.com", schema = { "$ref": "Email" } } -secondary_email = { value = "secondary@example.com", schema = { "$ref": "Email" } } -support_email = { value = "support@example.com", schema = { "$ref": "Email" } } +user_profile = { + value = { email = "primary@example.com" }, + schema = { + type = "object", + properties = { + email = { "$ref" = "#/definitions/email" } + }, + required = ["email"], + definitions = { + email = { type = "string", format = "email" } + } + } +} ``` ### 5. Validate Object Structures Strictly diff --git a/examples/cac_redis_module/README.md b/examples/cac_redis_module/README.md index 863ad3c86..fe2201238 100644 --- a/examples/cac_redis_module/README.md +++ b/examples/cac_redis_module/README.md @@ -8,10 +8,10 @@ The `cac_redis_module` extends Redis with custom commands that allow clients to ## Features -- **Context-Aware Configuration**: Resolve configurations based on dynamic context conditions using JSONLogic +- **Context-Aware Configuration**: Resolve configurations based on dynamic context conditions using Superposition's JSONLogic-compatible evaluator - **Redis Integration**: Access configuration resolution through Redis commands - **Real-time Configuration**: Load and evaluate configurations from local JSON files -- **Multiple Merge Strategies**: Support for MERGE and REPLACE strategies when applying overrides +- **MERGE Resolution**: `CAC.EVAL` applies selected overrides with the `MERGE` strategy. `REPLACE` exists in the internal evaluator, but is not exposed by the Redis command. - **Error Handling**: Comprehensive error reporting with timestamps and detailed messages ## Architecture @@ -38,12 +38,14 @@ git clone https://github.com/juspay/superposition.git cd superposition/examples/cac_redis_module ``` -2. Build the Redis module: +2. As checked in today, this example is under the repository workspace but is not listed in the root `workspace.members`. Before running Cargo commands from this directory, either add `examples/cac_redis_module` to the root workspace members or make the example an independent workspace by adding an empty `[workspace]` table to `examples/cac_redis_module/Cargo.toml`. + +3. Build the Redis module: ```bash cargo build --release ``` -3. The compiled module will be available at `target/release/libredis_module_cac.so` (Linux) or `target/release/libredis_module_cac.dylib` (macOS). +4. The compiled module is emitted to Cargo's target directory. In a root workspace build, that is typically `/target/release/libredis_module_cac.so` on Linux or `/target/release/libredis_module_cac.dylib` on macOS. In a standalone example workspace, the same files are under `target/release/` in the example directory. ### Loading the Module @@ -223,7 +225,7 @@ The module provides detailed error messages for various scenarios: ## Performance Considerations - **File I/O**: Configuration is loaded from disk on each evaluation. For production use, consider caching mechanisms -- **JSON Parsing**: JSONLogic evaluation is performed for each context condition +- **Condition Evaluation**: The Superposition logic helper evaluates each context condition - **Memory Usage**: Module keeps minimal state; most data is processed per request ## Development @@ -234,8 +236,7 @@ The module uses these key dependencies: - `redis-module`: Redis module SDK for Rust - `serde_json`: JSON serialization/deserialization -- `jsonlogic`: JSONLogic rule evaluation engine -- `superposition_types`: Superposition's type definitions +- `superposition_types`: Superposition's shared types and condition evaluation helpers ### Extending the Module @@ -304,8 +305,8 @@ redis-cli> CAC.EVAL '{"test": "value"}' - The module reads configuration files from the local filesystem - No authentication is performed on Redis commands (use Redis AUTH if needed) -- JSONLogic evaluation should be considered when exposing user-controllable context data +- Condition evaluation should be considered when exposing user-controllable context data ## License -This module is part of the Superposition project. Please refer to the main project's license terms. \ No newline at end of file +This module is part of the Superposition project. Please refer to the main project's license terms. diff --git a/examples/k8s-staggered-releaser/README.md b/examples/k8s-staggered-releaser/README.md index 7bf800232..a9f4ff60b 100644 --- a/examples/k8s-staggered-releaser/README.md +++ b/examples/k8s-staggered-releaser/README.md @@ -1,6 +1,6 @@ # Kubernetes Staggered Releaser (Mayday) -A Kubernetes deployment automation tool that leverages Superposition's experimentation features to perform safe, gradual rollouts with automatic traffic management and rollback capabilities. +A Kubernetes deployment automation example that leverages Superposition's experimentation events to perform gradual rollouts with NGINX Ingress traffic management. ## Overview @@ -13,7 +13,7 @@ The `k8s-staggered-releaser` (internally named "Mayday") is a webhook-driven dep - **Automatic Resource Management**: Creates and manages Kubernetes Deployments, Services, and Ingresses - **Multi-Namespace Support**: Deploy to different Kubernetes namespaces based on context - **Traffic Management**: Uses NGINX Ingress Controller for weighted traffic distribution -- **Rollback Capability**: Automatic cleanup and rollback when experiments conclude +- **Conclusion Cleanup**: Updates the main Service for the chosen variant and deletes experimental Services and Ingresses when experiments conclude. Deployment deletion is not implemented in the checked-in example. - **Configuration Integration**: Fetches deployment configurations from Superposition ## Architecture @@ -31,7 +31,7 @@ The `k8s-staggered-releaser` (internally named "Mayday") is a webhook-driven dep 1. **Experiment Started**: Creates new Deployment, Service, and Ingress resources 2. **Experiment In Progress**: Updates traffic weights for gradual rollout -3. **Experiment Concluded**: Promotes winning variant and cleans up resources +3. **Experiment Concluded**: Promotes the winning variant by updating the main Service, then deletes experimental Services and Ingresses ## Installation @@ -56,12 +56,14 @@ pub const K8S_API_SERVER: &str = "https://your-k8s-api-server:6443"; pub const TOKEN: &str = "your-k8s-api-token"; ``` -3. Build the application: +3. As checked in today, this example is under the repository workspace but is not listed in the root `workspace.members`. Before running Cargo commands from this directory, either add `examples/k8s-staggered-releaser` to the root workspace members or make the example an independent workspace by adding an empty `[workspace]` table to `examples/k8s-staggered-releaser/Cargo.toml`. + +4. Build the application: ```bash cargo build --release ``` -4. Run the webhook server: +5. Run the webhook server: ```bash cargo run ``` @@ -70,13 +72,13 @@ The server will start on `127.0.0.1:8090` and listen for webhook events at `/hi` ## Configuration -### Environment Variables +### Source Constants -You can configure the following constants in `src/utils.rs`: +The checked-in example reads these values as constants in `src/utils.rs`, not as environment variables: - `K8S_API_SERVER`: Kubernetes API server URL - `TOKEN`: Kubernetes API authentication token -- `NAMESPACE`: Default namespace for deployments +- `NAMESPACE`: Declared as `"default"`, but the webhook handlers derive the active namespace from `payload.context` through `get_namespace` ### Superposition Integration @@ -186,17 +188,17 @@ Triggered when an experiment ends with a chosen variant: **Actions Performed:** - Updates main service to point to winning variant deployment - Removes experimental Ingress resources -- Cleans up unused Services and Deployments +- Deletes experimental Ingresses and Services. Deployment cleanup is not implemented. ### Configuration Resolution The system fetches deployment configurations from Superposition using the `/config/resolve` endpoint: ``` -GET /config/resolve?namespace={namespace}&variantIds={variant_id} +GET http://localhost:8080/config/resolve?dimension[namespace]={namespace}&dimension[variantIds]={variant_id} Headers: x-org-id: localorg - x-tenant: {service_name} + x-tenant: {workspace_id} ``` Expected configuration format: @@ -235,11 +237,13 @@ metadata: nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "25" spec: + ingressClassName: nginx rules: - host: nginxservice.mumbai http: paths: - path: / + pathType: Prefix backend: service: name: nginxservice-service-variant-123 @@ -344,12 +348,7 @@ Common error scenarios and troubleshooting: ### Health Checks -Monitor the webhook endpoint: - -```bash -# Basic health check -curl http://127.0.0.1:8090/hi -``` +There is no separate health endpoint. The `/hi` route is a GET webhook receiver that expects a JSON request body matching the Superposition event shape. ## Advanced Configuration @@ -364,6 +363,8 @@ let app_state = Data::new(AppState { }); ``` +The checked-in handler stores this `AppState`, but does not currently validate webhook namespaces or tenants against those lists. + ### Custom Resource Templates Modify resource generation in respective modules: @@ -451,4 +452,4 @@ When contributing to this example: ## License -This example is part of the Superposition project. Please refer to the main project's license terms. \ No newline at end of file +This example is part of the Superposition project. Please refer to the main project's license terms.