Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/
8 changes: 7 additions & 1 deletion docs/CDN_APPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ crate-type = ["cdylib"]

[dependencies]
proxy-wasm = "0.2"
fastedge = { version = "0.3", features = ["proxywasm"] }
fastedge = { version = "0.4", features = ["proxywasm"] }
```

The `proxywasm` feature flag is required to access `fastedge::proxywasm::*`. Without it, `fastedge` only exposes Component Model APIs, which are not available in the proxy-wasm environment.
Expand Down 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
12 changes: 6 additions & 6 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 @@ -124,10 +124,10 @@ The compiled `.wasm` file is written to `target/wasm32-wasip1/release/`.

## Build

| Handler path | Build command |
| ----------------------- | ---------------------------------------------- |
| Async (`wstd`) | `cargo build --target wasm32-wasip2 --release` |
| Sync (`fastedge::http`) | `cargo build --target wasm32-wasip1 --release` |
| Handler path | Build command |
| ----------------------- | ------------------------------------------------ |
| Async (`wstd`) | `cargo build --target wasm32-wasip2 --release` |
| Sync (`fastedge::http`) | `cargo build --target wasm32-wasip1 --release` |

Both commands produce a `.wasm` binary in the respective `target/<target>/release/` directory. Neither path requires `cargo-component`.

Expand All @@ -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
22 changes: 18 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,19 @@ 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`.

Each example depends on the [`fastedge`](https://crates.io/crates/fastedge) crate from crates.io
(except a handful of `http/wasi/*` examples that only need `wstd`).
Comment thread
godronus marked this conversation as resolved.
Outdated
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"
}
}
Loading
Loading