Skip to content
Open
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
9 changes: 9 additions & 0 deletions Cargo.lock

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

9 changes: 9 additions & 0 deletions chompfile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ version = 0.1

[[task]]
name = 'build:types'
deps = ['build:types:email', 'build:types:flagship']

[[task]]
name = 'build:types:email'
deps = ['install:ts-gen']
# `Env` / `ExecutionContext` are project-specific re-exports that ts-gen
# can't infer; everything else (`ReadableStream`, `Headers`, `Event`, …)
Expand All @@ -10,6 +14,11 @@ run = '''ts-gen --input types/email.d.ts --output worker/src/email.rs \
--external "Env=crate::Env" \
--external "ExecutionContext=crate::Context"'''

[[task]]
name = 'build:types:flagship'
deps = ['install:ts-gen']
run = 'ts-gen --input types/flagship.d.ts --output worker/src/flagship_gen.rs'

[[task]]
name = 'install:ts-gen'
# ts-gen pulls in oxc which needs a newer rustc than the workspace's pinned 1.88;
Expand Down
15 changes: 15 additions & 0 deletions examples/flagship/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "flagship-on-workers"
version = "0.1.0"
edition = "2021"

[package.metadata.release]
release = false

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
worker.workspace = true
107 changes: 107 additions & 0 deletions examples/flagship/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! Example demonstrating Cloudflare Flagship (feature flags) from a Rust Worker.
//!
//! Routes:
//! * `/boolean?flag=<key>` — evaluate a boolean flag
//! * `/string?flag=<key>` — evaluate a string flag, optionally with `?userId=<id>` context
//! * `/object?flag=<key>` — evaluate an object flag into a typed struct
//! * `/details?flag=<key>` — return the full evaluation details envelope

use serde::{Deserialize, Serialize};
use worker::{event, Env, EvaluationContext, Request, Response, Result, Router, Url};

const BINDING: &str = "FLAGS";

#[derive(Serialize, Deserialize)]
struct Theme {
primary: String,
secondary: String,
}

#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
Router::new()
.get_async(
"/boolean",
|req, ctx| async move { boolean(req, ctx.env).await },
)
.get_async(
"/string",
|req, ctx| async move { string(req, ctx.env).await },
)
.get_async(
"/object",
|req, ctx| async move { object(req, ctx.env).await },
)
.get_async(
"/details",
|req, ctx| async move { details(req, ctx.env).await },
)
.run(req, env)
.await
}

