Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ rust-version = "1.74.0"
publish = false

[dependencies]
featurevisor = "0.1"
featurevisor = "1.0"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"] }
61 changes: 28 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,52 @@
# featurevisor-example-rust

A small Rust application showing how to consume the published [Featurevisor Rust SDK](https://github.com/featurevisor/featurevisor-rust) from crates.io.
A small Rust application showing how to use the published [Featurevisor Rust SDK](https://github.com/featurevisor/featurevisor-rust) from crates.io.

The application downloads a Featurevisor datafile, creates an SDK instance, sets a context, and evaluates one feature as a flag, variation, and string variable.
Learn more about Featurevisor [here](https://featurevisor.com).

It fetches a production datafile, creates a Featurevisor instance with customer context, then evaluates a flag, a variation, feature variables, and global variables.

## Requirements

Rust 1.74 or newer.

## Run it
## Run the example

```sh
cargo run
```

The application fetches this production datafile:
The application evaluates `commerce_platform`, `checkout_experience`, `max_items`, `payment_methods`, `serviceEndpoints`, and `supportContact` from this datafile:

```text
https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-mobile.json
https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-sdk-v3.json
```

The important SDK usage is in `src/main.rs`:

```rust
use featurevisor::{context, create_featurevisor, DatafileInput, FeaturevisorOptions};

let f = create_featurevisor(FeaturevisorOptions {
datafile: Some(DatafileInput::Json(datafile)),
..Default::default()
});

f.set_context(
context! {
"userId" => "mobile-user",
"country" => "nl",
},
false,
);

let enabled = f.is_enabled("mobile_experience", None);
let variation = f.get_variation("mobile_experience", None, None);
let welcome_message = f.get_variable_string(
"mobile_experience",
"welcome_message",
None,
None,
);
Expected output:

```text
Commerce platform enabled: true
Checkout variation: express
Maximum checkout items: 25
Payment methods: [card, wallet]
Service endpoint: https://api.eu.example.com (timeout: 1200 ms, retries: 4)
Support contact: support-nl@example.com
```

Change the context in `src/main.rs` to see how Featurevisor selects different rules, variations, and global variable overrides.

The `featurevisor` dependency comes from crates.io. It is not a local path dependency, so this project demonstrates the same setup used by a new consumer.

Learn more in the [Featurevisor Rust SDK documentation](https://featurevisor.com/docs/sdks/rust/).
## Checks

```sh
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
```

## Featurevisor project

The datafile is generated by the [Featurevisor example Cloudflare project](https://github.com/featurevisor/featurevisor-example-cloudflare).
The datafile is generated by the [Featurevisor Cloudflare example](https://github.com/featurevisor/featurevisor-example-cloudflare).

Learn more in the [Featurevisor Rust SDK documentation](https://featurevisor.com/docs/sdks/rust/).
96 changes: 78 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,100 @@
use std::error::Error;
use std::io;
use std::time::Duration;

use featurevisor::{context, create_featurevisor, DatafileInput, FeaturevisorOptions};
use featurevisor::{
context, create_featurevisor, DatafileInput, FeaturevisorOptions, LogLevel, VariableValue,
};

const DATAFILE_URL: &str =
"https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-mobile.json";
"https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-sdk-v3.json";

fn main() -> Result<(), Box<dyn Error>> {
let datafile = reqwest::blocking::get(DATAFILE_URL)?
let datafile = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(10))
.build()?
.get(DATAFILE_URL)
.header("Accept", "application/json")
.send()?
.error_for_status()?
.text()?;

let f = create_featurevisor(FeaturevisorOptions {
datafile: Some(DatafileInput::Json(datafile)),
context: Some(context! {
"userId" => "customer-123",
"country" => "nl",
"locale" => "nl-NL",
"accountPlan" => "pro",
}),
log_level: Some(LogLevel::Error),
..Default::default()
});

f.set_context(
context! {
"userId" => "mobile-user",
"country" => "nl",
},
false,
);
let commerce_enabled = f.is_enabled("commerce_platform", None);
let checkout_variation = f.get_variation("checkout_experience", None, None);
let max_items = f.get_variable_integer("checkout_experience", "max_items", None, None);
let payment_methods =
f.get_variable_array("checkout_experience", "payment_methods", None, None);
let endpoints = f
.get_global_variable_object("serviceEndpoints", None, None)
.ok_or_else(|| io::Error::other("serviceEndpoints is unavailable"))?;
let support_contact = f.get_global_variable_string("supportContact", None, None);

let enabled = f.is_enabled("mobile_experience", None);
let variation = f.get_variation("mobile_experience", None, None);
let welcome_message = f.get_variable_string("mobile_experience", "welcome_message", None, None);
let base_url = string_field(&endpoints, "baseUrl")?;
let timeout_ms = integer_field(&endpoints, "timeoutMs")?;
let retries = integer_field(&endpoints, "retries")?;

println!("Datafile revision: {}", f.get_revision());
println!("Feature 'mobile_experience' enabled: {enabled}");
println!("Variation: {}", variation.as_deref().unwrap_or("none"));
println!("Commerce platform enabled: {commerce_enabled}");
println!(
"Checkout variation: {}",
checkout_variation.as_deref().unwrap_or("unavailable")
);
println!(
"Maximum checkout items: {}",
max_items
.map(|value| value.to_string())
.unwrap_or_else(|| "unavailable".to_string())
);
println!("Payment methods: {}", format_array(payment_methods));
println!("Service endpoint: {base_url} (timeout: {timeout_ms} ms, retries: {retries})");
println!(
"Variable 'welcome_message': {}",
welcome_message.as_deref().unwrap_or("none")
"Support contact: {}",
support_contact.as_deref().unwrap_or("unavailable")
);

f.close();
Ok(())
}

fn string_field<'a>(
object: &'a std::collections::HashMap<String, VariableValue>,
key: &str,
) -> Result<&'a str, Box<dyn Error>> {
match object.get(key) {
Some(VariableValue::String(value)) => Ok(value),
_ => Err(io::Error::other(format!("{key} is unavailable")).into()),
}
}

fn integer_field(
object: &std::collections::HashMap<String, VariableValue>,
key: &str,
) -> Result<i64, Box<dyn Error>> {
match object.get(key) {
Some(VariableValue::Integer(value)) => Ok(*value),
_ => Err(io::Error::other(format!("{key} is unavailable")).into()),
}
}

fn format_array(values: Option<Vec<VariableValue>>) -> String {
let items = values
.unwrap_or_default()
.into_iter()
.filter_map(|value| match value {
VariableValue::String(value) => Some(value),
_ => None,
})
.collect::<Vec<_>>();
format!("[{}]", items.join(", "))
}