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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
6 changes: 6 additions & 0 deletions docs/CDN_APPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {}
Expand Down
2 changes: 1 addition & 1 deletion docs/INDEX.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
90 changes: 45 additions & 45 deletions docs/SDK_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand All @@ -25,7 +25,7 @@ For `#[fastedge::http]` (basic):

```toml
[dependencies]
fastedge = "0.3.5"
fastedge = "0.4.0"
anyhow = "1"

[lib]
Expand Down Expand Up @@ -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<u8>)` | `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<Body, serde_json::Error>` |
| 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<u8>)` | `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<Body, serde_json::Error>` |

```rust
use fastedge::body::Body;
Expand All @@ -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;
Expand All @@ -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`:

Expand All @@ -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>` / `&[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>` / `&[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};

Expand Down Expand Up @@ -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};
Expand All @@ -401,23 +401,23 @@ fn fetch(uri: &str) -> Result<String, Error> {

## 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 }
```

---
Expand All @@ -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` |

---

Expand Down
4 changes: 2 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Add dependencies to `Cargo.toml`:

```toml
[dependencies]
fastedge = "0.3.5"
fastedge = "0.4.0"
anyhow = "1"

[lib]
Expand Down Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 <example-name>
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).
69 changes: 65 additions & 4 deletions examples/http/basic/api_wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <your-password>
```

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/<DEVICE>/status` → parse `components.main.switch.switch.value` (`"on"` or `"off"`)
6. `POST /v1/devices/<DEVICE>/commands` with the opposite command
7. Return 204 on `ACCEPTED`, or the upstream status code on error
6 changes: 6 additions & 0 deletions examples/http/basic/api_wrapper/fixtures/happy-path.live.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"expected": {
"status": 401,
"body": ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"expected": {
"status": 405,
"headers": {
"allow": "GET, HEAD"
},
"body": "This method is not allowed\n"
}
}
Original file line number Diff line number Diff line change
@@ -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": "."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"expected": {
"status": 500,
"body": "Misconfigured app\n"
}
}
Loading
Loading