async fn boolean(req: Request, env: Env) -> Result<Response> {
let url = req.url()?;
let flag = query(&url, "flag").unwrap_or_else(|| "example-bool".into());
let value: bool = env
.flagship(BINDING)?
.get_boolean_value(&flag, false)
.await?
.value_of();
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

async fn string(req: Request, env: Env) -> Result<Response> {
let url = req.url()?;
let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into());
let flagship = env.flagship(BINDING)?;
let value = String::from(match query(&url, "userId") {
Some(user_id) => {
let ctx = EvaluationContext::new()
.string("userId", &user_id)
.string("country", "US");
flagship
.get_string_value_with_context(&flag, "control", ctx.as_ref())
.await?
}
None => flagship.get_string_value(&flag, "control").await?,
});
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

async fn object(req: Request, env: Env) -> Result<Response> {
let url = req.url()?;
let flag = query(&url, "flag").unwrap_or_else(|| "theme".into());
let default = Theme {
primary: "#000000".into(),
secondary: "#ffffff".into(),
};
let value: Theme = env
.flagship(BINDING)?
.get_object_value(&flag, &default)
.await?;
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

async fn details(req: Request, env: Env) -> Result<Response> {
let url = req.url()?;
let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into());
let details = env
.flagship(BINDING)?
.get_string_details(&flag, "control")
.await?;
Response::from_json(&serde_json::json!({
"flagKey": details.flag_key(),
"value": details.value().as_string(),
"variant": details.variant(),
"reason": details.reason(),
"errorCode": details.error_code(),
"errorMessage": details.error_message(),
}))
}

fn query(url: &Url, key: &str) -> Option<String> {
url.query_pairs()
.find(|(k, _)| k == key)
.map(|(_, v)| v.into_owned())
}
12 changes: 12 additions & 0 deletions examples/flagship/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "flagship-worker"
main = "build/worker/shim.mjs"
compatibility_date = "2026-04-17"

[build]
command = "cargo install worker-build@^0.8 && worker-build --release"

# Replace <APP_ID> with your Flagship app ID from the Cloudflare dashboard.
# https://developers.cloudflare.com/flagship/get-started/
[[flagship]]
binding = "FLAGS"
app_id = "<APP_ID>"
172 changes: 172 additions & 0 deletions test/src/flagship.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use crate::SomeSharedData;
use serde::{Deserialize, Serialize};
use worker::wasm_bindgen::JsValue;
use worker::{
Env, EvaluationContext, EvaluationDetails, FlagshipEvaluationDetails, Request, Response, Result,
};

const BINDING: &str = "FLAGS";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Theme {
primary: String,
secondary: String,
}

fn default_theme() -> Theme {
Theme {
primary: "#000000".to_string(),
secondary: "#ffffff".to_string(),
}
}

fn last_segment(req: &Request) -> Result<String> {
let url = req.url()?;
Ok(url
.path_segments()
.and_then(|mut s| s.next_back().map(str::to_owned))
.unwrap_or_default())
}

#[worker::send]
pub async fn handle_boolean(req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let flag = last_segment(&req)?;
let value: bool = env
.flagship(BINDING)?
.get_boolean_value(&flag, false)
.await?
.value_of();
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

#[worker::send]
pub async fn handle_string(req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let flag = last_segment(&req)?;
let value = String::from(
env.flagship(BINDING)?
.get_string_value(&flag, "fallback")
.await?,
);
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

#[worker::send]
pub async fn handle_number(req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let flag = last_segment(&req)?;
let value: f64 = env
.flagship(BINDING)?
.get_number_value(&flag, 0.0)
.await?
.value_of();
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

#[worker::send]
pub async fn handle_object(req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let flag = last_segment(&req)?;
let value: Theme = env
.flagship(BINDING)?
.get_object_value(&flag, &default_theme())
.await?;
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

#[worker::send]
pub async fn handle_get(req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let flag = last_segment(&req)?;
let default = JsValue::from_str("fallback");
let raw = env
.flagship(BINDING)?
.get_with_default_value(&flag, &default)
.await?;
let value: serde_json::Value = serde_wasm_bindgen::from_value(raw)?;
Response::from_json(&serde_json::json!({ "flag": flag, "value": value }))
}

#[worker::send]
pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let user_id = last_segment(&req)?;
let eval_ctx = EvaluationContext::new()
.string("userId", &user_id)
.number("age", 30.0)
.bool("premium", true);
let value = String::from(
env.flagship(BINDING)?
.get_string_value_with_context("user-branch", "default", eval_ctx.as_ref())
.await?,
);
Response::from_json(&serde_json::json!({ "userId": user_id, "value": value }))
}

#[worker::send]
pub async fn handle_boolean_details(
req: Request,
env: Env,
_data: SomeSharedData,
) -> Result<Response> {
let flag = last_segment(&req)?;
let details = env
.flagship(BINDING)?
.get_boolean_details(&flag, false)
.await?;
let value = details.value().as_bool();
Response::from_json(&details_to_json(&details, value))
}

#[worker::send]
pub async fn handle_string_details(
req: Request,
env: Env,
_data: SomeSharedData,
) -> Result<Response> {
let flag = last_segment(&req)?;
let details = env
.flagship(BINDING)?
.get_string_details(&flag, "fallback")
.await?;
let value = details.value().as_string();
Response::from_json(&details_to_json(&details, value))
}

#[worker::send]
pub async fn handle_number_details(
req: Request,
env: Env,
_data: SomeSharedData,
) -> Result<Response> {
let flag = last_segment(&req)?;
let details = env
.flagship(BINDING)?
.get_number_details(&flag, 0.0)
.await?;
let value = details.value().as_f64();
Response::from_json(&details_to_json(&details, value))
}

#[worker::send]
pub async fn handle_object_details(
req: Request,
env: Env,
_data: SomeSharedData,
) -> Result<Response> {
let flag = last_segment(&req)?;
let details: EvaluationDetails<Theme> = env
.flagship(BINDING)?
.get_object_details(&flag, &default_theme())
.await?;
Response::from_json(&details)
}

fn details_to_json<T: Serialize>(
details: &FlagshipEvaluationDetails,
value: T,
) -> serde_json::Value {
serde_json::json!({
"flagKey": details.flag_key(),
"value": value,
"variant": details.variant(),
"reason": details.reason(),
"errorCode": details.error_code(),
"errorMessage": details.error_message(),
})
}
1 change: 1 addition & 0 deletions test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod counter;
mod d1;
mod durable;
mod fetch;
mod flagship;
mod form;
mod js_snippets;
mod kv;
Expand Down
16 changes: 13 additions & 3 deletions test/src/router.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::signal;
use crate::{
alarm, analytics_engine, assets, auto_response, cache, container, counter, d1, durable, fetch,
form, js_snippets, kv, put_raw, queue, r2, rate_limit, request, secret_store, send_email,
service, socket, sql_counter, sql_iterator, user, ws, SomeSharedData, GLOBAL_SECOND_START,
GLOBAL_STATE,
flagship, form, js_snippets, kv, put_raw, queue, r2, rate_limit, request, secret_store,
send_email, service, socket, sql_counter, sql_iterator, user, ws, SomeSharedData,
GLOBAL_SECOND_START, GLOBAL_STATE,
};
#[cfg(feature = "http")]
use std::convert::TryInto;
Expand Down Expand Up @@ -241,6 +241,16 @@ macro_rules! add_routes (
add_route!($obj, get, format_route!("/rate-limit/key/{}", "key"), rate_limit::handle_rate_limit_with_key);
add_route!($obj, get, "/rate-limit/bulk-test", rate_limit::handle_rate_limit_bulk_test);
add_route!($obj, get, "/rate-limit/reset", rate_limit::handle_rate_limit_reset);
add_route!($obj, get, format_route!("/flagship/bool/{}", "flag"), flagship::handle_boolean);
add_route!($obj, get, format_route!("/flagship/string/{}", "flag"), flagship::handle_string);
add_route!($obj, get, format_route!("/flagship/number/{}", "flag"), flagship::handle_number);
add_route!($obj, get, format_route!("/flagship/object/{}", "flag"), flagship::handle_object);
add_route!($obj, get, format_route!("/flagship/get/{}", "flag"), flagship::handle_get);
add_route!($obj, get, format_route!("/flagship/context/{}", "userId"), flagship::handle_context);
add_route!($obj, get, format_route!("/flagship/details/bool/{}", "flag"), flagship::handle_boolean_details);
add_route!($obj, get, format_route!("/flagship/details/string/{}", "flag"), flagship::handle_string_details);
add_route!($obj, get, format_route!("/flagship/details/number/{}", "flag"), flagship::handle_number_details);
add_route!($obj, get, format_route!("/flagship/details/object/{}", "flag"), flagship::handle_object_details);
add_route!($obj, get, "/send-email", send_email::handle_send_email);
add_route!($obj, get, "/signal/poll", signal::handle_signal_poll);
});
Expand Down
Loading