From e717dad6cc317131ee37e4d5f3d1ec8261322f1a Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Mon, 20 Apr 2026 08:15:22 -0500 Subject: [PATCH 01/13] feat(worker): add Flagship binding Adds first-class support for the Flagship feature-flag binding announced on 2026-04-17. Users can now evaluate flags via `env.flagship("FLAGS")` with a fluent `EvaluationContext` builder and typed `EvaluationDetails` responses. - worker-sys: raw wasm_bindgen externs for all 9 Flagship JS methods - worker: Flagship wrapper, EvaluationContext builder, EvaluationDetails - test: mini-flagship miniflare mock + 10 handlers + 16 vitest specs - examples/flagship: runnable worker demoing value/details/context usage --- Cargo.lock | 9 ++ examples/flagship/Cargo.toml | 15 ++ examples/flagship/src/lib.rs | 103 ++++++++++++ examples/flagship/wrangler.toml | 12 ++ test/src/flagship.rs | 157 ++++++++++++++++++ test/src/lib.rs | 1 + test/src/router.rs | 13 +- test/tests/flagship.spec.ts | 121 ++++++++++++++ test/tests/mf.ts | 62 ++++++++ worker-sys/src/types.rs | 2 + worker-sys/src/types/flagship.rs | 81 ++++++++++ worker/src/env.rs | 7 + worker/src/flagship.rs | 262 +++++++++++++++++++++++++++++++ worker/src/lib.rs | 2 + 14 files changed, 845 insertions(+), 2 deletions(-) create mode 100644 examples/flagship/Cargo.toml create mode 100644 examples/flagship/src/lib.rs create mode 100644 examples/flagship/wrangler.toml create mode 100644 test/src/flagship.rs create mode 100644 test/tests/flagship.spec.ts create mode 100644 worker-sys/src/types/flagship.rs create mode 100644 worker/src/flagship.rs diff --git a/Cargo.lock b/Cargo.lock index a2789e668..e6dcf4a39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -888,6 +888,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flagship-on-workers" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "worker", +] + [[package]] name = "flate2" version = "1.1.9" diff --git a/examples/flagship/Cargo.toml b/examples/flagship/Cargo.toml new file mode 100644 index 000000000..03fca2b68 --- /dev/null +++ b/examples/flagship/Cargo.toml @@ -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 diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs new file mode 100644 index 000000000..857dfd87f --- /dev/null +++ b/examples/flagship/src/lib.rs @@ -0,0 +1,103 @@ +//! Example demonstrating Cloudflare Flagship (feature flags) from a Rust Worker. +//! +//! Routes: +//! * `/boolean?flag=` — evaluate a boolean flag +//! * `/string?flag=` — evaluate a string flag, optionally with `?userId=` context +//! * `/object?flag=` — evaluate an object flag into a typed struct +//! * `/details?flag=` — return the full `EvaluationDetails` 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 { + 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 { + let url = req.url()?; + let flag = query(&url, "flag").unwrap_or_else(|| "example-bool".into()); + let value = env + .flagship(BINDING)? + .get_boolean_value(&flag, false, None) + .await?; + Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) +} + +async fn string(req: Request, env: Env) -> Result { + let url = req.url()?; + let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into()); + let ctx = query(&url, "userId").map(|user_id| { + EvaluationContext::new() + .string("userId", &user_id) + .string("country", "US") + }); + let value = env + .flagship(BINDING)? + .get_string_value(&flag, "control", ctx.as_ref()) + .await?; + Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) +} + +async fn object(req: Request, env: Env) -> Result { + 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, None) + .await?; + Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) +} + +async fn details(req: Request, env: Env) -> Result { + 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", None) + .await?; + Response::from_json(&serde_json::json!({ + "flagKey": details.flag_key, + "value": details.value, + "variant": details.variant, + "reason": details.reason, + "errorCode": details.error_code, + "errorMessage": details.error_message, + })) +} + +fn query(url: &Url, key: &str) -> Option { + url.query_pairs() + .find(|(k, _)| k == key) + .map(|(_, v)| v.into_owned()) +} diff --git a/examples/flagship/wrangler.toml b/examples/flagship/wrangler.toml new file mode 100644 index 000000000..0a108fea5 --- /dev/null +++ b/examples/flagship/wrangler.toml @@ -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 with your Flagship app ID from the Cloudflare dashboard. +# https://developers.cloudflare.com/flagship/get-started/ +[[flagship]] +binding = "FLAGS" +app_id = "" diff --git a/test/src/flagship.rs b/test/src/flagship.rs new file mode 100644 index 000000000..aa51caa40 --- /dev/null +++ b/test/src/flagship.rs @@ -0,0 +1,157 @@ +use crate::SomeSharedData; +use serde::{Deserialize, Serialize}; +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 { + 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 { + let flag = last_segment(&req)?; + let value = env + .flagship(BINDING)? + .get_boolean_value(&flag, false, None) + .await?; + Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) +} + +#[worker::send] +pub async fn handle_string(req: Request, env: Env, _data: SomeSharedData) -> Result { + let flag = last_segment(&req)?; + let value = env + .flagship(BINDING)? + .get_string_value(&flag, "fallback", None) + .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 { + let flag = last_segment(&req)?; + let value = env + .flagship(BINDING)? + .get_number_value(&flag, 0.0, None) + .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 { + let flag = last_segment(&req)?; + let value: Theme = env + .flagship(BINDING)? + .get_object_value(&flag, &default_theme(), None) + .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 { + let flag = last_segment(&req)?; + let value = env + .flagship(BINDING)? + .get::(&flag, "fallback", None) + .await?; + Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) +} + +#[worker::send] +pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Result { + 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("user-branch", "default", Some(&eval_ctx)) + .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 { + let flag = last_segment(&req)?; + let details = env + .flagship(BINDING)? + .get_boolean_details(&flag, false, None) + .await?; + Response::from_json(&details_to_json(&details)) +} + +#[worker::send] +pub async fn handle_string_details( + req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let flag = last_segment(&req)?; + let details = env + .flagship(BINDING)? + .get_string_details(&flag, "fallback", None) + .await?; + Response::from_json(&details_to_json(&details)) +} + +#[worker::send] +pub async fn handle_number_details( + req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let flag = last_segment(&req)?; + let details = env + .flagship(BINDING)? + .get_number_details(&flag, 0.0, None) + .await?; + Response::from_json(&details_to_json(&details)) +} + +#[worker::send] +pub async fn handle_object_details( + req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let flag = last_segment(&req)?; + let details = env + .flagship(BINDING)? + .get_object_details(&flag, &default_theme(), None) + .await?; + Response::from_json(&details_to_json(&details)) +} + +fn details_to_json(details: &EvaluationDetails) -> serde_json::Value { + serde_json::json!({ + "flagKey": details.flag_key, + "value": details.value, + "variant": details.variant, + "reason": details.reason, + "errorCode": details.error_code, + "errorMessage": details.error_message, + }) +} diff --git a/test/src/lib.rs b/test/src/lib.rs index 37f718fd0..090921b0e 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -22,6 +22,7 @@ mod counter; mod d1; mod durable; mod fetch; +mod flagship; mod form; mod js_snippets; mod kv; diff --git a/test/src/router.rs b/test/src/router.rs index b5a4b56b2..a9d299e31 100644 --- a/test/src/router.rs +++ b/test/src/router.rs @@ -1,9 +1,8 @@ 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, + 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; @@ -243,6 +242,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")] diff --git a/test/tests/flagship.spec.ts b/test/tests/flagship.spec.ts new file mode 100644 index 000000000..96a6f2df8 --- /dev/null +++ b/test/tests/flagship.spec.ts @@ -0,0 +1,121 @@ +import { describe, test, expect } from "vitest"; +import { mf, mfUrl } from "./mf"; + +type Details = { + flagKey: string; + value: T; + variant: string | null; + reason: string | null; + errorCode: string | null; + errorMessage: string | null; +}; + +async function json(path: string): Promise { + const resp = await mf.dispatchFetch(`${mfUrl}${path}`); + expect(resp.status).toBe(200); + return (await resp.json()) as T; +} + +describe("flagship", () => { + describe("value methods", () => { + test("get_boolean_value resolves a known flag", async () => { + const data = await json<{ flag: string; value: boolean }>("flagship/bool/dark-mode"); + expect(data.value).toBe(true); + }); + + test("get_boolean_value returns the default for unknown flags", async () => { + const data = await json<{ flag: string; value: boolean }>("flagship/bool/missing"); + expect(data.value).toBe(false); + }); + + test("get_string_value resolves a known flag", async () => { + const data = await json<{ flag: string; value: string }>("flagship/string/checkout-flow"); + expect(data.value).toBe("v2"); + }); + + test("get_string_value returns the default for unknown flags", async () => { + const data = await json<{ flag: string; value: string }>("flagship/string/missing"); + expect(data.value).toBe("fallback"); + }); + + test("get_number_value resolves a known flag", async () => { + const data = await json<{ flag: string; value: number }>("flagship/number/max-retries"); + expect(data.value).toBe(5); + }); + + test("get_number_value returns the default for unknown flags", async () => { + const data = await json<{ flag: string; value: number }>("flagship/number/missing"); + expect(data.value).toBe(0); + }); + + test("get_object_value deserializes into a typed struct", async () => { + const data = await json<{ flag: string; value: { primary: string; secondary: string } }>( + "flagship/object/theme-colors", + ); + expect(data.value).toEqual({ primary: "#ff0000", secondary: "#00ff00" }); + }); + + test("get_object_value returns the default for unknown flags", async () => { + const data = await json<{ flag: string; value: { primary: string; secondary: string } }>( + "flagship/object/missing", + ); + expect(data.value).toEqual({ primary: "#000000", secondary: "#ffffff" }); + }); + + test("get (untyped) round-trips arbitrary JSON", async () => { + const data = await json<{ flag: string; value: unknown }>("flagship/get/theme-colors"); + expect(data.value).toEqual({ primary: "#ff0000", secondary: "#00ff00" }); + }); + }); + + describe("evaluation context", () => { + test("context routing picks the targeted branch", async () => { + const data = await json<{ userId: string; value: string }>("flagship/context/alice"); + expect(data.value).toBe("alice-branch"); + }); + + test("context routing falls back when targeting misses", async () => { + const data = await json<{ userId: string; value: string }>("flagship/context/bob"); + expect(data.value).toBe("default"); + }); + }); + + describe("details methods", () => { + test("boolean details include variant + reason on a match", async () => { + const details = await json>("flagship/details/bool/dark-mode"); + expect(details.flagKey).toBe("dark-mode"); + expect(details.value).toBe(true); + expect(details.variant).toBe("on"); + expect(details.reason).toBe("TARGETING_MATCH"); + expect(details.errorCode).toBeNull(); + }); + + test("boolean details surface errorCode on miss", async () => { + const details = await json>("flagship/details/bool/missing"); + expect(details.value).toBe(false); + expect(details.errorCode).toBe("GENERAL"); + expect(details.errorMessage).toBe("flag not found"); + expect(details.reason).toBe("DEFAULT"); + }); + + test("string details include variant metadata", async () => { + const details = await json>("flagship/details/string/checkout-flow"); + expect(details.value).toBe("v2"); + expect(details.variant).toBe("rollout-25"); + }); + + test("number details round-trip numeric payloads", async () => { + const details = await json>("flagship/details/number/max-retries"); + expect(details.value).toBe(5); + expect(details.variant).toBe("bumped"); + }); + + test("object details deserialize into a typed struct", async () => { + const details = await json>( + "flagship/details/object/theme-colors", + ); + expect(details.value).toEqual({ primary: "#ff0000", secondary: "#00ff00" }); + expect(details.variant).toBe("brand-v2"); + }); + }); +}); diff --git a/test/tests/mf.ts b/test/tests/mf.ts index 8d8003e93..e8af6ac65 100644 --- a/test/tests/mf.ts +++ b/test/tests/mf.ts @@ -110,6 +110,9 @@ const mf_instance = new Miniflare({ wrappedBindings: { HTTP_ANALYTICS: { scriptName: "mini-analytics-engine" // mock out analytics engine binding to the "mini-analytics-engine" worker + }, + FLAGS: { + scriptName: "mini-flagship" // mock out Flagship binding to the "mini-flagship" worker } }, ratelimits: { @@ -140,6 +143,65 @@ const mf_instance = new Miniflare({ } } }` + }, + { + name: "mini-flagship", + modules: true, + // A deterministic stand-in for the Flagship binding. Known flags resolve to fixed values; + // anything else returns the supplied default. The *Details methods round-trip targeting + // metadata (variant/reason) so the Rust wrapper's EvaluationDetails can be asserted. + script: ` + const KNOWN = { + "dark-mode": { type: "boolean", value: true, variant: "on" }, + "checkout-flow": { type: "string", value: "v2", variant: "rollout-25" }, + "max-retries": { type: "number", value: 5, variant: "bumped" }, + "theme-colors": { type: "object", value: { primary: "#ff0000", secondary: "#00ff00" }, variant: "brand-v2" }, + }; + function resolve(flagKey, expectedType, defaultValue, context) { + const flag = KNOWN[flagKey]; + if (flagKey === "user-branch" && context && context.userId === "alice") { + return { value: "alice-branch", variant: "alice", reason: "TARGETING_MATCH" }; + } + if (!flag) { + return { value: defaultValue, reason: "DEFAULT", errorCode: "GENERAL", errorMessage: "flag not found" }; + } + if (flag.type !== expectedType) { + return { value: defaultValue, reason: "ERROR", errorCode: "TYPE_MISMATCH", errorMessage: "flag type mismatch" }; + } + return { value: flag.value, variant: flag.variant, reason: "TARGETING_MATCH" }; + } + export default function (env) { + return { + async get(flagKey, defaultValue, context) { + return KNOWN[flagKey]?.value ?? defaultValue; + }, + async getBooleanValue(flagKey, defaultValue, context) { + return resolve(flagKey, "boolean", defaultValue, context).value; + }, + async getStringValue(flagKey, defaultValue, context) { + return resolve(flagKey, "string", defaultValue, context).value; + }, + async getNumberValue(flagKey, defaultValue, context) { + return resolve(flagKey, "number", defaultValue, context).value; + }, + async getObjectValue(flagKey, defaultValue, context) { + return resolve(flagKey, "object", defaultValue, context).value; + }, + async getBooleanDetails(flagKey, defaultValue, context) { + return { flagKey, ...resolve(flagKey, "boolean", defaultValue, context) }; + }, + async getStringDetails(flagKey, defaultValue, context) { + return { flagKey, ...resolve(flagKey, "string", defaultValue, context) }; + }, + async getNumberDetails(flagKey, defaultValue, context) { + return { flagKey, ...resolve(flagKey, "number", defaultValue, context) }; + }, + async getObjectDetails(flagKey, defaultValue, context) { + return { flagKey, ...resolve(flagKey, "object", defaultValue, context) }; + }, + }; + } + ` }] }); diff --git a/worker-sys/src/types.rs b/worker-sys/src/types.rs index afef5bc11..afc262a47 100644 --- a/worker-sys/src/types.rs +++ b/worker-sys/src/types.rs @@ -9,6 +9,7 @@ mod durable_object; mod dynamic_dispatcher; mod fetcher; mod fixed_length_stream; +mod flagship; mod hyperdrive; mod incoming_request_cf_properties; #[cfg(feature = "queue")] @@ -34,6 +35,7 @@ pub use durable_object::*; pub use dynamic_dispatcher::*; pub use fetcher::*; pub use fixed_length_stream::*; +pub use flagship::*; pub use hyperdrive::*; pub use incoming_request_cf_properties::*; #[cfg(feature = "queue")] diff --git a/worker-sys/src/types/flagship.rs b/worker-sys/src/types/flagship.rs new file mode 100644 index 000000000..2cc485db3 --- /dev/null +++ b/worker-sys/src/types/flagship.rs @@ -0,0 +1,81 @@ +use js_sys::Promise; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=js_sys::Object, js_name=Flagship)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Flagship; + + #[wasm_bindgen(method, js_class=Flagship, js_name=get)] + pub fn get( + this: &Flagship, + flag_key: &str, + default_value: JsValue, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getBooleanValue)] + pub fn get_boolean_value( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getStringValue)] + pub fn get_string_value( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getNumberValue)] + pub fn get_number_value( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getObjectValue)] + pub fn get_object_value( + this: &Flagship, + flag_key: &str, + default_value: JsValue, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getBooleanDetails)] + pub fn get_boolean_details( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getStringDetails)] + pub fn get_string_details( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getNumberDetails)] + pub fn get_number_details( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: JsValue, + ) -> Promise; + + #[wasm_bindgen(method, js_class=Flagship, js_name=getObjectDetails)] + pub fn get_object_details( + this: &Flagship, + flag_key: &str, + default_value: JsValue, + context: JsValue, + ) -> Promise; +} diff --git a/worker/src/env.rs b/worker/src/env.rs index 61d02c879..cb6828012 100644 --- a/worker/src/env.rs +++ b/worker/src/env.rs @@ -4,6 +4,7 @@ use crate::analytics_engine::AnalyticsEngineDataset; #[cfg(feature = "d1")] use crate::d1::D1Database; use crate::email::SendEmail; +use crate::flagship::Flagship; use crate::kv::KvStore; use crate::rate_limit::RateLimiter; use crate::Ai; @@ -138,6 +139,12 @@ impl Env { pub fn send_email(&self, binding: &str) -> Result { self.get_binding(binding) } + + /// Access a [Flagship](https://developers.cloudflare.com/flagship/) feature-flag store by + /// the binding name configured in your wrangler.toml file. + pub fn flagship(&self, binding: &str) -> Result { + self.get_binding(binding) + } } pub trait EnvBinding: Sized + JsCast { diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs new file mode 100644 index 000000000..4b38cc62f --- /dev/null +++ b/worker/src/flagship.rs @@ -0,0 +1,262 @@ +use crate::{send::SendFuture, EnvBinding, Error, Result}; +use js_sys::Object; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use worker_sys::Flagship as FlagshipSys; + +/// A binding to a [Cloudflare Flagship](https://developers.cloudflare.com/flagship/) feature-flag +/// store. Retrieved via [`Env::flagship`](crate::Env::flagship). +/// +/// `get_*_value` methods never surface evaluation failures as errors — if the flag is missing, +/// the type doesn't match, or Flagship can't reach a decision, they return the supplied +/// `default_value`. Use the `_details` variants when you need to inspect `error_code`, +/// `error_message`, or `reason`. +#[derive(Debug)] +pub struct Flagship(FlagshipSys); + +unsafe impl Send for Flagship {} +unsafe impl Sync for Flagship {} + +impl EnvBinding for Flagship { + const TYPE_NAME: &'static str = "Flagship"; + + // Miniflare's `wrappedBindings` expose the binding as a plain `Object`, so the default + // constructor-name check fails under local dev. Skip it — method calls surface mismatched + // bindings loudly enough. Mirrors `AnalyticsEngineDataset::get`. + fn get(val: JsValue) -> Result { + let obj = Object::from(val); + Ok(obj.unchecked_into()) + } +} + +impl JsCast for Flagship { + fn instanceof(val: &JsValue) -> bool { + val.is_instance_of::() + } + + fn unchecked_from_js(val: JsValue) -> Self { + Self(val.into()) + } + + fn unchecked_from_js_ref(val: &JsValue) -> &Self { + unsafe { &*(val as *const JsValue as *const Self) } + } +} + +impl AsRef for Flagship { + fn as_ref(&self) -> &JsValue { + &self.0 + } +} + +impl From for JsValue { + fn from(flagship: Flagship) -> Self { + JsValue::from(flagship.0) + } +} + +impl From for Flagship { + fn from(inner: FlagshipSys) -> Self { + Self(inner) + } +} + +impl Flagship { + /// Evaluate a flag without a compile-time type constraint. The return type is inferred from + /// the caller's turbofish or assignment. + pub async fn get( + &self, + flag_key: &str, + default_value: impl Serialize, + context: Option<&EvaluationContext>, + ) -> Result { + let default = serde_wasm_bindgen::to_value(&default_value)?; + let promise = self + .0 + .get(flag_key, default, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(out)?) + } + + pub async fn get_boolean_value( + &self, + flag_key: &str, + default_value: bool, + context: Option<&EvaluationContext>, + ) -> Result { + let promise = + self.0 + .get_boolean_value(flag_key, default_value, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + out.as_bool() + .ok_or_else(|| Error::RustError("expected boolean from Flagship".into())) + } + + pub async fn get_string_value( + &self, + flag_key: &str, + default_value: &str, + context: Option<&EvaluationContext>, + ) -> Result { + let promise = + self.0 + .get_string_value(flag_key, default_value, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + out.as_string() + .ok_or_else(|| Error::RustError("expected string from Flagship".into())) + } + + pub async fn get_number_value( + &self, + flag_key: &str, + default_value: f64, + context: Option<&EvaluationContext>, + ) -> Result { + let promise = + self.0 + .get_number_value(flag_key, default_value, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + out.as_f64() + .ok_or_else(|| Error::RustError("expected number from Flagship".into())) + } + + pub async fn get_object_value( + &self, + flag_key: &str, + default_value: &T, + context: Option<&EvaluationContext>, + ) -> Result { + let default = serde_wasm_bindgen::to_value(default_value)?; + let promise = self + .0 + .get_object_value(flag_key, default, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(out)?) + } + + pub async fn get_boolean_details( + &self, + flag_key: &str, + default_value: bool, + context: Option<&EvaluationContext>, + ) -> Result> { + let promise = + self.0 + .get_boolean_details(flag_key, default_value, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(out)?) + } + + pub async fn get_string_details( + &self, + flag_key: &str, + default_value: &str, + context: Option<&EvaluationContext>, + ) -> Result> { + let promise = + self.0 + .get_string_details(flag_key, default_value, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(out)?) + } + + pub async fn get_number_details( + &self, + flag_key: &str, + default_value: f64, + context: Option<&EvaluationContext>, + ) -> Result> { + let promise = + self.0 + .get_number_details(flag_key, default_value, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(out)?) + } + + pub async fn get_object_details( + &self, + flag_key: &str, + default_value: &T, + context: Option<&EvaluationContext>, + ) -> Result> { + let default = serde_wasm_bindgen::to_value(default_value)?; + let promise = + self.0 + .get_object_details(flag_key, default, EvaluationContext::as_js(context)); + let out = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(out)?) + } +} + +/// Evaluation attributes passed to Flagship for targeting rules. Values are constrained to +/// `string`, `number`, and `boolean` to match the JS `Record`. +#[derive(Debug, Clone)] +pub struct EvaluationContext { + inner: js_sys::Object, +} + +impl Default for EvaluationContext { + fn default() -> Self { + Self::new() + } +} + +impl EvaluationContext { + pub fn new() -> Self { + Self { + inner: js_sys::Object::new(), + } + } + + pub fn string(self, key: &str, value: &str) -> Self { + self.set(key, &JsValue::from_str(value)); + self + } + + pub fn number(self, key: &str, value: f64) -> Self { + self.set(key, &JsValue::from_f64(value)); + self + } + + pub fn bool(self, key: &str, value: bool) -> Self { + self.set(key, &JsValue::from_bool(value)); + self + } + + fn set(&self, key: &str, value: &JsValue) { + let _ = js_sys::Reflect::set(&self.inner, &JsValue::from_str(key), value); + } + + /// Convert an optional context into the `JsValue` the JS bindings expect (`undefined` when + /// absent). + fn as_js(context: Option<&Self>) -> JsValue { + context.map_or(JsValue::UNDEFINED, |c| c.inner.clone().into()) + } +} + +impl AsRef for EvaluationContext { + fn as_ref(&self) -> &JsValue { + self.inner.as_ref() + } +} + +/// Full evaluation record returned by the `get_*_details` methods. +/// +/// `reason` describes why Flagship resolved the value (for example `"TARGETING_MATCH"` or +/// `"DEFAULT"`); `error_code` and `error_message` are populated when the evaluation fell back +/// to the provided default (common codes: `"TYPE_MISMATCH"`, `"GENERAL"`). +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EvaluationDetails { + pub flag_key: String, + pub value: T, + #[serde(default)] + pub variant: Option, + #[serde(default)] + pub reason: Option, + #[serde(default)] + pub error_code: Option, + #[serde(default)] + pub error_message: Option, +} diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 604cf0c09..6ce1fbabf 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -178,6 +178,7 @@ pub use crate::email::*; pub use crate::env::{Env, EnvBinding, Secret, Var}; pub use crate::error::Error; pub use crate::fetcher::Fetcher; +pub use crate::flagship::*; pub use crate::formdata::*; pub use crate::global::Fetch; pub use crate::headers::Headers; @@ -235,6 +236,7 @@ pub mod email; mod env; mod error; mod fetcher; +mod flagship; mod formdata; mod global; mod headers; From 1e0b3134cb1130cc5fea2611014c9b5b14b8aa6f Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Sun, 26 Apr 2026 21:11:41 -0500 Subject: [PATCH 02/13] cleanup comments --- worker/src/flagship.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index 4b38cc62f..77b4e2fff 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -8,9 +8,9 @@ use worker_sys::Flagship as FlagshipSys; /// A binding to a [Cloudflare Flagship](https://developers.cloudflare.com/flagship/) feature-flag /// store. Retrieved via [`Env::flagship`](crate::Env::flagship). /// -/// `get_*_value` methods never surface evaluation failures as errors — if the flag is missing, -/// the type doesn't match, or Flagship can't reach a decision, they return the supplied -/// `default_value`. Use the `_details` variants when you need to inspect `error_code`, +/// The `get_*_value` methods never surface evaluation failures as errors. If anything goes wrong +/// (missing flag, type mismatch, evaluation error) they just hand back the `default_value` you +/// passed in. Reach for the `_details` variants when you need to see `error_code`, /// `error_message`, or `reason`. #[derive(Debug)] pub struct Flagship(FlagshipSys); @@ -22,8 +22,8 @@ impl EnvBinding for Flagship { const TYPE_NAME: &'static str = "Flagship"; // Miniflare's `wrappedBindings` expose the binding as a plain `Object`, so the default - // constructor-name check fails under local dev. Skip it — method calls surface mismatched - // bindings loudly enough. Mirrors `AnalyticsEngineDataset::get`. + // constructor-name check fails under local dev. Skip it; a mismatched binding will blow up + // loudly enough on the first method call. Mirrors `AnalyticsEngineDataset::get`. fn get(val: JsValue) -> Result { let obj = Object::from(val); Ok(obj.unchecked_into()) @@ -243,9 +243,9 @@ impl AsRef for EvaluationContext { /// Full evaluation record returned by the `get_*_details` methods. /// -/// `reason` describes why Flagship resolved the value (for example `"TARGETING_MATCH"` or -/// `"DEFAULT"`); `error_code` and `error_message` are populated when the evaluation fell back -/// to the provided default (common codes: `"TYPE_MISMATCH"`, `"GENERAL"`). +/// `reason` says why Flagship picked the value it did (e.g. `"TARGETING_MATCH"`, `"DEFAULT"`). +/// `error_code` and `error_message` are only set when evaluation fell back to the default; +/// `"TYPE_MISMATCH"` and `"GENERAL"` are the codes you'll see most often. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EvaluationDetails { From 6ab131fd135c7ba12ef57546a354f98958255d46 Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Wed, 29 Apr 2026 16:41:19 -0500 Subject: [PATCH 03/13] initial swing at moving to ts-gen --- chompfile.toml | 9 + examples/flagship/src/lib.rs | 44 +- test/src/flagship.rs | 66 +-- types/flagship.d.ts | 130 ++++++ worker-sys/src/types.rs | 2 - worker-sys/src/types/flagship.rs | 81 ---- worker/src/flagship.rs | 282 +++++-------- worker/src/flagship_gen.rs | 670 +++++++++++++++++++++++++++++++ worker/src/lib.rs | 11 + 9 files changed, 981 insertions(+), 314 deletions(-) create mode 100644 types/flagship.d.ts delete mode 100644 worker-sys/src/types/flagship.rs create mode 100644 worker/src/flagship_gen.rs diff --git a/chompfile.toml b/chompfile.toml index 036964a85..fab71d770 100644 --- a/chompfile.toml +++ b/chompfile.toml @@ -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`, …) @@ -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/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; diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index 857dfd87f..e151cdc15 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -4,7 +4,7 @@ //! * `/boolean?flag=` — evaluate a boolean flag //! * `/string?flag=` — evaluate a string flag, optionally with `?userId=` context //! * `/object?flag=` — evaluate an object flag into a typed struct -//! * `/details?flag=` — return the full `EvaluationDetails` envelope +//! * `/details?flag=` — return the full evaluation details envelope use serde::{Deserialize, Serialize}; use worker::{event, Env, EvaluationContext, Request, Response, Result, Router, Url}; @@ -43,25 +43,25 @@ async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result async fn boolean(req: Request, env: Env) -> Result { let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "example-bool".into()); - let value = env - .flagship(BINDING)? - .get_boolean_value(&flag, false, None) - .await?; + let value = env.flagship(BINDING)?.get_boolean_value(&flag, false).await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } async fn string(req: Request, env: Env) -> Result { let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into()); - let ctx = query(&url, "userId").map(|user_id| { - EvaluationContext::new() - .string("userId", &user_id) - .string("country", "US") - }); - let value = env - .flagship(BINDING)? - .get_string_value(&flag, "control", ctx.as_ref()) - .await?; + 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_record(&flag, "control", ctx.as_record()) + .await? + } + None => flagship.get_string_value(&flag, "control").await?, + }; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -74,7 +74,7 @@ async fn object(req: Request, env: Env) -> Result { }; let value: Theme = env .flagship(BINDING)? - .get_object_value(&flag, &default, None) + .get_object_value(&flag, &default) .await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -84,15 +84,15 @@ async fn details(req: Request, env: Env) -> Result { let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into()); let details = env .flagship(BINDING)? - .get_string_details(&flag, "control", None) + .get_string_details(&flag, "control") .await?; Response::from_json(&serde_json::json!({ - "flagKey": details.flag_key, - "value": details.value, - "variant": details.variant, - "reason": details.reason, - "errorCode": details.error_code, - "errorMessage": details.error_message, + "flagKey": details.flag_key(), + "value": details.value().as_string(), + "variant": details.variant(), + "reason": details.reason(), + "errorCode": details.error_code(), + "errorMessage": details.error_message(), })) } diff --git a/test/src/flagship.rs b/test/src/flagship.rs index aa51caa40..6522bc29d 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -1,6 +1,9 @@ use crate::SomeSharedData; use serde::{Deserialize, Serialize}; -use worker::{Env, EvaluationContext, EvaluationDetails, Request, Response, Result}; +use worker::wasm_bindgen::JsValue; +use worker::{ + Env, EvaluationContext, EvaluationDetails, FlagshipEvaluationDetails, Request, Response, Result, +}; const BINDING: &str = "FLAGS"; @@ -28,10 +31,7 @@ fn last_segment(req: &Request) -> Result { #[worker::send] pub async fn handle_boolean(req: Request, env: Env, _data: SomeSharedData) -> Result { let flag = last_segment(&req)?; - let value = env - .flagship(BINDING)? - .get_boolean_value(&flag, false, None) - .await?; + let value = env.flagship(BINDING)?.get_boolean_value(&flag, false).await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -40,7 +40,7 @@ pub async fn handle_string(req: Request, env: Env, _data: SomeSharedData) -> Res let flag = last_segment(&req)?; let value = env .flagship(BINDING)? - .get_string_value(&flag, "fallback", None) + .get_string_value(&flag, "fallback") .await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -48,10 +48,7 @@ pub async fn handle_string(req: Request, env: Env, _data: SomeSharedData) -> Res #[worker::send] pub async fn handle_number(req: Request, env: Env, _data: SomeSharedData) -> Result { let flag = last_segment(&req)?; - let value = env - .flagship(BINDING)? - .get_number_value(&flag, 0.0, None) - .await?; + let value = env.flagship(BINDING)?.get_number_value(&flag, 0.0).await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -60,7 +57,7 @@ pub async fn handle_object(req: Request, env: Env, _data: SomeSharedData) -> Res let flag = last_segment(&req)?; let value: Theme = env .flagship(BINDING)? - .get_object_value(&flag, &default_theme(), None) + .get_object_value(&flag, &default_theme()) .await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -68,10 +65,12 @@ pub async fn handle_object(req: Request, env: Env, _data: SomeSharedData) -> Res #[worker::send] pub async fn handle_get(req: Request, env: Env, _data: SomeSharedData) -> Result { let flag = last_segment(&req)?; - let value = env + let default = JsValue::from_str("fallback"); + let raw = env .flagship(BINDING)? - .get::(&flag, "fallback", None) + .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 })) } @@ -84,7 +83,7 @@ pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Re .bool("premium", true); let value = env .flagship(BINDING)? - .get_string_value("user-branch", "default", Some(&eval_ctx)) + .get_string_value_with_record("user-branch", "default", eval_ctx.as_record()) .await?; Response::from_json(&serde_json::json!({ "userId": user_id, "value": value })) } @@ -98,9 +97,10 @@ pub async fn handle_boolean_details( let flag = last_segment(&req)?; let details = env .flagship(BINDING)? - .get_boolean_details(&flag, false, None) + .get_boolean_details(&flag, false) .await?; - Response::from_json(&details_to_json(&details)) + let value = details.value().as_bool(); + Response::from_json(&details_to_json(&details, value)) } #[worker::send] @@ -112,9 +112,10 @@ pub async fn handle_string_details( let flag = last_segment(&req)?; let details = env .flagship(BINDING)? - .get_string_details(&flag, "fallback", None) + .get_string_details(&flag, "fallback") .await?; - Response::from_json(&details_to_json(&details)) + let value = details.value().as_string(); + Response::from_json(&details_to_json(&details, value)) } #[worker::send] @@ -126,9 +127,10 @@ pub async fn handle_number_details( let flag = last_segment(&req)?; let details = env .flagship(BINDING)? - .get_number_details(&flag, 0.0, None) + .get_number_details(&flag, 0.0) .await?; - Response::from_json(&details_to_json(&details)) + let value = details.value().as_f64(); + Response::from_json(&details_to_json(&details, value)) } #[worker::send] @@ -138,20 +140,30 @@ pub async fn handle_object_details( _data: SomeSharedData, ) -> Result { let flag = last_segment(&req)?; - let details = env + let details: EvaluationDetails = env .flagship(BINDING)? - .get_object_details(&flag, &default_theme(), None) + .get_object_details(&flag, &default_theme()) .await?; - Response::from_json(&details_to_json(&details)) -} - -fn details_to_json(details: &EvaluationDetails) -> serde_json::Value { - serde_json::json!({ + Response::from_json(&serde_json::json!({ "flagKey": details.flag_key, "value": details.value, "variant": details.variant, "reason": details.reason, "errorCode": details.error_code, "errorMessage": details.error_message, + })) +} + +fn details_to_json( + 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(), }) } diff --git a/types/flagship.d.ts b/types/flagship.d.ts new file mode 100644 index 000000000..ba56fa1cb --- /dev/null +++ b/types/flagship.d.ts @@ -0,0 +1,130 @@ +/* + * Flagship binding types from @cloudflare/workers-types. Mirrors + * workerd/types/defines/flagship.d.ts (valid as of 28/04/2026). This + * file builds worker/src/flagship_gen.rs as auto-generated bindings + * via ts-gen. + * + * NOTE: All hand edits to the upstream types are marked with an + * "EDIT:" comment. + */ + +/** + * Evaluation context for targeting rules. + * Keys are attribute names (e.g. "userId", "country"), values are the attribute values. + */ +type FlagshipEvaluationContext = Record; + +interface FlagshipEvaluationDetails { + flagKey: string; + value: T; + variant?: string | undefined; + reason?: string | undefined; + errorCode?: string | undefined; + errorMessage?: string | undefined; +} + +// EDIT: dropped empty `interface FlagshipEvaluationError extends Error {}`; +// errors round-trip through `JsValue`. + +/** + * Feature flags binding for evaluating feature flags from a Cloudflare Workers script. + * + * @example + * ```typescript + * // Get a boolean flag value with a default + * const enabled = await env.FLAGS.getBooleanValue('my-feature', false); + * + * // Get a flag value with evaluation context for targeting + * const variant = await env.FLAGS.getStringValue('experiment', 'control', { + * userId: 'user-123', + * country: 'US', + * }); + * + * // Get full evaluation details including variant and reason + * const details = await env.FLAGS.getBooleanDetails('my-feature', false); + * console.log(details.variant, details.reason); + * ``` + */ +declare abstract class Flagship { + /** + * Get a flag value without type checking. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Optional default value returned when evaluation fails. + * @param context Optional evaluation context for targeting rules. + */ + get( + flagKey: string, + defaultValue?: unknown, + context?: FlagshipEvaluationContext, + ): Promise; + /** + * Get a boolean flag value. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Default value returned when evaluation fails or the flag type does not match. + * @param context Optional evaluation context for targeting rules. + */ + getBooleanValue( + flagKey: string, + defaultValue: boolean, + context?: FlagshipEvaluationContext, + ): Promise; + /** + * Get a string flag value. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Default value returned when evaluation fails or the flag type does not match. + * @param context Optional evaluation context for targeting rules. + */ + getStringValue( + flagKey: string, + defaultValue: string, + context?: FlagshipEvaluationContext, + ): Promise; + /** + * Get a number flag value. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Default value returned when evaluation fails or the flag type does not match. + * @param context Optional evaluation context for targeting rules. + */ + getNumberValue( + flagKey: string, + defaultValue: number, + context?: FlagshipEvaluationContext, + ): Promise; + // EDIT: `getObjectValue(...)` is hand-written in + // worker/src/flagship.rs (ts-gen erases the generic to JsValue). + /** + * Get a boolean flag value with full evaluation details. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Default value returned when evaluation fails or the flag type does not match. + * @param context Optional evaluation context for targeting rules. + */ + getBooleanDetails( + flagKey: string, + defaultValue: boolean, + context?: FlagshipEvaluationContext, + ): Promise>; + /** + * Get a string flag value with full evaluation details. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Default value returned when evaluation fails or the flag type does not match. + * @param context Optional evaluation context for targeting rules. + */ + getStringDetails( + flagKey: string, + defaultValue: string, + context?: FlagshipEvaluationContext, + ): Promise>; + /** + * Get a number flag value with full evaluation details. + * @param flagKey The key of the flag to evaluate. + * @param defaultValue Default value returned when evaluation fails or the flag type does not match. + * @param context Optional evaluation context for targeting rules. + */ + getNumberDetails( + flagKey: string, + defaultValue: number, + context?: FlagshipEvaluationContext, + ): Promise>; + // EDIT: `getObjectDetails(...)` is hand-written in + // worker/src/flagship.rs (returns a typed `EvaluationDetails`). +} diff --git a/worker-sys/src/types.rs b/worker-sys/src/types.rs index afc262a47..afef5bc11 100644 --- a/worker-sys/src/types.rs +++ b/worker-sys/src/types.rs @@ -9,7 +9,6 @@ mod durable_object; mod dynamic_dispatcher; mod fetcher; mod fixed_length_stream; -mod flagship; mod hyperdrive; mod incoming_request_cf_properties; #[cfg(feature = "queue")] @@ -35,7 +34,6 @@ pub use durable_object::*; pub use dynamic_dispatcher::*; pub use fetcher::*; pub use fixed_length_stream::*; -pub use flagship::*; pub use hyperdrive::*; pub use incoming_request_cf_properties::*; #[cfg(feature = "queue")] diff --git a/worker-sys/src/types/flagship.rs b/worker-sys/src/types/flagship.rs deleted file mode 100644 index 2cc485db3..000000000 --- a/worker-sys/src/types/flagship.rs +++ /dev/null @@ -1,81 +0,0 @@ -use js_sys::Promise; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(extends=js_sys::Object, js_name=Flagship)] - #[derive(Debug, Clone, PartialEq, Eq)] - pub type Flagship; - - #[wasm_bindgen(method, js_class=Flagship, js_name=get)] - pub fn get( - this: &Flagship, - flag_key: &str, - default_value: JsValue, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getBooleanValue)] - pub fn get_boolean_value( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getStringValue)] - pub fn get_string_value( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getNumberValue)] - pub fn get_number_value( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getObjectValue)] - pub fn get_object_value( - this: &Flagship, - flag_key: &str, - default_value: JsValue, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getBooleanDetails)] - pub fn get_boolean_details( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getStringDetails)] - pub fn get_string_details( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getNumberDetails)] - pub fn get_number_details( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: JsValue, - ) -> Promise; - - #[wasm_bindgen(method, js_class=Flagship, js_name=getObjectDetails)] - pub fn get_object_details( - this: &Flagship, - flag_key: &str, - default_value: JsValue, - context: JsValue, - ) -> Promise; -} diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index 77b4e2fff..d0146e438 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -1,199 +1,142 @@ -use crate::{send::SendFuture, EnvBinding, Error, Result}; -use js_sys::Object; +use crate::{send::SendFuture, EnvBinding, Result}; +use js_sys::{JsString, Object, Promise}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; -use worker_sys::Flagship as FlagshipSys; -/// A binding to a [Cloudflare Flagship](https://developers.cloudflare.com/flagship/) feature-flag -/// store. Retrieved via [`Env::flagship`](crate::Env::flagship). -/// -/// The `get_*_value` methods never surface evaluation failures as errors. If anything goes wrong -/// (missing flag, type mismatch, evaluation error) they just hand back the `default_value` you -/// passed in. Reach for the `_details` variants when you need to see `error_code`, -/// `error_message`, or `reason`. -#[derive(Debug)] -pub struct Flagship(FlagshipSys); - -unsafe impl Send for Flagship {} -unsafe impl Sync for Flagship {} +// Hand-written companion to `flagship_gen.rs`. ts-gen erases +// `` to `JsValue`, so the typed `get_object_*` methods +// live here with serde conversions folded in. `EvaluationContext` and the +// `EnvBinding` impl are also here because ts-gen doesn't synthesize them. +pub use crate::flagship_gen::{Flagship, FlagshipEvaluationDetails}; impl EnvBinding for Flagship { const TYPE_NAME: &'static str = "Flagship"; - // Miniflare's `wrappedBindings` expose the binding as a plain `Object`, so the default - // constructor-name check fails under local dev. Skip it; a mismatched binding will blow up - // loudly enough on the first method call. Mirrors `AnalyticsEngineDataset::get`. + // Miniflare's `wrappedBindings` expose the binding as a plain `Object`, + // so the default `constructor.name` check fails under local dev. fn get(val: JsValue) -> Result { - let obj = Object::from(val); - Ok(obj.unchecked_into()) - } -} - -impl JsCast for Flagship { - fn instanceof(val: &JsValue) -> bool { - val.is_instance_of::() - } - - fn unchecked_from_js(val: JsValue) -> Self { - Self(val.into()) - } - - fn unchecked_from_js_ref(val: &JsValue) -> &Self { - unsafe { &*(val as *const JsValue as *const Self) } - } -} - -impl AsRef for Flagship { - fn as_ref(&self) -> &JsValue { - &self.0 - } -} - -impl From for JsValue { - fn from(flagship: Flagship) -> Self { - JsValue::from(flagship.0) + Ok(val.unchecked_into()) } } -impl From for Flagship { - fn from(inner: FlagshipSys) -> Self { - Self(inner) - } -} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(method, js_name = "getObjectValue")] + fn get_object_value_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; -impl Flagship { - /// Evaluate a flag without a compile-time type constraint. The return type is inferred from - /// the caller's turbofish or assignment. - pub async fn get( - &self, + #[wasm_bindgen(method, js_name = "getObjectValue")] + fn get_object_value_with_record_raw( + this: &Flagship, flag_key: &str, - default_value: impl Serialize, - context: Option<&EvaluationContext>, - ) -> Result { - let default = serde_wasm_bindgen::to_value(&default_value)?; - let promise = self - .0 - .get(flag_key, default, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(out)?) - } + default_value: &JsValue, + context: &Object, + ) -> Promise; - pub async fn get_boolean_value( - &self, - flag_key: &str, - default_value: bool, - context: Option<&EvaluationContext>, - ) -> Result { - let promise = - self.0 - .get_boolean_value(flag_key, default_value, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - out.as_bool() - .ok_or_else(|| Error::RustError("expected boolean from Flagship".into())) - } + #[wasm_bindgen(method, js_name = "getObjectDetails")] + fn get_object_details_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) + -> Promise; - pub async fn get_string_value( - &self, + #[wasm_bindgen(method, js_name = "getObjectDetails")] + fn get_object_details_with_record_raw( + this: &Flagship, flag_key: &str, - default_value: &str, - context: Option<&EvaluationContext>, - ) -> Result { - let promise = - self.0 - .get_string_value(flag_key, default_value, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - out.as_string() - .ok_or_else(|| Error::RustError("expected string from Flagship".into())) - } - - pub async fn get_number_value( - &self, - flag_key: &str, - default_value: f64, - context: Option<&EvaluationContext>, - ) -> Result { - let promise = - self.0 - .get_number_value(flag_key, default_value, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - out.as_f64() - .ok_or_else(|| Error::RustError("expected number from Flagship".into())) - } + default_value: &JsValue, + context: &Object, + ) -> Promise; +} +impl Flagship { + /// Evaluate an object-typed flag, returning the resolved value + /// deserialized into `T`. pub async fn get_object_value( &self, flag_key: &str, default_value: &T, - context: Option<&EvaluationContext>, ) -> Result { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self - .0 - .get_object_value(flag_key, default, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(out)?) - } - - pub async fn get_boolean_details( - &self, - flag_key: &str, - default_value: bool, - context: Option<&EvaluationContext>, - ) -> Result> { - let promise = - self.0 - .get_boolean_details(flag_key, default_value, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(out)?) + let raw = SendFuture::new(JsFuture::from(self.get_object_value_raw(flag_key, &default))) + .await?; + Ok(serde_wasm_bindgen::from_value(raw)?) } - pub async fn get_string_details( + /// Evaluate an object-typed flag with a targeting context. + pub async fn get_object_value_with_record( &self, flag_key: &str, - default_value: &str, - context: Option<&EvaluationContext>, - ) -> Result> { + default_value: &T, + context: &EvaluationContext, + ) -> Result { + let default = serde_wasm_bindgen::to_value(default_value)?; let promise = - self.0 - .get_string_details(flag_key, default_value, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(out)?) + self.get_object_value_with_record_raw(flag_key, &default, context.as_record()); + let raw = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(raw)?) } - pub async fn get_number_details( + /// Evaluate an object-typed flag and return the full evaluation + /// envelope (variant, reason, error code) with `value` deserialized + /// into `T`. + pub async fn get_object_details( &self, flag_key: &str, - default_value: f64, - context: Option<&EvaluationContext>, - ) -> Result> { - let promise = - self.0 - .get_number_details(flag_key, default_value, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(out)?) + default_value: &T, + ) -> Result> { + let default = serde_wasm_bindgen::to_value(default_value)?; + let raw = SendFuture::new(JsFuture::from( + self.get_object_details_raw(flag_key, &default), + )) + .await?; + Ok(serde_wasm_bindgen::from_value(raw)?) } - pub async fn get_object_details( + /// Evaluate an object-typed flag with a targeting context, returning + /// the full evaluation envelope. + pub async fn get_object_details_with_record( &self, flag_key: &str, default_value: &T, - context: Option<&EvaluationContext>, + context: &EvaluationContext, ) -> Result> { let default = serde_wasm_bindgen::to_value(default_value)?; let promise = - self.0 - .get_object_details(flag_key, default, EvaluationContext::as_js(context)); - let out = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(out)?) + self.get_object_details_with_record_raw(flag_key, &default, context.as_record()); + let raw = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(raw)?) } } -/// Evaluation attributes passed to Flagship for targeting rules. Values are constrained to -/// `string`, `number`, and `boolean` to match the JS `Record`. +/// Typed evaluation record returned by [`Flagship::get_object_details`]. +/// For boolean / string / number flags, the auto-generated +/// [`FlagshipEvaluationDetails`] is used instead. +/// +/// `error_code` and `error_message` are only populated when evaluation +/// fell back to `default_value`. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EvaluationDetails { + pub flag_key: String, + pub value: T, + #[serde(default)] + pub variant: Option, + #[serde(default)] + pub reason: Option, + #[serde(default)] + pub error_code: Option, + #[serde(default)] + pub error_message: Option, +} + +/// Evaluation attributes passed to Flagship for targeting rules. Values are +/// constrained to `string`, `number`, and `boolean` to match the JS +/// `Record`. +/// +/// Pass via the auto-generated `_with_record` method variants, e.g. +/// [`Flagship::get_boolean_value_with_record`], or via the hand-written +/// [`Flagship::get_object_value_with_record`]. #[derive(Debug, Clone)] pub struct EvaluationContext { - inner: js_sys::Object, + inner: Object, } impl Default for EvaluationContext { @@ -205,7 +148,7 @@ impl Default for EvaluationContext { impl EvaluationContext { pub fn new() -> Self { Self { - inner: js_sys::Object::new(), + inner: Object::new(), } } @@ -228,35 +171,10 @@ impl EvaluationContext { let _ = js_sys::Reflect::set(&self.inner, &JsValue::from_str(key), value); } - /// Convert an optional context into the `JsValue` the JS bindings expect (`undefined` when - /// absent). - fn as_js(context: Option<&Self>) -> JsValue { - context.map_or(JsValue::UNDEFINED, |c| c.inner.clone().into()) - } -} - -impl AsRef for EvaluationContext { - fn as_ref(&self) -> &JsValue { - self.inner.as_ref() + /// The `Object` phantom is a compile-time tag for the + /// `_with_record` extern variants — at runtime the same JS object + /// satisfies the `Object` and `Object` overloads too. + pub fn as_record(&self) -> &Object { + self.inner.unchecked_ref() } } - -/// Full evaluation record returned by the `get_*_details` methods. -/// -/// `reason` says why Flagship picked the value it did (e.g. `"TARGETING_MATCH"`, `"DEFAULT"`). -/// `error_code` and `error_message` are only set when evaluation fell back to the default; -/// `"TYPE_MISMATCH"` and `"GENERAL"` are the codes you'll see most often. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EvaluationDetails { - pub flag_key: String, - pub value: T, - #[serde(default)] - pub variant: Option, - #[serde(default)] - pub reason: Option, - #[serde(default)] - pub error_code: Option, - #[serde(default)] - pub error_message: Option, -} diff --git a/worker/src/flagship_gen.rs b/worker/src/flagship_gen.rs new file mode 100644 index 000000000..9f6c10db6 --- /dev/null +++ b/worker/src/flagship_gen.rs @@ -0,0 +1,670 @@ +#[allow(unused_imports)] +use js_sys::*; +#[allow(unused_imports)] +use wasm_bindgen::prelude::*; +#[allow(dead_code)] +use JsValue as T; +#[allow(dead_code)] +pub type FlagshipEvaluationContext = Object; +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type FlagshipEvaluationDetails; + #[wasm_bindgen(method, getter, js_name = "flagKey")] + pub fn flag_key(this: &FlagshipEvaluationDetails) -> String; + #[wasm_bindgen(method, setter, js_name = "flagKey")] + pub fn set_flag_key(this: &FlagshipEvaluationDetails, val: &str); + #[wasm_bindgen(method, getter)] + pub fn value(this: &FlagshipEvaluationDetails) -> T; + #[wasm_bindgen(method, setter)] + pub fn set_value(this: &FlagshipEvaluationDetails, val: &T); + #[wasm_bindgen(method, getter)] + pub fn variant(this: &FlagshipEvaluationDetails) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_variant(this: &FlagshipEvaluationDetails, val: &str); + #[wasm_bindgen(method, setter, js_name = "variant")] + pub fn set_variant_with_null(this: &FlagshipEvaluationDetails, val: &Null); + #[wasm_bindgen(method, getter)] + pub fn reason(this: &FlagshipEvaluationDetails) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_reason(this: &FlagshipEvaluationDetails, val: &str); + #[wasm_bindgen(method, setter, js_name = "reason")] + pub fn set_reason_with_null(this: &FlagshipEvaluationDetails, val: &Null); + #[wasm_bindgen(method, getter, js_name = "errorCode")] + pub fn error_code(this: &FlagshipEvaluationDetails) -> Option; + #[wasm_bindgen(method, setter, js_name = "errorCode")] + pub fn set_error_code(this: &FlagshipEvaluationDetails, val: &str); + #[wasm_bindgen(method, setter, js_name = "errorCode")] + pub fn set_error_code_with_null(this: &FlagshipEvaluationDetails, val: &Null); + #[wasm_bindgen(method, getter, js_name = "errorMessage")] + pub fn error_message(this: &FlagshipEvaluationDetails) -> Option; + #[wasm_bindgen(method, setter, js_name = "errorMessage")] + pub fn set_error_message(this: &FlagshipEvaluationDetails, val: &str); + #[wasm_bindgen(method, setter, js_name = "errorMessage")] + pub fn set_error_message_with_null(this: &FlagshipEvaluationDetails, val: &Null); +} +impl FlagshipEvaluationDetails { + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flag_key`"] + #[doc = " * `value`"] + pub fn new(flag_key: &str, value: &T) -> FlagshipEvaluationDetails { + Self::builder(flag_key, value).build() + } + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flag_key`"] + #[doc = " * `value`"] + pub fn builder(flag_key: &str, value: &T) -> FlagshipEvaluationDetailsBuilder { + let inner: Self = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_flag_key(flag_key); + inner.set_value(value); + FlagshipEvaluationDetailsBuilder { inner } + } +} +pub struct FlagshipEvaluationDetailsBuilder { + inner: FlagshipEvaluationDetails, +} +impl FlagshipEvaluationDetailsBuilder { + pub fn variant(self, val: &str) -> Self { + self.inner.set_variant(val); + self + } + pub fn variant_with_null(self, val: &Null) -> Self { + self.inner.set_variant_with_null(val); + self + } + pub fn reason(self, val: &str) -> Self { + self.inner.set_reason(val); + self + } + pub fn reason_with_null(self, val: &Null) -> Self { + self.inner.set_reason_with_null(val); + self + } + pub fn error_code(self, val: &str) -> Self { + self.inner.set_error_code(val); + self + } + pub fn error_code_with_null(self, val: &Null) -> Self { + self.inner.set_error_code_with_null(val); + self + } + pub fn error_message(self, val: &str) -> Self { + self.inner.set_error_message(val); + self + } + pub fn error_message_with_null(self, val: &Null) -> Self { + self.inner.set_error_message_with_null(val); + self + } + pub fn build(self) -> FlagshipEvaluationDetails { + self.inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Flagship; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch)] + pub async fn get(this: &Flagship, flag_key: &str) -> Result; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "get")] + pub async fn get_with_default_value( + this: &Flagship, + flag_key: &str, + default_value: &JsValue, + ) -> Result; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "get")] + pub async fn get_with_default_value_and_record( + this: &Flagship, + flag_key: &str, + default_value: &JsValue, + context: &Object, + ) -> Result; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "get")] + pub async fn get_with_default_value_and_record_1( + this: &Flagship, + flag_key: &str, + default_value: &JsValue, + context: &Object, + ) -> Result; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "get")] + pub async fn get_with_default_value_and_record_2( + this: &Flagship, + flag_key: &str, + default_value: &JsValue, + context: &Object, + ) -> Result; + #[wasm_bindgen(method, js_name = "getBooleanValue")] + fn get_boolean_value_raw( + this: &Flagship, + flag_key: &str, + default_value: bool, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getBooleanValue")] + fn get_boolean_value_with_record_raw( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getBooleanValue")] + fn get_boolean_value_with_record_1_raw( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getBooleanValue")] + fn get_boolean_value_with_record_2_raw( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getStringValue")] + fn get_string_value_raw( + this: &Flagship, + flag_key: &str, + default_value: &str, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getStringValue")] + fn get_string_value_with_record_raw( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getStringValue")] + fn get_string_value_with_record_1_raw( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getStringValue")] + fn get_string_value_with_record_2_raw( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getNumberValue")] + fn get_number_value_raw( + this: &Flagship, + flag_key: &str, + default_value: f64, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getNumberValue")] + fn get_number_value_with_record_raw( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getNumberValue")] + fn get_number_value_with_record_1_raw( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> ::js_sys::Promise; + #[wasm_bindgen(method, js_name = "getNumberValue")] + fn get_number_value_with_record_2_raw( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> ::js_sys::Promise; + #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details( + this: &Flagship, + flag_key: &str, + default_value: bool, + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details_with_record( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details_with_record_1( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details_with_record_2( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details( + this: &Flagship, + flag_key: &str, + default_value: &str, + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details_with_record( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details_with_record_1( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details_with_record_2( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details( + this: &Flagship, + flag_key: &str, + default_value: f64, + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details_with_record( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details_with_record_1( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details_with_record_2( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result; +} +impl Flagship { + #[doc = " Get a boolean flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_boolean_value( + &self, + flag_key: &str, + default_value: bool, + ) -> Result { + let __promise = self.get_boolean_value_raw(flag_key, default_value); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_bool() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) + } +} +impl Flagship { + #[doc = " Get a boolean flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_boolean_value_with_record( + &self, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result { + let __promise = self.get_boolean_value_with_record_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_bool() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) + } +} +impl Flagship { + #[doc = " Get a boolean flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_boolean_value_with_record_1( + &self, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result { + let __promise = self.get_boolean_value_with_record_1_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_bool() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) + } +} +impl Flagship { + #[doc = " Get a boolean flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_boolean_value_with_record_2( + &self, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result { + let __promise = self.get_boolean_value_with_record_2_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_bool() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) + } +} +impl Flagship { + #[doc = " Get a string flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_string_value( + &self, + flag_key: &str, + default_value: &str, + ) -> Result { + let __promise = self.get_string_value_raw(flag_key, default_value); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_string() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) + } +} +impl Flagship { + #[doc = " Get a string flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_string_value_with_record( + &self, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result { + let __promise = self.get_string_value_with_record_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_string() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) + } +} +impl Flagship { + #[doc = " Get a string flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_string_value_with_record_1( + &self, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result { + let __promise = self.get_string_value_with_record_1_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_string() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) + } +} +impl Flagship { + #[doc = " Get a string flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_string_value_with_record_2( + &self, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result { + let __promise = self.get_string_value_with_record_2_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_string() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) + } +} +impl Flagship { + #[doc = " Get a number flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_number_value( + &self, + flag_key: &str, + default_value: f64, + ) -> Result { + let __promise = self.get_number_value_raw(flag_key, default_value); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_f64() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) + } +} +impl Flagship { + #[doc = " Get a number flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_number_value_with_record( + &self, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result { + let __promise = self.get_number_value_with_record_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_f64() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) + } +} +impl Flagship { + #[doc = " Get a number flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_number_value_with_record_1( + &self, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result { + let __promise = self.get_number_value_with_record_1_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_f64() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) + } +} +impl Flagship { + #[doc = " Get a number flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + pub async fn get_number_value_with_record_2( + &self, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result { + let __promise = self.get_number_value_with_record_2_raw(flag_key, default_value, context); + let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; + __raw + .as_f64() + .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) + } +} diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 6ce1fbabf..c7abc3d30 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -237,6 +237,17 @@ mod env; mod error; mod fetcher; mod flagship; +// Generated by `chomp build:types:flagship` from types/flagship.d.ts. Same +// allow-list as `email`: ts-gen owns the file, so suppress the lints it +// can't avoid emitting. +#[allow( + unused_imports, + dead_code, + missing_debug_implementations, + clippy::module_inception, + clippy::new_without_default +)] +mod flagship_gen; mod formdata; mod global; mod headers; From 76e2312038dbdea79976598f9731cc6d858cac52 Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Wed, 29 Apr 2026 17:10:52 -0500 Subject: [PATCH 04/13] collapse record methods --- examples/flagship/src/lib.rs | 7 +- test/src/flagship.rs | 7 +- worker/src/flagship.rs | 58 ++++--- worker/src/flagship_gen.rs | 326 +++-------------------------------- 4 files changed, 66 insertions(+), 332 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index e151cdc15..3a10ac63c 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -43,7 +43,10 @@ async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result async fn boolean(req: Request, env: Env) -> Result { let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "example-bool".into()); - let value = env.flagship(BINDING)?.get_boolean_value(&flag, false).await?; + let value = env + .flagship(BINDING)? + .get_boolean_value(&flag, false) + .await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -57,7 +60,7 @@ async fn string(req: Request, env: Env) -> Result { .string("userId", &user_id) .string("country", "US"); flagship - .get_string_value_with_record(&flag, "control", ctx.as_record()) + .get_string_value_with_context(&flag, "control", ctx.as_ref()) .await? } None => flagship.get_string_value(&flag, "control").await?, diff --git a/test/src/flagship.rs b/test/src/flagship.rs index 6522bc29d..fc9b9805e 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -31,7 +31,10 @@ fn last_segment(req: &Request) -> Result { #[worker::send] pub async fn handle_boolean(req: Request, env: Env, _data: SomeSharedData) -> Result { let flag = last_segment(&req)?; - let value = env.flagship(BINDING)?.get_boolean_value(&flag, false).await?; + let value = env + .flagship(BINDING)? + .get_boolean_value(&flag, false) + .await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -83,7 +86,7 @@ pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Re .bool("premium", true); let value = env .flagship(BINDING)? - .get_string_value_with_record("user-branch", "default", eval_ctx.as_record()) + .get_string_value_with_context("user-branch", "default", eval_ctx.as_ref()) .await?; Response::from_json(&serde_json::json!({ "userId": user_id, "value": value })) } diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index d0146e438..3275766e8 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -1,5 +1,5 @@ use crate::{send::SendFuture, EnvBinding, Result}; -use js_sys::{JsString, Object, Promise}; +use js_sys::{Object, Promise}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -20,32 +20,41 @@ impl EnvBinding for Flagship { } } +// Raw extern bindings for the object methods stripped from `flagship.d.ts`. +// Names mirror the auto-gen pattern from `flagship_gen.rs`: +// `_raw` for the no-context form, `_with_context_raw` for the +// targeting form. The inherent wrappers below own the +// `JsFuture::from(...).await?` step and the serde conversion. #[wasm_bindgen] extern "C" { #[wasm_bindgen(method, js_name = "getObjectValue")] fn get_object_value_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; #[wasm_bindgen(method, js_name = "getObjectValue")] - fn get_object_value_with_record_raw( + fn get_object_value_with_context_raw( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, ) -> Promise; #[wasm_bindgen(method, js_name = "getObjectDetails")] - fn get_object_details_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) - -> Promise; + fn get_object_details_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; #[wasm_bindgen(method, js_name = "getObjectDetails")] - fn get_object_details_with_record_raw( + fn get_object_details_with_context_raw( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, ) -> Promise; } +// Public surface mirrors the auto-gen primitive accessors: one method for +// the no-context evaluation, one `_with_context` variant taking `&Object` +// (the same shape the auto-gen `_with_context` extern variants accept). +// Callers pass `eval_ctx.as_record()` exactly like they would for +// `get_boolean_value_with_context`. impl Flagship { /// Evaluate an object-typed flag, returning the resolved value /// deserialized into `T`. @@ -55,21 +64,22 @@ impl Flagship { default_value: &T, ) -> Result { let default = serde_wasm_bindgen::to_value(default_value)?; - let raw = SendFuture::new(JsFuture::from(self.get_object_value_raw(flag_key, &default))) - .await?; + let raw = SendFuture::new(JsFuture::from( + self.get_object_value_raw(flag_key, &default), + )) + .await?; Ok(serde_wasm_bindgen::from_value(raw)?) } /// Evaluate an object-typed flag with a targeting context. - pub async fn get_object_value_with_record( + pub async fn get_object_value_with_context( &self, flag_key: &str, default_value: &T, - context: &EvaluationContext, + context: &Object, ) -> Result { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = - self.get_object_value_with_record_raw(flag_key, &default, context.as_record()); + let promise = self.get_object_value_with_context_raw(flag_key, &default, context); let raw = SendFuture::new(JsFuture::from(promise)).await?; Ok(serde_wasm_bindgen::from_value(raw)?) } @@ -92,15 +102,14 @@ impl Flagship { /// Evaluate an object-typed flag with a targeting context, returning /// the full evaluation envelope. - pub async fn get_object_details_with_record( + pub async fn get_object_details_with_context( &self, flag_key: &str, default_value: &T, - context: &EvaluationContext, + context: &Object, ) -> Result> { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = - self.get_object_details_with_record_raw(flag_key, &default, context.as_record()); + let promise = self.get_object_details_with_context_raw(flag_key, &default, context); let raw = SendFuture::new(JsFuture::from(promise)).await?; Ok(serde_wasm_bindgen::from_value(raw)?) } @@ -131,9 +140,9 @@ pub struct EvaluationDetails { /// constrained to `string`, `number`, and `boolean` to match the JS /// `Record`. /// -/// Pass via the auto-generated `_with_record` method variants, e.g. -/// [`Flagship::get_boolean_value_with_record`], or via the hand-written -/// [`Flagship::get_object_value_with_record`]. +/// Pass via `.as_ref()` to any `_with_context` method, e.g. +/// [`Flagship::get_boolean_value_with_context`] or +/// [`Flagship::get_object_value_with_context`]. #[derive(Debug, Clone)] pub struct EvaluationContext { inner: Object, @@ -170,11 +179,10 @@ impl EvaluationContext { fn set(&self, key: &str, value: &JsValue) { let _ = js_sys::Reflect::set(&self.inner, &JsValue::from_str(key), value); } +} - /// The `Object` phantom is a compile-time tag for the - /// `_with_record` extern variants — at runtime the same JS object - /// satisfies the `Object` and `Object` overloads too. - pub fn as_record(&self) -> &Object { - self.inner.unchecked_ref() +impl AsRef for EvaluationContext { + fn as_ref(&self) -> &Object { + &self.inner } } diff --git a/worker/src/flagship_gen.rs b/worker/src/flagship_gen.rs index 9f6c10db6..7d5daba6f 100644 --- a/worker/src/flagship_gen.rs +++ b/worker/src/flagship_gen.rs @@ -138,39 +138,11 @@ extern "C" { #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_record( + pub async fn get_with_default_value_and_context( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, - ) -> Result; - #[doc = " Get a flag value without type checking."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_record_1( - this: &Flagship, - flag_key: &str, - default_value: &JsValue, - context: &Object, - ) -> Result; - #[doc = " Get a flag value without type checking."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_record_2( - this: &Flagship, - flag_key: &str, - default_value: &JsValue, - context: &Object, + context: &Object, ) -> Result; #[wasm_bindgen(method, js_name = "getBooleanValue")] fn get_boolean_value_raw( @@ -179,25 +151,11 @@ extern "C" { default_value: bool, ) -> ::js_sys::Promise; #[wasm_bindgen(method, js_name = "getBooleanValue")] - fn get_boolean_value_with_record_raw( + fn get_boolean_value_with_context_raw( this: &Flagship, flag_key: &str, default_value: bool, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getBooleanValue")] - fn get_boolean_value_with_record_1_raw( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getBooleanValue")] - fn get_boolean_value_with_record_2_raw( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, + context: &Object, ) -> ::js_sys::Promise; #[wasm_bindgen(method, js_name = "getStringValue")] fn get_string_value_raw( @@ -206,25 +164,11 @@ extern "C" { default_value: &str, ) -> ::js_sys::Promise; #[wasm_bindgen(method, js_name = "getStringValue")] - fn get_string_value_with_record_raw( + fn get_string_value_with_context_raw( this: &Flagship, flag_key: &str, default_value: &str, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getStringValue")] - fn get_string_value_with_record_1_raw( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getStringValue")] - fn get_string_value_with_record_2_raw( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, + context: &Object, ) -> ::js_sys::Promise; #[wasm_bindgen(method, js_name = "getNumberValue")] fn get_number_value_raw( @@ -233,25 +177,11 @@ extern "C" { default_value: f64, ) -> ::js_sys::Promise; #[wasm_bindgen(method, js_name = "getNumberValue")] - fn get_number_value_with_record_raw( + fn get_number_value_with_context_raw( this: &Flagship, flag_key: &str, default_value: f64, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getNumberValue")] - fn get_number_value_with_record_1_raw( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getNumberValue")] - fn get_number_value_with_record_2_raw( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, + context: &Object, ) -> ::js_sys::Promise; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] @@ -274,39 +204,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_record( + pub async fn get_boolean_details_with_context( this: &Flagship, flag_key: &str, default_value: bool, - context: &Object, - ) -> Result; - #[doc = " Get a boolean flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_record_1( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> Result; - #[doc = " Get a boolean flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_record_2( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] @@ -329,39 +231,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_record( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a string flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_record_1( + pub async fn get_string_details_with_context( this: &Flagship, flag_key: &str, default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a string flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_record_2( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] @@ -384,39 +258,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_record( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_record_1( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_record_2( + pub async fn get_number_details_with_context( this: &Flagship, flag_key: &str, default_value: f64, - context: &Object, + context: &Object, ) -> Result; } impl Flagship { @@ -447,55 +293,13 @@ impl Flagship { #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_boolean_value_with_record( - &self, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> Result { - let __promise = self.get_boolean_value_with_record_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_bool() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) - } -} -impl Flagship { - #[doc = " Get a boolean flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_boolean_value_with_record_1( + pub async fn get_boolean_value_with_context( &self, flag_key: &str, default_value: bool, - context: &Object, + context: &Object, ) -> Result { - let __promise = self.get_boolean_value_with_record_1_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_bool() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) - } -} -impl Flagship { - #[doc = " Get a boolean flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_boolean_value_with_record_2( - &self, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> Result { - let __promise = self.get_boolean_value_with_record_2_raw(flag_key, default_value, context); + let __promise = self.get_boolean_value_with_context_raw(flag_key, default_value, context); let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; __raw .as_bool() @@ -530,55 +334,13 @@ impl Flagship { #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_string_value_with_record( + pub async fn get_string_value_with_context( &self, flag_key: &str, default_value: &str, - context: &Object, + context: &Object, ) -> Result { - let __promise = self.get_string_value_with_record_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_string() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) - } -} -impl Flagship { - #[doc = " Get a string flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_string_value_with_record_1( - &self, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result { - let __promise = self.get_string_value_with_record_1_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_string() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) - } -} -impl Flagship { - #[doc = " Get a string flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_string_value_with_record_2( - &self, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result { - let __promise = self.get_string_value_with_record_2_raw(flag_key, default_value, context); + let __promise = self.get_string_value_with_context_raw(flag_key, default_value, context); let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; __raw .as_string() @@ -613,55 +375,13 @@ impl Flagship { #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_number_value_with_record( - &self, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result { - let __promise = self.get_number_value_with_record_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_f64() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) - } -} -impl Flagship { - #[doc = " Get a number flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_number_value_with_record_1( - &self, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result { - let __promise = self.get_number_value_with_record_1_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_f64() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) - } -} -impl Flagship { - #[doc = " Get a number flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_number_value_with_record_2( + pub async fn get_number_value_with_context( &self, flag_key: &str, default_value: f64, - context: &Object, + context: &Object, ) -> Result { - let __promise = self.get_number_value_with_record_2_raw(flag_key, default_value, context); + let __promise = self.get_number_value_with_context_raw(flag_key, default_value, context); let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; __raw .as_f64() From e07d20f68d9951a2deae352b557749867baffa97 Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Wed, 29 Apr 2026 17:28:32 -0500 Subject: [PATCH 05/13] better primitive methods --- examples/flagship/src/lib.rs | 9 +- test/src/flagship.rs | 29 ++++-- worker/src/flagship_gen.rs | 189 ++++++++++------------------------- 3 files changed, 77 insertions(+), 150 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index 3a10ac63c..5b765d142 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -43,10 +43,11 @@ async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result async fn boolean(req: Request, env: Env) -> Result { let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "example-bool".into()); - let value = env + let value: bool = env .flagship(BINDING)? .get_boolean_value(&flag, false) - .await?; + .await? + .value_of(); Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -54,7 +55,7 @@ async fn string(req: Request, env: Env) -> Result { 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") { + let value = String::from(match query(&url, "userId") { Some(user_id) => { let ctx = EvaluationContext::new() .string("userId", &user_id) @@ -64,7 +65,7 @@ async fn string(req: Request, env: Env) -> Result { .await? } None => flagship.get_string_value(&flag, "control").await?, - }; + }); Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } diff --git a/test/src/flagship.rs b/test/src/flagship.rs index fc9b9805e..c77197c63 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -31,27 +31,33 @@ fn last_segment(req: &Request) -> Result { #[worker::send] pub async fn handle_boolean(req: Request, env: Env, _data: SomeSharedData) -> Result { let flag = last_segment(&req)?; - let value = env + let value: bool = env .flagship(BINDING)? .get_boolean_value(&flag, false) - .await?; + .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 { let flag = last_segment(&req)?; - let value = env - .flagship(BINDING)? - .get_string_value(&flag, "fallback") - .await?; + 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 { let flag = last_segment(&req)?; - let value = env.flagship(BINDING)?.get_number_value(&flag, 0.0).await?; + 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 })) } @@ -84,10 +90,11 @@ pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Re .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?; + 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 })) } diff --git a/worker/src/flagship_gen.rs b/worker/src/flagship_gen.rs index 7d5daba6f..058966ff4 100644 --- a/worker/src/flagship_gen.rs +++ b/worker/src/flagship_gen.rs @@ -144,247 +144,166 @@ extern "C" { default_value: &JsValue, context: &Object, ) -> Result; - #[wasm_bindgen(method, js_name = "getBooleanValue")] - fn get_boolean_value_raw( - this: &Flagship, - flag_key: &str, - default_value: bool, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getBooleanValue")] - fn get_boolean_value_with_context_raw( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getStringValue")] - fn get_string_value_raw( - this: &Flagship, - flag_key: &str, - default_value: &str, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getStringValue")] - fn get_string_value_with_context_raw( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getNumberValue")] - fn get_number_value_raw( - this: &Flagship, - flag_key: &str, - default_value: f64, - ) -> ::js_sys::Promise; - #[wasm_bindgen(method, js_name = "getNumberValue")] - fn get_number_value_with_context_raw( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> ::js_sys::Promise; - #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = " Get a boolean flag value."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details( + #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] + pub async fn get_boolean_value( this: &Flagship, flag_key: &str, default_value: bool, - ) -> Result; - #[doc = " Get a boolean flag value with full evaluation details."] + ) -> Result; + #[doc = " Get a boolean flag value."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_context( + #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] + pub async fn get_boolean_value_with_context( this: &Flagship, flag_key: &str, default_value: bool, context: &Object, - ) -> Result; - #[doc = " Get a string flag value with full evaluation details."] + ) -> Result; + #[doc = " Get a string flag value."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details( + #[wasm_bindgen(method, catch, js_name = "getStringValue")] + pub async fn get_string_value( this: &Flagship, flag_key: &str, default_value: &str, - ) -> Result; - #[doc = " Get a string flag value with full evaluation details."] + ) -> Result; + #[doc = " Get a string flag value."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_context( + #[wasm_bindgen(method, catch, js_name = "getStringValue")] + pub async fn get_string_value_with_context( this: &Flagship, flag_key: &str, default_value: &str, context: &Object, - ) -> Result; - #[doc = " Get a number flag value with full evaluation details."] + ) -> Result; + #[doc = " Get a number flag value."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details( + #[wasm_bindgen(method, catch, js_name = "getNumberValue")] + pub async fn get_number_value( this: &Flagship, flag_key: &str, default_value: f64, - ) -> Result; - #[doc = " Get a number flag value with full evaluation details."] + ) -> Result; + #[doc = " Get a number flag value."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_context( + #[wasm_bindgen(method, catch, js_name = "getNumberValue")] + pub async fn get_number_value_with_context( this: &Flagship, flag_key: &str, default_value: f64, context: &Object, - ) -> Result; -} -impl Flagship { - #[doc = " Get a boolean flag value."] + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_boolean_value( - &self, + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details( + this: &Flagship, flag_key: &str, default_value: bool, - ) -> Result { - let __promise = self.get_boolean_value_raw(flag_key, default_value); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_bool() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) - } -} -impl Flagship { - #[doc = " Get a boolean flag value."] + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_boolean_value_with_context( - &self, + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details_with_context( + this: &Flagship, flag_key: &str, default_value: bool, context: &Object, - ) -> Result { - let __promise = self.get_boolean_value_with_context_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_bool() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected boolean")) - } -} -impl Flagship { - #[doc = " Get a string flag value."] + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_string_value( - &self, + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details( + this: &Flagship, flag_key: &str, default_value: &str, - ) -> Result { - let __promise = self.get_string_value_raw(flag_key, default_value); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_string() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) - } -} -impl Flagship { - #[doc = " Get a string flag value."] + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_string_value_with_context( - &self, + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details_with_context( + this: &Flagship, flag_key: &str, default_value: &str, context: &Object, - ) -> Result { - let __promise = self.get_string_value_with_context_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_string() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected string")) - } -} -impl Flagship { - #[doc = " Get a number flag value."] + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_number_value( - &self, + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details( + this: &Flagship, flag_key: &str, default_value: f64, - ) -> Result { - let __promise = self.get_number_value_raw(flag_key, default_value); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_f64() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) - } -} -impl Flagship { - #[doc = " Get a number flag value."] + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] #[doc = " ## Arguments"] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] - pub async fn get_number_value_with_context( - &self, + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details_with_context( + this: &Flagship, flag_key: &str, default_value: f64, context: &Object, - ) -> Result { - let __promise = self.get_number_value_with_context_raw(flag_key, default_value, context); - let __raw = ::wasm_bindgen_futures::JsFuture::from(__promise).await?; - __raw - .as_f64() - .ok_or_else(|| ::wasm_bindgen::JsValue::from_str("expected number")) - } + ) -> Result; } From 1d509477ed67a377f6397ebdc7dae8e3be24713c Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Wed, 29 Apr 2026 18:59:42 -0500 Subject: [PATCH 06/13] regen with latest ts-gen, without record collapse for now --- examples/flagship/src/lib.rs | 2 +- test/src/flagship.rs | 2 +- worker/src/flagship.rs | 54 +++++---- worker/src/flagship_gen.rs | 224 ++++++++++++++++++++++++++++++++--- 4 files changed, 243 insertions(+), 39 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index 5b765d142..2c3cd313e 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -61,7 +61,7 @@ async fn string(req: Request, env: Env) -> Result { .string("userId", &user_id) .string("country", "US"); flagship - .get_string_value_with_context(&flag, "control", ctx.as_ref()) + .get_string_value_with_record(&flag, "control", ctx.as_ref()) .await? } None => flagship.get_string_value(&flag, "control").await?, diff --git a/test/src/flagship.rs b/test/src/flagship.rs index c77197c63..7a9f0273b 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -92,7 +92,7 @@ pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Re .bool("premium", true); let value = String::from( env.flagship(BINDING)? - .get_string_value_with_context("user-branch", "default", eval_ctx.as_ref()) + .get_string_value_with_record("user-branch", "default", eval_ctx.as_ref()) .await?, ); Response::from_json(&serde_json::json!({ "userId": user_id, "value": value })) diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index 3275766e8..94da6b12c 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -1,5 +1,5 @@ use crate::{send::SendFuture, EnvBinding, Result}; -use js_sys::{Object, Promise}; +use js_sys::{JsString, Object, Promise}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -22,39 +22,47 @@ impl EnvBinding for Flagship { // Raw extern bindings for the object methods stripped from `flagship.d.ts`. // Names mirror the auto-gen pattern from `flagship_gen.rs`: -// `_raw` for the no-context form, `_with_context_raw` for the +// `_raw` for the no-context form, `_with_record_raw` for the // targeting form. The inherent wrappers below own the // `JsFuture::from(...).await?` step and the serde conversion. +// +// The `_with_record` extern takes `&Object` — the auto-gen's +// primary record-typed variant. ts-gen also clones the method as +// `_with_record_1` (Object) and `_with_record_2` (Object) +// for the `Record` value-side union, +// but the phantom `` is purely a compile-time tag and the runtime JS +// object is identical regardless of which variant we call — so we only +// bind the JsString one. #[wasm_bindgen] extern "C" { #[wasm_bindgen(method, js_name = "getObjectValue")] fn get_object_value_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; #[wasm_bindgen(method, js_name = "getObjectValue")] - fn get_object_value_with_context_raw( + fn get_object_value_with_record_raw( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, ) -> Promise; #[wasm_bindgen(method, js_name = "getObjectDetails")] fn get_object_details_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; #[wasm_bindgen(method, js_name = "getObjectDetails")] - fn get_object_details_with_context_raw( + fn get_object_details_with_record_raw( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, ) -> Promise; } // Public surface mirrors the auto-gen primitive accessors: one method for -// the no-context evaluation, one `_with_context` variant taking `&Object` -// (the same shape the auto-gen `_with_context` extern variants accept). -// Callers pass `eval_ctx.as_record()` exactly like they would for -// `get_boolean_value_with_context`. +// the no-context evaluation, one `_with_record` variant taking +// `&Object` (the auto-gen's primary record-typed extern shape). +// Callers pass `eval_ctx.as_ref()` exactly like they would for +// `get_boolean_value_with_record`. impl Flagship { /// Evaluate an object-typed flag, returning the resolved value /// deserialized into `T`. @@ -72,14 +80,14 @@ impl Flagship { } /// Evaluate an object-typed flag with a targeting context. - pub async fn get_object_value_with_context( + pub async fn get_object_value_with_record( &self, flag_key: &str, default_value: &T, - context: &Object, + context: &Object, ) -> Result { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self.get_object_value_with_context_raw(flag_key, &default, context); + let promise = self.get_object_value_with_record_raw(flag_key, &default, context); let raw = SendFuture::new(JsFuture::from(promise)).await?; Ok(serde_wasm_bindgen::from_value(raw)?) } @@ -102,14 +110,14 @@ impl Flagship { /// Evaluate an object-typed flag with a targeting context, returning /// the full evaluation envelope. - pub async fn get_object_details_with_context( + pub async fn get_object_details_with_record( &self, flag_key: &str, default_value: &T, - context: &Object, + context: &Object, ) -> Result> { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self.get_object_details_with_context_raw(flag_key, &default, context); + let promise = self.get_object_details_with_record_raw(flag_key, &default, context); let raw = SendFuture::new(JsFuture::from(promise)).await?; Ok(serde_wasm_bindgen::from_value(raw)?) } @@ -140,12 +148,12 @@ pub struct EvaluationDetails { /// constrained to `string`, `number`, and `boolean` to match the JS /// `Record`. /// -/// Pass via `.as_ref()` to any `_with_context` method, e.g. -/// [`Flagship::get_boolean_value_with_context`] or -/// [`Flagship::get_object_value_with_context`]. +/// Pass via `.as_ref()` to any `_with_record` method, e.g. +/// [`Flagship::get_boolean_value_with_record`] or +/// [`Flagship::get_object_value_with_record`]. #[derive(Debug, Clone)] pub struct EvaluationContext { - inner: Object, + inner: Object, } impl Default for EvaluationContext { @@ -157,7 +165,7 @@ impl Default for EvaluationContext { impl EvaluationContext { pub fn new() -> Self { Self { - inner: Object::new(), + inner: Object::new().unchecked_into(), } } @@ -181,8 +189,8 @@ impl EvaluationContext { } } -impl AsRef for EvaluationContext { - fn as_ref(&self) -> &Object { +impl AsRef> for EvaluationContext { + fn as_ref(&self) -> &Object { &self.inner } } diff --git a/worker/src/flagship_gen.rs b/worker/src/flagship_gen.rs index 058966ff4..5fa510263 100644 --- a/worker/src/flagship_gen.rs +++ b/worker/src/flagship_gen.rs @@ -138,11 +138,39 @@ extern "C" { #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_context( + pub async fn get_with_default_value_and_record( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, + ) -> Result; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "get")] + pub async fn get_with_default_value_and_record_1( + this: &Flagship, + flag_key: &str, + default_value: &JsValue, + context: &Object, + ) -> Result; + #[doc = " Get a flag value without type checking."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "get")] + pub async fn get_with_default_value_and_record_2( + this: &Flagship, + flag_key: &str, + default_value: &JsValue, + context: &Object, ) -> Result; #[doc = " Get a boolean flag value."] #[doc = ""] @@ -165,11 +193,39 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] - pub async fn get_boolean_value_with_context( + pub async fn get_boolean_value_with_record( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result; + #[doc = " Get a boolean flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] + pub async fn get_boolean_value_with_record_1( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result; + #[doc = " Get a boolean flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] + pub async fn get_boolean_value_with_record_2( this: &Flagship, flag_key: &str, default_value: bool, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a string flag value."] #[doc = ""] @@ -192,11 +248,39 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getStringValue")] - pub async fn get_string_value_with_context( + pub async fn get_string_value_with_record( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringValue")] + pub async fn get_string_value_with_record_1( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringValue")] + pub async fn get_string_value_with_record_2( this: &Flagship, flag_key: &str, default_value: &str, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a number flag value."] #[doc = ""] @@ -219,11 +303,39 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getNumberValue")] - pub async fn get_number_value_with_context( + pub async fn get_number_value_with_record( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberValue")] + pub async fn get_number_value_with_record_1( this: &Flagship, flag_key: &str, default_value: f64, - context: &Object, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberValue")] + pub async fn get_number_value_with_record_2( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, ) -> Result; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] @@ -246,11 +358,39 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_context( + pub async fn get_boolean_details_with_record( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details_with_record_1( this: &Flagship, flag_key: &str, default_value: bool, - context: &Object, + context: &Object, + ) -> Result; + #[doc = " Get a boolean flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] + pub async fn get_boolean_details_with_record_2( + this: &Flagship, + flag_key: &str, + default_value: bool, + context: &Object, ) -> Result; #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] @@ -273,11 +413,39 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_context( + pub async fn get_string_details_with_record( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details_with_record_1( this: &Flagship, flag_key: &str, default_value: &str, - context: &Object, + context: &Object, + ) -> Result; + #[doc = " Get a string flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getStringDetails")] + pub async fn get_string_details_with_record_2( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, ) -> Result; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] @@ -300,10 +468,38 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_context( + pub async fn get_number_details_with_record( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details_with_record_1( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result; + #[doc = " Get a number flag value with full evaluation details."] + #[doc = ""] + #[doc = " ## Arguments"] + #[doc = ""] + #[doc = " * `flagKey` - The key of the flag to evaluate."] + #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] + #[doc = " * `context` - Optional evaluation context for targeting rules."] + #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] + pub async fn get_number_details_with_record_2( this: &Flagship, flag_key: &str, default_value: f64, - context: &Object, + context: &Object, ) -> Result; } From 932af3a4e2463ccaed45d7ff7a79525cd402123e Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Wed, 29 Apr 2026 20:08:34 -0500 Subject: [PATCH 07/13] record collapse --- examples/flagship/src/lib.rs | 2 +- test/src/flagship.rs | 2 +- worker/src/flagship.rs | 51 ++++---- worker/src/flagship_gen.rs | 224 +++-------------------------------- 4 files changed, 37 insertions(+), 242 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index 2c3cd313e..5b765d142 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -61,7 +61,7 @@ async fn string(req: Request, env: Env) -> Result { .string("userId", &user_id) .string("country", "US"); flagship - .get_string_value_with_record(&flag, "control", ctx.as_ref()) + .get_string_value_with_context(&flag, "control", ctx.as_ref()) .await? } None => flagship.get_string_value(&flag, "control").await?, diff --git a/test/src/flagship.rs b/test/src/flagship.rs index 7a9f0273b..c77197c63 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -92,7 +92,7 @@ pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Re .bool("premium", true); let value = String::from( env.flagship(BINDING)? - .get_string_value_with_record("user-branch", "default", eval_ctx.as_ref()) + .get_string_value_with_context("user-branch", "default", eval_ctx.as_ref()) .await?, ); Response::from_json(&serde_json::json!({ "userId": user_id, "value": value })) diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index 94da6b12c..e7f3c53fe 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -1,5 +1,5 @@ use crate::{send::SendFuture, EnvBinding, Result}; -use js_sys::{JsString, Object, Promise}; +use js_sys::{Object, Promise}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -22,47 +22,38 @@ impl EnvBinding for Flagship { // Raw extern bindings for the object methods stripped from `flagship.d.ts`. // Names mirror the auto-gen pattern from `flagship_gen.rs`: -// `_raw` for the no-context form, `_with_record_raw` for the +// `_raw` for the no-context form, `_with_context_raw` for the // targeting form. The inherent wrappers below own the // `JsFuture::from(...).await?` step and the serde conversion. -// -// The `_with_record` extern takes `&Object` — the auto-gen's -// primary record-typed variant. ts-gen also clones the method as -// `_with_record_1` (Object) and `_with_record_2` (Object) -// for the `Record` value-side union, -// but the phantom `` is purely a compile-time tag and the runtime JS -// object is identical regardless of which variant we call — so we only -// bind the JsString one. #[wasm_bindgen] extern "C" { #[wasm_bindgen(method, js_name = "getObjectValue")] fn get_object_value_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; #[wasm_bindgen(method, js_name = "getObjectValue")] - fn get_object_value_with_record_raw( + fn get_object_value_with_context_raw( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, ) -> Promise; #[wasm_bindgen(method, js_name = "getObjectDetails")] fn get_object_details_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; #[wasm_bindgen(method, js_name = "getObjectDetails")] - fn get_object_details_with_record_raw( + fn get_object_details_with_context_raw( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, + context: &Object, ) -> Promise; } // Public surface mirrors the auto-gen primitive accessors: one method for -// the no-context evaluation, one `_with_record` variant taking -// `&Object` (the auto-gen's primary record-typed extern shape). +// the no-context evaluation, one `_with_context` variant taking `&Object`. // Callers pass `eval_ctx.as_ref()` exactly like they would for -// `get_boolean_value_with_record`. +// `get_boolean_value_with_context`. impl Flagship { /// Evaluate an object-typed flag, returning the resolved value /// deserialized into `T`. @@ -80,14 +71,14 @@ impl Flagship { } /// Evaluate an object-typed flag with a targeting context. - pub async fn get_object_value_with_record( + pub async fn get_object_value_with_context( &self, flag_key: &str, default_value: &T, - context: &Object, + context: &Object, ) -> Result { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self.get_object_value_with_record_raw(flag_key, &default, context); + let promise = self.get_object_value_with_context_raw(flag_key, &default, context); let raw = SendFuture::new(JsFuture::from(promise)).await?; Ok(serde_wasm_bindgen::from_value(raw)?) } @@ -110,14 +101,14 @@ impl Flagship { /// Evaluate an object-typed flag with a targeting context, returning /// the full evaluation envelope. - pub async fn get_object_details_with_record( + pub async fn get_object_details_with_context( &self, flag_key: &str, default_value: &T, - context: &Object, + context: &Object, ) -> Result> { let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self.get_object_details_with_record_raw(flag_key, &default, context); + let promise = self.get_object_details_with_context_raw(flag_key, &default, context); let raw = SendFuture::new(JsFuture::from(promise)).await?; Ok(serde_wasm_bindgen::from_value(raw)?) } @@ -148,12 +139,12 @@ pub struct EvaluationDetails { /// constrained to `string`, `number`, and `boolean` to match the JS /// `Record`. /// -/// Pass via `.as_ref()` to any `_with_record` method, e.g. -/// [`Flagship::get_boolean_value_with_record`] or -/// [`Flagship::get_object_value_with_record`]. +/// Pass via `.as_ref()` to any `_with_context` method, e.g. +/// [`Flagship::get_boolean_value_with_context`] or +/// [`Flagship::get_object_value_with_context`]. #[derive(Debug, Clone)] pub struct EvaluationContext { - inner: Object, + inner: Object, } impl Default for EvaluationContext { @@ -165,7 +156,7 @@ impl Default for EvaluationContext { impl EvaluationContext { pub fn new() -> Self { Self { - inner: Object::new().unchecked_into(), + inner: Object::new(), } } @@ -189,8 +180,8 @@ impl EvaluationContext { } } -impl AsRef> for EvaluationContext { - fn as_ref(&self) -> &Object { +impl AsRef for EvaluationContext { + fn as_ref(&self) -> &Object { &self.inner } } diff --git a/worker/src/flagship_gen.rs b/worker/src/flagship_gen.rs index 5fa510263..058966ff4 100644 --- a/worker/src/flagship_gen.rs +++ b/worker/src/flagship_gen.rs @@ -138,39 +138,11 @@ extern "C" { #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_record( + pub async fn get_with_default_value_and_context( this: &Flagship, flag_key: &str, default_value: &JsValue, - context: &Object, - ) -> Result; - #[doc = " Get a flag value without type checking."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_record_1( - this: &Flagship, - flag_key: &str, - default_value: &JsValue, - context: &Object, - ) -> Result; - #[doc = " Get a flag value without type checking."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "get")] - pub async fn get_with_default_value_and_record_2( - this: &Flagship, - flag_key: &str, - default_value: &JsValue, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a boolean flag value."] #[doc = ""] @@ -193,39 +165,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] - pub async fn get_boolean_value_with_record( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> Result; - #[doc = " Get a boolean flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] - pub async fn get_boolean_value_with_record_1( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> Result; - #[doc = " Get a boolean flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanValue")] - pub async fn get_boolean_value_with_record_2( + pub async fn get_boolean_value_with_context( this: &Flagship, flag_key: &str, default_value: bool, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a string flag value."] #[doc = ""] @@ -248,39 +192,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getStringValue")] - pub async fn get_string_value_with_record( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a string flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringValue")] - pub async fn get_string_value_with_record_1( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a string flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringValue")] - pub async fn get_string_value_with_record_2( + pub async fn get_string_value_with_context( this: &Flagship, flag_key: &str, default_value: &str, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a number flag value."] #[doc = ""] @@ -303,39 +219,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getNumberValue")] - pub async fn get_number_value_with_record( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberValue")] - pub async fn get_number_value_with_record_1( + pub async fn get_number_value_with_context( this: &Flagship, flag_key: &str, default_value: f64, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberValue")] - pub async fn get_number_value_with_record_2( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] @@ -358,39 +246,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_record( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, - ) -> Result; - #[doc = " Get a boolean flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_record_1( + pub async fn get_boolean_details_with_context( this: &Flagship, flag_key: &str, default_value: bool, - context: &Object, - ) -> Result; - #[doc = " Get a boolean flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getBooleanDetails")] - pub async fn get_boolean_details_with_record_2( - this: &Flagship, - flag_key: &str, - default_value: bool, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] @@ -413,39 +273,11 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_record( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a string flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_record_1( + pub async fn get_string_details_with_context( this: &Flagship, flag_key: &str, default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a string flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_record_2( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, + context: &Object, ) -> Result; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] @@ -468,38 +300,10 @@ extern "C" { #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_record( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_record_1( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value with full evaluation details."] - #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_record_2( + pub async fn get_number_details_with_context( this: &Flagship, flag_key: &str, default_value: f64, - context: &Object, + context: &Object, ) -> Result; } From 1d85fffe9fd4217400f1ebd27e4a5fb8d369e8d4 Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Wed, 29 Apr 2026 20:19:27 -0500 Subject: [PATCH 08/13] small cleanups --- test/src/flagship.rs | 9 +------ worker/src/flagship.rs | 57 ++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/test/src/flagship.rs b/test/src/flagship.rs index c77197c63..bcfecf276 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -154,14 +154,7 @@ pub async fn handle_object_details( .flagship(BINDING)? .get_object_details(&flag, &default_theme()) .await?; - Response::from_json(&serde_json::json!({ - "flagKey": details.flag_key, - "value": details.value, - "variant": details.variant, - "reason": details.reason, - "errorCode": details.error_code, - "errorMessage": details.error_message, - })) + Response::from_json(&details) } fn details_to_json( diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index e7f3c53fe..87cb3d68c 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -20,11 +20,7 @@ impl EnvBinding for Flagship { } } -// Raw extern bindings for the object methods stripped from `flagship.d.ts`. -// Names mirror the auto-gen pattern from `flagship_gen.rs`: -// `_raw` for the no-context form, `_with_context_raw` for the -// targeting form. The inherent wrappers below own the -// `JsFuture::from(...).await?` step and the serde conversion. +// Object-typed methods stripped from `flagship.d.ts` (ts-gen erases ``). #[wasm_bindgen] extern "C" { #[wasm_bindgen(method, js_name = "getObjectValue")] @@ -50,10 +46,6 @@ extern "C" { ) -> Promise; } -// Public surface mirrors the auto-gen primitive accessors: one method for -// the no-context evaluation, one `_with_context` variant taking `&Object`. -// Callers pass `eval_ctx.as_ref()` exactly like they would for -// `get_boolean_value_with_context`. impl Flagship { /// Evaluate an object-typed flag, returning the resolved value /// deserialized into `T`. @@ -62,12 +54,10 @@ impl Flagship { flag_key: &str, default_value: &T, ) -> Result { - let default = serde_wasm_bindgen::to_value(default_value)?; - let raw = SendFuture::new(JsFuture::from( - self.get_object_value_raw(flag_key, &default), - )) - .await?; - Ok(serde_wasm_bindgen::from_value(raw)?) + call_object(default_value, |default| { + self.get_object_value_raw(flag_key, default) + }) + .await } /// Evaluate an object-typed flag with a targeting context. @@ -77,10 +67,10 @@ impl Flagship { default_value: &T, context: &Object, ) -> Result { - let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self.get_object_value_with_context_raw(flag_key, &default, context); - let raw = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(raw)?) + call_object(default_value, |default| { + self.get_object_value_with_context_raw(flag_key, default, context) + }) + .await } /// Evaluate an object-typed flag and return the full evaluation @@ -91,12 +81,10 @@ impl Flagship { flag_key: &str, default_value: &T, ) -> Result> { - let default = serde_wasm_bindgen::to_value(default_value)?; - let raw = SendFuture::new(JsFuture::from( - self.get_object_details_raw(flag_key, &default), - )) - .await?; - Ok(serde_wasm_bindgen::from_value(raw)?) + call_object(default_value, |default| { + self.get_object_details_raw(flag_key, default) + }) + .await } /// Evaluate an object-typed flag with a targeting context, returning @@ -107,20 +95,29 @@ impl Flagship { default_value: &T, context: &Object, ) -> Result> { - let default = serde_wasm_bindgen::to_value(default_value)?; - let promise = self.get_object_details_with_context_raw(flag_key, &default, context); - let raw = SendFuture::new(JsFuture::from(promise)).await?; - Ok(serde_wasm_bindgen::from_value(raw)?) + call_object(default_value, |default| { + self.get_object_details_with_context_raw(flag_key, default, context) + }) + .await } } +async fn call_object( + default_value: &I, + promise_fn: impl FnOnce(&JsValue) -> Promise, +) -> Result { + let default = serde_wasm_bindgen::to_value(default_value)?; + let raw = SendFuture::new(JsFuture::from(promise_fn(&default))).await?; + Ok(serde_wasm_bindgen::from_value(raw)?) +} + /// Typed evaluation record returned by [`Flagship::get_object_details`]. /// For boolean / string / number flags, the auto-generated /// [`FlagshipEvaluationDetails`] is used instead. /// /// `error_code` and `error_message` are only populated when evaluation /// fell back to `default_value`. -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EvaluationDetails { pub flag_key: String, From 2e0c8f3aded898afcd8b034dcd5b6d41f35bf0b9 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 14 May 2026 11:24:03 -0700 Subject: [PATCH 09/13] latest ts-gen --- .gitmodules | 4 - chompfile.toml | 2 +- examples/flagship/src/lib.rs | 2 +- gloo | 1 - test/src/flagship.rs | 5 +- test/src/router.rs | 2 +- .../{flagship_gen.rs => bindings/flagship.rs} | 140 ++++++------------ worker/src/bindings/mod.rs | 1 + worker/src/flagship.rs | 2 +- worker/src/lib.rs | 11 -- 10 files changed, 57 insertions(+), 113 deletions(-) delete mode 160000 gloo rename worker/src/{flagship_gen.rs => bindings/flagship.rs} (73%) diff --git a/.gitmodules b/.gitmodules index d7faf495a..2886a98fd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,3 @@ [submodule "ts-gen"] path = ts-gen url = https://github.com/wasm-bindgen/ts-gen -[submodule "gloo"] - path = gloo - url = https://github.com/guybedford/gloo - branch = fix-gloo-timers-unwind-safety diff --git a/chompfile.toml b/chompfile.toml index fab71d770..d6568ea46 100644 --- a/chompfile.toml +++ b/chompfile.toml @@ -26,7 +26,7 @@ run = '''ts-gen --input types/email.d.ts --output worker/src/bindings/email.rs \ [[task]] name = 'build:types:flagship' deps = ['install:ts-gen'] -run = 'ts-gen --input types/flagship.d.ts --output worker/src/flagship_gen.rs' +run = 'ts-gen --input types/flagship.d.ts --output worker/src/bindings/flagship.rs' [[task]] name = 'install:ts-gen' diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index 5b765d142..b0aa7ffd6 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -7,7 +7,7 @@ //! * `/details?flag=` — return the full evaluation details envelope use serde::{Deserialize, Serialize}; -use worker::{event, Env, EvaluationContext, Request, Response, Result, Router, Url}; +use worker::{Env, EvaluationContext, Request, Response, Result, Router, Url, event}; const BINDING: &str = "FLAGS"; diff --git a/gloo b/gloo deleted file mode 160000 index d73276ed3..000000000 --- a/gloo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d73276ed358882349d5edfb26c97573653dc389c diff --git a/test/src/flagship.rs b/test/src/flagship.rs index bcfecf276..bd65e97e2 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -1,5 +1,6 @@ use crate::SomeSharedData; use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsGeneric; use worker::wasm_bindgen::JsValue; use worker::{ Env, EvaluationContext, EvaluationDetails, FlagshipEvaluationDetails, Request, Response, Result, @@ -157,8 +158,8 @@ pub async fn handle_object_details( Response::from_json(&details) } -fn details_to_json( - details: &FlagshipEvaluationDetails, +fn details_to_json( + details: &FlagshipEvaluationDetails, value: T, ) -> serde_json::Value { serde_json::json!({ diff --git a/test/src/router.rs b/test/src/router.rs index a9d299e31..fe1842ce9 100644 --- a/test/src/router.rs +++ b/test/src/router.rs @@ -2,7 +2,7 @@ use crate::signal; use crate::{ alarm, analytics_engine, assets, auto_response, cache, container, counter, d1, durable, fetch, 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, + service, socket, sql_counter, sql_iterator, user, ws, SomeSharedData, GLOBAL_SECOND_START, GLOBAL_STATE }; #[cfg(feature = "http")] use std::convert::TryInto; diff --git a/worker/src/flagship_gen.rs b/worker/src/bindings/flagship.rs similarity index 73% rename from worker/src/flagship_gen.rs rename to worker/src/bindings/flagship.rs index 058966ff4..98f3d71e1 100644 --- a/worker/src/flagship_gen.rs +++ b/worker/src/bindings/flagship.rs @@ -1,105 +1,93 @@ +// Generated by ts-gen. Do not edit. + #[allow(unused_imports)] use js_sys::*; #[allow(unused_imports)] use wasm_bindgen::prelude::*; #[allow(dead_code)] -use JsValue as T; -#[allow(dead_code)] pub type FlagshipEvaluationContext = Object; #[wasm_bindgen] extern "C" { # [wasm_bindgen (extends = Object)] #[derive(Debug, Clone, PartialEq, Eq)] - pub type FlagshipEvaluationDetails; + pub type FlagshipEvaluationDetails; #[wasm_bindgen(method, getter, js_name = "flagKey")] - pub fn flag_key(this: &FlagshipEvaluationDetails) -> String; + pub fn flag_key(this: &FlagshipEvaluationDetails) -> String; #[wasm_bindgen(method, setter, js_name = "flagKey")] - pub fn set_flag_key(this: &FlagshipEvaluationDetails, val: &str); + pub fn set_flag_key( + this: &FlagshipEvaluationDetails, + val: &str, + ); #[wasm_bindgen(method, getter)] - pub fn value(this: &FlagshipEvaluationDetails) -> T; + pub fn value(this: &FlagshipEvaluationDetails) -> T; #[wasm_bindgen(method, setter)] - pub fn set_value(this: &FlagshipEvaluationDetails, val: &T); + pub fn set_value(this: &FlagshipEvaluationDetails, val: &T); #[wasm_bindgen(method, getter)] - pub fn variant(this: &FlagshipEvaluationDetails) -> Option; + pub fn variant( + this: &FlagshipEvaluationDetails, + ) -> Option; #[wasm_bindgen(method, setter)] - pub fn set_variant(this: &FlagshipEvaluationDetails, val: &str); - #[wasm_bindgen(method, setter, js_name = "variant")] - pub fn set_variant_with_null(this: &FlagshipEvaluationDetails, val: &Null); + pub fn set_variant( + this: &FlagshipEvaluationDetails, + val: &str, + ); #[wasm_bindgen(method, getter)] - pub fn reason(this: &FlagshipEvaluationDetails) -> Option; + pub fn reason( + this: &FlagshipEvaluationDetails, + ) -> Option; #[wasm_bindgen(method, setter)] - pub fn set_reason(this: &FlagshipEvaluationDetails, val: &str); - #[wasm_bindgen(method, setter, js_name = "reason")] - pub fn set_reason_with_null(this: &FlagshipEvaluationDetails, val: &Null); + pub fn set_reason(this: &FlagshipEvaluationDetails, val: &str); #[wasm_bindgen(method, getter, js_name = "errorCode")] - pub fn error_code(this: &FlagshipEvaluationDetails) -> Option; - #[wasm_bindgen(method, setter, js_name = "errorCode")] - pub fn set_error_code(this: &FlagshipEvaluationDetails, val: &str); + pub fn error_code( + this: &FlagshipEvaluationDetails, + ) -> Option; #[wasm_bindgen(method, setter, js_name = "errorCode")] - pub fn set_error_code_with_null(this: &FlagshipEvaluationDetails, val: &Null); + pub fn set_error_code( + this: &FlagshipEvaluationDetails, + val: &str, + ); #[wasm_bindgen(method, getter, js_name = "errorMessage")] - pub fn error_message(this: &FlagshipEvaluationDetails) -> Option; + pub fn error_message( + this: &FlagshipEvaluationDetails, + ) -> Option; #[wasm_bindgen(method, setter, js_name = "errorMessage")] - pub fn set_error_message(this: &FlagshipEvaluationDetails, val: &str); - #[wasm_bindgen(method, setter, js_name = "errorMessage")] - pub fn set_error_message_with_null(this: &FlagshipEvaluationDetails, val: &Null); + pub fn set_error_message( + this: &FlagshipEvaluationDetails, + val: &str, + ); } -impl FlagshipEvaluationDetails { - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flag_key`"] - #[doc = " * `value`"] - pub fn new(flag_key: &str, value: &T) -> FlagshipEvaluationDetails { +impl FlagshipEvaluationDetails { + pub fn new(flag_key: &str, value: &T) -> FlagshipEvaluationDetails { Self::builder(flag_key, value).build() } - #[doc = " ## Arguments"] - #[doc = ""] - #[doc = " * `flag_key`"] - #[doc = " * `value`"] - pub fn builder(flag_key: &str, value: &T) -> FlagshipEvaluationDetailsBuilder { - let inner: Self = JsCast::unchecked_into(js_sys::Object::new()); + pub fn builder(flag_key: &str, value: &T) -> FlagshipEvaluationDetailsBuilder { + let inner: FlagshipEvaluationDetails = JsCast::unchecked_into(js_sys::Object::new()); inner.set_flag_key(flag_key); inner.set_value(value); FlagshipEvaluationDetailsBuilder { inner } } } -pub struct FlagshipEvaluationDetailsBuilder { - inner: FlagshipEvaluationDetails, +pub struct FlagshipEvaluationDetailsBuilder { + inner: FlagshipEvaluationDetails, } -impl FlagshipEvaluationDetailsBuilder { +impl FlagshipEvaluationDetailsBuilder { pub fn variant(self, val: &str) -> Self { self.inner.set_variant(val); self } - pub fn variant_with_null(self, val: &Null) -> Self { - self.inner.set_variant_with_null(val); - self - } pub fn reason(self, val: &str) -> Self { self.inner.set_reason(val); self } - pub fn reason_with_null(self, val: &Null) -> Self { - self.inner.set_reason_with_null(val); - self - } pub fn error_code(self, val: &str) -> Self { self.inner.set_error_code(val); self } - pub fn error_code_with_null(self, val: &Null) -> Self { - self.inner.set_error_code_with_null(val); - self - } pub fn error_message(self, val: &str) -> Self { self.inner.set_error_message(val); self } - pub fn error_message_with_null(self, val: &Null) -> Self { - self.inner.set_error_message_with_null(val); - self - } - pub fn build(self) -> FlagshipEvaluationDetails { + pub fn build(self) -> FlagshipEvaluationDetails { self.inner } } @@ -110,8 +98,6 @@ extern "C" { pub type Flagship; #[doc = " Get a flag value without type checking."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -119,8 +105,6 @@ extern "C" { pub async fn get(this: &Flagship, flag_key: &str) -> Result; #[doc = " Get a flag value without type checking."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -132,8 +116,6 @@ extern "C" { ) -> Result; #[doc = " Get a flag value without type checking."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Optional default value returned when evaluation fails."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -146,8 +128,6 @@ extern "C" { ) -> Result; #[doc = " Get a boolean flag value."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -159,8 +139,6 @@ extern "C" { ) -> Result; #[doc = " Get a boolean flag value."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -173,8 +151,6 @@ extern "C" { ) -> Result; #[doc = " Get a string flag value."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -186,8 +162,6 @@ extern "C" { ) -> Result; #[doc = " Get a string flag value."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -200,8 +174,6 @@ extern "C" { ) -> Result; #[doc = " Get a number flag value."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -213,8 +185,6 @@ extern "C" { ) -> Result; #[doc = " Get a number flag value."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -227,8 +197,6 @@ extern "C" { ) -> Result; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -237,11 +205,9 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: bool, - ) -> Result; + ) -> Result, JsValue>; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -251,11 +217,9 @@ extern "C" { flag_key: &str, default_value: bool, context: &Object, - ) -> Result; + ) -> Result, JsValue>; #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -264,11 +228,9 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: &str, - ) -> Result; + ) -> Result, JsValue>; #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -278,11 +240,9 @@ extern "C" { flag_key: &str, default_value: &str, context: &Object, - ) -> Result; + ) -> Result, JsValue>; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -291,11 +251,9 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: f64, - ) -> Result; + ) -> Result, JsValue>; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] - #[doc = " ## Arguments"] - #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] #[doc = " * `context` - Optional evaluation context for targeting rules."] @@ -305,5 +263,5 @@ extern "C" { flag_key: &str, default_value: f64, context: &Object, - ) -> Result; + ) -> Result, JsValue>; } diff --git a/worker/src/bindings/mod.rs b/worker/src/bindings/mod.rs index aa5f45d47..263736b86 100644 --- a/worker/src/bindings/mod.rs +++ b/worker/src/bindings/mod.rs @@ -1 +1,2 @@ pub mod email; +pub mod flagship; diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index 87cb3d68c..c6f425ce0 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -8,7 +8,7 @@ use wasm_bindgen_futures::JsFuture; // `` to `JsValue`, so the typed `get_object_*` methods // live here with serde conversions folded in. `EvaluationContext` and the // `EnvBinding` impl are also here because ts-gen doesn't synthesize them. -pub use crate::flagship_gen::{Flagship, FlagshipEvaluationDetails}; +pub use crate::bindings::flagship::{Flagship, FlagshipEvaluationDetails}; impl EnvBinding for Flagship { const TYPE_NAME: &'static str = "Flagship"; diff --git a/worker/src/lib.rs b/worker/src/lib.rs index c7abc3d30..6ce1fbabf 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -237,17 +237,6 @@ mod env; mod error; mod fetcher; mod flagship; -// Generated by `chomp build:types:flagship` from types/flagship.d.ts. Same -// allow-list as `email`: ts-gen owns the file, so suppress the lints it -// can't avoid emitting. -#[allow( - unused_imports, - dead_code, - missing_debug_implementations, - clippy::module_inception, - clippy::new_without_default -)] -mod flagship_gen; mod formdata; mod global; mod headers; From 79452f81c50c1d6c743d28933f503de6d4d093f9 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 25 May 2026 11:57:18 -0700 Subject: [PATCH 10/13] fmt --- examples/flagship/src/lib.rs | 2 +- test/src/router.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index b0aa7ffd6..5b765d142 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -7,7 +7,7 @@ //! * `/details?flag=` — return the full evaluation details envelope use serde::{Deserialize, Serialize}; -use worker::{Env, EvaluationContext, Request, Response, Result, Router, Url, event}; +use worker::{event, Env, EvaluationContext, Request, Response, Result, Router, Url}; const BINDING: &str = "FLAGS"; diff --git a/test/src/router.rs b/test/src/router.rs index fe1842ce9..1b988592c 100644 --- a/test/src/router.rs +++ b/test/src/router.rs @@ -1,8 +1,9 @@ use crate::signal; use crate::{ alarm, analytics_engine, assets, auto_response, cache, container, counter, d1, durable, fetch, - 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 + 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; From c379530308465eeda2faf32618cdc2c9bade1f6b Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 25 May 2026 20:04:59 -0700 Subject: [PATCH 11/13] better generics --- test/src/flagship.rs | 7 +++---- wasm-bindgen | 2 +- worker/src/bindings/flagship.rs | 16 ++++++++-------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/test/src/flagship.rs b/test/src/flagship.rs index bd65e97e2..3b4b71c20 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -57,8 +57,7 @@ pub async fn handle_number(req: Request, env: Env, _data: SomeSharedData) -> Res let value: f64 = env .flagship(BINDING)? .get_number_value(&flag, 0.0) - .await? - .value_of(); + .await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -125,7 +124,7 @@ pub async fn handle_string_details( .flagship(BINDING)? .get_string_details(&flag, "fallback") .await?; - let value = details.value().as_string(); + let value = details.value(); Response::from_json(&details_to_json(&details, value)) } @@ -140,7 +139,7 @@ pub async fn handle_number_details( .flagship(BINDING)? .get_number_details(&flag, 0.0) .await?; - let value = details.value().as_f64(); + let value = details.value(); Response::from_json(&details_to_json(&details, value)) } diff --git a/wasm-bindgen b/wasm-bindgen index ddd322514..ad419ff74 160000 --- a/wasm-bindgen +++ b/wasm-bindgen @@ -1 +1 @@ -Subproject commit ddd322514d87a4b21342b7ab9a9d70796fc60576 +Subproject commit ad419ff749f3794fe11b8d103c1309b85251e335 diff --git a/worker/src/bindings/flagship.rs b/worker/src/bindings/flagship.rs index 98f3d71e1..53bc6577f 100644 --- a/worker/src/bindings/flagship.rs +++ b/worker/src/bindings/flagship.rs @@ -159,7 +159,7 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: &str, - ) -> Result; + ) -> Result; #[doc = " Get a string flag value."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -171,7 +171,7 @@ extern "C" { flag_key: &str, default_value: &str, context: &Object, - ) -> Result; + ) -> Result; #[doc = " Get a number flag value."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -182,7 +182,7 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: f64, - ) -> Result; + ) -> Result; #[doc = " Get a number flag value."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -194,7 +194,7 @@ extern "C" { flag_key: &str, default_value: f64, context: &Object, - ) -> Result; + ) -> Result; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -228,7 +228,7 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: &str, - ) -> Result, JsValue>; + ) -> Result, JsValue>; #[doc = " Get a string flag value with full evaluation details."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -240,7 +240,7 @@ extern "C" { flag_key: &str, default_value: &str, context: &Object, - ) -> Result, JsValue>; + ) -> Result, JsValue>; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -251,7 +251,7 @@ extern "C" { this: &Flagship, flag_key: &str, default_value: f64, - ) -> Result, JsValue>; + ) -> Result, JsValue>; #[doc = " Get a number flag value with full evaluation details."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -263,5 +263,5 @@ extern "C" { flag_key: &str, default_value: f64, context: &Object, - ) -> Result, JsValue>; + ) -> Result, JsValue>; } From 758a1f060dede6431c0c9901f77b0fff67ab36aa Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Mon, 22 Jun 2026 16:52:04 -0500 Subject: [PATCH 12/13] Move to manual bindings for string and number. need https://github.com/wasm-bindgen/wasm-bindgen/pull/5180 for better ergonomics on generated bindings --- examples/flagship/src/lib.rs | 13 +-- test/src/flagship.rs | 62 +++++--------- types/flagship.d.ts | 53 ++---------- worker/src/bindings/flagship.rs | 92 -------------------- worker/src/flagship.rs | 147 +++++++++++++++++++++++++++++--- 5 files changed, 171 insertions(+), 196 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index 5b765d142..af208082b 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -55,7 +55,7 @@ async fn string(req: Request, env: Env) -> Result { 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") { + let value = match query(&url, "userId") { Some(user_id) => { let ctx = EvaluationContext::new() .string("userId", &user_id) @@ -65,7 +65,7 @@ async fn string(req: Request, env: Env) -> Result { .await? } None => flagship.get_string_value(&flag, "control").await?, - }); + }; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -90,14 +90,7 @@ async fn details(req: Request, env: Env) -> Result { .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(), - })) + Response::from_json(&details) } fn query(url: &Url, key: &str) -> Option { diff --git a/test/src/flagship.rs b/test/src/flagship.rs index 3b4b71c20..80c18b96b 100644 --- a/test/src/flagship.rs +++ b/test/src/flagship.rs @@ -1,10 +1,7 @@ use crate::SomeSharedData; use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsGeneric; use worker::wasm_bindgen::JsValue; -use worker::{ - Env, EvaluationContext, EvaluationDetails, FlagshipEvaluationDetails, Request, Response, Result, -}; +use worker::{Env, EvaluationContext, EvaluationDetails, Request, Response, Result}; const BINDING: &str = "FLAGS"; @@ -43,21 +40,17 @@ pub async fn handle_boolean(req: Request, env: Env, _data: SomeSharedData) -> Re #[worker::send] pub async fn handle_string(req: Request, env: Env, _data: SomeSharedData) -> Result { let flag = last_segment(&req)?; - let value = String::from( - env.flagship(BINDING)? - .get_string_value(&flag, "fallback") - .await?, - ); + 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 { let flag = last_segment(&req)?; - let value: f64 = env - .flagship(BINDING)? - .get_number_value(&flag, 0.0) - .await?; + let value: f64 = env.flagship(BINDING)?.get_number_value(&flag, 0.0).await?; Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } @@ -90,11 +83,10 @@ pub async fn handle_context(req: Request, env: Env, _data: SomeSharedData) -> Re .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?, - ); + 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 })) } @@ -109,8 +101,14 @@ pub async fn handle_boolean_details( .flagship(BINDING)? .get_boolean_details(&flag, false) .await?; - let value = details.value().as_bool(); - Response::from_json(&details_to_json(&details, value)) + 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] @@ -120,12 +118,11 @@ pub async fn handle_string_details( _data: SomeSharedData, ) -> Result { let flag = last_segment(&req)?; - let details = env + let details: EvaluationDetails = env .flagship(BINDING)? .get_string_details(&flag, "fallback") .await?; - let value = details.value(); - Response::from_json(&details_to_json(&details, value)) + Response::from_json(&details) } #[worker::send] @@ -135,12 +132,11 @@ pub async fn handle_number_details( _data: SomeSharedData, ) -> Result { let flag = last_segment(&req)?; - let details = env + let details: EvaluationDetails = env .flagship(BINDING)? .get_number_details(&flag, 0.0) .await?; - let value = details.value(); - Response::from_json(&details_to_json(&details, value)) + Response::from_json(&details) } #[worker::send] @@ -156,17 +152,3 @@ pub async fn handle_object_details( .await?; Response::from_json(&details) } - -fn details_to_json( - 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(), - }) -} diff --git a/types/flagship.d.ts b/types/flagship.d.ts index ba56fa1cb..4780b270e 100644 --- a/types/flagship.d.ts +++ b/types/flagship.d.ts @@ -68,28 +68,10 @@ declare abstract class Flagship { defaultValue: boolean, context?: FlagshipEvaluationContext, ): Promise; - /** - * Get a string flag value. - * @param flagKey The key of the flag to evaluate. - * @param defaultValue Default value returned when evaluation fails or the flag type does not match. - * @param context Optional evaluation context for targeting rules. - */ - getStringValue( - flagKey: string, - defaultValue: string, - context?: FlagshipEvaluationContext, - ): Promise; - /** - * Get a number flag value. - * @param flagKey The key of the flag to evaluate. - * @param defaultValue Default value returned when evaluation fails or the flag type does not match. - * @param context Optional evaluation context for targeting rules. - */ - getNumberValue( - flagKey: string, - defaultValue: number, - context?: FlagshipEvaluationContext, - ): Promise; + // EDIT: `getStringValue` / `getNumberValue` are hand-written in + // worker/src/flagship.rs. ts-gen types them as `JsString` / `Number`; + // returning plain `String` / `f64` needs ABI-level generics that no + // released wasm-bindgen has yet. // EDIT: `getObjectValue(...)` is hand-written in // worker/src/flagship.rs (ts-gen erases the generic to JsValue). /** @@ -103,28 +85,11 @@ declare abstract class Flagship { defaultValue: boolean, context?: FlagshipEvaluationContext, ): Promise>; - /** - * Get a string flag value with full evaluation details. - * @param flagKey The key of the flag to evaluate. - * @param defaultValue Default value returned when evaluation fails or the flag type does not match. - * @param context Optional evaluation context for targeting rules. - */ - getStringDetails( - flagKey: string, - defaultValue: string, - context?: FlagshipEvaluationContext, - ): Promise>; - /** - * Get a number flag value with full evaluation details. - * @param flagKey The key of the flag to evaluate. - * @param defaultValue Default value returned when evaluation fails or the flag type does not match. - * @param context Optional evaluation context for targeting rules. - */ - getNumberDetails( - flagKey: string, - defaultValue: number, - context?: FlagshipEvaluationContext, - ): Promise>; + // EDIT: `getStringDetails` / `getNumberDetails` are hand-written in + // worker/src/flagship.rs (return a typed `EvaluationDetails`). ts-gen + // types them as `FlagshipEvaluationDetails` / ``; + // returning a plain primitive generic needs the same ABI-level generics no + // released wasm-bindgen has yet. // EDIT: `getObjectDetails(...)` is hand-written in // worker/src/flagship.rs (returns a typed `EvaluationDetails`). } diff --git a/worker/src/bindings/flagship.rs b/worker/src/bindings/flagship.rs index 53bc6577f..8629c4720 100644 --- a/worker/src/bindings/flagship.rs +++ b/worker/src/bindings/flagship.rs @@ -149,52 +149,6 @@ extern "C" { default_value: bool, context: &Object, ) -> Result; - #[doc = " Get a string flag value."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringValue")] - pub async fn get_string_value( - this: &Flagship, - flag_key: &str, - default_value: &str, - ) -> Result; - #[doc = " Get a string flag value."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringValue")] - pub async fn get_string_value_with_context( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result; - #[doc = " Get a number flag value."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberValue")] - pub async fn get_number_value( - this: &Flagship, - flag_key: &str, - default_value: f64, - ) -> Result; - #[doc = " Get a number flag value."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberValue")] - pub async fn get_number_value_with_context( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result; #[doc = " Get a boolean flag value with full evaluation details."] #[doc = ""] #[doc = " * `flagKey` - The key of the flag to evaluate."] @@ -218,50 +172,4 @@ extern "C" { default_value: bool, context: &Object, ) -> Result, JsValue>; - #[doc = " Get a string flag value with full evaluation details."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details( - this: &Flagship, - flag_key: &str, - default_value: &str, - ) -> Result, JsValue>; - #[doc = " Get a string flag value with full evaluation details."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getStringDetails")] - pub async fn get_string_details_with_context( - this: &Flagship, - flag_key: &str, - default_value: &str, - context: &Object, - ) -> Result, JsValue>; - #[doc = " Get a number flag value with full evaluation details."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details( - this: &Flagship, - flag_key: &str, - default_value: f64, - ) -> Result, JsValue>; - #[doc = " Get a number flag value with full evaluation details."] - #[doc = ""] - #[doc = " * `flagKey` - The key of the flag to evaluate."] - #[doc = " * `defaultValue` - Default value returned when evaluation fails or the flag type does not match."] - #[doc = " * `context` - Optional evaluation context for targeting rules."] - #[wasm_bindgen(method, catch, js_name = "getNumberDetails")] - pub async fn get_number_details_with_context( - this: &Flagship, - flag_key: &str, - default_value: f64, - context: &Object, - ) -> Result, JsValue>; } diff --git a/worker/src/flagship.rs b/worker/src/flagship.rs index c6f425ce0..0d020d8b0 100644 --- a/worker/src/flagship.rs +++ b/worker/src/flagship.rs @@ -4,10 +4,19 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; -// Hand-written companion to `flagship_gen.rs`. ts-gen erases -// `` to `JsValue`, so the typed `get_object_*` methods -// live here with serde conversions folded in. `EvaluationContext` and the -// `EnvBinding` impl are also here because ts-gen doesn't synthesize them. +// Hand-written companion to the ts-gen output in `bindings/flagship.rs`. +// +// ts-gen types `getStringValue` / `getNumberValue` (and their `*Details` +// variants) as `JsString` / `Number`. Returning plain `String` / `f64` needs +// ABI-level generics that no released wasm-bindgen has yet +// (see https://github.com/wasm-bindgen/wasm-bindgen/pull/5180), so those +// methods are hand-rolled here until it lands. The `getObject*` methods are +// here for the same reason: ts-gen erases their `` to `JsValue`. They fold +// serde conversions in for typed `T`. `EvaluationContext` and the `EnvBinding` +// impl are here too, because ts-gen doesn't synthesize them. +// +// Boolean flags are fully auto-generated: ts-gen maps them onto the js_sys +// `Boolean` / `FlagshipEvaluationDetails` types, which work as-is. pub use crate::bindings::flagship::{Flagship, FlagshipEvaluationDetails}; impl EnvBinding for Flagship { @@ -20,9 +29,53 @@ impl EnvBinding for Flagship { } } -// Object-typed methods stripped from `flagship.d.ts` (ts-gen erases ``). +// String / number / object methods hand-rolled off raw `Promise` externs. #[wasm_bindgen] extern "C" { + #[wasm_bindgen(method, js_name = "getStringValue")] + fn get_string_value_raw(this: &Flagship, flag_key: &str, default_value: &str) -> Promise; + + #[wasm_bindgen(method, js_name = "getStringValue")] + fn get_string_value_with_context_raw( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Promise; + + #[wasm_bindgen(method, js_name = "getNumberValue")] + fn get_number_value_raw(this: &Flagship, flag_key: &str, default_value: f64) -> Promise; + + #[wasm_bindgen(method, js_name = "getNumberValue")] + fn get_number_value_with_context_raw( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Promise; + + #[wasm_bindgen(method, js_name = "getStringDetails")] + fn get_string_details_raw(this: &Flagship, flag_key: &str, default_value: &str) -> Promise; + + #[wasm_bindgen(method, js_name = "getStringDetails")] + fn get_string_details_with_context_raw( + this: &Flagship, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Promise; + + #[wasm_bindgen(method, js_name = "getNumberDetails")] + fn get_number_details_raw(this: &Flagship, flag_key: &str, default_value: f64) -> Promise; + + #[wasm_bindgen(method, js_name = "getNumberDetails")] + fn get_number_details_with_context_raw( + this: &Flagship, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Promise; + #[wasm_bindgen(method, js_name = "getObjectValue")] fn get_object_value_raw(this: &Flagship, flag_key: &str, default_value: &JsValue) -> Promise; @@ -47,6 +100,76 @@ extern "C" { } impl Flagship { + /// Evaluate a string-typed flag. + pub async fn get_string_value(&self, flag_key: &str, default_value: &str) -> Result { + from_js(self.get_string_value_raw(flag_key, default_value)).await + } + + /// Evaluate a string-typed flag with a targeting context. + pub async fn get_string_value_with_context( + &self, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result { + from_js(self.get_string_value_with_context_raw(flag_key, default_value, context)).await + } + + /// Evaluate a number-typed flag. + pub async fn get_number_value(&self, flag_key: &str, default_value: f64) -> Result { + from_js(self.get_number_value_raw(flag_key, default_value)).await + } + + /// Evaluate a number-typed flag with a targeting context. + pub async fn get_number_value_with_context( + &self, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result { + from_js(self.get_number_value_with_context_raw(flag_key, default_value, context)).await + } + + /// Evaluate a string-typed flag and return the full evaluation envelope. + pub async fn get_string_details( + &self, + flag_key: &str, + default_value: &str, + ) -> Result> { + from_js(self.get_string_details_raw(flag_key, default_value)).await + } + + /// Evaluate a string-typed flag with a targeting context, returning the + /// full evaluation envelope. + pub async fn get_string_details_with_context( + &self, + flag_key: &str, + default_value: &str, + context: &Object, + ) -> Result> { + from_js(self.get_string_details_with_context_raw(flag_key, default_value, context)).await + } + + /// Evaluate a number-typed flag and return the full evaluation envelope. + pub async fn get_number_details( + &self, + flag_key: &str, + default_value: f64, + ) -> Result> { + from_js(self.get_number_details_raw(flag_key, default_value)).await + } + + /// Evaluate a number-typed flag with a targeting context, returning the + /// full evaluation envelope. + pub async fn get_number_details_with_context( + &self, + flag_key: &str, + default_value: f64, + context: &Object, + ) -> Result> { + from_js(self.get_number_details_with_context_raw(flag_key, default_value, context)).await + } + /// Evaluate an object-typed flag, returning the resolved value /// deserialized into `T`. pub async fn get_object_value( @@ -102,18 +225,22 @@ impl Flagship { } } +async fn from_js(promise: Promise) -> Result { + let raw = SendFuture::new(JsFuture::from(promise)).await?; + Ok(serde_wasm_bindgen::from_value(raw)?) +} + async fn call_object( default_value: &I, promise_fn: impl FnOnce(&JsValue) -> Promise, ) -> Result { let default = serde_wasm_bindgen::to_value(default_value)?; - let raw = SendFuture::new(JsFuture::from(promise_fn(&default))).await?; - Ok(serde_wasm_bindgen::from_value(raw)?) + from_js(promise_fn(&default)).await } -/// Typed evaluation record returned by [`Flagship::get_object_details`]. -/// For boolean / string / number flags, the auto-generated -/// [`FlagshipEvaluationDetails`] is used instead. +/// Typed evaluation record returned by the `get_*_details` methods for string, +/// number, and object flags. Boolean flags use the auto-generated +/// [`FlagshipEvaluationDetails`] instead. /// /// `error_code` and `error_message` are only populated when evaluation /// fell back to `default_value`. From 6d5cd7369dda43714a6d5ea84fc00406b9c8f20f Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Mon, 22 Jun 2026 17:07:03 -0500 Subject: [PATCH 13/13] small simplification to example inline closures were odd and didnt match the other examples --- examples/flagship/src/lib.rs | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/examples/flagship/src/lib.rs b/examples/flagship/src/lib.rs index af208082b..c5aa0518c 100644 --- a/examples/flagship/src/lib.rs +++ b/examples/flagship/src/lib.rs @@ -7,7 +7,7 @@ //! * `/details?flag=` — return the full evaluation details envelope use serde::{Deserialize, Serialize}; -use worker::{event, Env, EvaluationContext, Request, Response, Result, Router, Url}; +use worker::{event, Env, EvaluationContext, Request, Response, Result, RouteContext, Router, Url}; const BINDING: &str = "FLAGS"; @@ -20,27 +20,16 @@ struct Theme { #[event(fetch)] async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { 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 }, - ) + .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, env: Env) -> Result { +async fn boolean(req: Request, ctx: RouteContext<()>) -> Result { + let env = ctx.env; let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "example-bool".into()); let value: bool = env @@ -51,7 +40,8 @@ async fn boolean(req: Request, env: Env) -> Result { Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } -async fn string(req: Request, env: Env) -> Result { +async fn string(req: Request, ctx: RouteContext<()>) -> Result { + let env = ctx.env; let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into()); let flagship = env.flagship(BINDING)?; @@ -69,7 +59,8 @@ async fn string(req: Request, env: Env) -> Result { Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } -async fn object(req: Request, env: Env) -> Result { +async fn object(req: Request, ctx: RouteContext<()>) -> Result { + let env = ctx.env; let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "theme".into()); let default = Theme { @@ -83,7 +74,8 @@ async fn object(req: Request, env: Env) -> Result { Response::from_json(&serde_json::json!({ "flag": flag, "value": value })) } -async fn details(req: Request, env: Env) -> Result { +async fn details(req: Request, ctx: RouteContext<()>) -> Result { + let env = ctx.env; let url = req.url()?; let flag = query(&url, "flag").unwrap_or_else(|| "checkout-flow".into()); let details = env