Skip to content
Open
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 @@ -19,6 +23,11 @@ run = '''ts-gen --input types/email.d.ts --output worker/src/bindings/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/bindings/flagship.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
92 changes: 92 additions & 0 deletions examples/flagship/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! 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, RouteContext, 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", boolean)
.get_async("/string", string)
.get_async("/object", object)
.get_async("/details", details)
.run(req, env)
.await
}

async fn boolean(req: Request, ctx: RouteContext<()>) -> Result<Response> {
let env = ctx.env;
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, ctx: RouteContext<()>) -> Result<Response> {
let env = ctx.env;
let url = req.url()?;
let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into());
let flagship = env.flagship(BINDING)?;
let value = 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, ctx: RouteContext<()>) -> Result<Response> {
let env = ctx.env;
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, ctx: RouteContext<()>) -> Result<Response> {
let env = ctx.env;
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(&details)
}

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>"
154 changes: 154 additions & 0 deletions test/src/flagship.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use crate::SomeSharedData;
use serde::{Deserialize, Serialize};
use worker::wasm_bindgen::JsValue;
use worker::{Env, EvaluationContext, EvaluationDetails, 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 = 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?;
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 = 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?;
Response::from_json(&serde_json::json!({
"flagKey": details.flag_key(),
"value": details.value().as_bool(),
"variant": details.variant(),
"reason": details.reason(),
"errorCode": details.error_code(),
"errorMessage": details.error_message(),
}))
}

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

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

#[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)
}
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 @@ -243,6 +243,16 @@ macro_rules! add_routes (
add_route!($obj, get, "/rate-limit/reset", rate_limit::handle_rate_limit_reset);
add_route!($obj, get, "/send-email", send_email::handle_send_email);
add_route!($obj, get, "/signal/poll", signal::handle_signal_poll);
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);
});

#[cfg(feature = "http")]
Expand Down
Loading