diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ce9f661..dd40ac1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -62,6 +62,8 @@ Files in `docs/` are **machine-generated** from source code by `./fastedge-plugi | `src/proxywasm/dictionary.rs` | `docs/HOST_SERVICES.md` | | `src/proxywasm/utils.rs` | `docs/HOST_SERVICES.md` | | `src/proxywasm/` (CDN lifecycle, FFI) | `docs/CDN_APPS.md` | +| `examples/http/wasi/hello_world/src/lib.rs` | `docs/quickstart.md` | +| `examples/http/basic/hello_world/src/lib.rs` | `docs/quickstart.md` | | `Cargo.toml` (version, features) | `docs/INDEX.md` | | `fastedge-plugin-source/manifest.json` | `.github/copilot-instructions.md` | diff --git a/.gitignore b/.gitignore index 79085ea..614a059 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,16 @@ target/ # FastEdge debugger artifacts **/.fastedge-debug/ + +# build artifacts +**/*.wasm + +# example project lock files +examples/**/pnpm-lock.yaml +examples/**/package-lock.json +examples/**/livetest.config.json + + +# Doc-generator failure artifacts — rejected/preamble-leaked claude -p +# outputs preserved for prompt-debugging. Prune manually. +docs/.failures/ diff --git a/docs/CDN_APPS.md b/docs/CDN_APPS.md index 9ff7bcd..edb82e0 100644 --- a/docs/CDN_APPS.md +++ b/docs/CDN_APPS.md @@ -141,6 +141,9 @@ proxy_wasm::main! {{ Box::new(MyAppRoot) }); }} +# struct MyAppRoot; +# impl proxy_wasm::traits::Context for MyAppRoot {} +# impl proxy_wasm::traits::RootContext for MyAppRoot {} ``` ### Root Context @@ -150,6 +153,9 @@ The root context is a singleton created once when the filter loads. Its primary ```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; +# struct MyApp; +# impl Context for MyApp {} +# impl HttpContext for MyApp {} struct MyAppRoot; impl Context for MyAppRoot {} diff --git a/docs/INDEX.md b/docs/INDEX.md index a519dc0..7773ed2 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,6 +1,6 @@ # FastEdge Rust SDK Documentation -Documentation for the `fastedge` crate (v0.3.5) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. +Documentation for the `fastedge` crate (v0.4.0) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. ## Documents diff --git a/docs/SDK_API.md b/docs/SDK_API.md index fb4ad9f..9d60fdb 100644 --- a/docs/SDK_API.md +++ b/docs/SDK_API.md @@ -8,7 +8,7 @@ Reference for the `fastedge` crate. Covers the handler macros, body type, outbou ### Cargo.toml -The current crate version is `0.3.5` (from `[workspace.package]` in the repository's `Cargo.toml`). +The current crate version is `0.4.0` (from `[workspace.package]` in the repository's `Cargo.toml`). For `#[wstd::http_server]` (recommended): @@ -25,7 +25,7 @@ For `#[fastedge::http]` (basic): ```toml [dependencies] -fastedge = "0.3.5" +fastedge = "0.4.0" anyhow = "1" [lib] @@ -215,14 +215,14 @@ pub struct Body { /* private fields */ } ### Constructors -| Constructor | Content-Type | Notes | -| -------------------------------------------- | --------------------------- | ------------------------------------------------------------------ | -| `Body::from(value: String)` | `text/plain; charset=utf-8` | | -| `Body::from(value: &'static str)` | `text/plain; charset=utf-8` | | -| `Body::from(value: Vec)` | `application/octet-stream` | | -| `Body::from(value: &'static [u8])` | `application/octet-stream` | | -| `Body::empty()` | `text/plain; charset=utf-8` | Zero-length body | -| `Body::try_from(value: serde_json::Value)` | `application/json` | Requires `json` feature; returns `Result` | +| Constructor | Content-Type | Notes | +| ------------------------------------------ | --------------------------- | ------------------------------------------------------------------ | +| `Body::from(value: String)` | `text/plain; charset=utf-8` | | +| `Body::from(value: &'static str)` | `text/plain; charset=utf-8` | | +| `Body::from(value: Vec)` | `application/octet-stream` | | +| `Body::from(value: &'static [u8])` | `application/octet-stream` | | +| `Body::empty()` | `text/plain; charset=utf-8` | Zero-length body | +| `Body::try_from(value: serde_json::Value)` | `application/json` | Requires `json` feature; returns `Result` | ```rust use fastedge::body::Body; @@ -233,7 +233,7 @@ let bytes = Body::from(vec![0x48u8, 0x69]); let empty = Body::empty(); ``` -```rust +```rust,ignore // json feature required use fastedge::body::Body; use serde_json::json; @@ -247,10 +247,10 @@ assert_eq!(body.content_type(), "application/json"); ### Methods -| Method | Return Type | Description | -| -------------------------------- | ----------- | --------------------------------------------------- | -| `content_type(&self) -> String` | `String` | Returns the MIME type set when the body was created | -| `empty() -> Self` | `Body` | Constructs a zero-length body | +| Method | Return Type | Description | +| ------------------------------- | ----------- | --------------------------------------------------- | +| `content_type(&self) -> String` | `String` | Returns the MIME type set when the body was created | +| `empty() -> Self` | `Body` | Constructs a zero-length body | All methods from `bytes::Bytes` are available via `Deref`: @@ -267,16 +267,16 @@ let slice: &[u8] = &body[..]; Content-type is determined at construction time and cannot be changed after creation. -| Input type | Resulting content-type | -| --------------------- | --------------------------- | -| `String` / `&str` | `text/plain; charset=utf-8` | -| `Vec` / `&[u8]` | `application/octet-stream` | -| `serde_json::Value` | `application/json` | -| `Body::empty()` | `text/plain; charset=utf-8` | +| Input type | Resulting content-type | +| ------------------- | --------------------------- | +| `String` / `&str` | `text/plain; charset=utf-8` | +| `Vec` / `&[u8]` | `application/octet-stream` | +| `serde_json::Value` | `application/json` | +| `Body::empty()` | `text/plain; charset=utf-8` | To send a response with a content-type that does not match automatic detection, set the `Content-Type` header explicitly on the response builder: -```rust +```rust,no_run use fastedge::body::Body; use fastedge::http::{Response, StatusCode}; @@ -370,17 +370,17 @@ pub enum Error { } ``` -| Variant | When it occurs | -| ----------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `UnsupportedMethod(http::Method)` | `send_request` was called with a method other than GET, POST, PUT, DELETE, HEAD, PATCH, or OPTIONS | -| `BindgenHttpError` | The host runtime returned an error during request execution | -| `HttpError(http::Error)` | An error occurred constructing or parsing an HTTP message | -| `InvalidBody` | The request or response body could not be encoded or decoded | -| `InvalidStatusCode(u16)` | A status code outside the range 100–599 was encountered | +| Variant | When it occurs | +| --------------------------------- | -------------------------------------------------------------------------------------------------- | +| `UnsupportedMethod(http::Method)` | `send_request` was called with a method other than GET, POST, PUT, DELETE, HEAD, PATCH, or OPTIONS | +| `BindgenHttpError` | The host runtime returned an error during request execution | +| `HttpError(http::Error)` | An error occurred constructing or parsing an HTTP message | +| `InvalidBody` | The request or response body could not be encoded or decoded | +| `InvalidStatusCode(u16)` | A status code outside the range 100–599 was encountered | `Error` implements `std::error::Error` and `std::fmt::Display`. It is compatible with `anyhow` and `?` propagation. -```rust +```rust,no_run use fastedge::{Error, send_request}; use fastedge::body::Body; use fastedge::http::{Method, Request}; @@ -401,23 +401,23 @@ fn fetch(uri: &str) -> Result { ## Feature Flags -| Flag | Default | Effect | -| ------------- | ---------- | ----------------------------------------------------------------------------------- | -| `proxywasm` | enabled | Enables the `fastedge::proxywasm` module for ProxyWasm ABI compatibility | -| `json` | disabled | Enables `Body::try_from(serde_json::Value)` and adds `serde_json` as a dependency | +| Flag | Default | Effect | +| ----------- | -------- | ---------------------------------------------------------------------------------- | +| `proxywasm` | enabled | Enables the `fastedge::proxywasm` module for ProxyWasm ABI compatibility | +| `json` | disabled | Enables `Body::try_from(serde_json::Value)` and adds `serde_json` as a dependency | Enable non-default features in `Cargo.toml`: ```toml [dependencies] -fastedge = { version = "0.3.5", features = ["json"] } +fastedge = { version = "0.4.0", features = ["json"] } ``` Disable the default `proxywasm` feature if you do not need it: ```toml [dependencies] -fastedge = { version = "0.3.5", default-features = false } +fastedge = { version = "0.4.0", default-features = false } ``` --- @@ -432,15 +432,15 @@ use fastedge::http::{Method, Request, Response, StatusCode, HeaderMap, Uri}; **Supported HTTP methods** (the complete set accepted by `send_request`): -| Constant | Method | -| ------------------- | ----------- | -| `Method::GET` | `GET` | -| `Method::POST` | `POST` | -| `Method::PUT` | `PUT` | -| `Method::DELETE` | `DELETE` | -| `Method::HEAD` | `HEAD` | -| `Method::PATCH` | `PATCH` | -| `Method::OPTIONS` | `OPTIONS` | +| Constant | Method | +| ----------------- | --------- | +| `Method::GET` | `GET` | +| `Method::POST` | `POST` | +| `Method::PUT` | `PUT` | +| `Method::DELETE` | `DELETE` | +| `Method::HEAD` | `HEAD` | +| `Method::PATCH` | `PATCH` | +| `Method::OPTIONS` | `OPTIONS` | --- diff --git a/docs/quickstart.md b/docs/quickstart.md index 418dda5..78c4754 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -88,7 +88,7 @@ Add dependencies to `Cargo.toml`: ```toml [dependencies] -fastedge = "0.3.5" +fastedge = "0.4.0" anyhow = "1" [lib] @@ -142,7 +142,7 @@ Enable the `json` feature in `Cargo.toml`: ```toml [dependencies] -fastedge = { version = "0.3.5", features = ["json"] } +fastedge = { version = "0.4.0", features = ["json"] } ``` ## CDN Apps diff --git a/examples/README.md b/examples/README.md index 762e1f7..4660737 100644 --- a/examples/README.md +++ b/examples/README.md @@ -55,10 +55,16 @@ Examples are organized into three categories: | Example | Description | | --- | --- | +| [ab_testing](./http/wasi/ab_testing/) | Cookie-based A/B testing — weighted variant headers and persistent xid cookie | +| [bloom_filter_denylist](./http/wasi/bloom_filter_denylist/) | Reject requests from IPs present in a KV Store bloom filter (`bf_exists`) | +| [diagnostic_logging](./http/wasi/diagnostic_logging/) | Tag each request with a single `set_user_diag` outcome label (logfmt) | | [geo_redirect](./http/wasi/geo_redirect/) | Redirect requests to country-specific origins based on geoIP | | [key_value](./http/wasi/key_value/) | KV store operations — get, scan, zrange, zscan, bfExists | -| [outbound_fetch](./http/wasi/outbound_fetch/) | Make outbound HTTP requests to a JSON API and transform the response | +| [outbound_fetch](./http/wasi/outbound_fetch/) | Fetch from an outbound HTTP origin and return the response directly | +| [outbound_modify_response](./http/wasi/outbound_modify_response/) | Fetch outbound, parse the JSON body, and return a reshaped response | | [secret_rollover](./http/wasi/secret_rollover/) | Slot-based secret retrieval for secret rotation scenarios | +| [static_assets](./http/wasi/static_assets/) | Serve HTML, CSS, and SVG embedded into the wasm binary at compile time | +| [streaming](./http/wasi/streaming/) | Generate a streaming response body with `Body::from_stream` and timed chunks | | [large_env_variable](./http/wasi/large_env_variable/) | Read large (> 64KB) environment variables using the dictionary API | ### cdn (proxy-wasm) @@ -80,11 +86,20 @@ Examples are organized into three categories: ## Usage -Each example is a standalone project. To build one: +Each example is a standalone crate. To build one: ```sh cd -cargo build --target wasm32-wasip1 --release +cargo build --release ``` -Each example depends on the [`fastedge`](https://crates.io/crates/fastedge) crate from crates.io. +The correct WASM target is picked up automatically from the nearest `.cargo/config.toml`: + +- `http/basic/*` and `cdn/*` → `wasm32-wasip1` (from the repo-root config) +- `http/wasi/*` → `wasm32-wasip2` (from `examples/http/wasi/.cargo/config.toml`) + +Install both targets once with `rustup target add wasm32-wasip1 wasm32-wasip2`. + +Most examples depend on the [`fastedge`](https://crates.io/crates/fastedge) crate. The majority +reference a published version from crates.io; a small number use a path dependency to the local +workspace (e.g. examples that exercise unreleased APIs). diff --git a/examples/http/basic/api_wrapper/README.md b/examples/http/basic/api_wrapper/README.md index 6055391..e2bd465 100644 --- a/examples/http/basic/api_wrapper/README.md +++ b/examples/http/basic/api_wrapper/README.md @@ -2,10 +2,71 @@ # API Wrapper -Wraps multiple SmartThings API calls to get and toggle device state, with password-based authentication. +Demonstrates how to wrap multiple outbound API calls in a single FastEdge edge function, using the legacy synchronous `#[fastedge::http]` handler (`wasm32-wasip1`). The function implements password-based authentication, fetches the current state of a SmartThings smart-switch device, and sends a toggle command to flip it. + +> **Handler note:** This example uses `#[fastedge::http]` (sync, `wasm32-wasip1`), which is the legacy handler for basic HTTP apps. For new projects, prefer `#[wstd::http_server]` (async, `wasm32-wasip2`). + +## What this example teaches + +- How to make multiple sequential outbound HTTP calls with `fastedge::send_request` +- How to implement password-based authentication via a request header +- How to guard against misconfigured apps (missing env vars → 500) +- How to parse JSON responses with `serde_json` +- HTTP redirect handling in the outbound call helper + +## APIs used + +| API | Description | +|---|---| +| `#[fastedge::http]` | Sync handler macro — entry point | +| `fastedge::send_request(req)` | Outbound HTTP call to the SmartThings API | +| `env::var("NAME")` | Read env vars (PASSWORD, DEVICE, TOKEN) | +| `serde_json::from_str` | Parse JSON from the SmartThings response | +| `Response::builder()` | Build HTTP responses | +| `Request::builder()` | Build outbound HTTP requests | ## Configuration -- Environment variable: `PASSWORD` — expected password for request authentication -- Environment variable: `DEVICE` — SmartThings device ID -- Environment variable: `TOKEN` — SmartThings API token +| Env var | Description | +|---|---| +| `PASSWORD` | Expected password value — compared against the `Authorization` request header | +| `DEVICE` | SmartThings device ID | +| `TOKEN` | SmartThings API bearer token | + +## Request format + +Send a GET or HEAD request with the password in the `Authorization` header: + +``` +GET / HTTP/1.1 +Authorization: +``` + +Only `GET` and `HEAD` are accepted; any other method returns `405 Method Not Allowed` with an `Allow: GET, HEAD` header. + +## Response summary + +| Condition | Status | Body | +|---|---|---| +| Missing `Authorization` header | 403 | `No auth header` | +| Wrong password | 403 | _(empty)_ | +| Missing env var (PASSWORD, DEVICE, or TOKEN) | 500 | `Misconfigured app` | +| SmartThings API error | Reflects upstream status | _(empty)_ | +| Device toggled successfully | 204 | _(empty)_ | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/api_wrapper.wasm +``` + +## App flow + +1. Validate HTTP method (GET / HEAD only) +2. Read `PASSWORD` env var — 500 if missing +3. Check `Authorization` header matches `PASSWORD` — 403 if missing or wrong +4. Read `DEVICE` and `TOKEN` env vars — 500 if missing +5. `GET /v1/devices//status` → parse `components.main.switch.switch.value` (`"on"` or `"off"`) +6. `POST /v1/devices//commands` with the opposite command +7. Return 204 on `ACCEPTED`, or the upstream status code on error diff --git a/examples/http/basic/api_wrapper/fixtures/happy-path.live.json b/examples/http/basic/api_wrapper/fixtures/happy-path.live.json new file mode 100644 index 0000000..d0a278e --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/happy-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 401, + "body": "" + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/method-not-allowed.live.json b/examples/http/basic/api_wrapper/fixtures/method-not-allowed.live.json new file mode 100644 index 0000000..4e0bb73 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/method-not-allowed.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 405, + "headers": { + "allow": "GET, HEAD" + }, + "body": "This method is not allowed\n" + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/method-not-allowed.test.json b/examples/http/basic/api_wrapper/fixtures/method-not-allowed.test.json new file mode 100644 index 0000000..28dd225 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/method-not-allowed.test.json @@ -0,0 +1,14 @@ +{ + "appType": "http-wasm", + "description": "POST method — expects 405 with Allow header and body", + "request": { + "method": "POST", + "path": "/", + "headers": {}, + "body": "" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/missing-password-env.live.json b/examples/http/basic/api_wrapper/fixtures/missing-password-env.live.json new file mode 100644 index 0000000..14cfcc5 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/missing-password-env.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "Misconfigured app\n" + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/missing-password-env.test.json b/examples/http/basic/api_wrapper/fixtures/missing-password-env.test.json new file mode 100644 index 0000000..2571658 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/missing-password-env.test.json @@ -0,0 +1,11 @@ +{ + "appType": "http-wasm", + "description": "Missing PASSWORD env var — expects 500 Misconfigured app", + "request": { + "method": "GET", + "path": "/", + "headers": { + "authorization": "any-password" + } + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/no-auth-header.live.json b/examples/http/basic/api_wrapper/fixtures/no-auth-header.live.json new file mode 100644 index 0000000..8d60115 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/no-auth-header.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 403, + "body": "No auth header\n" + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/no-auth-header.test.json b/examples/http/basic/api_wrapper/fixtures/no-auth-header.test.json new file mode 100644 index 0000000..2ffdb6b --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/no-auth-header.test.json @@ -0,0 +1,13 @@ +{ + "appType": "http-wasm", + "description": "Missing Authorization header — expects 403 with 'No auth header' body", + "request": { + "method": "GET", + "path": "/", + "headers": {} + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/wrong-password.live.json b/examples/http/basic/api_wrapper/fixtures/wrong-password.live.json new file mode 100644 index 0000000..0a9fd64 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/wrong-password.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 403, + "body": "" + } +} diff --git a/examples/http/basic/api_wrapper/fixtures/wrong-password.test.json b/examples/http/basic/api_wrapper/fixtures/wrong-password.test.json new file mode 100644 index 0000000..9b23f56 --- /dev/null +++ b/examples/http/basic/api_wrapper/fixtures/wrong-password.test.json @@ -0,0 +1,15 @@ +{ + "appType": "http-wasm", + "description": "Wrong Authorization header value — expects 403 with empty body", + "request": { + "method": "GET", + "path": "/", + "headers": { + "authorization": "wrong-password" + } + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/basic/api_wrapper/src/lib.rs b/examples/http/basic/api_wrapper/src/lib.rs index b6e15d7..80556e4 100644 --- a/examples/http/basic/api_wrapper/src/lib.rs +++ b/examples/http/basic/api_wrapper/src/lib.rs @@ -169,7 +169,13 @@ fn send_device_command(token: &str, device: &str, command: &str) -> Result) -> Result, StatusCode> { + request_inner(req, 0) +} + +fn request_inner(req: Request, depth: u8) -> Result, StatusCode> { let rsp = match fastedge::send_request(req) { Err(error) => { let status_code = match error { @@ -185,7 +191,7 @@ fn request(req: Request) -> Result, StatusCode> { }; let status = rsp.status(); - if is_redirect(status) { + if is_redirect(status) && depth < MAX_REDIRECTS { if let Some(location) = rsp.headers().get(header::LOCATION) { let new_url = Url::parse( location @@ -207,7 +213,7 @@ fn request(req: Request) -> Result, StatusCode> { .body(Body::empty()) .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - return request(sub_req); + return request_inner(sub_req, depth + 1); } } if status == StatusCode::OK { @@ -217,7 +223,7 @@ fn request(req: Request) -> Result, StatusCode> { Err(status) } -// List of acceptible 300-series redirect codes. +// List of acceptable 300-series redirect codes. const REDIRECT_CODES: &[StatusCode] = &[ StatusCode::MOVED_PERMANENTLY, StatusCode::FOUND, diff --git a/examples/http/basic/backend/Cargo.lock b/examples/http/basic/backend/Cargo.lock new file mode 100644 index 0000000..6e43284 --- /dev/null +++ b/examples/http/basic/backend/Cargo.lock @@ -0,0 +1,515 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "querystring", + "urlencoding", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "querystring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/basic/backend/README.md b/examples/http/basic/backend/README.md index 05ee3cf..42019f4 100644 --- a/examples/http/basic/backend/README.md +++ b/examples/http/basic/backend/README.md @@ -1,11 +1,45 @@ [← Back to examples](../../../README.md) -# Backend +# Backend (URL Proxy) -API wrapper that authenticates requests and proxies calls to a SmartThings backend to get device status and toggle it. +A FastEdge application that accepts a `?url=` query parameter, makes an outbound GET request to that URL via `fastedge::send_request`, and returns a summary of the upstream response (`len` and `content-type`) in the response body. -## Configuration +> **When to use this example:** When you want to see how to make outbound HTTP requests from a FastEdge edge function using the legacy sync handler (`#[fastedge::http]`). For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). -- Environment variable: `PASSWORD` — expected password for request authentication -- Environment variable: `DEVICE` — SmartThings device ID -- Environment variable: `TOKEN` — SmartThings API token +## What it does + +1. Parses the `?url=` query parameter from the request URI (percent-decodes it via `urlencoding::decode`). +2. Builds an outbound `GET` request to that URL using `fastedge::send_request`. +3. Returns HTTP 200 with a plain-text body: + ``` + len = , content-type = + ``` +4. Returns HTTP 500 with an error message if `?url=` is absent or the query string is missing. + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::send_request(request)` | Blocking outbound HTTP request | +| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | +| `fastedge::body::Body` | Request and response bodies | +| `querystring::querify` | Parse query string into key-value pairs | +| `urlencoding::decode` | Percent-decode the `?url=` value | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/backend.wasm +``` + +## Expected behavior + +| Request | Response status | Response body | +|---|---|---| +| `GET /?url=https%3A%2F%2Fhttpbin.org%2Fget` | 200 | `len = , content-type = Some("")` | +| `GET /?q=hello` (no `url` key) | 500 | `missing url parameter` | +| `GET /` (no query string) | 500 | `missing uri query parameter` | + +The `len` value is the byte length of the upstream response body. The `content-type` value is the `Content-Type` header returned by the upstream server, formatted as a Rust `Option` debug string (e.g. `Some("application/json")`). diff --git a/examples/http/basic/backend/fixtures/happy-path-url-proxy.live.json b/examples/http/basic/backend/fixtures/happy-path-url-proxy.live.json new file mode 100644 index 0000000..c76d8b2 --- /dev/null +++ b/examples/http/basic/backend/fixtures/happy-path-url-proxy.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "len = " + } +} diff --git a/examples/http/basic/backend/fixtures/happy-path-url-proxy.test.json b/examples/http/basic/backend/fixtures/happy-path-url-proxy.test.json new file mode 100644 index 0000000..ece8c03 --- /dev/null +++ b/examples/http/basic/backend/fixtures/happy-path-url-proxy.test.json @@ -0,0 +1,10 @@ +{ + "appType": "http-wasm", + "description": "Proxies ?url= to an upstream and returns len + content-type", + "request": { + "method": "GET", + "url": "/?url=https%3A%2F%2Fhttpbin.org%2Fget", + "headers": {}, + "body": "" + } +} diff --git a/examples/http/basic/backend/fixtures/missing-query-string.live.json b/examples/http/basic/backend/fixtures/missing-query-string.live.json new file mode 100644 index 0000000..76056bf --- /dev/null +++ b/examples/http/basic/backend/fixtures/missing-query-string.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "bodyContains": "missing uri query parameter" + } +} diff --git a/examples/http/basic/backend/fixtures/missing-query-string.test.json b/examples/http/basic/backend/fixtures/missing-query-string.test.json new file mode 100644 index 0000000..0d6d98c --- /dev/null +++ b/examples/http/basic/backend/fixtures/missing-query-string.test.json @@ -0,0 +1,10 @@ +{ + "appType": "http-wasm", + "description": "Returns 500 when no query string is present at all", + "request": { + "method": "GET", + "url": "/", + "headers": {}, + "body": "" + } +} diff --git a/examples/http/basic/backend/fixtures/missing-url-param.live.json b/examples/http/basic/backend/fixtures/missing-url-param.live.json new file mode 100644 index 0000000..6d94f0b --- /dev/null +++ b/examples/http/basic/backend/fixtures/missing-url-param.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "bodyContains": "missing url parameter" + } +} diff --git a/examples/http/basic/backend/fixtures/missing-url-param.test.json b/examples/http/basic/backend/fixtures/missing-url-param.test.json new file mode 100644 index 0000000..9776b6a --- /dev/null +++ b/examples/http/basic/backend/fixtures/missing-url-param.test.json @@ -0,0 +1,10 @@ +{ + "appType": "http-wasm", + "description": "Returns 500 when ?url= query parameter is absent", + "request": { + "method": "GET", + "url": "/?q=hello", + "headers": {}, + "body": "" + } +} diff --git a/examples/http/basic/cache/README.md b/examples/http/basic/cache/README.md new file mode 100644 index 0000000..8a32c4a --- /dev/null +++ b/examples/http/basic/cache/README.md @@ -0,0 +1,42 @@ +[← Back to examples](../../../README.md) + +# Cache (Basic) + +Demonstrates the cache-aside pattern using `fastedge::cache` — store a generated response body with a TTL and serve it directly on subsequent requests without re-computing it. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `CACHE_TTL_MS` | No | How long to cache each response in milliseconds. Default: `30000` (30 s). | + +## How it works + +``` +GET /api/data → cache miss → generate body → store in cache → 200 (x-cache: miss) +GET /api/data → cache hit → return cached body → 200 (x-cache: hit) +``` + +The cache key is `page:`. Each unique path gets its own cache entry. The response body is a simple HTML page that includes the request path — stand in for any expensive computation or template render. + +## What it returns + +``` +HTTP/1.1 200 OK +content-type: text/html +x-cache: hit | miss + +... +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache_basic.wasm +``` + +## APIs used + +- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` +- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry diff --git a/examples/http/basic/hello_world/README.md b/examples/http/basic/hello_world/README.md index 54625b8..d67be2a 100644 --- a/examples/http/basic/hello_world/README.md +++ b/examples/http/basic/hello_world/README.md @@ -1,5 +1,42 @@ [← Back to examples](../../../README.md) -# Hello World +# Hello World (Basic HTTP) -The simplest possible FastEdge application — returns a greeting with the request URL in the response body. +The simplest possible FastEdge application using the **legacy sync handler** (`#[fastedge::http]`). Returns a greeting with the full request URI in the response body. + +> **When to use this example:** If you need a synchronous, single-function HTTP handler targeting `wasm32-wasip1`. For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). + +## What it does + +Handles any GET request and returns a plain-text body containing the request URI: + +``` +Hello, you made a basic request to /your/path?query=params +``` + +The handler uses `req.uri().to_string()` to include the path and query string in the response. + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Response body | +| `Response::builder()` | Fluent response construction | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/hello_world.wasm +``` + +## Expected behavior + +| Request | Response status | Response body | +|---|---|---| +| `GET /api/hello/world?name=FastEdge` | 200 | `Hello, you made a basic request to /api/hello/world?name=FastEdge` | +| `GET /` | 200 | `Hello, you made a basic request to /` | + +Response always includes `content-type: text/plain;charset=UTF-8`. diff --git a/examples/http/basic/hello_world/fixtures/path-and-params.live.json b/examples/http/basic/hello_world/fixtures/path-and-params.live.json new file mode 100644 index 0000000..8acf996 --- /dev/null +++ b/examples/http/basic/hello_world/fixtures/path-and-params.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "text/plain;charset=UTF-8" + }, + "bodyContains": "Hello, you made a basic request to /api/hello/world?name=FastEdge&lang=Rust" + } +} diff --git a/examples/http/basic/markdown_render/Cargo.lock b/examples/http/basic/markdown_render/Cargo.lock new file mode 100644 index 0000000..f2f2fde --- /dev/null +++ b/examples/http/basic/markdown_render/Cargo.lock @@ -0,0 +1,821 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "markdown_render" +version = "0.1.0" +dependencies = [ + "fastedge", + "mime", + "pulldown-cmark", + "url", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/basic/markdown_render/README.md b/examples/http/basic/markdown_render/README.md index 1528089..0acb843 100644 --- a/examples/http/basic/markdown_render/README.md +++ b/examples/http/basic/markdown_render/README.md @@ -2,9 +2,64 @@ # Markdown Render -Fetches Markdown files from a configurable base URL and renders them to HTML with optional custom `` content. +Demonstrates outbound HTTP fetch and content transformation: fetches a file from a configurable origin URL and renders it as an HTML page using [`pulldown-cmark`](https://github.com/raphlinus/pulldown-cmark). Uses the legacy synchronous `#[fastedge::http]` handler (`wasm32-wasip1`). For new apps, prefer the async [`#[wstd::http_server]`](../../wasi/) handler. + +## What it teaches + +- Making outbound HTTP requests from a FastEdge app with `fastedge::send_request` +- Following HTTP redirects manually (301, 302, 303, 307, 308) +- Transforming response bodies (Markdown → HTML) +- Reading environment variables at request time +- Method guard (405 for non-GET/HEAD) ## Configuration -- Environment variable: `BASE` — base URL to fetch Markdown files from -- Environment variable: `HEAD` — optional custom HTML to inject into the `` element +| Variable | Required | Description | +|---|---|---| +| `BASE` | Yes | Origin base URL. The request path is appended: `BASE + path` | +| `HEAD` | No | Optional HTML injected into `` (e.g. a `` stylesheet tag) | + +## Build + +```sh +# From this directory: +cargo build --release + +# WASM output: +# target/wasm32-wasip1/release/markdown_render.wasm +``` + +## Expected behaviour + +| Condition | Response | +|---|---| +| `BASE` env var not set | 500 `Misconfigured app\n` | +| Method is not GET or HEAD | 405 `This method is not allowed\n`, `Allow: GET, HEAD` | +| Path is empty or `/` | 400 `Missing file path\n` | +| Origin returns non-200 | Forwards the upstream status, empty body | +| Origin returns a redirect | Follows the redirect (up to one level) and retries | +| Normal request | 200 HTML page with rendered Markdown, `Content-Type: text/html` | + +## Example + +```sh +# Set BASE to a server that serves Markdown files +BASE=https://raw.githubusercontent.com/example/repo/main + +# Request /README.md -> fetches BASE/README.md -> renders as HTML +curl http://localhost:/README.md +# Returns:

...

... + +# With a custom stylesheet injected into +HEAD='' +``` + +## APIs used + +| API | Purpose | +|---|---| +| `fastedge::send_request(req)` | Outbound HTTP request to origin | +| `pulldown_cmark::Parser::new_ext` | Parse Markdown with tables and footnotes | +| `pulldown_cmark::html::push_html` | Render Markdown AST to HTML string | +| `std::env::var("BASE")` | Read required env var at request time | +| `url::Url::parse` | Validate redirect `Location` header | diff --git a/examples/http/basic/markdown_render/fixtures/.env b/examples/http/basic/markdown_render/fixtures/.env new file mode 100644 index 0000000..e0b598f --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/.env @@ -0,0 +1 @@ +BASE=https://example.com diff --git a/examples/http/basic/markdown_render/fixtures/method-not-allowed.live.json b/examples/http/basic/markdown_render/fixtures/method-not-allowed.live.json new file mode 100644 index 0000000..4e0bb73 --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/method-not-allowed.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 405, + "headers": { + "allow": "GET, HEAD" + }, + "body": "This method is not allowed\n" + } +} diff --git a/examples/http/basic/markdown_render/fixtures/method-not-allowed.test.json b/examples/http/basic/markdown_render/fixtures/method-not-allowed.test.json new file mode 100644 index 0000000..59a1ed1 --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/method-not-allowed.test.json @@ -0,0 +1,9 @@ +{ + "appType": "http-wasm", + "description": "Non-GET/HEAD method → 405 with Allow header", + "request": { + "method": "POST", + "url": "/readme.md", + "headers": {} + } +} diff --git a/examples/http/basic/markdown_render/fixtures/missing-base-env.live.json b/examples/http/basic/markdown_render/fixtures/missing-base-env.live.json new file mode 100644 index 0000000..14cfcc5 --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/missing-base-env.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "Misconfigured app\n" + } +} diff --git a/examples/http/basic/markdown_render/fixtures/missing-base-env.test.json b/examples/http/basic/markdown_render/fixtures/missing-base-env.test.json new file mode 100644 index 0000000..c130201 --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/missing-base-env.test.json @@ -0,0 +1,9 @@ +{ + "appType": "http-wasm", + "description": "Missing BASE env var → 500 Misconfigured app", + "request": { + "method": "GET", + "url": "/readme.md", + "headers": {} + } +} diff --git a/examples/http/basic/markdown_render/fixtures/missing-path.live.json b/examples/http/basic/markdown_render/fixtures/missing-path.live.json new file mode 100644 index 0000000..eced3ea --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/missing-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 400, + "body": "Missing file path\n" + } +} diff --git a/examples/http/basic/markdown_render/fixtures/missing-path.test.json b/examples/http/basic/markdown_render/fixtures/missing-path.test.json new file mode 100644 index 0000000..c840060 --- /dev/null +++ b/examples/http/basic/markdown_render/fixtures/missing-path.test.json @@ -0,0 +1,13 @@ +{ + "appType": "http-wasm", + "description": "Empty path (root '/') with BASE set → 400 Missing file path", + "dotenv": { + "enabled": true, + "path": "." + }, + "request": { + "method": "GET", + "url": "/", + "headers": {} + } +} diff --git a/examples/http/basic/markdown_render/src/lib.rs b/examples/http/basic/markdown_render/src/lib.rs index 4a4eced..0d3e838 100644 --- a/examples/http/basic/markdown_render/src/lib.rs +++ b/examples/http/basic/markdown_render/src/lib.rs @@ -87,7 +87,13 @@ fn main(req: Request) -> Result, Error> { Ok(rsp) } +const MAX_REDIRECTS: u8 = 5; + fn request(req: Request) -> Result, StatusCode> { + request_inner(req, 0) +} + +fn request_inner(req: Request, depth: u8) -> Result, StatusCode> { let rsp = match fastedge::send_request(req) { Err(error) => { let status_code = match error { @@ -103,7 +109,7 @@ fn request(req: Request) -> Result, StatusCode> { }; let status = rsp.status(); - if is_redirect(status) { + if is_redirect(status) && depth < MAX_REDIRECTS { if let Some(location) = rsp.headers().get(header::LOCATION) { let new_url = Url::parse( location @@ -118,7 +124,7 @@ fn request(req: Request) -> Result, StatusCode> { .body(Body::empty()) .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - return request(sub_req); + return request_inner(sub_req, depth + 1); } } if status == StatusCode::OK { @@ -128,7 +134,7 @@ fn request(req: Request) -> Result, StatusCode> { Err(status) } -// List of acceptible 300-series redirect codes. +// List of acceptable 300-series redirect codes. const REDIRECT_CODES: &[StatusCode] = &[ StatusCode::MOVED_PERMANENTLY, StatusCode::FOUND, diff --git a/examples/http/basic/outbound_fetch/README.md b/examples/http/basic/outbound_fetch/README.md index 4bf5c8c..7be8aaf 100644 --- a/examples/http/basic/outbound_fetch/README.md +++ b/examples/http/basic/outbound_fetch/README.md @@ -1,5 +1,50 @@ [← Back to examples](../../../README.md) -# Outbound Fetch +# Outbound Fetch (Basic HTTP) -Fetches data from a remote JSON API (jsonplaceholder) and returns the first 5 users in a transformed JSON response. +Demonstrates outbound HTTP from a FastEdge application using the **legacy sync handler** (`#[fastedge::http]`). Fetches user data from the [JSONPlaceholder](https://jsonplaceholder.typicode.com) public API, selects the first 5 users, and returns them in a paginated JSON envelope. + +> **When to use this example:** If you need a synchronous, single-function HTTP handler with outbound requests targeting `wasm32-wasip1`. For new apps, prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +On any incoming request: + +1. Makes a GET request to `http://jsonplaceholder.typicode.com/users` using `fastedge::send_request`. +2. Parses the JSON response body. +3. Takes the first 5 users from the array. +4. Returns a JSON envelope with pagination metadata. + +Example response body: + +```json +{ + "users": [ { "id": 1, "name": "Leanne Graham", ... }, ... ], + "total": 5, + "skip": 0, + "limit": 30 +} +``` + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::send_request` | Outbound HTTP request to upstream API | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Request and response bodies | +| `serde_json` | JSON parsing and serialisation | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/outbound_fetch.wasm +``` + +## Expected behavior + +| Request | Response status | Response content-type | Response body | +|---|---|---|---| +| `GET /` | 200 | `application/json` | JSON object with `users` (array of ≤5), `total`, `skip`, `limit` | diff --git a/examples/http/basic/outbound_fetch/fixtures/happy-path.live.json b/examples/http/basic/outbound_fetch/fixtures/happy-path.live.json new file mode 100644 index 0000000..bdedfea --- /dev/null +++ b/examples/http/basic/outbound_fetch/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "application/json" + }, + "bodyContains": ["\"users\"", "\"total\"", "\"skip\"", "\"limit\""] + } +} diff --git a/examples/http/basic/outbound_fetch/fixtures/happy-path.test.json b/examples/http/basic/outbound_fetch/fixtures/happy-path.test.json new file mode 100644 index 0000000..9372344 --- /dev/null +++ b/examples/http/basic/outbound_fetch/fixtures/happy-path.test.json @@ -0,0 +1,9 @@ +{ + "appType": "http-wasm", + "description": "Fetches users from jsonplaceholder, returns first 5 wrapped in pagination object", + "request": { + "method": "GET", + "url": "/", + "headers": {} + } +} diff --git a/examples/http/basic/print/Cargo.lock b/examples/http/basic/print/Cargo.lock new file mode 100644 index 0000000..12470e8 --- /dev/null +++ b/examples/http/basic/print/Cargo.lock @@ -0,0 +1,501 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "print" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/basic/print/README.md b/examples/http/basic/print/README.md index 0700474..13981d5 100644 --- a/examples/http/basic/print/README.md +++ b/examples/http/basic/print/README.md @@ -2,4 +2,50 @@ # Print -Prints the request method, URL, and all headers to the response body. Useful for debugging and inspecting incoming requests. +Echoes the incoming request's method, URL, and all headers back in the response body as plain text. Useful for debugging and inspecting what a FastEdge app receives from clients and the platform. + +> **Note:** This example uses the legacy `#[fastedge::http]` sync handler (`wasm32-wasip1`). For new apps, prefer `#[wstd::http_server]` (async, `wasm32-wasip2`) — see [`examples/http/wasi/`](../../wasi/). + +## What it demonstrates + +- Reading request method via `req.method().as_str()` +- Reading the request URI via `req.uri().to_string()` +- Iterating all request headers via `req.headers()` +- Handling non-UTF-8 header values gracefully with a `match` on `v.to_str()` +- Building a plain-text response with `Response::builder()` and `Body::from(...)` + +## APIs used + +| API | Purpose | +|-----|---------| +| `req.method().as_str()` | HTTP method as a string slice | +| `req.uri().to_string()` | Full request URI as a `String` | +| `req.headers()` | Iterator over `(HeaderName, HeaderValue)` pairs | +| `v.to_str()` | Decode a header value to `&str` (returns `Err` for non-UTF-8) | +| `Response::builder().status(...).body(...)` | Build the HTTP response | +| `Body::from(string)` | Create a response body from a `String` | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/print.wasm +``` + +## Expected behaviour + +For any request, the response body is a plain-text dump of the request details: + +``` +Method: GET +URL: /some/path?query=value +Headers: + host: example.com + accept: */* + ... +``` + +- Status: `200 OK` +- Content: plain text (no `content-type` header is set explicitly; the platform may add one) +- Each header appears on its own line, indented with four spaces +- Non-UTF-8 header values are replaced with `not a valid text` diff --git a/examples/http/basic/print/fixtures/get-request.live.json b/examples/http/basic/print/fixtures/get-request.live.json new file mode 100644 index 0000000..7b39e78 --- /dev/null +++ b/examples/http/basic/print/fixtures/get-request.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "Method: GET\nURL: /test\nHeaders:" + } +} diff --git a/examples/http/basic/print/fixtures/get-request.test.json b/examples/http/basic/print/fixtures/get-request.test.json new file mode 100644 index 0000000..af9ccff --- /dev/null +++ b/examples/http/basic/print/fixtures/get-request.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET request - expects method, URL, and headers echoed in response body", + "request": { + "method": "GET", + "path": "/test" + } +} diff --git a/examples/http/basic/print/fixtures/post-with-path.live.json b/examples/http/basic/print/fixtures/post-with-path.live.json new file mode 100644 index 0000000..515cd6b --- /dev/null +++ b/examples/http/basic/print/fixtures/post-with-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "Method: POST\nURL: /api/inspect\nHeaders:" + } +} diff --git a/examples/http/basic/print/fixtures/post-with-path.test.json b/examples/http/basic/print/fixtures/post-with-path.test.json new file mode 100644 index 0000000..49ce609 --- /dev/null +++ b/examples/http/basic/print/fixtures/post-with-path.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "POST request with path - expects method POST and URL path echoed in response body", + "request": { + "method": "POST", + "path": "/api/inspect" + } +} diff --git a/examples/http/basic/s3upload/README.md b/examples/http/basic/s3upload/README.md index 891bfdb..3de9c36 100644 --- a/examples/http/basic/s3upload/README.md +++ b/examples/http/basic/s3upload/README.md @@ -2,25 +2,54 @@ # S3 Upload -Uploads files to an S3-compatible bucket via POST/PUT requests, generating signed URLs for the upload. +FastEdge edge function that accepts a file upload, signs an S3 PUT request on the fly, uploads the file directly to an S3-compatible bucket, and returns the clean object URL to the caller. + +> **Legacy handler:** Uses `#[fastedge::http]` (sync, `wasm32-wasip1`). For new apps prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +1. Accepts `POST` or `PUT` only — returns 405 for other methods +2. Requires `?name=` query parameter and a non-empty body — returns 400 otherwise +3. Enforces `MAX_FILE_SIZE` if set — returns 413 if exceeded +4. Calls `prepare_s3()` to build a 1-hour presigned `PUT` URL using `rusty_s3` +5. Forwards the file body to S3 via `fastedge::send_request` +6. On success (S3 returns 200): responds with the clean object URL (no query string) +7. On S3 error: forwards the S3 status and error body back to the caller ## Configuration -- Environment variable: `ACCESS_KEY` — S3 access key -- Environment variable: `SECRET_KEY` — S3 secret key -- Environment variable: `REGION` — S3 region (e.g. `s-ed1`) -- Environment variable: `BASE_HOSTNAME` — S3 base hostname (e.g. `cloud.gcore.lu`) -- Environment variable: `BUCKET` — S3 bucket name -- Environment variable: `SCHEME` — (optional) URL scheme, defaults to `http` -- Environment variable: `MAX_FILE_SIZE` — (optional) maximum upload size in bytes +| Env var | Required | Description | +|---|---|---| +| `ACCESS_KEY` | ✅ | S3 access key | +| `SECRET_KEY` | ✅ | S3 secret key | +| `REGION` | ✅ | S3 region (e.g. `s-ed1`) | +| `BASE_HOSTNAME` | ✅ | S3 base hostname (e.g. `cloud.gcore.lu`) | +| `BUCKET` | ✅ | S3 bucket name | +| `SCHEME` | optional | URL scheme — defaults to `http` | +| `MAX_FILE_SIZE` | optional | Maximum upload size in bytes — no limit if unset | -## Usage +The constructed endpoint is `://.//`. -Send the file content as the request body using POST or PUT. Specify the filename via the `name` query parameter: +## Build +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/s3upload.wasm ``` -PUT /upload?name=photo.jpg + +## Usage + +``` +POST /upload?name=photo.jpg Content-Type: image/jpeg ``` + +On success (200), the response body is the clean S3 object URL (presign query parameters stripped). + +## Notes + +- The `OPTIONS` method returns 204 but does **not** include CORS headers — add `Access-Control-Allow-*` headers if browser preflight support is needed. +- The presigned URL expires after 1 hour, but since the upload is performed server-side this has no practical impact. +- `MAX_FILE_SIZE` is silently ignored if set to a non-numeric value. diff --git a/examples/http/basic/s3upload/src/lib.rs b/examples/http/basic/s3upload/src/lib.rs index ab1eced..fef3f43 100644 --- a/examples/http/basic/s3upload/src/lib.rs +++ b/examples/http/basic/s3upload/src/lib.rs @@ -57,8 +57,8 @@ fn main(req: Request) -> Result, Error> { .body(Body::from("Malformed request\n")); } let content_type = match req.headers().get("Content-Type") { - None => "application/octet-stream", // default MIME type - Some(v) => v.to_str().unwrap(), + None => "application/octet-stream", + Some(v) => v.to_str().unwrap_or("application/octet-stream"), }; let content_type = content_type.to_owned(); let content = req.into_body(); diff --git a/examples/http/basic/secret/README.md b/examples/http/basic/secret/README.md index 971d198..83677a9 100644 --- a/examples/http/basic/secret/README.md +++ b/examples/http/basic/secret/README.md @@ -2,4 +2,69 @@ # Secret -Retrieves a secret value using both `secret::get()` and `secret::get_effective_at()`, demonstrating timestamp-based secret rotation support. +Demonstrates accessing encrypted secrets injected by the FastEdge platform using `secret::get()` and `secret::get_effective_at()`. Shows how to handle all error variants (access denied, decrypt error) and the time-based secret rotation API. + +## What it does + +On every request, the handler: + +1. Calls `secret::get("SECRET")` — retrieves the current value of the secret named `SECRET`. +2. If the secret is missing (returned as `None`), returns **404**. +3. Calls `secret::get_effective_at("SECRET", )` — retrieves the secret value effective at the current Unix timestamp, demonstrating the rotation/versioning API. +4. If that value is also missing, returns **404**. +5. On success, returns **200** with both values in the body (Debug format). + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::secret::get(key)` | Retrieve the current value of a named secret | +| `fastedge::secret::get_effective_at(key, timestamp)` | Retrieve the secret value effective at a specific Unix timestamp | +| `fastedge::secret::Error` | Error variants: `AccessDenied`, `Other(msg)`, `DecryptError` | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Response body | + +## Secret error variants + +| Variant | HTTP response | Meaning | +|---|---|---| +| `Ok(Some(value))` | 200 with body | Secret found | +| `Ok(None)` | 404 empty | Secret name is valid but not set | +| `Err(AccessDenied)` | 403 empty | App is not permitted to read this secret | +| `Err(Other(msg))` | 403 with `msg` body | Other denial with a human-readable message | +| `Err(DecryptError)` | 500 empty | Secret exists but could not be decrypted | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/secret.wasm +``` + +## Expected behavior + +| Scenario | Secret `SECRET` | Response status | Response body | +|---|---|---|---| +| Happy path | `"my-value"` | 200 | `get=Some("my-value")\nget_efective_at=Some("my-value")\n` | +| Secret not set | (absent) | 404 | (empty) | +| Access denied | — | 403 | (empty) | + +> **Note:** The response body contains a typo in the field name (`get_efective_at` instead of `get_effective_at`). This is a known cosmetic issue in the source. + +## Local testing + +Inject the secret via a `.env` file in your fixtures directory using the `FASTEDGE_VAR_SECRET_` prefix: + +``` +# fixtures/.env +FASTEDGE_VAR_SECRET_SECRET=my-test-value +``` + +Run with the fixture validator: + +```sh +node tools/fixture-validator/index.mjs \ + FastEdge-sdk-rust/examples/http/basic/secret/ \ + --wasm FastEdge-sdk-rust/examples/http/basic/secret/target/wasm32-wasip1/release/secret.wasm +``` diff --git a/examples/http/basic/secret/fixtures/.env b/examples/http/basic/secret/fixtures/.env new file mode 100644 index 0000000..fc181ee --- /dev/null +++ b/examples/http/basic/secret/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_SECRET_SECRET=test-secret-value diff --git a/examples/http/basic/secret/fixtures/happy-path.live.json b/examples/http/basic/secret/fixtures/happy-path.live.json new file mode 100644 index 0000000..2279528 --- /dev/null +++ b/examples/http/basic/secret/fixtures/happy-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "get=Some(\"test-secret-value\")" + } +} diff --git a/examples/http/basic/secret/fixtures/happy-path.test.json b/examples/http/basic/secret/fixtures/happy-path.test.json new file mode 100644 index 0000000..d955003 --- /dev/null +++ b/examples/http/basic/secret/fixtures/happy-path.test.json @@ -0,0 +1,12 @@ +{ + "appType": "http-wasm", + "description": "Happy path: secret exists — expects both get() and get_effective_at() to return the secret value", + "request": { + "method": "GET", + "path": "/" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/basic/secret/fixtures/missing-secret.live.json b/examples/http/basic/secret/fixtures/missing-secret.live.json new file mode 100644 index 0000000..830ea9e --- /dev/null +++ b/examples/http/basic/secret/fixtures/missing-secret.live.json @@ -0,0 +1,5 @@ +{ + "expected": { + "status": 404 + } +} diff --git a/examples/http/basic/secret/fixtures/missing-secret.test.json b/examples/http/basic/secret/fixtures/missing-secret.test.json new file mode 100644 index 0000000..94e4ecf --- /dev/null +++ b/examples/http/basic/secret/fixtures/missing-secret.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "Missing secret: no secret injected — expects 404 Not Found", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/basic/smart_switch/Cargo.lock b/examples/http/basic/smart_switch/Cargo.lock new file mode 100644 index 0000000..203cdc7 --- /dev/null +++ b/examples/http/basic/smart_switch/Cargo.lock @@ -0,0 +1,781 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smart_switch" +version = "0.1.0" +dependencies = [ + "fastedge", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/basic/smart_switch/README.md b/examples/http/basic/smart_switch/README.md index c9ab717..ef2ef88 100644 --- a/examples/http/basic/smart_switch/README.md +++ b/examples/http/basic/smart_switch/README.md @@ -1,18 +1,54 @@ [← Back to examples](../../../README.md) -# Smart Switch +# Smart Switch (Basic HTTP) -Toggles a SmartThings smart outlet on/off by wrapping multiple API calls into a single edge application. +Demonstrates **outbound HTTP composition** on the edge: a single GET request to this app triggers two SmartThings API calls (status fetch + toggle command) before returning a result. + +Uses the **legacy sync handler** (`#[fastedge::http]`, `wasm32-wasip1`). For new apps prefer the async WASI handler. + +> **See also:** [`api_wrapper`](../api_wrapper/README.md) — an earlier version of the same pattern. Both examples implement identical logic; `smart_switch` is the standalone crate extracted for clarity. + +## What it does + +1. Rejects non-GET/HEAD requests with `405 Method Not Allowed`. +2. Requires an `Authorization` header matching the `PASSWORD` env var — returns `403` if missing or wrong. +3. Calls the SmartThings API to fetch the current switch state (`on`/`off`). +4. Sends a toggle command (`on`→`off` or `off`→`on`). +5. Returns `204 No Content` on success, or forwards the API error status. + +Handles HTTP redirects from the SmartThings API automatically. ## Configuration -- Environment variable: `PASSWORD` — password for simple authentication -- Environment variable: `DEVICE` — SmartThings device ID -- Environment variable: `TOKEN` — SmartThings API token +| Env var | Purpose | +|---|---| +| `PASSWORD` | Password checked against the `Authorization` request header | +| `DEVICE` | SmartThings device ID | +| `TOKEN` | SmartThings API bearer token | + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::send_request(req)` | Outbound HTTP call to the SmartThings API | +| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | +| `std::env::var()` | Read `PASSWORD`, `DEVICE`, `TOKEN` at request time | +| `serde_json` | Parse SmartThings JSON responses | +| `url::Url` | Parse and follow redirect `Location` headers | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/smart_switch.wasm +``` -## How it works +## Expected behavior -1. Validates the `Authorization` header against the `PASSWORD` env var -2. Queries the SmartThings API for current device status -3. Sends a command to toggle the switch (on→off or off→on) -4. Handles API redirects automatically +| Request | `Authorization` header | Result | +|---|---|---| +| `POST /` (any non-GET/HEAD) | any | `405` — `Allow: GET, HEAD` | +| `GET /` | absent | `403` — `No auth header` | +| `GET /` | wrong value | `403` — empty body | +| `GET /` | correct password | `204` on success; `5xx` if SmartThings API unavailable | diff --git a/examples/http/basic/smart_switch/fixtures/.env b/examples/http/basic/smart_switch/fixtures/.env new file mode 100644 index 0000000..3f587b2 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/.env @@ -0,0 +1 @@ +PASSWORD=test-password diff --git a/examples/http/basic/smart_switch/fixtures/01-method-not-allowed.live.json b/examples/http/basic/smart_switch/fixtures/01-method-not-allowed.live.json new file mode 100644 index 0000000..4e0bb73 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/01-method-not-allowed.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 405, + "headers": { + "allow": "GET, HEAD" + }, + "body": "This method is not allowed\n" + } +} diff --git a/examples/http/basic/smart_switch/fixtures/01-method-not-allowed.test.json b/examples/http/basic/smart_switch/fixtures/01-method-not-allowed.test.json new file mode 100644 index 0000000..e767659 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/01-method-not-allowed.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "POST request → 405 Method Not Allowed before auth check", + "request": { + "method": "POST", + "path": "/" + } +} diff --git a/examples/http/basic/smart_switch/fixtures/02-missing-password-env.live.json b/examples/http/basic/smart_switch/fixtures/02-missing-password-env.live.json new file mode 100644 index 0000000..14cfcc5 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/02-missing-password-env.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "Misconfigured app\n" + } +} diff --git a/examples/http/basic/smart_switch/fixtures/02-missing-password-env.test.json b/examples/http/basic/smart_switch/fixtures/02-missing-password-env.test.json new file mode 100644 index 0000000..693b4b4 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/02-missing-password-env.test.json @@ -0,0 +1,11 @@ +{ + "appType": "http-wasm", + "description": "GET with no env vars set → 500 Misconfigured app (PASSWORD missing)", + "request": { + "method": "GET", + "path": "/", + "headers": { + "authorization": "any-value" + } + } +} diff --git a/examples/http/basic/smart_switch/fixtures/03-no-auth-header.live.json b/examples/http/basic/smart_switch/fixtures/03-no-auth-header.live.json new file mode 100644 index 0000000..8d60115 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/03-no-auth-header.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 403, + "body": "No auth header\n" + } +} diff --git a/examples/http/basic/smart_switch/fixtures/03-no-auth-header.test.json b/examples/http/basic/smart_switch/fixtures/03-no-auth-header.test.json new file mode 100644 index 0000000..6cba9e3 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/03-no-auth-header.test.json @@ -0,0 +1,12 @@ +{ + "appType": "http-wasm", + "description": "GET with PASSWORD env set but no Authorization header → 403 Forbidden", + "request": { + "method": "GET", + "path": "/" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/basic/smart_switch/fixtures/04-wrong-password.live.json b/examples/http/basic/smart_switch/fixtures/04-wrong-password.live.json new file mode 100644 index 0000000..0a9fd64 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/04-wrong-password.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 403, + "body": "" + } +} diff --git a/examples/http/basic/smart_switch/fixtures/04-wrong-password.test.json b/examples/http/basic/smart_switch/fixtures/04-wrong-password.test.json new file mode 100644 index 0000000..c358948 --- /dev/null +++ b/examples/http/basic/smart_switch/fixtures/04-wrong-password.test.json @@ -0,0 +1,15 @@ +{ + "appType": "http-wasm", + "description": "GET with wrong Authorization value → 403 Forbidden (empty body)", + "request": { + "method": "GET", + "path": "/", + "headers": { + "authorization": "wrong-password" + } + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/basic/smart_switch/src/lib.rs b/examples/http/basic/smart_switch/src/lib.rs index cb594ad..98d5999 100644 --- a/examples/http/basic/smart_switch/src/lib.rs +++ b/examples/http/basic/smart_switch/src/lib.rs @@ -120,7 +120,13 @@ fn send_device_command(token: &str, device: &str, command: &str) -> Result) -> Result, StatusCode> { + request_inner(req, 0) +} + +fn request_inner(req: Request, depth: u8) -> Result, StatusCode> { let rsp = match fastedge::send_request(req) { Err(error) => { let status_code = match error { @@ -136,7 +142,7 @@ fn request(req: Request) -> Result, StatusCode> { }; let status = rsp.status(); - if is_redirect(status) { + if is_redirect(status) && depth < MAX_REDIRECTS { if let Some(location) = rsp.headers().get(header::LOCATION) { let new_url = Url::parse( location.to_str().or(Err(StatusCode::INTERNAL_SERVER_ERROR))?) @@ -152,7 +158,7 @@ fn request(req: Request) -> Result, StatusCode> { .body(Body::empty()) .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - return request(sub_req); + return request_inner(sub_req, depth + 1); } } if status == StatusCode::OK { diff --git a/examples/http/basic/watermark/Cargo.lock b/examples/http/basic/watermark/Cargo.lock new file mode 100644 index 0000000..69bdcd7 --- /dev/null +++ b/examples/http/basic/watermark/Cargo.lock @@ -0,0 +1,1274 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" +dependencies = [ + "rayon", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rusty-s3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa883f1b986a5249641e574ca0e11ac4fb9970b009c6fbb96fedaf4fa78db8" +dependencies = [ + "base64", + "hmac", + "md-5", + "percent-encoding", + "quick-xml", + "serde", + "serde_json", + "sha2", + "time", + "url", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "watermark" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "image", + "rusty-s3", + "url", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags 2.11.1", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/examples/http/basic/watermark/README.md b/examples/http/basic/watermark/README.md index 29d3dae..8f32ec1 100644 --- a/examples/http/basic/watermark/README.md +++ b/examples/http/basic/watermark/README.md @@ -2,14 +2,62 @@ # Watermark -Reads an image from S3, applies a watermark with configurable opacity using alpha blending, and returns the composited image. +Demonstrates outbound S3 fetch and image compositing on the edge. The app retrieves an image from an S3-compatible bucket using request signing (`rusty_s3`), overlays an embedded watermark PNG (`sample.png`) using per-pixel alpha blending, and returns the composited image in the original format. + +The watermark file (`src/sample.png`) is embedded at compile time via `include_bytes!` — no runtime filesystem access is needed. + +Uses the legacy `#[fastedge::http]` sync handler (`wasm32-wasip1`). For new HTTP apps, prefer `#[wstd::http_server]` (async, `wasm32-wasip2`). + +## What it does + +1. Rejects non-GET/HEAD requests with `405 Method Not Allowed`. +2. Extracts the image filename from the URL path (e.g. `GET /photo.jpg`). +3. Constructs a time-limited AWS Signature V4 signed URL for the file in the configured S3 bucket. +4. Fetches the image from S3 via `fastedge::send_request`. +5. If the S3 response body is a valid image format, overlays the embedded watermark at the top-left corner. +6. Returns the composited image with the original MIME type. +7. If the S3 body is not a valid image, it is forwarded to the caller unchanged. ## Configuration -- Environment variable: `ACCESS_KEY` — S3 access key -- Environment variable: `SECRET_KEY` — S3 secret key -- Environment variable: `REGION` — S3 region -- Environment variable: `BASE_HOSTNAME` — S3 endpoint hostname -- Environment variable: `BUCKET` — S3 bucket name -- Environment variable: `SCHEME` — URL scheme (defaults to `http`) -- Environment variable: `OPACITY` — watermark opacity (0.0 to 1.0) +All environment variables are set on the deployed FastEdge app. + +| Variable | Required | Description | +|---|---|---| +| `ACCESS_KEY` | ✅ | S3 access key ID | +| `SECRET_KEY` | ✅ | S3 secret access key | +| `REGION` | ✅ | S3 region (e.g. `us-east-1`) | +| `BASE_HOSTNAME` | ✅ | S3 endpoint hostname (e.g. `cloud.gcore.lu`) | +| `BUCKET` | ✅ | S3 bucket name | +| `SCHEME` | optional | URL scheme for S3 endpoint (default: `http`) | +| `OPACITY` | optional | Watermark opacity as a float in `0.0`–`1.0` (default: `1.0`) | + +## Build + +```sh +cargo build --release +# WASM output: target/wasm32-wasip1/release/watermark.wasm +``` + +## Expected behavior + +| Request | Response | +|---|---| +| `POST /image.png` (wrong method) | `405 Method Not Allowed`, body: `This method is not allowed\n` | +| `GET /` (no filename) | `400 Bad Request`, body: `Malformed request - filename expected\n` | +| `GET /image.png` (env vars missing) | `500 Internal Server Error`, body: `App misconfigured\n` | +| `GET /image.png` (configured, image in bucket) | `200 OK`, watermarked image in original format | + +## APIs used + +| API | Purpose | +|---|---| +| `fastedge::send_request(req)` | Outbound HTTP request to S3 | +| `rusty_s3::{Bucket, Credentials, S3Action}` | AWS Signature V4 URL signing | +| `image::{load_from_memory, DynamicImage}` | Image decode, compositing, encode | +| `include_bytes!("sample.png")` | Embed watermark at compile time | +| `std::env::var` | Read S3 credentials and `OPACITY` from app env | + +## Live testing + +This example requires a real S3-compatible bucket (Gcore Object Storage or AWS S3) with valid credentials. The deterministic error paths (wrong method, empty path, missing env vars) can be validated locally with the fixture validator. The watermark compositing path requires a live deployment with credentials configured as app env vars. diff --git a/examples/http/basic/watermark/fixtures/empty-path.live.json b/examples/http/basic/watermark/fixtures/empty-path.live.json new file mode 100644 index 0000000..afcb701 --- /dev/null +++ b/examples/http/basic/watermark/fixtures/empty-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 400, + "bodyContains": "Malformed request - filename expected" + } +} diff --git a/examples/http/basic/watermark/fixtures/empty-path.test.json b/examples/http/basic/watermark/fixtures/empty-path.test.json new file mode 100644 index 0000000..990b3a6 --- /dev/null +++ b/examples/http/basic/watermark/fixtures/empty-path.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET request with no filename in path should return 400 Bad Request", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/basic/watermark/fixtures/method-not-allowed.live.json b/examples/http/basic/watermark/fixtures/method-not-allowed.live.json new file mode 100644 index 0000000..fc03e89 --- /dev/null +++ b/examples/http/basic/watermark/fixtures/method-not-allowed.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 405, + "headers": { + "allow": "GET, HEAD" + }, + "bodyContains": "This method is not allowed" + } +} diff --git a/examples/http/basic/watermark/fixtures/method-not-allowed.test.json b/examples/http/basic/watermark/fixtures/method-not-allowed.test.json new file mode 100644 index 0000000..2d47cfc --- /dev/null +++ b/examples/http/basic/watermark/fixtures/method-not-allowed.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "POST request should be rejected with 405 Method Not Allowed", + "request": { + "method": "POST", + "path": "/image.png" + } +} diff --git a/examples/http/basic/watermark/fixtures/missing-env-vars.live.json b/examples/http/basic/watermark/fixtures/missing-env-vars.live.json new file mode 100644 index 0000000..362f939 --- /dev/null +++ b/examples/http/basic/watermark/fixtures/missing-env-vars.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "bodyContains": "App misconfigured" + } +} diff --git a/examples/http/basic/watermark/fixtures/missing-env-vars.test.json b/examples/http/basic/watermark/fixtures/missing-env-vars.test.json new file mode 100644 index 0000000..3b4c427 --- /dev/null +++ b/examples/http/basic/watermark/fixtures/missing-env-vars.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET request with no S3 env vars configured should return 500 (App misconfigured)", + "request": { + "method": "GET", + "path": "/image.png" + } +} diff --git a/examples/http/wasi/ab_testing/Cargo.lock b/examples/http/wasi/ab_testing/Cargo.lock new file mode 100644 index 0000000..5d8aae6 --- /dev/null +++ b/examples/http/wasi/ab_testing/Cargo.lock @@ -0,0 +1,288 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_testing_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "wstd", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/ab_testing/Cargo.toml b/examples/http/wasi/ab_testing/Cargo.toml new file mode 100644 index 0000000..238c582 --- /dev/null +++ b/examples/http/wasi/ab_testing/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] + +[package] +name = "ab_testing_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" diff --git a/examples/http/wasi/ab_testing/README.md b/examples/http/wasi/ab_testing/README.md new file mode 100644 index 0000000..27cdea0 --- /dev/null +++ b/examples/http/wasi/ab_testing/README.md @@ -0,0 +1,35 @@ +[← Back to examples](../../../README.md) + +# A/B Testing (WASI) + +Cookie-based A/B testing. Reads or creates an `x-fastedge-abid` cookie, uses its value to +deterministically assign the visitor to weighted variants of two tests (`logo` and `font`), +then proxies the request to an outbound origin with the variant assignments attached as +`ab-test-` headers. The response sets the cookie so returning visitors receive the +same variants on subsequent visits. + +Demonstrates cookie parsing, request-header mutation, deterministic assignment from a +persistent identifier, and an outbound fetch with header overrides. + +## Configuration + +- `OUTBOUND_URL` environment variable — the downstream origin that consumes the + `ab-test-*` headers (for example, a templated backend). + +## How assignment works + +The `xid` cookie value is a decimal like `0.4532`. Multiplied by 100, it selects a slot in +each test's cumulative weight range. Weights don't need to sum to 100 — they're normalised +at assignment time. See `TESTS` in `src/lib.rs` to tweak the split. + +## Response + +The origin response is returned verbatim, with one added header: + +``` +set-cookie: x-fastedge-abid=; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax +``` + +## Related + +Mirror of `FastEdge-sdk-js/examples/ab-testing/`. diff --git a/examples/http/wasi/ab_testing/fixtures/.env b/examples/http/wasi/ab_testing/fixtures/.env new file mode 100644 index 0000000..fdfd22f --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_ENV_OUTBOUND_URL=https://httpbin.org/get diff --git a/examples/http/wasi/ab_testing/fixtures/missing-env.live.json b/examples/http/wasi/ab_testing/fixtures/missing-env.live.json new file mode 100644 index 0000000..2c3cfe6 --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/missing-env.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "OUTBOUND_URL environment variable is not configured" + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/missing-env.test.json b/examples/http/wasi/ab_testing/fixtures/missing-env.test.json new file mode 100644 index 0000000..ccad0d5 --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/missing-env.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "OUTBOUND_URL not configured — returns 500", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/no-cookie.live.json b/examples/http/wasi/ab_testing/fixtures/no-cookie.live.json new file mode 100644 index 0000000..26e42dd --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/no-cookie.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "set-cookie": { "contains": "x-fastedge-abid=" }, + "content-type": { "contains": "application/json" } + } + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/no-cookie.test.json b/examples/http/wasi/ab_testing/fixtures/no-cookie.test.json new file mode 100644 index 0000000..6cea206 --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/no-cookie.test.json @@ -0,0 +1,12 @@ +{ + "appType": "http-wasm", + "description": "No existing cookie — new xid generated from nanosecond timestamp", + "request": { + "method": "GET", + "path": "/" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/variant-a.live.json b/examples/http/wasi/ab_testing/fixtures/variant-a.live.json new file mode 100644 index 0000000..91b9319 --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/variant-a.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "set-cookie": { "contains": "x-fastedge-abid=0.25" }, + "content-type": { "contains": "application/json" } + } + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/variant-a.test.json b/examples/http/wasi/ab_testing/fixtures/variant-a.test.json new file mode 100644 index 0000000..1ddefa1 --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/variant-a.test.json @@ -0,0 +1,15 @@ +{ + "appType": "http-wasm", + "description": "xid=0.25 — logo=hops (25%<50%), font=exo2 (25%<26.67%)", + "request": { + "method": "GET", + "path": "/", + "headers": { + "cookie": "x-fastedge-abid=0.25" + } + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/variant-b.live.json b/examples/http/wasi/ab_testing/fixtures/variant-b.live.json new file mode 100644 index 0000000..dad232a --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/variant-b.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "set-cookie": { "contains": "x-fastedge-abid=0.75" }, + "content-type": { "contains": "application/json" } + } + } +} diff --git a/examples/http/wasi/ab_testing/fixtures/variant-b.test.json b/examples/http/wasi/ab_testing/fixtures/variant-b.test.json new file mode 100644 index 0000000..c67015a --- /dev/null +++ b/examples/http/wasi/ab_testing/fixtures/variant-b.test.json @@ -0,0 +1,15 @@ +{ + "appType": "http-wasm", + "description": "xid=0.75 — logo=bottle (75%>=50%), font=standard (75%>=70%)", + "request": { + "method": "GET", + "path": "/", + "headers": { + "cookie": "x-fastedge-abid=0.75" + } + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/ab_testing/src/lib.rs b/examples/http/wasi/ab_testing/src/lib.rs new file mode 100644 index 0000000..f3b5e72 --- /dev/null +++ b/examples/http/wasi/ab_testing/src/lib.rs @@ -0,0 +1,186 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Cookie-based A/B testing example. + +Reads or creates an `x-fastedge-abid` cookie, uses its value to deterministically +assign the visitor to weighted variants of each configured test, then proxies the +request to `OUTBOUND_URL` with the variant assignments attached as `ab-test-` +headers. The origin response is returned verbatim with a `set-cookie` header so +returning visitors receive the same variants on subsequent visits. + +Required configuration: + - Environment variable: OUTBOUND_URL (downstream origin to proxy to) + +Mirror of the FastEdge-sdk-js `ab-testing` example. +*/ + +use std::env; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::anyhow; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +struct VariantWeight { + variant: &'static str, + weight: f64, +} + +struct AbTest { + name: &'static str, + variants: &'static [VariantWeight], +} + +static TESTS: &[AbTest] = &[ + AbTest { + name: "logo", + variants: &[ + VariantWeight { variant: "hops", weight: 50.0 }, + VariantWeight { variant: "bottle", weight: 50.0 }, + ], + }, + AbTest { + name: "font", + variants: &[ + VariantWeight { variant: "exo2", weight: 40.0 }, + VariantWeight { variant: "gloria", weight: 65.0 }, + VariantWeight { variant: "standard", weight: 45.0 }, + ], + }, +]; + +const AB_COOKIE: &str = "x-fastedge-abid"; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let outbound_url = match env::var("OUTBOUND_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + return Ok(Response::builder() + .status(500) + .body(Body::from( + "OUTBOUND_URL environment variable is not configured", + ))?); + } + }; + + let raw_cookie = req + .headers() + .get("cookie") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let (xid, cleaned_cookie) = match extract_abid(&raw_cookie) { + Some(existing) if is_valid_xid(existing) => { + (existing.to_string(), strip_abid(&raw_cookie)) + } + _ => (generate_xid(), raw_cookie.clone()), + }; + + // Build the outbound request: copy incoming headers except `host` and + // `cookie` (which we handle specially), replace the cookie with a version + // that has the abid stripped (so the origin never sees it), and attach an + // `ab-test-` header for every configured test. + let mut builder = Request::get(&outbound_url); + for (name, value) in req.headers() { + let n = name.as_str(); + if n == "host" || n == "cookie" { + continue; + } + if let Ok(v) = value.to_str() { + builder = builder.header(n, v); + } + } + if !cleaned_cookie.trim().is_empty() { + builder = builder.header("cookie", cleaned_cookie); + } + for test in TESTS { + if let Some(variant) = assign_variant(&xid, test) { + builder = builder.header(format!("ab-test-{}", test.name), variant); + } + } + + let outbound_req = builder + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build outbound request: {e}"))?; + + let outbound_resp = Client::new() + .send(outbound_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + let (parts, mut body) = outbound_resp.into_parts(); + let body_bytes = body.contents().await?; + + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + let xid_cookie = + format!("{AB_COOKIE}={xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax"); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .header("set-cookie", xid_cookie) + .body(Body::from(body_bytes))?) +} + +fn extract_abid(cookie_header: &str) -> Option<&str> { + let needle = format!("{AB_COOKIE}="); + cookie_header + .split(';') + .map(str::trim) + .find_map(|p| p.strip_prefix(needle.as_str())) +} + +fn strip_abid(cookie_header: &str) -> String { + let needle = format!("{AB_COOKIE}="); + cookie_header + .split(';') + .map(str::trim) + .filter(|p| !p.is_empty()) + .filter(|p| !p.starts_with(needle.as_str())) + .collect::>() + .join("; ") +} + +fn is_valid_xid(xid: &str) -> bool { + matches!(xid.parse::(), Ok(v) if (0.0..1.0).contains(&v)) +} + +/// Generate a pseudo-random A/B id of the form `"0.NNNN"`. +/// +/// Uses request-time nanoseconds as a weak entropy source. For production, +/// prefer a cryptographic RNG (e.g. `rand` wired to `wasi-random`). +fn generate_xid() -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + format!("0.{:04}", now.subsec_nanos() % 10000) +} + +fn assign_variant(xid: &str, test: &AbTest) -> Option<&'static str> { + let xid_value: f64 = xid.parse().ok()?; + let xid_percentage = xid_value * 100.0; + let total: f64 = test.variants.iter().map(|v| v.weight).sum(); + if total == 0.0 { + return None; + } + let mut start = 0.0; + for vw in test.variants { + let percentage = (vw.weight / total) * 100.0; + let end = start + percentage; + if xid_percentage >= start && xid_percentage < end { + return Some(vw.variant); + } + start = end; + } + None +} diff --git a/examples/http/wasi/bloom_filter_denylist/Cargo.lock b/examples/http/wasi/bloom_filter_denylist/Cargo.lock new file mode 100644 index 0000000..668f538 --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/Cargo.lock @@ -0,0 +1,633 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bloom_filter_denylist_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "serde_json", + "wstd", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/bloom_filter_denylist/Cargo.toml b/examples/http/wasi/bloom_filter_denylist/Cargo.toml new file mode 100644 index 0000000..789fb2d --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] + +[package] +name = "bloom_filter_denylist_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +serde_json = "1" diff --git a/examples/http/wasi/bloom_filter_denylist/README.md b/examples/http/wasi/bloom_filter_denylist/README.md new file mode 100644 index 0000000..dd4a08b --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/README.md @@ -0,0 +1,40 @@ +[← Back to examples](../../../README.md) + +# Bloom Filter — IP Denylist (WASI) + +Rejects requests from IPs present in a KV Store bloom filter. Reads the client IP from the +`x-real-ip` request header (falling back to `x-forwarded-for`), checks it against a +pre-populated bloom filter, and returns **403** on a hit or **200** otherwise. + +Demonstrates `fastedge::key_value::Store` + `bf_exists()` and the conventional way to obtain +the client IP from a Component Model HTTP handler. + +## Configuration + +- Environment variable `DENYLIST_STORE` — name of the KV store that holds the bloom filter. +- Bloom-filter key — hardcoded to `blocked-ips`. Change `BLOOM_KEY` in `src/lib.rs` if your + key is different. + +## Behaviour + +| `bf_exists("blocked-ips", ip)` | Response | +| --- | --- | +| `true` | `403` `{ "allowed": false, "ip": "..." }` | +| `false` | `200` `{ "allowed": true, "ip": "..." }` | + +## Tradeoff: false positives + +Bloom filters answer "**definitely not** in set" vs "**maybe** in set". When `bf_exists` +returns `true`, the IP *probably* was added — but a small fraction of hits will be false +positives, meaning some legitimate IPs will be over-blocked. Acceptable for a denylist; for +allowlists or anything requiring exact membership, use `store.get()` against a regular key +instead. + +## Populating the filter + +The edge handler is read-only. Populate `blocked-ips` out of band (for example, via the +FastEdge API). + +## Related + +Mirror of `FastEdge-sdk-js/examples/bloom-filter-denylist/`. diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/.env b/examples/http/wasi/bloom_filter_denylist/fixtures/.env new file mode 100644 index 0000000..c02214a --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_ENV_DENYLIST_STORE=denylist-store diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/allowed-ip.live.json b/examples/http/wasi/bloom_filter_denylist/fixtures/allowed-ip.live.json new file mode 100644 index 0000000..cfd5c2d --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/allowed-ip.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 500, + "headers": { + "content-type": "application/json" + }, + "bodyContains": "access denied opening denylist store" + } +} diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/allowed-ip.test.json b/examples/http/wasi/bloom_filter_denylist/fixtures/allowed-ip.test.json new file mode 100644 index 0000000..3e92f91 --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/allowed-ip.test.json @@ -0,0 +1,15 @@ +{ + "appType": "http-wasm", + "description": "Non-blocked IP — requires KV bloom filter stub (WASI-KV-RUNNER-1)", + "request": { + "method": "GET", + "path": "/", + "headers": { + "x-real-ip": "1.2.3.4" + } + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/missing-ip.live.json b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-ip.live.json new file mode 100644 index 0000000..87b4784 --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-ip.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 500, + "headers": { + "content-type": "application/json" + }, + "bodyContains": "client IP not available" + } +} diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/missing-ip.test.json b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-ip.test.json new file mode 100644 index 0000000..3b4673e --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-ip.test.json @@ -0,0 +1,12 @@ +{ + "appType": "http-wasm", + "description": "No client IP header — returns 500 before opening KV store", + "request": { + "method": "GET", + "path": "/" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/missing-store-config.live.json b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-store-config.live.json new file mode 100644 index 0000000..d2d3660 --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-store-config.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 500, + "headers": { + "content-type": "application/json" + }, + "bodyContains": "DENYLIST_STORE environment variable is not configured" + } +} diff --git a/examples/http/wasi/bloom_filter_denylist/fixtures/missing-store-config.test.json b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-store-config.test.json new file mode 100644 index 0000000..38db8c0 --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/fixtures/missing-store-config.test.json @@ -0,0 +1,11 @@ +{ + "appType": "http-wasm", + "description": "DENYLIST_STORE not configured — returns 500 before opening KV store", + "request": { + "method": "GET", + "path": "/", + "headers": { + "x-real-ip": "1.2.3.4" + } + } +} diff --git a/examples/http/wasi/bloom_filter_denylist/src/lib.rs b/examples/http/wasi/bloom_filter_denylist/src/lib.rs new file mode 100644 index 0000000..7e1ccf6 --- /dev/null +++ b/examples/http/wasi/bloom_filter_denylist/src/lib.rs @@ -0,0 +1,87 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Bloom-filter IP denylist example. + +Checks the client IP (from the `x-real-ip` request header, falling back to +`x-forwarded-for`) against a bloom filter stored in FastEdge KV. Returns 403 +on a hit, 200 otherwise. + +Required configuration: + - Environment variable: DENYLIST_STORE (KV store name holding the bloom filter) + +The bloom-filter key is hardcoded to `blocked-ips`. The handler is read-only; +populate the filter out of band. + +Mirror of the FastEdge-sdk-js `bloom-filter-denylist` example. +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::key_value::{Error as StoreError, Store}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +const BLOOM_KEY: &str = "blocked-ips"; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let store_name = match env::var("DENYLIST_STORE") { + Ok(s) if !s.trim().is_empty() => s, + _ => { + return json_response( + 500, + json!({ "error": "DENYLIST_STORE environment variable is not configured" }), + ); + } + }; + + let headers = req.headers(); + let client_ip = headers + .get("x-real-ip") + .or_else(|| headers.get("x-forwarded-for")) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.split(',').next()) + .map(str::trim) + .filter(|s| !s.is_empty()); + + let Some(ip) = client_ip else { + return json_response(500, json!({ "error": "client IP not available" })); + }; + + let store = match Store::open(&store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return json_response( + 403, + json!({ "error": "access denied opening denylist store" }), + ); + } + Err(e) => { + return json_response(500, json!({ "error": format!("store open error: {e}") })); + } + }; + + let blocked = store + .bf_exists(BLOOM_KEY, ip) + .map_err(|e| anyhow!("bf_exists error: {e}"))?; + + if blocked { + // Bloom filter says "maybe in set" — a small fraction of hits will be false + // positives. Acceptable for a denylist (you over-block some legitimate users); + // not acceptable for allowlists — use `store.get()` against a regular key instead. + return json_response(403, json!({ "allowed": false, "ip": ip })); + } + + json_response(200, json!({ "allowed": true, "ip": ip })) +} + +fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(value.to_string()))?) +} diff --git a/examples/http/wasi/cache/README.md b/examples/http/wasi/cache/README.md new file mode 100644 index 0000000..921d652 --- /dev/null +++ b/examples/http/wasi/cache/README.md @@ -0,0 +1,34 @@ +[← Back to examples](../../../README.md) + +# Cache (WASI) + +Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | +| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | + +## How it works + +``` +GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) +GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) +``` + +Cache key is `cache:?`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/cache_wasi.wasm +``` + +## APIs used + +- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` +- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry +- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin diff --git a/examples/http/wasi/diagnostic_logging/Cargo.lock b/examples/http/wasi/diagnostic_logging/Cargo.lock new file mode 100644 index 0000000..a970309 --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/Cargo.lock @@ -0,0 +1,632 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "diagnostic_logging_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "wstd", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/diagnostic_logging/Cargo.toml b/examples/http/wasi/diagnostic_logging/Cargo.toml new file mode 100644 index 0000000..e9bb2fd --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] + +[package] +name = "diagnostic_logging_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" diff --git a/examples/http/wasi/diagnostic_logging/README.md b/examples/http/wasi/diagnostic_logging/README.md new file mode 100644 index 0000000..061d5b1 --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/README.md @@ -0,0 +1,42 @@ +[← Back to examples](../../../README.md) + +# Diagnostic Logging (WASI) + +Pass-through proxy that writes a single `fastedge::utils::set_user_diag` tag per request +summarising the outcome. The tag appears in the FastEdge platform's per-request log viewer, +distinct from stdout, and is designed to be filtered/counted/aggregated by SREs looking at +per-request outcomes. + +## Configuration + +- `ORIGIN_URL` environment variable — the origin that requests are proxied to. + +## Outcomes + +Each request writes exactly one of: + +| Condition | Tag | +| --- | --- | +| `ORIGIN_URL` missing | `outcome=config_error reason=origin_missing` | +| Origin unreachable | `outcome=origin_unreachable method= path=

