diff --git a/docs/reference/technologies/server/rust.mdx b/docs/reference/technologies/server/rust.mdx new file mode 100644 index 000000000..68465de36 --- /dev/null +++ b/docs/reference/technologies/server/rust.mdx @@ -0,0 +1,402 @@ +--- +title: OpenFeature Rust SDK +sidebar_label: Rust +--- + + + +

+ + Specification + + + + Release + + +
+ + + Crates.io + + + Min rust version + + + Repo status + +

+ +## Quick start + +### Requirements + +This package was built with Rust version `1.70.0`. +Earlier versions might work, but is not guaranteed. + +### Install + +Add the following content to the `Cargo.toml` file: + +```toml +open-feature = "0.2.5" +``` + +### Usage + +```rust +async fn example() -> Result<(), Error> { + // Acquire an OpenFeature API instance. + // Note the `await` call here because asynchronous lock is used to + // guarantee thread safety. + let mut api = OpenFeature::singleton_mut().await; + + // Configure a provider. + // By default [`NoOpProvider`] is used. + api.set_provider(NoOpProvider::default()).await; + + // create a client + let client = api.get_client(); + + // get a bool flag value + let is_feature_enabled = client + .get_bool_value("v2_enabled", None, None) + .unwrap_or(false) + .await; + + Ok(()) +} +``` + +Note that the default `NoOpProvider` always returns `Err` for any given input. + +#### Extended Example + +```rust +#[tokio::test] +async fn extended_example() { + // Acquire an OpenFeature API instance. + let mut api = OpenFeature::singleton_mut().await; + + // Set the default (unnamed) provider. + api.set_provider(NoOpProvider::default()).await; + + // Create an unnamed client. + let client = api.create_client(); + + // Create an evaluation context. + // It supports types mentioned in the specification. + let evaluation_context = EvaluationContext::default() + .with_targeting_key("Targeting") + .with_custom_field("bool_key", true) + .with_custom_field("int_key", 100) + .with_custom_field("float_key", 3.14) + .with_custom_field("string_key", "Hello".to_string()) + .with_custom_field("datetime_key", time::OffsetDateTime::now_utc()) + .with_custom_field( + "struct_key", + EvaluationContextFieldValue::Struct(Arc::new(MyStruct::default())), + ) + .with_custom_field("another_struct_key", Arc::new(MyStruct::default())) + .with_custom_field( + "yet_another_struct_key", + EvaluationContextFieldValue::new_struct(MyStruct::default()), + ); + + // This function returns a `Result`. + // You can process it with functions provided by std. + let is_feature_enabled = client + .get_bool_value("SomeFlagEnabled", Some(&evaluation_context), None) + .await + .unwrap_or(false); + + if is_feature_enabled { + // Let's get evaluation details. + let _result = client + .get_int_details("key", Some(&evaluation_context), None) + .await; + } +} +``` + +#### Getting a Struct from a Provider + +It is possible to extract a struct from the provider. +Internally, this SDK defines a type `StructValue` to store any structure value. +The `client.get_struct_value()` functions takes a type parameter `T`. +It will try to parse `StructValue` resolved by the provider to `T`, as long as `T` implements trait `TryFrom`. + +You can pass in a type that satisfies this trait bound. +When the conversion fails, it returns an `Err` with `EvaluationReason::TypeMismatch`. + +### API Reference + +See [here](https://docs.rs/open-feature/latest/open_feature/index.html) for the API docs. + +## Features + +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ + +### Providers + +[Providers](/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Look [here](/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Rust) for a complete list of available providers. +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```rust +// Set the default feature provider. Please replace the `NoOpProvider` with the one you want. +// If you do not do that, [`NoOpProvider`] will be used by default. +// +// [`NoOpProvider`] always returns `Err` despite any input. You can use functions like +// `unwrap_or()` to specify default values. +// +// If you set a new provider after creating some clients, the existing clients will pick up +// the new provider you just set. +// +// You must `await` it to let the provider's initialization to finish. +let mut api = OpenFeature::singleton_mut().await; +api.set_provider(NoOpProvider::default()).await; +``` + +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more detail below. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](/docs/reference/concepts/evaluation-context). + +```rust +// Create a global evaluation context and set it into the API. +// Note that this is optional. By default it uses an empty one. +let mut api = OpenFeature::singleton_mut().await; +api.set_evaluation_context(global_evaluation_context).await; + +// Set client level evaluation context. +// It will overwrite the global one for the existing keys. +let mut client = api.create_client(); +client.set_evaluation_context(client_evaluation_context); + +// Pass evaluation context in evaluation functions. +// This one will overwrite the global evaluation context and +// the client level one. +client.get_int_value("flag", Some(&evaluation_context), None); +``` + +### Hooks + +[Hooks](/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Look [here](/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Rust) for a complete list of available hooks. +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```rust +let mut api = OpenFeature::singleton_mut().await; + +// Set a global hook. +api.set_hook(MyHook::default()).await; + +// Create a client and set a client level hook. +let client = api.create_client(); +client.set_hook(MyHook::default()); + +// Get a flag value with a hook. +let eval = EvaluationOptions::default().with_hook(MyHook::default()); +client.get_int_value("key", None, Some(&eval)).await; +``` + +Example of a hook implementation you can find in [examples/hooks.rs](https://github.com/open-feature/rust-sdk/blob/main/examples/hooks.rs). + +To run the example, execute the following command: + +```shell +cargo run --example hooks +``` + +### Logging + +Note that in accordance with the OpenFeature specification, the SDK doesn't generally log messages during flag evaluation. + +#### Logging hook + +The Rust SDK provides a logging hook that can be used to log messages during flag evaluation. +This hook is not enabled by default and must be explicitly set. + +```rust +let mut api = OpenFeature::singleton_mut().await; + +let client = api.create_client().with_logging_hook(false); + +... + +// Note: You can include evaluation context to log output. +let client = api.create_client().with_logging_hook(true); +``` + +Both **text** and **structured** logging are supported. +To enable **structured** logging, enable feature `structured-logging` in your `Cargo.toml`: + +```toml +open-feature = { version = "0.2.4", features = ["structured-logging"] } +``` + +Example of a logging hook usage you can find in [examples/logging.rs](https://github.com/open-feature/rust-sdk/blob/main/examples/logging.rs). + +To run the example, execute the following command: + +```shell +cargo run --example logging +``` + +**Output**: + +```text +[2025-01-10T18:53:11Z DEBUG open_feature::hooks::logging] Before stage: domain=, provider_name=Dummy Provider, flag_key=my_feature, default_value=Some(Bool(false)), evaluation_context=EvaluationContext { targeting_key: None, custom_fields: {} } +[2025-01-10T18:53:11Z DEBUG open_feature::hooks::logging] After stage: domain=, provider_name=Dummy Provider, flag_key=my_feature, default_value=Some(Bool(false)), reason=None, variant=None, value=Bool(true), evaluation_context=EvaluationContext { targeting_key: None, custom_fields: {} } +``` + +or with structured logging: + +```shell +cargo run --example logging --features structured-logging +``` + +**Output**: + +```jsonl +{"default_value":"Some(Bool(false))","domain":"","evaluation_context":"EvaluationContext { targeting_key: None, custom_fields: {} }","flag_key":"my_feature","level":"DEBUG","message":"Before stage","provider_name":"No-op Provider","target":"open_feature","timestamp":1736537120828} +{"default_value":"Some(Bool(false))","domain":"","error_message":"Some(\"No-op provider is never ready\")","evaluation_context":"EvaluationContext { targeting_key: None, custom_fields: {} }","file":"src/hooks/logging.rs","flag_key":"my_feature","level":"ERROR","line":162,"message":"Error stage","module":"open_feature::hooks::logging::structured","provider_name":"No-op Provider","target":"open_feature","timestamp":1736537120828} +``` + +### Named clients + +Clients can be given a name. +A name is a logical identifier that can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```rust +// Create a named provider and bind it. +api.set_named_provider("named", NoOpProvider::default()).await; + +// This named client will use the feature provider bound to this name. +let client = api.create_named_client("named"); + +assert_eq!(client.get_int_value("key", None, None).await.unwrap(), 42); +``` + +### Eventing + +Events are not yet available in the Rust SDK. + + + +### Shutdown + +The OpenFeature API provides a close function to perform a cleanup of all registered providers. +This should only be called when your application is in the process of shutting down. + +```rust +// This will clean all the registered providers and invoke their `shutdown()` function. +let api = OpenFeature::singleton_mut().await; +api.shutdown(); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/rust-sdk-contrib) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +Check the source of [`NoOpProvider`](https://github.com/open-feature/rust-sdk/blob/main/src/provider/no_op_provider.rs) for an example. + +> Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs! + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/rust-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`before`/`after`/`finally`/`error`) need to be defined. + +```rust +use open_feature::{ + EvaluationContext, EvaluationDetails, EvaluationError, + Hook, HookContext, HookHints, Value, +}; + +struct MyHook; + +#[async_trait::async_trait] +impl Hook for MyHook { + async fn before<'a>( + &self, + context: &HookContext<'a>, + hints: Option<&'a HookHints>, + ) -> Result, EvaluationError> { + todo!() + } + + async fn after<'a>( + &self, + context: &HookContext<'a>, + details: &EvaluationDetails, + hints: Option<&'a HookHints>, + ) -> Result<(), EvaluationError> { + todo!() + } + + async fn error<'a>( + &self, + context: &HookContext<'a>, + error: &EvaluationError, + hints: Option<&'a HookHints>, + ) { + todo!() + } + + async fn finally<'a>( + &self, + context: &HookContext<'a>, + detaild: &EvaluationDetails, + hints: Option<&'a HookHints>, + ) { + todo!() + } +} +``` + +> Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! diff --git a/src/datasets/providers/flagd.ts b/src/datasets/providers/flagd.ts index 16548e1f0..b28e1ae28 100644 --- a/src/datasets/providers/flagd.ts +++ b/src/datasets/providers/flagd.ts @@ -47,5 +47,11 @@ export const Flagd: Provider = { href: 'https://github.com/open-feature/python-sdk-contrib/tree/main/providers/openfeature-provider-flagd', category: ['Server'], }, + { + technology: 'Rust', + vendorOfficial: true, + href: 'https://github.com/open-feature/rust-sdk-contrib/tree/main/crates/flagd', + category: ['Server'], + }, ], }; diff --git a/src/datasets/sdks/ecosystem.ts b/src/datasets/sdks/ecosystem.ts index a5d68f8d9..7a00ac22d 100644 --- a/src/datasets/sdks/ecosystem.ts +++ b/src/datasets/sdks/ecosystem.ts @@ -14,6 +14,7 @@ import PhpSvg from '@site/static/img/php-no-fill.svg'; import IosSvg from '@site/static/img/ios-no-fill.svg'; import RubySvg from '@site/static/img/ruby-no-fill.svg'; import AngularSvg from '@site/static/img/angular-no-fill.svg'; +import RustSvg from '@site/static/img/rust-no-fill.svg'; const LogoMap: Record = { 'c-sharp-no-fill.svg': CSharpSvg, @@ -29,6 +30,7 @@ const LogoMap: Record = { 'ios-no-fill.svg': IosSvg, 'ruby-no-fill.svg': RubySvg, 'angular-no-fill.svg': AngularSvg, + 'rust-no-fill.svg': RustSvg, }; export const ECOSYSTEM_SDKS: EcosystemElement[] = SDKS.map((sdk) => { diff --git a/src/datasets/sdks/index.ts b/src/datasets/sdks/index.ts index d1aa04f26..f46141198 100644 --- a/src/datasets/sdks/index.ts +++ b/src/datasets/sdks/index.ts @@ -12,8 +12,9 @@ import { React } from './react'; import { Nestjs } from './nestjs'; import { Ruby } from './ruby'; import { Angular } from './angular'; +import { Rust } from './rust'; -export const SDKS = [Java, Nodejs, Nestjs, Dotnet, Go, Python, PHP, Web, React, Kotlin, Swift, Ruby, Angular]; +export const SDKS = [Java, Nodejs, Nestjs, Dotnet, Go, Python, PHP, Web, React, Kotlin, Swift, Ruby, Angular, Rust]; export type SDK = { /** diff --git a/src/datasets/sdks/rust.ts b/src/datasets/sdks/rust.ts new file mode 100644 index 000000000..f78b11c7a --- /dev/null +++ b/src/datasets/sdks/rust.ts @@ -0,0 +1,10 @@ +import { SDK } from '.'; + +export const Rust: SDK = { + name: 'Rust', + category: 'Server', + repo: 'rust-sdk', + logoKey: 'rust-no-fill.svg', + technology: 'Rust', + href: '/docs/reference/technologies/server/rust', +}; diff --git a/src/datasets/sdks/sdk-compatibility.json b/src/datasets/sdks/sdk-compatibility.json index 3fc3c52ae..5f792de3c 100644 --- a/src/datasets/sdks/sdk-compatibility.json +++ b/src/datasets/sdks/sdk-compatibility.json @@ -546,5 +546,61 @@ "path": "/docs/reference/technologies/server/ruby#extending" } } + }, + { + "name": "Rust", + "path": "/docs/reference/technologies/server/rust", + "category": "Server", + "release": { + "href": "https://github.com/open-feature/rust-sdk/releases/tag/v0.2.5", + "version": "0.2.5", + "stable": false + }, + "spec": { + "href": "https://github.com/open-feature/spec/releases/tag/v0.5.2", + "version": "0.5.2" + }, + "features": { + "Providers": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#providers" + }, + "Targeting": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#targeting" + }, + "Hooks": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#hooks" + }, + "Logging": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#logging" + }, + "Domains": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#named-clients" + }, + "Eventing": { + "status": "❌", + "path": "/docs/reference/technologies/server/rust#eventing" + }, + "Tracking": { + "status": "❓", + "path": "/docs/reference/technologies/server/rust" + }, + "Transaction Context Propagation": { + "status": "❓", + "path": "/docs/reference/technologies/server/rust" + }, + "Shutdown": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#shutdown" + }, + "Extending": { + "status": "✅", + "path": "/docs/reference/technologies/server/rust#extending" + } + } } ] \ No newline at end of file diff --git a/src/datasets/types.ts b/src/datasets/types.ts index fe8dd3492..93602e06c 100644 --- a/src/datasets/types.ts +++ b/src/datasets/types.ts @@ -14,7 +14,6 @@ export type EcosystemElement = { category: Category[]; }; -// TODO: should this just be a list of technolgies from the SDKs? export type Technology = | 'JavaScript' | 'Java' @@ -28,6 +27,7 @@ export type Technology = | 'Ruby' | 'React' | 'Angular' + | 'Rust' | 'NestJS'; export type Category = 'Server' | 'Client'; diff --git a/static/img/rust-no-fill.svg b/static/img/rust-no-fill.svg new file mode 100644 index 000000000..b00e9d631 --- /dev/null +++ b/static/img/rust-no-fill.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file