err=` | +| Request proxied | `outcome=proxied method= path=

status=` | + +## `set_user_diag` vs `println!` + +| | `println!` | `set_user_diag` | +| --- | --- | --- | +| Channel | stdout — general application logs | per-request structured tag in platform log viewer | +| Cardinality | many per request | **one per request** — multiple calls leave only the last or are concatenated (undefined) | +| Best for | verbose traces, debug details | a single filterable outcome label | +| Forbidden | — | secrets and PII (tags appear in platform logs) | + +## Convention + +Call `set_user_diag` **once**, on every branch, late enough in the handler to know the +outcome. The `logfmt`-ish format (`outcome= key=value key=value …`) is easy to slice in +log search tooling — keep keys short and stable so they make good filter terms. + +## Related + +CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, +different module path. See `docs/CDN_APPS.md`. diff --git a/examples/http/wasi/diagnostic_logging/fixtures/.env b/examples/http/wasi/diagnostic_logging/fixtures/.env new file mode 100644 index 0000000..1dd6506 --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_ENV_ORIGIN_URL=https://httpbin.org/get diff --git a/examples/http/wasi/diagnostic_logging/fixtures/happy-path.live.json b/examples/http/wasi/diagnostic_logging/fixtures/happy-path.live.json new file mode 100644 index 0000000..bcf586b --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/fixtures/happy-path.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": { "contains": "application/json" } + } + } +} diff --git a/examples/http/wasi/diagnostic_logging/fixtures/happy-path.test.json b/examples/http/wasi/diagnostic_logging/fixtures/happy-path.test.json new file mode 100644 index 0000000..67fbaed --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/fixtures/happy-path.test.json @@ -0,0 +1,12 @@ +{ + "appType": "http-wasm", + "description": "Proxies request to ORIGIN_URL, returns response with set_user_diag tag", + "request": { + "method": "GET", + "path": "/" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/diagnostic_logging/fixtures/no-origin-url.live.json b/examples/http/wasi/diagnostic_logging/fixtures/no-origin-url.live.json new file mode 100644 index 0000000..f94313b --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/fixtures/no-origin-url.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 500, + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "body": "ORIGIN_URL is not configured" + } +} diff --git a/examples/http/wasi/diagnostic_logging/fixtures/no-origin-url.test.json b/examples/http/wasi/diagnostic_logging/fixtures/no-origin-url.test.json new file mode 100644 index 0000000..f7dc641 --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/fixtures/no-origin-url.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "ORIGIN_URL not configured — returns 500 with error message", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/diagnostic_logging/src/lib.rs b/examples/http/wasi/diagnostic_logging/src/lib.rs new file mode 100644 index 0000000..c4a60ee --- /dev/null +++ b/examples/http/wasi/diagnostic_logging/src/lib.rs @@ -0,0 +1,74 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Diagnostic logging example. + +Tiny pass-through proxy that writes a single `set_user_diag` tag per request +summarising the outcome (config_error, origin_unreachable, or proxied). The +tag appears in the FastEdge platform's per-request log viewer and is distinct +from stdout — it's intended for filterable outcome labels, not verbose +traces. + +Required configuration: + - Environment variable: ORIGIN_URL (origin to proxy to) + +Uses `logfmt`-ish formatting (`outcome= key=value ...`) so the tag is +easy to slice in log search tooling. +*/ + +use std::env; + +use fastedge::utils::set_user_diag; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let method = req.method().as_str().to_string(); + let path = req.uri().path().to_string(); + + let origin = match env::var("ORIGIN_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + set_user_diag("outcome=config_error reason=origin_missing"); + return Ok(Response::builder() + .status(500) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("ORIGIN_URL is not configured"))?); + } + }; + + let outbound = Request::get(&origin).body(Body::empty())?; + let resp = match Client::new().send(outbound).await { + Ok(r) => r, + Err(e) => { + set_user_diag(&format!( + "outcome=origin_unreachable method={method} path={path} err={e}" + )); + return Ok(Response::builder() + .status(502) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("origin unreachable"))?); + } + }; + + let status = resp.status().as_u16(); + set_user_diag(&format!( + "outcome=proxied method={method} path={path} status={status}" + )); + + let (parts, mut body) = resp.into_parts(); + let bytes = body.contents().await?; + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .body(Body::from(bytes))?) +} diff --git a/examples/http/wasi/geo_redirect/README.md b/examples/http/wasi/geo_redirect/README.md index a9dfac7..5f61115 100644 --- a/examples/http/wasi/geo_redirect/README.md +++ b/examples/http/wasi/geo_redirect/README.md @@ -2,11 +2,35 @@ # Geo Redirect (WASI) -Redirects requests to country-specific origins based on the `geoip-country-code` request header. +Redirects requests to country-specific origins based on the `geoip-country-code` request header. Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. -Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. +Demonstrates reading request headers, reading environment variables, and returning redirect responses. ## Configuration -- Environment variable: `BASE_ORIGIN` — fallback origin URL -- Environment variable: `` — optional per-country origin URLs (e.g. `US`, `DE`, `GB`) +| Env var | Required | Description | +|---|---|---| +| `BASE_ORIGIN` | Yes | Fallback redirect URL (e.g. `https://example.com`). Returns 500 if unset. | +| `` | No | Per-country redirect URL, keyed by 2-letter country code (e.g. `DE`, `US`, `GB`). Falls back to `BASE_ORIGIN` if not set. | + +## How it works + +``` +geoip-country-code: DE → env var DE is set → 302 to DE value +geoip-country-code: FR → env var FR not set → 302 to BASE_ORIGIN +(no header) → 302 to BASE_ORIGIN +BASE_ORIGIN not set → 500 +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/geo_redirect_wasi.wasm +``` + +## APIs used + +- `request.headers().get("geoip-country-code")` — read geo header injected by the FastEdge edge +- `std::env::var(country_code)` — dynamic env var lookup by country code +- `Response::builder().status(302).header("location", url)` — redirect response diff --git a/examples/http/wasi/geo_redirect/fixtures/default.live.json b/examples/http/wasi/geo_redirect/fixtures/default.live.json new file mode 100644 index 0000000..1442ff5 --- /dev/null +++ b/examples/http/wasi/geo_redirect/fixtures/default.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 302, + "headers": { + "location": "https://www.amazon.com" + } + } +} diff --git a/examples/http/wasi/geo_redirect/fixtures/france.live.json b/examples/http/wasi/geo_redirect/fixtures/france.live.json new file mode 100644 index 0000000..d0aaa1e --- /dev/null +++ b/examples/http/wasi/geo_redirect/fixtures/france.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 302, + "headers": { + "location": "https://www.amazon.fr" + } + } +} diff --git a/examples/http/wasi/geo_redirect/fixtures/germany.live.json b/examples/http/wasi/geo_redirect/fixtures/germany.live.json new file mode 100644 index 0000000..ac077a4 --- /dev/null +++ b/examples/http/wasi/geo_redirect/fixtures/germany.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 302, + "headers": { + "location": "https://www.amazon.de" + } + } +} diff --git a/examples/http/wasi/geo_redirect/fixtures/missing-config.live.json b/examples/http/wasi/geo_redirect/fixtures/missing-config.live.json new file mode 100644 index 0000000..6bfe794 --- /dev/null +++ b/examples/http/wasi/geo_redirect/fixtures/missing-config.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "BASE_ORIGIN is not set" + } +} diff --git a/examples/http/wasi/geo_redirect/fixtures/missing-config.test.json b/examples/http/wasi/geo_redirect/fixtures/missing-config.test.json new file mode 100644 index 0000000..9eaeda6 --- /dev/null +++ b/examples/http/wasi/geo_redirect/fixtures/missing-config.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "BASE_ORIGIN not configured — returns 500", + "request": { + "method": "GET", + "path": "/test" + } +} diff --git a/examples/http/wasi/headers/README.md b/examples/http/wasi/headers/README.md index 31256da..0d426be 100644 --- a/examples/http/wasi/headers/README.md +++ b/examples/http/wasi/headers/README.md @@ -2,8 +2,37 @@ # Headers (WASI) -Echoes all request headers back in the response and adds a custom header from an environment variable. +Echoes all request headers back in the response and adds a custom `x-my-custom-header` whose value comes from an environment variable. + +Demonstrates reading request headers via `request.headers()`, building a response with `Response::builder()`, and injecting environment-variable values into response headers. ## Configuration -- Environment variable: `MY_CUSTOM_ENV_VAR` — value to include as the `my-custom-header` response header +| Env var | Required | Description | +|---|---|---| +| `MY_CUSTOM_ENV_VAR` | No | Value placed in the `x-my-custom-header` response header. Empty string if unset. | + +## What it returns + +All request headers are copied to the response, then `x-my-custom-header` is appended. + +``` +HTTP/1.1 200 OK +x-my-custom-header: +<...all other request headers echoed back...> + +Returned all headers with a custom header added +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/headers.wasm +``` + +## APIs used + +- `request.headers()` — iterate over incoming request headers +- `Response::builder().header(name, value)` — build response with individual headers +- `std::env::var("KEY").unwrap_or_default()` — read optional env var diff --git a/examples/http/wasi/headers/fixtures/.env b/examples/http/wasi/headers/fixtures/.env index a09f25e..76e0ad9 100644 --- a/examples/http/wasi/headers/fixtures/.env +++ b/examples/http/wasi/headers/fixtures/.env @@ -1 +1 @@ -MY_CUSTOM_ENV_VAR=my-custom-value +FASTEDGE_VAR_ENV_MY_CUSTOM_ENV_VAR=my-custom-value diff --git a/examples/http/wasi/headers/fixtures/happy-path.live.json b/examples/http/wasi/headers/fixtures/happy-path.live.json new file mode 100644 index 0000000..1b86b1f --- /dev/null +++ b/examples/http/wasi/headers/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "x-my-custom-header": "my-custom-value" + }, + "bodyContains": "Returned all headers with a custom header added" + } +} diff --git a/examples/http/wasi/headers/fixtures/no-env-var.live.json b/examples/http/wasi/headers/fixtures/no-env-var.live.json new file mode 100644 index 0000000..d5518a3 --- /dev/null +++ b/examples/http/wasi/headers/fixtures/no-env-var.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "Returned all headers with a custom header added" + } +} diff --git a/examples/http/wasi/headers/fixtures/no-env-var.test.json b/examples/http/wasi/headers/fixtures/no-env-var.test.json new file mode 100644 index 0000000..28b79a7 --- /dev/null +++ b/examples/http/wasi/headers/fixtures/no-env-var.test.json @@ -0,0 +1,11 @@ +{ + "appType": "http-wasm", + "description": "No env var configured — x-my-custom-header is empty string", + "request": { + "method": "GET", + "path": "/", + "headers": { + "x-test-header": "test-value" + } + } +} diff --git a/examples/http/wasi/hello_world/README.md b/examples/http/wasi/hello_world/README.md index f9bc792..bb8152e 100644 --- a/examples/http/wasi/hello_world/README.md +++ b/examples/http/wasi/hello_world/README.md @@ -2,4 +2,29 @@ # Hello World (WASI) -The simplest possible async FastEdge application — returns a greeting with the request URL in the response body. +The simplest possible async FastEdge application — echoes the full request URI in the response body. + +Demonstrates the `#[wstd::http_server]` entry-point macro and the async handler signature used by all WASI HTTP examples. + +## What it returns + +``` +HTTP/1.1 200 OK +content-type: text/plain;charset=UTF-8 + +Hello, you made a wasi request to http:///? +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/hello_world.wasm +``` + +## APIs used + +- `#[wstd::http_server]` — WASI HTTP entry-point macro +- `wstd::http::{Request, Response}` — request/response types +- `wstd::http::body::Body` — response body construction +- `request.uri().to_string()` — full absolute request URI diff --git a/examples/http/wasi/hello_world/fixtures/happy-path.live.json b/examples/http/wasi/hello_world/fixtures/happy-path.live.json new file mode 100644 index 0000000..c652499 --- /dev/null +++ b/examples/http/wasi/hello_world/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "text/plain;charset=UTF-8" + }, + "bodyContains": "Hello, you made a wasi request to http://test.localhost/" + } +} diff --git a/examples/http/wasi/hello_world/fixtures/happy-path.test.json b/examples/http/wasi/hello_world/fixtures/happy-path.test.json new file mode 100644 index 0000000..b010f75 --- /dev/null +++ b/examples/http/wasi/hello_world/fixtures/happy-path.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "Base case — GET request returns greeting with full request URI", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/hello_world/fixtures/path-and-params.live.json b/examples/http/wasi/hello_world/fixtures/path-and-params.live.json new file mode 100644 index 0000000..e5a448a --- /dev/null +++ b/examples/http/wasi/hello_world/fixtures/path-and-params.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "text/plain;charset=UTF-8" + }, + "bodyContains": "Hello, you made a wasi request to http://test.localhost/api/hello/world?name=FastEdge&lang=Rust" + } +} diff --git a/examples/http/wasi/key_value/Cargo.lock b/examples/http/wasi/key_value/Cargo.lock new file mode 100644 index 0000000..d9802b1 --- /dev/null +++ b/examples/http/wasi/key_value/Cargo.lock @@ -0,0 +1,640 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "key_value_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "querystring", + "serde_json", + "wstd", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "querystring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/key_value/Cargo.toml b/examples/http/wasi/key_value/Cargo.toml index 37b1ae0..2b031b8 100644 --- a/examples/http/wasi/key_value/Cargo.toml +++ b/examples/http/wasi/key_value/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib"] [dependencies] wstd = "0.6" -fastedge = "0.3" +fastedge = "0.4" anyhow = "1" querystring = "1.1" serde_json = "1" diff --git a/examples/http/wasi/key_value/README.md b/examples/http/wasi/key_value/README.md index ca1d35b..a1d7dda 100644 --- a/examples/http/wasi/key_value/README.md +++ b/examples/http/wasi/key_value/README.md @@ -2,4 +2,53 @@ # Key Value (WASI) -Demonstrates KV store operations via query parameters: get, scan, zrange, zscan, and bfExists. +Demonstrates all KV store operations via a query-parameter driven HTTP API: get, scan, zrange, zscan, and bfExists. + +## Usage + +All operations require `?store=` and `?action=` query parameters: + +| Action | Additional params | Description | +|---|---|---| +| `get` | `key=` | Fetch a single value by key | +| `scan` | `match=` | List keys matching a glob pattern | +| `zrange` | `key=&min=&max=` | Fetch sorted-set members by score range | +| `zscan` | `key=&match=` | List sorted-set members matching a pattern | +| `bfExists` | `key=&item=` | Check bloom filter membership | + +`action` defaults to `get` if omitted. + +## Example + +``` +GET /?store=my-store&action=get&key=hello +→ 200 {"store":"my-store","action":"get","key":"hello","response":"world"} + +GET /?store=my-store&action=scan&match=user:* +→ 200 {"store":"my-store","action":"scan","match":"user:*","response":["user:1","user:2"]} +``` + +## Error responses + +| Condition | Status | Body | +|---|---|---| +| Store not found / access denied | 403 | `{"error":"access denied"}` | +| Missing required params | 530 | Runtime error | +| Store open error | 500 | `{"error":"store open error: ..."}` | +| Invalid action | 400 | `{"error":"Invalid action '...'. Supported: ..."}` | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/key_value_wasi.wasm +``` + +## APIs used + +- `fastedge::key_value::Store::open(name)` — open a named KV store +- `store.get(key)` — fetch value by key; returns `Ok(Option>)` +- `store.scan(pattern)` — list keys by glob pattern +- `store.zrange_by_score(key, min, max)` — range query on sorted set +- `store.zscan(key, pattern)` — pattern scan on sorted set +- `store.bf_exists(key, item)` — bloom filter membership test diff --git a/examples/http/wasi/key_value/fixtures/kv-get.live.json b/examples/http/wasi/key_value/fixtures/kv-get.live.json new file mode 100644 index 0000000..20f67d1 --- /dev/null +++ b/examples/http/wasi/key_value/fixtures/kv-get.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 403, + "headers": { + "content-type": "application/json" + }, + "bodyContains": "access denied" + } +} diff --git a/examples/http/wasi/key_value/fixtures/kv-get.test.json b/examples/http/wasi/key_value/fixtures/kv-get.test.json new file mode 100644 index 0000000..9bad6ff --- /dev/null +++ b/examples/http/wasi/key_value/fixtures/kv-get.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "KV get operation — requires KV store stub (WASI-KV-RUNNER-1)", + "request": { + "method": "GET", + "path": "/?store=my-store&action=get&key=hello" + } +} diff --git a/examples/http/wasi/key_value/fixtures/missing-params.live.json b/examples/http/wasi/key_value/fixtures/missing-params.live.json new file mode 100644 index 0000000..b5d8a37 --- /dev/null +++ b/examples/http/wasi/key_value/fixtures/missing-params.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 530, + "bodyContains": "Execute error" + } +} diff --git a/examples/http/wasi/key_value/fixtures/missing-params.test.json b/examples/http/wasi/key_value/fixtures/missing-params.test.json new file mode 100644 index 0000000..09b3764 --- /dev/null +++ b/examples/http/wasi/key_value/fixtures/missing-params.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "No query parameters — handler returns error (KV store not reached)", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/large_env_variable/Cargo.lock b/examples/http/wasi/large_env_variable/Cargo.lock new file mode 100644 index 0000000..3449079 --- /dev/null +++ b/examples/http/wasi/large_env_variable/Cargo.lock @@ -0,0 +1,632 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "large_env_variable" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "wstd", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/large_env_variable/Cargo.toml b/examples/http/wasi/large_env_variable/Cargo.toml index 09c0101..b655bae 100644 --- a/examples/http/wasi/large_env_variable/Cargo.toml +++ b/examples/http/wasi/large_env_variable/Cargo.toml @@ -10,5 +10,5 @@ crate-type = ["cdylib"] [dependencies] wstd = "0.6" -fastedge = "0.3" +fastedge = "0.4" anyhow = "1" diff --git a/examples/http/wasi/large_env_variable/fixtures/.env b/examples/http/wasi/large_env_variable/fixtures/.env new file mode 100644 index 0000000..83360d3 --- /dev/null +++ b/examples/http/wasi/large_env_variable/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_ENV_LARGE_CONFIG={"key":"value","description":"test payload"} diff --git a/examples/http/wasi/large_env_variable/fixtures/empty-config.live.json b/examples/http/wasi/large_env_variable/fixtures/empty-config.live.json new file mode 100644 index 0000000..6b3c98c --- /dev/null +++ b/examples/http/wasi/large_env_variable/fixtures/empty-config.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "LARGE_CONFIG loaded: 0 bytes" + } +} diff --git a/examples/http/wasi/large_env_variable/fixtures/empty-config.test.json b/examples/http/wasi/large_env_variable/fixtures/empty-config.test.json new file mode 100644 index 0000000..ce23481 --- /dev/null +++ b/examples/http/wasi/large_env_variable/fixtures/empty-config.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "LARGE_CONFIG not set — falls back to empty string, reports 0 bytes", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/large_env_variable/fixtures/happy-path.live.json b/examples/http/wasi/large_env_variable/fixtures/happy-path.live.json new file mode 100644 index 0000000..0fe0d2e --- /dev/null +++ b/examples/http/wasi/large_env_variable/fixtures/happy-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "LARGE_CONFIG loaded: 44 bytes" + } +} diff --git a/examples/http/wasi/large_env_variable/fixtures/happy-path.test.json b/examples/http/wasi/large_env_variable/fixtures/happy-path.test.json new file mode 100644 index 0000000..dbec128 --- /dev/null +++ b/examples/http/wasi/large_env_variable/fixtures/happy-path.test.json @@ -0,0 +1,12 @@ +{ + "appType": "http-wasm", + "description": "LARGE_CONFIG loaded via dictionary API — reports byte count", + "request": { + "method": "GET", + "path": "/" + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/outbound_fetch/Cargo.lock b/examples/http/wasi/outbound_fetch/Cargo.lock index db8454d..933df5d 100644 --- a/examples/http/wasi/outbound_fetch/Cargo.lock +++ b/examples/http/wasi/outbound_fetch/Cargo.lock @@ -127,7 +127,6 @@ name = "outbound_fetch" version = "0.1.0" dependencies = [ "anyhow", - "serde_json", "wstd", ] diff --git a/examples/http/wasi/outbound_fetch/Cargo.toml b/examples/http/wasi/outbound_fetch/Cargo.toml index 58c8a28..4bd98aa 100644 --- a/examples/http/wasi/outbound_fetch/Cargo.toml +++ b/examples/http/wasi/outbound_fetch/Cargo.toml @@ -11,4 +11,3 @@ crate-type = ["cdylib"] [dependencies] wstd = "0.6" anyhow = "1" -serde_json = "1" diff --git a/examples/http/wasi/outbound_fetch/README.md b/examples/http/wasi/outbound_fetch/README.md index 730629a..cd4f7db 100644 --- a/examples/http/wasi/outbound_fetch/README.md +++ b/examples/http/wasi/outbound_fetch/README.md @@ -2,4 +2,15 @@ # Outbound Fetch (WASI) -Fetches data from a remote JSON API (jsonplaceholder) and returns the first 5 users in a transformed JSON response. +Fetch data from an outbound HTTP origin and return the response directly — status, headers, +and body pass through unchanged. + +The body is never buffered (no `.contents().await`), so upstream chunks stream to the client +as they arrive. + +## Related + +- [outbound_modify_response](../outbound_modify_response/) — same fetch, but reads the body + and reshapes it into a new JSON response. +- [streaming](../streaming/) — a handler that generates its own streaming response body. +- Mirror of `FastEdge-sdk-js/examples/outbound-fetch/`. diff --git a/examples/http/wasi/outbound_fetch/fixtures/happy-path.live.json b/examples/http/wasi/outbound_fetch/fixtures/happy-path.live.json new file mode 100644 index 0000000..501f1e7 --- /dev/null +++ b/examples/http/wasi/outbound_fetch/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": { "contains": "application/json" } + }, + "bodyContains": "\"username\"" + } +} diff --git a/examples/http/wasi/outbound_fetch/fixtures/happy-path.test.json b/examples/http/wasi/outbound_fetch/fixtures/happy-path.test.json new file mode 100644 index 0000000..3f3ba12 --- /dev/null +++ b/examples/http/wasi/outbound_fetch/fixtures/happy-path.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "Fetches https://jsonplaceholder.typicode.com/users and returns it verbatim", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/outbound_fetch/src/lib.rs b/examples/http/wasi/outbound_fetch/src/lib.rs index 70c6fca..39fc27a 100644 --- a/examples/http/wasi/outbound_fetch/src/lib.rs +++ b/examples/http/wasi/outbound_fetch/src/lib.rs @@ -1,7 +1,21 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Minimal outbound fetch example. + +Makes a GET request to an upstream HTTP origin and returns the upstream +response verbatim — status, headers, and body pass through unchanged. + +For a variant that reads and transforms the upstream body, see +`outbound_modify_response/`. For a streaming-response demo, see `streaming/`. + +Mirror of the FastEdge-sdk-js `outbound-fetch` example. +*/ + use anyhow::anyhow; use wstd::http::body::Body; use wstd::http::{Client, Request, Response}; -use serde_json::{json, Value}; #[wstd::http_server] async fn main(_request: Request) -> anyhow::Result> { @@ -9,30 +23,17 @@ async fn main(_request: Request) -> anyhow::Result> { .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; - let client = Client::new(); - let upstream_resp = client + let upstream_resp = Client::new() .send(upstream_req) .await - .map_err(|e| anyhow!("request failed: {e}"))?; - - let (_, mut body) = upstream_resp.into_parts(); - let body_bytes = body.contents().await?; - let users: Value = serde_json::from_slice(body_bytes)?; - - let sliced_users = match users.as_array() { - Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), - None => Value::Array(vec![]), - }; - - let result = json!({ - "users": sliced_users, - "total": 5, - "skip": 0, - "limit": 30, - }); + .map_err(|e| anyhow!("outbound request failed: {e}"))?; - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(Body::from(result.to_string()))?) + // Return the upstream response verbatim. The body is passed through + // without calling `.contents()`, so it streams to the client as upstream + // produces it. + let (parts, body) = upstream_resp.into_parts(); + let mut response = Response::new(body); + *response.status_mut() = parts.status; + *response.headers_mut() = parts.headers; + Ok(response) } diff --git a/examples/http/wasi/outbound_modify_response/Cargo.lock b/examples/http/wasi/outbound_modify_response/Cargo.lock new file mode 100644 index 0000000..96086fd --- /dev/null +++ b/examples/http/wasi/outbound_modify_response/Cargo.lock @@ -0,0 +1,289 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "outbound_modify_response_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde_json", + "wstd", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/outbound_modify_response/Cargo.toml b/examples/http/wasi/outbound_modify_response/Cargo.toml new file mode 100644 index 0000000..bb1215b --- /dev/null +++ b/examples/http/wasi/outbound_modify_response/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] + +[package] +name = "outbound_modify_response_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +serde_json = "1" diff --git a/examples/http/wasi/outbound_modify_response/README.md b/examples/http/wasi/outbound_modify_response/README.md new file mode 100644 index 0000000..5ebccaa --- /dev/null +++ b/examples/http/wasi/outbound_modify_response/README.md @@ -0,0 +1,15 @@ +[← Back to examples](../../../README.md) + +# Outbound Modify Response (WASI) + +Fetch data from an outbound HTTP origin, transform the JSON response (slice to first 5 +users), and return it with a fresh `content-type: application/json` header. + +Demonstrates reading the upstream body with `body.contents().await`, parsing JSON with +`serde_json`, and composing a new response from scratch. + +## Related + +- [outbound_fetch](../outbound_fetch/) — the simpler variant that just passes the upstream + response through unchanged. +- Mirror of `FastEdge-sdk-js/examples/outbound-modify-response/`. diff --git a/examples/http/wasi/outbound_modify_response/fixtures/happy-path.live.json b/examples/http/wasi/outbound_modify_response/fixtures/happy-path.live.json new file mode 100644 index 0000000..2bf134f --- /dev/null +++ b/examples/http/wasi/outbound_modify_response/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "application/json" + }, + "bodyContains": "\"total\":5" + } +} diff --git a/examples/http/wasi/outbound_modify_response/fixtures/happy-path.test.json b/examples/http/wasi/outbound_modify_response/fixtures/happy-path.test.json new file mode 100644 index 0000000..91e14c9 --- /dev/null +++ b/examples/http/wasi/outbound_modify_response/fixtures/happy-path.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "Fetches users, slices to first 5, returns reshaped JSON with pagination metadata", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/outbound_modify_response/src/lib.rs b/examples/http/wasi/outbound_modify_response/src/lib.rs new file mode 100644 index 0000000..e5c0ae7 --- /dev/null +++ b/examples/http/wasi/outbound_modify_response/src/lib.rs @@ -0,0 +1,53 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Outbound fetch with response transformation. + +Fetches JSON from an upstream origin, reads and parses the body, reshapes it +into a new JSON object (first 5 users with pagination metadata), and returns +it with a fresh `content-type: application/json` header. + +This is the stepping-stone beyond `outbound_fetch/` which just passes the +upstream response through unchanged. + +Mirror of the FastEdge-sdk-js `outbound-modify-response` example. +*/ + +use anyhow::anyhow; +use serde_json::{Value, json}; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + let (_, mut body) = upstream_resp.into_parts(); + let body_bytes = body.contents().await?; + let users: Value = serde_json::from_slice(body_bytes)?; + + let sliced_users = match users.as_array() { + Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), + None => Value::Array(vec![]), + }; + + let result = json!({ + "users": sliced_users, + "total": 5, + "skip": 0, + "limit": 30, + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} diff --git a/examples/http/wasi/secret_rollover/Cargo.toml b/examples/http/wasi/secret_rollover/Cargo.toml index 050ff36..efc2b1f 100644 --- a/examples/http/wasi/secret_rollover/Cargo.toml +++ b/examples/http/wasi/secret_rollover/Cargo.toml @@ -10,6 +10,6 @@ crate-type = ["cdylib"] [dependencies] wstd = "0.6" -fastedge = "0.3" +fastedge = "0.4" anyhow = "1" serde_json = "1" diff --git a/examples/http/wasi/secret_rollover/README.md b/examples/http/wasi/secret_rollover/README.md index 2cfd8cc..f473033 100644 --- a/examples/http/wasi/secret_rollover/README.md +++ b/examples/http/wasi/secret_rollover/README.md @@ -4,13 +4,50 @@ Demonstrates slot-based secret retrieval for secret rotation scenarios using `secret::get_effective_at()`. -Compares the current secret value with the value effective at a given slot, returning both as JSON. +Compares the current secret value with the value effective at a given slot, returning both as JSON. This lets you validate that rotation is working correctly before removing the old slot. ## Usage -- `x-secret-name` request header — secret name to look up (defaults to `TOKEN_SECRET`) -- `x-slot` request header — slot value to query (defaults to current unix timestamp) +| Request header | Default | Description | +|---|---|---| +| `x-secret-name` | `TOKEN_SECRET` | Name of the secret to query | +| `x-slot` | current Unix timestamp | Slot value passed to `get_effective_at` | -## How Slots Work +## How slots work -Slots use a greatest matching rule: the slot with the highest value that is `<=` the requested `effective_at` is returned. This supports both index-based and timestamp-based rotation patterns. See the [secret rollover documentation](../../../../FastEdge-sdk-js/github-pages/src/content/docs/reference/fastedge/secret/get-secret-effective-at.md) for detailed examples. +Slots use a **greatest-match rule**: the slot with the highest value that is `<= effective_at` is returned. + +``` +Secret slots: { 0: "old-password", 1741790697: "new-password" } + +get_effective_at("TOKEN_SECRET", 0) → "old-password" +get_effective_at("TOKEN_SECRET", 100) → "old-password" +get_effective_at("TOKEN_SECRET", 1741790697) → "new-password" +get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" +``` + +When used with token `iat` (issued-at) timestamps, `get_effective_at(name, claims.iat)` returns the password that was active when the token was issued — enabling zero-downtime rotation without invalidating existing tokens. + +## What it returns + +```json +{ + "secret_name": "TOKEN_SECRET", + "slot": 0, + "current": "new-password", + "effective_at_slot": "old-password", + "is_same": false +} +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/secret_rollover.wasm +``` + +## APIs used + +- `fastedge::secret::get(name)` — current (latest-slot) secret value; `Ok(Option)` +- `fastedge::secret::get_effective_at(name, slot)` — secret value at a given slot; `Ok(Option)` diff --git a/examples/http/wasi/secret_rollover/fixtures/.env b/examples/http/wasi/secret_rollover/fixtures/.env new file mode 100644 index 0000000..5049975 --- /dev/null +++ b/examples/http/wasi/secret_rollover/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_SECRET_TOKEN_SECRET=test-secret-value diff --git a/examples/http/wasi/secret_rollover/fixtures/happy-path.live.json b/examples/http/wasi/secret_rollover/fixtures/happy-path.live.json new file mode 100644 index 0000000..3eba657 --- /dev/null +++ b/examples/http/wasi/secret_rollover/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "application/json" + }, + "bodyContains": "\"secret_name\":\"TOKEN_SECRET\"" + } +} diff --git a/examples/http/wasi/secret_rollover/fixtures/happy-path.test.json b/examples/http/wasi/secret_rollover/fixtures/happy-path.test.json new file mode 100644 index 0000000..0797aa9 --- /dev/null +++ b/examples/http/wasi/secret_rollover/fixtures/happy-path.test.json @@ -0,0 +1,15 @@ +{ + "appType": "http-wasm", + "description": "Slot 0 with default secret name — current and effective values returned", + "request": { + "method": "GET", + "path": "/", + "headers": { + "x-slot": "0" + } + }, + "dotenv": { + "enabled": true, + "path": "." + } +} diff --git a/examples/http/wasi/simple_fetch/Cargo.lock b/examples/http/wasi/simple_fetch/Cargo.lock new file mode 100644 index 0000000..9a285ee --- /dev/null +++ b/examples/http/wasi/simple_fetch/Cargo.lock @@ -0,0 +1,288 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "simple_fetch" +version = "0.1.0" +dependencies = [ + "anyhow", + "wstd", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/simple_fetch/README.md b/examples/http/wasi/simple_fetch/README.md index b71d871..929c88b 100644 --- a/examples/http/wasi/simple_fetch/README.md +++ b/examples/http/wasi/simple_fetch/README.md @@ -4,7 +4,7 @@ A minimal example demonstrating outbound HTTP requests using the [WASI-HTTP](https://github.com/WebAssembly/wasi-http) interface via the [`wstd`](https://crates.io/crates/wstd) crate. -Unlike the other HTTP examples that use the synchronous FastEdge SDK, this example uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). +Uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). The same async pattern is used by all examples in `examples/http/wasi/`. ## How it works @@ -26,31 +26,16 @@ curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// ## Build -### Prerequisites - -- Rust toolchain -- [`cargo-component`](https://github.com/bytecodealliance/cargo-component) - ```bash -cargo install cargo-component -``` - -### Compile - -```bash -cargo component build --release -``` - -The compiled component will be at: -``` -target/wasm32-wasip1/release/fetch.wasm +cargo build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm ``` -## Key differences from FastEdge SDK examples +## Key differences from basic HTTP examples -| | FastEdge SDK | This example (WASI-HTTP) | +| | Basic HTTP (`fastedge` crate) | WASI HTTP (`wstd` crate) | |---|---|---| | Handler | `fn main(req)` — sync | `async fn main(req)` — async | | Macro | `#[fastedge::http]` | `#[wstd::http_server]` | | Outbound HTTP | `fastedge::send_request(req)` | `Client::new().send(req).await` | -| Build tool | `cargo build` | `cargo component build` | +| Build target | `wasm32-wasip1` | `wasm32-wasip2` | diff --git a/examples/http/wasi/simple_fetch/fixtures/happy-path.live.json b/examples/http/wasi/simple_fetch/fixtures/happy-path.live.json new file mode 100644 index 0000000..bcf586b --- /dev/null +++ b/examples/http/wasi/simple_fetch/fixtures/happy-path.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": { "contains": "application/json" } + } + } +} diff --git a/examples/http/wasi/static_assets/Cargo.lock b/examples/http/wasi/static_assets/Cargo.lock new file mode 100644 index 0000000..5d16b90 --- /dev/null +++ b/examples/http/wasi/static_assets/Cargo.lock @@ -0,0 +1,288 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "static_assets_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "wstd", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/static_assets/Cargo.toml b/examples/http/wasi/static_assets/Cargo.toml new file mode 100644 index 0000000..fff438d --- /dev/null +++ b/examples/http/wasi/static_assets/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] + +[package] +name = "static_assets_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" diff --git a/examples/http/wasi/static_assets/README.md b/examples/http/wasi/static_assets/README.md new file mode 100644 index 0000000..dbfd438 --- /dev/null +++ b/examples/http/wasi/static_assets/README.md @@ -0,0 +1,26 @@ +[← Back to examples](../../../README.md) + +# Static Assets (WASI) + +Embeds three files (`index.html`, `style.css`, `logo.svg`) into the wasm binary at compile +time using `include_str!`, and serves them by path at request time. The wasm runtime has no +file system, so all assets must be embedded this way. + +Visit `/` for the landing page. `/style.css` and `/logo.svg` are referenced from the HTML +and served by the same handler. Any other path returns 404. + +## Adding a new asset + +1. Drop the file into `assets/`. +2. Add a `static` entry at the top of `src/lib.rs` pointing at it via `include_str!` + (text) or `include_bytes!` (binary). +3. Add a match arm in `lookup()` mapping the request path to the new asset. + +For binary assets, change the `Asset.body` field to `&'static [u8]` and build the response +body with `Body::from(bytes::Bytes::from_static(asset.body))`. + +## Related + +Mirror of `FastEdge-sdk-js/examples/static-assets/` (the JS version uses Hono + a compile- +time asset manifest generated by `fastedge-assets`; the Rust version does the same thing +with plain stdlib macros). diff --git a/examples/http/wasi/static_assets/assets/index.html b/examples/http/wasi/static_assets/assets/index.html new file mode 100644 index 0000000..bfa22e0 --- /dev/null +++ b/examples/http/wasi/static_assets/assets/index.html @@ -0,0 +1,23 @@ + + + + + FastEdge Static Assets + + + +

+ FastEdge logo +

Static assets, served from wasm

+
+

+ This page, its stylesheet, and its logo are embedded in the wasm binary at compile + time via include_str!. There is no file system at runtime — every + asset ships inside the handler. +

+

+ Fetch /style.css or + /logo.svg directly to see them served by the same handler. +

+ + diff --git a/examples/http/wasi/static_assets/assets/logo.svg b/examples/http/wasi/static_assets/assets/logo.svg new file mode 100644 index 0000000..bef828c --- /dev/null +++ b/examples/http/wasi/static_assets/assets/logo.svg @@ -0,0 +1,4 @@ + + + FE + diff --git a/examples/http/wasi/static_assets/assets/style.css b/examples/http/wasi/static_assets/assets/style.css new file mode 100644 index 0000000..ba2be3b --- /dev/null +++ b/examples/http/wasi/static_assets/assets/style.css @@ -0,0 +1,30 @@ +body { + font-family: system-ui, -apple-system, sans-serif; + max-width: 680px; + margin: 2rem auto; + padding: 0 1rem; + color: #222; + line-height: 1.5; +} + +header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 2rem; +} + +h1 { + margin: 0; + font-size: 1.5rem; +} + +code { + background: #f2f2f2; + padding: 0.1rem 0.3rem; + border-radius: 3px; +} + +a { + color: #0066cc; +} diff --git a/examples/http/wasi/static_assets/fixtures/index.live.json b/examples/http/wasi/static_assets/fixtures/index.live.json new file mode 100644 index 0000000..1db8051 --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/index.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "text/html; charset=utf-8" + }, + "bodyContains": "FastEdge Static Assets" + } +} diff --git a/examples/http/wasi/static_assets/fixtures/index.test.json b/examples/http/wasi/static_assets/fixtures/index.test.json new file mode 100644 index 0000000..f2c37b7 --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/index.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET / — serves embedded index.html", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/static_assets/fixtures/logo-svg.live.json b/examples/http/wasi/static_assets/fixtures/logo-svg.live.json new file mode 100644 index 0000000..b89013d --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/logo-svg.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "image/svg+xml" + } + } +} diff --git a/examples/http/wasi/static_assets/fixtures/logo-svg.test.json b/examples/http/wasi/static_assets/fixtures/logo-svg.test.json new file mode 100644 index 0000000..db5fce4 --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/logo-svg.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET /logo.svg — serves embedded SVG", + "request": { + "method": "GET", + "path": "/logo.svg" + } +} diff --git a/examples/http/wasi/static_assets/fixtures/not-found.live.json b/examples/http/wasi/static_assets/fixtures/not-found.live.json new file mode 100644 index 0000000..9181ccf --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/not-found.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 404, + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "bodyContains": "Not found: /missing.txt" + } +} diff --git a/examples/http/wasi/static_assets/fixtures/not-found.test.json b/examples/http/wasi/static_assets/fixtures/not-found.test.json new file mode 100644 index 0000000..d3bd449 --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/not-found.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET /missing.txt — 404 for unknown path", + "request": { + "method": "GET", + "path": "/missing.txt" + } +} diff --git a/examples/http/wasi/static_assets/fixtures/style-css.live.json b/examples/http/wasi/static_assets/fixtures/style-css.live.json new file mode 100644 index 0000000..e98f90e --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/style-css.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "text/css; charset=utf-8" + }, + "bodyContains": "font-family" + } +} diff --git a/examples/http/wasi/static_assets/fixtures/style-css.test.json b/examples/http/wasi/static_assets/fixtures/style-css.test.json new file mode 100644 index 0000000..b9d605f --- /dev/null +++ b/examples/http/wasi/static_assets/fixtures/style-css.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "GET /style.css — serves embedded stylesheet", + "request": { + "method": "GET", + "path": "/style.css" + } +} diff --git a/examples/http/wasi/static_assets/src/lib.rs b/examples/http/wasi/static_assets/src/lib.rs new file mode 100644 index 0000000..68810fa --- /dev/null +++ b/examples/http/wasi/static_assets/src/lib.rs @@ -0,0 +1,60 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Static assets example. + +Embeds three files (`index.html`, `style.css`, `logo.svg`) into the wasm binary +at compile time via `include_str!` and serves them by path at request time. The +wasm runtime has no file system, so all assets must be embedded this way. + +For binary assets, use `include_bytes!` and wrap the result in +`bytes::Bytes::from_static` when constructing the response body. + +Mirror of the FastEdge-sdk-js `static-assets` example. +*/ + +use wstd::http::body::Body; +use wstd::http::{Request, Response, StatusCode}; + +struct Asset { + content_type: &'static str, + body: &'static str, +} + +static INDEX_HTML: Asset = Asset { + content_type: "text/html; charset=utf-8", + body: include_str!("../assets/index.html"), +}; +static STYLE_CSS: Asset = Asset { + content_type: "text/css; charset=utf-8", + body: include_str!("../assets/style.css"), +}; +static LOGO_SVG: Asset = Asset { + content_type: "image/svg+xml", + body: include_str!("../assets/logo.svg"), +}; + +fn lookup(path: &str) -> Option<&'static Asset> { + match path { + "/" | "/index.html" => Some(&INDEX_HTML), + "/style.css" => Some(&STYLE_CSS), + "/logo.svg" => Some(&LOGO_SVG), + _ => None, + } +} + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let path = req.uri().path(); + match lookup(path) { + Some(asset) => Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", asset.content_type) + .body(Body::from(asset.body))?), + None => Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from(format!("Not found: {path}\n")))?), + } +} diff --git a/examples/http/wasi/streaming/Cargo.lock b/examples/http/wasi/streaming/Cargo.lock new file mode 100644 index 0000000..43f0f6b --- /dev/null +++ b/examples/http/wasi/streaming/Cargo.lock @@ -0,0 +1,289 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "streaming_wasi" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures-lite", + "wstd", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/http/wasi/streaming/Cargo.toml b/examples/http/wasi/streaming/Cargo.toml new file mode 100644 index 0000000..f431a3f --- /dev/null +++ b/examples/http/wasi/streaming/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] + +[package] +name = "streaming_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +futures-lite = "1" diff --git a/examples/http/wasi/streaming/README.md b/examples/http/wasi/streaming/README.md new file mode 100644 index 0000000..4667d21 --- /dev/null +++ b/examples/http/wasi/streaming/README.md @@ -0,0 +1,32 @@ +[← Back to examples](../../../README.md) + +# Streaming Response (WASI) + +Generates a response body on the fly — five text chunks, one every 200 ms — using +`Body::from_stream` backed by a `futures_lite::Stream`. Each chunk flows to the client as it +is produced, not all at once at the end. + +Demonstrates `wstd::http::body::Body::from_stream`, `futures_lite::stream::unfold` for async +stream generation, and `wstd::time::Timer` for per-chunk delays. + +## Testing the streaming behaviour + +```sh +curl -N https://.fastedge.cdn.gc.onl/ +``` + +`-N` disables curl's client-side buffering; without it you won't see chunks appear one at a +time. You should see `chunk 0`…`chunk 4` print at ~200ms intervals. + +## Other streaming patterns + +- **Pass-through streaming** — return an upstream response's body directly. See + [outbound_fetch/](../outbound_fetch/) for the no-buffer variant. +- **Transform streaming** — use `http_body_util::BodyExt::map_frame` on the incoming body, + then `Body::from_http_body` to wrap it back. Useful for chunk-level rewrites. +- **Stream from bytes** — `Body::from_stream(futures_lite::stream::iter(chunks))` where + `chunks` is any iterable of `Into`. + +## Related + +Mirror of `FastEdge-sdk-js/examples/streaming/`. diff --git a/examples/http/wasi/streaming/fixtures/happy-path.live.json b/examples/http/wasi/streaming/fixtures/happy-path.live.json new file mode 100644 index 0000000..308dc88 --- /dev/null +++ b/examples/http/wasi/streaming/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "bodyContains": ["chunk 0", "chunk 4"] + } +} diff --git a/examples/http/wasi/streaming/fixtures/happy-path.test.json b/examples/http/wasi/streaming/fixtures/happy-path.test.json new file mode 100644 index 0000000..45dddde --- /dev/null +++ b/examples/http/wasi/streaming/fixtures/happy-path.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "Streams 5 text chunks (one every 200ms) — runner collects full body", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/examples/http/wasi/streaming/src/lib.rs b/examples/http/wasi/streaming/src/lib.rs new file mode 100644 index 0000000..59e9c52 --- /dev/null +++ b/examples/http/wasi/streaming/src/lib.rs @@ -0,0 +1,37 @@ +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Streaming response example. + +Generates a response body on the fly — five text chunks, one every 200ms — +using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime +polls the stream as the body is sent, so chunks flow to the client as they +are produced instead of all at once at the end. + +Watch it stream with `curl -N https:///` (`-N` disables client-side +buffering). + +Mirror of the FastEdge-sdk-js `streaming` example. +*/ + +use futures_lite::stream; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; +use wstd::time::{Duration, Timer}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let chunk_stream = stream::unfold(0u32, |i| async move { + if i >= 5 { + return None; + } + Timer::after(Duration::from_millis(200)).wait().await; + Some((format!("chunk {i}\n"), i + 1)) + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from_stream(chunk_stream))?) +} diff --git a/examples/http/wasi/variables_and_secrets/Cargo.toml b/examples/http/wasi/variables_and_secrets/Cargo.toml index d23d4e1..f12a359 100644 --- a/examples/http/wasi/variables_and_secrets/Cargo.toml +++ b/examples/http/wasi/variables_and_secrets/Cargo.toml @@ -10,5 +10,5 @@ crate-type = ["cdylib"] [dependencies] wstd = "0.6" -fastedge = "0.3" +fastedge = "0.4" anyhow = "1" diff --git a/examples/http/wasi/variables_and_secrets/README.md b/examples/http/wasi/variables_and_secrets/README.md index fb3cb09..005eef6 100644 --- a/examples/http/wasi/variables_and_secrets/README.md +++ b/examples/http/wasi/variables_and_secrets/README.md @@ -2,4 +2,33 @@ # Variables and Secrets (WASI) -Demonstrates reading a environment variable (`USERNAME`) and a secret (`PASSWORD`), returning both in the response body. +Demonstrates reading an environment variable (`USERNAME`) and a secret (`PASSWORD`), returning both in the response body. + +Environment variables are set via the FastEdge app configuration and accessed with `std::env::var`. Secrets are stored encrypted and accessed with `fastedge::secret::get` — they are never exposed in platform logs or configuration UIs. + +## Configuration + +| Key | Type | Required | Description | +|---|---|---|---| +| `USERNAME` | Environment variable | No | Username to include in response. Empty string if unset. | +| `PASSWORD` | Secret | No | Password to include in response. Empty string if unset or unavailable. | + +## What it returns + +``` +HTTP/1.1 200 OK + +Username: , Password: +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/variables_and_secrets.wasm +``` + +## APIs used + +- `std::env::var("USERNAME").unwrap_or_default()` — read env var with fallback +- `fastedge::secret::get("PASSWORD")` — read secret by name; returns `Ok(Some(String))` on success diff --git a/examples/http/wasi/variables_and_secrets/fixtures/happy-path.live.json b/examples/http/wasi/variables_and_secrets/fixtures/happy-path.live.json new file mode 100644 index 0000000..33c126c --- /dev/null +++ b/examples/http/wasi/variables_and_secrets/fixtures/happy-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "bodyContains": "Username: test-username, Password: test-password" + } +} diff --git a/examples/http/wasi/variables_and_secrets/fixtures/missing-vars.live.json b/examples/http/wasi/variables_and_secrets/fixtures/missing-vars.live.json new file mode 100644 index 0000000..5110fee --- /dev/null +++ b/examples/http/wasi/variables_and_secrets/fixtures/missing-vars.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 200, + "body": "Username: , Password: " + } +} diff --git a/examples/http/wasi/variables_and_secrets/fixtures/missing-vars.test.json b/examples/http/wasi/variables_and_secrets/fixtures/missing-vars.test.json new file mode 100644 index 0000000..b285e8c --- /dev/null +++ b/examples/http/wasi/variables_and_secrets/fixtures/missing-vars.test.json @@ -0,0 +1,8 @@ +{ + "appType": "http-wasm", + "description": "No env/secret configured — both values fall back to empty string", + "request": { + "method": "GET", + "path": "/" + } +} diff --git a/fastedge-plugin-source/generate-docs.sh b/fastedge-plugin-source/generate-docs.sh index 6381563..dc8519b 100755 --- a/fastedge-plugin-source/generate-docs.sh +++ b/fastedge-plugin-source/generate-docs.sh @@ -210,6 +210,8 @@ $(cat "$full_path") # Existing Content for docs/$target Use this as the baseline. Preserve all accurate content and manual additions. Only change what is incorrect, incomplete, or missing per the source code. Keep sections not covered by the instructions above. Apply table formatting rules to all tables. +If the existing content is already accurate against the source code, output it verbatim with zero preamble or acknowledgement — your output starts at the # of the level-1 heading regardless of whether you made changes. + $existing_doc @@ -223,7 +225,7 @@ $existing_doc # sometimes produces conversational preamble or asks for permission. local prompt prompt="$(cat < "$tmpfile" <<<"$prompt" - # Validate: first non-empty line must start with # - local first_line - first_line=$(grep -m1 '.' "$tmpfile" || true) - if [[ "$first_line" == \#* ]]; then - mv "$tmpfile" "$DOCS_DIR/$target" + # Validate + salvage. The model intermittently leaks a conversational + # preamble like "I'll write the markdown now." before the real document, + # despite the OUTPUT CONSTRAINT in the prompt. Rather than discard those + # outputs and retry (wasting API quota on otherwise-good content), find + # the first level-1 heading and treat everything from there forward as + # the doc. The original is still saved to .failures/ so the prompt can + # be tuned later. + local stripped + # Fence-aware level-1-only salvage: track ``` fences so that #-prefixed + # lines inside a fence (shell comments, #include, etc.) don't trigger the + # heading-detection scan. Fence delimiter lines are NOT skipped — they fall + # through to `found { print }` so fenced code blocks are preserved intact + # in the output. Only a bare `# ` heading outside a fence sets found=1. + stripped=$(awk '/^```/ { in_fence = !in_fence } !in_fence && /^# / { found=1 } found' "$tmpfile") + + # Post-strip validation: confirm the salvaged output actually starts with a + # level-1 heading (# followed by a space). Belt-and-suspenders against any + # remaining edge case; reject and retry rather than silently writing junk. + local first_nonempty + first_nonempty=$(printf '%s\n' "$stripped" | grep -m1 '.') + if [ -n "$stripped" ] && [[ "$first_nonempty" =~ ^"# " ]]; then + # Detect preamble: anything before the first level-1 heading is preamble. + local first_heading_line + first_heading_line=$(grep -n -m1 '^# ' "$tmpfile" | cut -d: -f1) + if [ "${first_heading_line:-1}" -gt 1 ]; then + local preamble_copy="$failure_dir/${target}.preamble.attempt-${attempt}.$(date +%s).md" + cp "$tmpfile" "$preamble_copy" + echo " Stripped $((first_heading_line - 1)) preamble line(s) from $target (attempt $attempt) — original saved to $preamble_copy" + fi + printf '%s\n' "$stripped" > "$DOCS_DIR/$target" + rm -f "$tmpfile" echo " Done: docs/$target" return 0 fi - echo " Attempt $attempt/$max_attempts failed for $target (got conversational output), retrying..." + # No valid level-1 heading found — genuine failure. Save and retry. + local failed_copy="$failure_dir/${target}.attempt-${attempt}.$(date +%s).md" + cp "$tmpfile" "$failed_copy" + echo " Attempt $attempt/$max_attempts failed for $target (no level-1 heading found in output) — saved to $failed_copy, retrying..." attempt=$((attempt + 1)) done diff --git a/fastedge-plugin-source/manifest.json b/fastedge-plugin-source/manifest.json index b31495d..3259c88 100644 --- a/fastedge-plugin-source/manifest.json +++ b/fastedge-plugin-source/manifest.json @@ -1,7 +1,7 @@ { "$schema": "https://fastedge-plugin-source/manifest/v1", "repo_id": "fastedge-sdk-rust", - "version": "1.4.0", + "version": "1.6.0", "sources": { "sdk-api": { "files": ["docs/SDK_API.md"], @@ -19,13 +19,28 @@ "description": "CDN app guide — proxy-wasm lifecycle, fastedge::proxywasm::* API surface, request/response manipulation" }, + "quickstart": { + "files": ["docs/quickstart.md"], + "required": true, + "description": "Installation, project setup, async vs sync handler, basic build for HTTP and CDN apps" + }, + "http-hello-world-blueprint": { "files": [ "examples/http/wasi/hello_world/src/lib.rs", "examples/http/wasi/hello_world/Cargo.toml" ], "required": true, - "description": "HTTP WASI Hello World example — base skeleton blueprint extraction (HTTP Rust)" + "description": "HTTP WASI Hello World example — base skeleton blueprint (scaffold)" + }, + "http-hello-world-pattern": { + "files": [ + "examples/http/wasi/hello_world/src/lib.rs", + "examples/http/wasi/hello_world/Cargo.toml", + "examples/http/wasi/hello_world/README.md" + ], + "required": true, + "description": "HTTP WASI Hello World example — reference pattern (docs)" }, "cdn-hello-world-blueprint": { @@ -34,7 +49,7 @@ "examples/cdn/hello_world/Cargo.toml" ], "required": true, - "description": "CDN Hello World example — base skeleton blueprint extraction (CDN Rust)" + "description": "CDN Hello World example — base skeleton blueprint (scaffold)" }, "cdn-body-blueprint": { @@ -43,7 +58,7 @@ "examples/cdn/body/Cargo.toml" ], "required": true, - "description": "CDN Body manipulation example — scaffold blueprint extraction" + "description": "CDN Body manipulation example — scaffold blueprint" }, "cdn-body-pattern": { "files": [ @@ -51,7 +66,7 @@ "examples/cdn/body/Cargo.toml" ], "required": true, - "description": "CDN Body manipulation example — docs pattern extraction" + "description": "CDN Body manipulation example — reference pattern (docs)" }, "http-key-value-blueprint": { @@ -60,15 +75,16 @@ "examples/http/wasi/key_value/Cargo.toml" ], "required": true, - "description": "HTTP WASI KV Store example — scaffold blueprint extraction" + "description": "HTTP WASI KV Store example — scaffold blueprint" }, "http-key-value-pattern": { "files": [ "examples/http/wasi/key_value/src/lib.rs", - "examples/http/wasi/key_value/Cargo.toml" + "examples/http/wasi/key_value/Cargo.toml", + "examples/http/wasi/key_value/README.md" ], "required": true, - "description": "HTTP WASI KV Store example — docs pattern extraction" + "description": "HTTP WASI KV Store example — reference pattern (docs)" }, "cdn-jwt-blueprint": { @@ -77,7 +93,7 @@ "examples/cdn/jwt/Cargo.toml" ], "required": true, - "description": "CDN JWT Auth example — scaffold blueprint extraction" + "description": "CDN JWT Auth example — scaffold blueprint" }, "cdn-jwt-pattern": { "files": [ @@ -85,7 +101,7 @@ "examples/cdn/jwt/Cargo.toml" ], "required": true, - "description": "CDN JWT Auth example — docs pattern extraction" + "description": "CDN JWT Auth example — reference pattern (docs)" }, "cdn-geoblock-blueprint": { @@ -94,7 +110,7 @@ "examples/cdn/geoblock/Cargo.toml" ], "required": true, - "description": "CDN Geoblock example — scaffold blueprint extraction" + "description": "CDN Geoblock example — scaffold blueprint" }, "cdn-geoblock-pattern": { "files": [ @@ -102,7 +118,7 @@ "examples/cdn/geoblock/Cargo.toml" ], "required": true, - "description": "CDN Geoblock example — docs pattern extraction" + "description": "CDN Geoblock example — reference pattern (docs)" }, "cdn-env-secrets-blueprint": { @@ -112,7 +128,7 @@ "examples/cdn/variables_and_secrets/README.md" ], "required": true, - "description": "CDN Variables and Secrets example — scaffold blueprint extraction" + "description": "CDN Variables and Secrets example — scaffold blueprint" }, "cdn-env-secrets-pattern": { "files": [ @@ -121,7 +137,7 @@ "examples/cdn/variables_and_secrets/README.md" ], "required": true, - "description": "CDN Variables and Secrets example — docs pattern extraction" + "description": "CDN Variables and Secrets example — reference pattern (docs)" }, "cdn-properties-blueprint": { @@ -131,7 +147,7 @@ "examples/cdn/properties/README.md" ], "required": true, - "description": "CDN Properties example — scaffold blueprint extraction" + "description": "CDN Properties example — scaffold blueprint" }, "cdn-properties-pattern": { "files": [ @@ -140,7 +156,7 @@ "examples/cdn/properties/README.md" ], "required": true, - "description": "CDN Properties example — docs pattern extraction" + "description": "CDN Properties example — reference pattern (docs)" }, "cdn-georedirect-blueprint": { @@ -150,7 +166,7 @@ "examples/cdn/geo_redirect/README.md" ], "required": true, - "description": "CDN Geo Redirect example — scaffold blueprint extraction" + "description": "CDN Geo Redirect example — scaffold blueprint" }, "cdn-georedirect-pattern": { "files": [ @@ -159,7 +175,7 @@ "examples/cdn/geo_redirect/README.md" ], "required": true, - "description": "CDN Geo Redirect example — docs pattern extraction" + "description": "CDN Geo Redirect example — reference pattern (docs)" }, "cdn-httpcall-blueprint": { @@ -169,7 +185,7 @@ "examples/cdn/http_call/README.md" ], "required": true, - "description": "CDN HTTP Call example — scaffold blueprint extraction" + "description": "CDN HTTP Call example — scaffold blueprint" }, "cdn-httpcall-pattern": { "files": [ @@ -178,7 +194,7 @@ "examples/cdn/http_call/README.md" ], "required": true, - "description": "CDN HTTP Call example — docs pattern extraction" + "description": "CDN HTTP Call example — reference pattern (docs)" }, "cdn-key-value-blueprint": { @@ -188,7 +204,7 @@ "examples/cdn/key_value/README.md" ], "required": true, - "description": "CDN KV Store example — scaffold blueprint extraction" + "description": "CDN KV Store example — scaffold blueprint" }, "cdn-key-value-pattern": { "files": [ @@ -197,7 +213,7 @@ "examples/cdn/key_value/README.md" ], "required": true, - "description": "CDN KV Store example — docs pattern extraction" + "description": "CDN KV Store example — reference pattern (docs)" }, "cdn-headers-blueprint": { @@ -207,7 +223,7 @@ "examples/cdn/headers/README.md" ], "required": true, - "description": "CDN Headers manipulation example — scaffold blueprint extraction" + "description": "CDN Headers manipulation example — scaffold blueprint" }, "cdn-headers-pattern": { "files": [ @@ -216,7 +232,7 @@ "examples/cdn/headers/README.md" ], "required": true, - "description": "CDN Headers manipulation example — docs pattern extraction" + "description": "CDN Headers manipulation example — reference pattern (docs)" }, "cdn-cors-blueprint": { @@ -226,7 +242,7 @@ "examples/cdn/cors/README.md" ], "required": true, - "description": "CDN CORS example — scaffold blueprint extraction" + "description": "CDN CORS example — scaffold blueprint" }, "cdn-cors-pattern": { "files": [ @@ -235,7 +251,7 @@ "examples/cdn/cors/README.md" ], "required": true, - "description": "CDN CORS example — docs pattern extraction" + "description": "CDN CORS example — reference pattern (docs)" }, "cdn-abtesting-blueprint": { @@ -245,7 +261,7 @@ "examples/cdn/ab_testing/README.md" ], "required": true, - "description": "CDN A/B Testing example — scaffold blueprint extraction" + "description": "CDN A/B Testing example — scaffold blueprint" }, "cdn-abtesting-pattern": { "files": [ @@ -254,7 +270,7 @@ "examples/cdn/ab_testing/README.md" ], "required": true, - "description": "CDN A/B Testing example — docs pattern extraction" + "description": "CDN A/B Testing example — reference pattern (docs)" }, "cdn-apikey-blueprint": { @@ -264,7 +280,7 @@ "examples/cdn/api_key/README.md" ], "required": true, - "description": "CDN API Key validation example — scaffold blueprint extraction" + "description": "CDN API Key validation example — scaffold blueprint" }, "cdn-apikey-pattern": { "files": [ @@ -273,7 +289,7 @@ "examples/cdn/api_key/README.md" ], "required": true, - "description": "CDN API Key validation example — docs pattern extraction" + "description": "CDN API Key validation example — reference pattern (docs)" }, "cdn-custom-error-pages-blueprint": { @@ -283,7 +299,7 @@ "examples/cdn/custom_error_pages/README.md" ], "required": true, - "description": "CDN Custom Error Pages example — scaffold blueprint extraction" + "description": "CDN Custom Error Pages example — scaffold blueprint" }, "cdn-custom-error-pages-pattern": { "files": [ @@ -292,7 +308,7 @@ "examples/cdn/custom_error_pages/README.md" ], "required": true, - "description": "CDN Custom Error Pages example — docs pattern extraction" + "description": "CDN Custom Error Pages example — reference pattern (docs)" }, "cdn-large-env-variable-blueprint": { @@ -302,7 +318,7 @@ "examples/cdn/large_env_variable/README.md" ], "required": true, - "description": "CDN Large Env Variable / Dictionary example — scaffold blueprint extraction" + "description": "CDN Large Env Variable / Dictionary example — scaffold blueprint" }, "cdn-large-env-variable-pattern": { "files": [ @@ -311,7 +327,7 @@ "examples/cdn/large_env_variable/README.md" ], "required": true, - "description": "CDN Large Env Variable / Dictionary example — docs pattern extraction" + "description": "CDN Large Env Variable / Dictionary example — reference pattern (docs)" }, "cdn-cache-control-blueprint": { @@ -321,7 +337,7 @@ "examples/cdn/cache_control/README.md" ], "required": true, - "description": "CDN Cache Control example — scaffold blueprint extraction" + "description": "CDN Cache Control example — scaffold blueprint" }, "cdn-cache-control-pattern": { "files": [ @@ -330,7 +346,453 @@ "examples/cdn/cache_control/README.md" ], "required": true, - "description": "CDN Cache Control example — docs pattern extraction" + "description": "CDN Cache Control example — reference pattern (docs)" + }, + + "cdn-convert-image-blueprint": { + "files": [ + "examples/cdn/convert_image/src/lib.rs", + "examples/cdn/convert_image/Cargo.toml", + "examples/cdn/convert_image/README.md" + ], + "required": true, + "description": "CDN Image Conversion example — scaffold blueprint" + }, + "cdn-convert-image-pattern": { + "files": [ + "examples/cdn/convert_image/src/lib.rs", + "examples/cdn/convert_image/Cargo.toml", + "examples/cdn/convert_image/README.md" + ], + "required": true, + "description": "CDN Image Conversion example — reference pattern (docs)" + }, + + "cdn-custom-blueprint": { + "files": [ + "examples/cdn/custom/src/lib.rs", + "examples/cdn/custom/Cargo.toml", + "examples/cdn/custom/README.md" + ], + "required": true, + "description": "CDN Custom Response example — scaffold blueprint" + }, + "cdn-custom-pattern": { + "files": [ + "examples/cdn/custom/src/lib.rs", + "examples/cdn/custom/Cargo.toml", + "examples/cdn/custom/README.md" + ], + "required": true, + "description": "CDN Custom Response example — reference pattern (docs)" + }, + + "cdn-log-time-blueprint": { + "files": [ + "examples/cdn/log_time/src/lib.rs", + "examples/cdn/log_time/Cargo.toml", + "examples/cdn/log_time/README.md" + ], + "required": true, + "description": "CDN Log Time example — scaffold blueprint" + }, + "cdn-log-time-pattern": { + "files": [ + "examples/cdn/log_time/src/lib.rs", + "examples/cdn/log_time/Cargo.toml", + "examples/cdn/log_time/README.md" + ], + "required": true, + "description": "CDN Log Time example — reference pattern (docs)" + }, + + "cdn-md2html-blueprint": { + "files": [ + "examples/cdn/md2html/src/lib.rs", + "examples/cdn/md2html/Cargo.toml", + "examples/cdn/md2html/README.md" + ], + "required": true, + "description": "CDN Markdown-to-HTML example — scaffold blueprint" + }, + "cdn-md2html-pattern": { + "files": [ + "examples/cdn/md2html/src/lib.rs", + "examples/cdn/md2html/Cargo.toml", + "examples/cdn/md2html/README.md" + ], + "required": true, + "description": "CDN Markdown-to-HTML example — reference pattern (docs)" + }, + + "http-basic-hello-world-pattern": { + "files": [ + "examples/http/basic/hello_world/src/lib.rs", + "examples/http/basic/hello_world/Cargo.toml", + "examples/http/basic/hello_world/README.md" + ], + "required": true, + "description": "HTTP Basic Hello World example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-api-wrapper-pattern": { + "files": [ + "examples/http/basic/api_wrapper/src/lib.rs", + "examples/http/basic/api_wrapper/Cargo.toml", + "examples/http/basic/api_wrapper/README.md" + ], + "required": true, + "description": "HTTP Basic API Wrapper example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-backend-pattern": { + "files": [ + "examples/http/basic/backend/src/lib.rs", + "examples/http/basic/backend/Cargo.toml", + "examples/http/basic/backend/README.md" + ], + "required": true, + "description": "HTTP Basic Backend Proxy example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-cache-pattern": { + "files": [ + "examples/http/basic/cache/src/lib.rs", + "examples/http/basic/cache/Cargo.toml", + "examples/http/basic/cache/README.md" + ], + "required": true, + "description": "HTTP Basic Cache example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-markdown-render-pattern": { + "files": [ + "examples/http/basic/markdown_render/src/lib.rs", + "examples/http/basic/markdown_render/Cargo.toml", + "examples/http/basic/markdown_render/README.md" + ], + "required": true, + "description": "HTTP Basic Markdown Render example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-outbound-fetch-pattern": { + "files": [ + "examples/http/basic/outbound_fetch/src/lib.rs", + "examples/http/basic/outbound_fetch/Cargo.toml", + "examples/http/basic/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP Basic Outbound Fetch example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-print-pattern": { + "files": [ + "examples/http/basic/print/src/lib.rs", + "examples/http/basic/print/Cargo.toml", + "examples/http/basic/print/README.md" + ], + "required": true, + "description": "HTTP Basic Print / Logging example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-s3upload-pattern": { + "files": [ + "examples/http/basic/s3upload/src/lib.rs", + "examples/http/basic/s3upload/Cargo.toml", + "examples/http/basic/s3upload/README.md" + ], + "required": true, + "description": "HTTP Basic S3 Upload example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-secret-pattern": { + "files": [ + "examples/http/basic/secret/src/lib.rs", + "examples/http/basic/secret/Cargo.toml", + "examples/http/basic/secret/README.md" + ], + "required": true, + "description": "HTTP Basic Secrets example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-smart-switch-pattern": { + "files": [ + "examples/http/basic/smart_switch/src/lib.rs", + "examples/http/basic/smart_switch/Cargo.toml", + "examples/http/basic/smart_switch/README.md" + ], + "required": true, + "description": "HTTP Basic Smart Switch / Routing example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-basic-watermark-pattern": { + "files": [ + "examples/http/basic/watermark/src/lib.rs", + "examples/http/basic/watermark/Cargo.toml", + "examples/http/basic/watermark/README.md" + ], + "required": true, + "description": "HTTP Basic Watermark example (legacy sync handler) — reference pattern (docs only, not scaffold)" + }, + + "http-wasi-ab-testing-blueprint": { + "files": [ + "examples/http/wasi/ab_testing/src/lib.rs", + "examples/http/wasi/ab_testing/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI A/B Testing example — scaffold blueprint" + }, + "http-wasi-ab-testing-pattern": { + "files": [ + "examples/http/wasi/ab_testing/src/lib.rs", + "examples/http/wasi/ab_testing/Cargo.toml", + "examples/http/wasi/ab_testing/README.md" + ], + "required": true, + "description": "HTTP WASI A/B Testing example — reference pattern (docs)" + }, + + "http-wasi-bloom-filter-denylist-blueprint": { + "files": [ + "examples/http/wasi/bloom_filter_denylist/src/lib.rs", + "examples/http/wasi/bloom_filter_denylist/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Bloom Filter Denylist example — scaffold blueprint" + }, + "http-wasi-bloom-filter-denylist-pattern": { + "files": [ + "examples/http/wasi/bloom_filter_denylist/src/lib.rs", + "examples/http/wasi/bloom_filter_denylist/Cargo.toml", + "examples/http/wasi/bloom_filter_denylist/README.md" + ], + "required": true, + "description": "HTTP WASI Bloom Filter Denylist example — reference pattern (docs)" + }, + + "http-wasi-cache-blueprint": { + "files": [ + "examples/http/wasi/cache/src/lib.rs", + "examples/http/wasi/cache/Cargo.toml", + "examples/http/wasi/cache/README.md" + ], + "required": true, + "description": "HTTP WASI Cache example — scaffold blueprint" + }, + "http-wasi-cache-pattern": { + "files": [ + "examples/http/wasi/cache/src/lib.rs", + "examples/http/wasi/cache/Cargo.toml", + "examples/http/wasi/cache/README.md" + ], + "required": true, + "description": "HTTP WASI Cache example — reference pattern (docs)" + }, + + "http-wasi-diagnostic-logging-blueprint": { + "files": [ + "examples/http/wasi/diagnostic_logging/src/lib.rs", + "examples/http/wasi/diagnostic_logging/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Diagnostic Logging example — scaffold blueprint" + }, + "http-wasi-diagnostic-logging-pattern": { + "files": [ + "examples/http/wasi/diagnostic_logging/src/lib.rs", + "examples/http/wasi/diagnostic_logging/Cargo.toml", + "examples/http/wasi/diagnostic_logging/README.md" + ], + "required": true, + "description": "HTTP WASI Diagnostic Logging example — reference pattern (docs)" + }, + + "http-wasi-geo-redirect-blueprint": { + "files": [ + "examples/http/wasi/geo_redirect/src/lib.rs", + "examples/http/wasi/geo_redirect/Cargo.toml", + "examples/http/wasi/geo_redirect/README.md" + ], + "required": true, + "description": "HTTP WASI Geo Redirect example — scaffold blueprint" + }, + "http-wasi-geo-redirect-pattern": { + "files": [ + "examples/http/wasi/geo_redirect/src/lib.rs", + "examples/http/wasi/geo_redirect/Cargo.toml", + "examples/http/wasi/geo_redirect/README.md" + ], + "required": true, + "description": "HTTP WASI Geo Redirect example — reference pattern (docs)" + }, + + "http-wasi-headers-blueprint": { + "files": [ + "examples/http/wasi/headers/src/lib.rs", + "examples/http/wasi/headers/Cargo.toml", + "examples/http/wasi/headers/README.md" + ], + "required": true, + "description": "HTTP WASI Headers Manipulation example — scaffold blueprint" + }, + "http-wasi-headers-pattern": { + "files": [ + "examples/http/wasi/headers/src/lib.rs", + "examples/http/wasi/headers/Cargo.toml", + "examples/http/wasi/headers/README.md" + ], + "required": true, + "description": "HTTP WASI Headers Manipulation example — reference pattern (docs)" + }, + + "http-wasi-large-env-variable-blueprint": { + "files": [ + "examples/http/wasi/large_env_variable/src/lib.rs", + "examples/http/wasi/large_env_variable/Cargo.toml", + "examples/http/wasi/large_env_variable/README.md" + ], + "required": true, + "description": "HTTP WASI Large Env Variable / Dictionary example — scaffold blueprint" + }, + "http-wasi-large-env-variable-pattern": { + "files": [ + "examples/http/wasi/large_env_variable/src/lib.rs", + "examples/http/wasi/large_env_variable/Cargo.toml", + "examples/http/wasi/large_env_variable/README.md" + ], + "required": true, + "description": "HTTP WASI Large Env Variable / Dictionary example — reference pattern (docs)" + }, + + "http-wasi-outbound-fetch-blueprint": { + "files": [ + "examples/http/wasi/outbound_fetch/src/lib.rs", + "examples/http/wasi/outbound_fetch/Cargo.toml", + "examples/http/wasi/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Outbound Fetch example — scaffold blueprint" + }, + "http-wasi-outbound-fetch-pattern": { + "files": [ + "examples/http/wasi/outbound_fetch/src/lib.rs", + "examples/http/wasi/outbound_fetch/Cargo.toml", + "examples/http/wasi/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Outbound Fetch example — reference pattern (docs)" + }, + + "http-wasi-outbound-modify-response-blueprint": { + "files": [ + "examples/http/wasi/outbound_modify_response/src/lib.rs", + "examples/http/wasi/outbound_modify_response/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Outbound Modify Response example — scaffold blueprint" + }, + "http-wasi-outbound-modify-response-pattern": { + "files": [ + "examples/http/wasi/outbound_modify_response/src/lib.rs", + "examples/http/wasi/outbound_modify_response/Cargo.toml", + "examples/http/wasi/outbound_modify_response/README.md" + ], + "required": true, + "description": "HTTP WASI Outbound Modify Response example — reference pattern (docs)" + }, + + "http-wasi-secret-rollover-blueprint": { + "files": [ + "examples/http/wasi/secret_rollover/src/lib.rs", + "examples/http/wasi/secret_rollover/Cargo.toml", + "examples/http/wasi/secret_rollover/README.md" + ], + "required": true, + "description": "HTTP WASI Secret Rollover example — scaffold blueprint" + }, + "http-wasi-secret-rollover-pattern": { + "files": [ + "examples/http/wasi/secret_rollover/src/lib.rs", + "examples/http/wasi/secret_rollover/Cargo.toml", + "examples/http/wasi/secret_rollover/README.md" + ], + "required": true, + "description": "HTTP WASI Secret Rollover example — reference pattern (docs)" + }, + + "http-wasi-simple-fetch-blueprint": { + "files": [ + "examples/http/wasi/simple_fetch/src/lib.rs", + "examples/http/wasi/simple_fetch/Cargo.toml", + "examples/http/wasi/simple_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Simple Fetch example — scaffold blueprint" + }, + "http-wasi-simple-fetch-pattern": { + "files": [ + "examples/http/wasi/simple_fetch/src/lib.rs", + "examples/http/wasi/simple_fetch/Cargo.toml", + "examples/http/wasi/simple_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Simple Fetch example — reference pattern (docs)" + }, + + "http-wasi-static-assets-blueprint": { + "files": [ + "examples/http/wasi/static_assets/src/lib.rs", + "examples/http/wasi/static_assets/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Static Assets example — scaffold blueprint" + }, + "http-wasi-static-assets-pattern": { + "files": [ + "examples/http/wasi/static_assets/src/lib.rs", + "examples/http/wasi/static_assets/Cargo.toml", + "examples/http/wasi/static_assets/README.md" + ], + "required": true, + "description": "HTTP WASI Static Assets example — reference pattern (docs)" + }, + + "http-wasi-streaming-blueprint": { + "files": [ + "examples/http/wasi/streaming/src/lib.rs", + "examples/http/wasi/streaming/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Streaming Response example — scaffold blueprint" + }, + "http-wasi-streaming-pattern": { + "files": [ + "examples/http/wasi/streaming/src/lib.rs", + "examples/http/wasi/streaming/Cargo.toml", + "examples/http/wasi/streaming/README.md" + ], + "required": true, + "description": "HTTP WASI Streaming Response example — reference pattern (docs)" + }, + + "http-wasi-variables-and-secrets-blueprint": { + "files": [ + "examples/http/wasi/variables_and_secrets/src/lib.rs", + "examples/http/wasi/variables_and_secrets/Cargo.toml", + "examples/http/wasi/variables_and_secrets/README.md" + ], + "required": true, + "description": "HTTP WASI Variables and Secrets example — scaffold blueprint" + }, + "http-wasi-variables-and-secrets-pattern": { + "files": [ + "examples/http/wasi/variables_and_secrets/src/lib.rs", + "examples/http/wasi/variables_and_secrets/Cargo.toml", + "examples/http/wasi/variables_and_secrets/README.md" + ], + "required": true, + "description": "HTTP WASI Variables and Secrets example — reference pattern (docs)" } }, "target_mapping": { @@ -347,10 +809,19 @@ "section": null }, + "quickstart": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md", + "section": null + }, + "http-hello-world-blueprint": { "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/base-rust.md", "section": null }, + "http-hello-world-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", + "section": null + }, "cdn-hello-world-blueprint": { "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-rust.md", @@ -367,11 +838,11 @@ }, "http-key-value-blueprint": { - "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-rust.md", + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-wasi-rust.md", "section": null }, "http-key-value-pattern": { - "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-rust.md", + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "section": null }, @@ -499,6 +970,223 @@ "cdn-cache-control-pattern": { "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md", "section": null + }, + + "cdn-convert-image-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/convert-image-rust.md", + "section": null + }, + "cdn-convert-image-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", + "section": null + }, + + "cdn-custom-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-rust.md", + "section": null + }, + "cdn-custom-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", + "section": null + }, + + "cdn-log-time-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/log-time-rust.md", + "section": null + }, + "cdn-log-time-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md", + "section": null + }, + + "cdn-md2html-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/md2html-rust.md", + "section": null + }, + "cdn-md2html-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", + "section": null + }, + + "http-basic-hello-world-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md", + "section": null + }, + + "http-basic-api-wrapper-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md", + "section": null + }, + + "http-basic-backend-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", + "section": null + }, + + "http-basic-cache-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", + "section": null + }, + + "http-basic-markdown-render-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", + "section": null + }, + + "http-basic-outbound-fetch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", + "section": null + }, + + "http-basic-print-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", + "section": null + }, + + "http-basic-s3upload-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", + "section": null + }, + + "http-basic-secret-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", + "section": null + }, + + "http-basic-smart-switch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", + "section": null + }, + + "http-basic-watermark-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md", + "section": null + }, + + "http-wasi-ab-testing-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-wasi-rust.md", + "section": null + }, + "http-wasi-ab-testing-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md", + "section": null + }, + + "http-wasi-bloom-filter-denylist-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md", + "section": null + }, + "http-wasi-bloom-filter-denylist-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", + "section": null + }, + + "http-wasi-cache-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/cache-wasi-rust.md", + "section": null + }, + "http-wasi-cache-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", + "section": null + }, + + "http-wasi-diagnostic-logging-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md", + "section": null + }, + "http-wasi-diagnostic-logging-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", + "section": null + }, + + "http-wasi-geo-redirect-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-wasi-rust.md", + "section": null + }, + "http-wasi-geo-redirect-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", + "section": null + }, + + "http-wasi-headers-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/headers-wasi-rust.md", + "section": null + }, + "http-wasi-headers-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md", + "section": null + }, + + "http-wasi-large-env-variable-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/large-env-variable-wasi-rust.md", + "section": null + }, + "http-wasi-large-env-variable-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md", + "section": null + }, + + "http-wasi-outbound-fetch-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md", + "section": null + }, + "http-wasi-outbound-fetch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md", + "section": null + }, + + "http-wasi-outbound-modify-response-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md", + "section": null + }, + "http-wasi-outbound-modify-response-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", + "section": null + }, + + "http-wasi-secret-rollover-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rollover-wasi-rust.md", + "section": null + }, + "http-wasi-secret-rollover-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", + "section": null + }, + + "http-wasi-simple-fetch-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/simple-fetch-wasi-rust.md", + "section": null + }, + "http-wasi-simple-fetch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", + "section": null + }, + + "http-wasi-static-assets-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-wasi-rust.md", + "section": null + }, + "http-wasi-static-assets-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md", + "section": null + }, + + "http-wasi-streaming-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-wasi-rust.md", + "section": null + }, + "http-wasi-streaming-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", + "section": null + }, + + "http-wasi-variables-and-secrets-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md", + "section": null + }, + "http-wasi-variables-and-secrets-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md", + "section": null } }, "validation": { diff --git a/llms.txt b/llms.txt index 00742d5..865b16d 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # fastedge -> Documentation for the `fastedge` crate (v0.3.5) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. +> Documentation for the `fastedge` crate (v0.4.0) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. ## Documentation