diff --git a/.cargo/config.toml b/.cargo/config.toml index dc0be73..6b509f5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [build] -target = "wasm32-wasip1" \ No newline at end of file +target = "wasm32-wasip1" diff --git a/AGENTS.md b/AGENTS.md index 0f56e6e..c997212 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,826 +1,42 @@ -# AGENTS.md - AI Coding Agent Guide - -## Project Overview - -**FastEdge Rust SDK** is a library for building edge computing applications using WebAssembly. It provides a dual-API approach supporting both the WebAssembly Component Model and ProxyWasm specifications. - -### Quick Facts - -- **Language**: Rust (Edition 2021) -- **Target**: `wasm32-wasip1` (WebAssembly System Interface Preview 1) -- **License**: Apache-2.0 -- **Current Version**: 0.3.2 -- **Primary Maintainer**: G-Core (FastEdge Development Team) -- **Repository**: https://github.com/G-Core/FastEdge-sdk-rust - ---- - -## Project Structure - -``` -FastEdge-sdk-rust/ -├── Cargo.toml # Workspace manifest -├── src/ # Core SDK implementation -│ ├── lib.rs # Main library entry point -│ ├── http_client.rs # Outbound HTTP client implementation -│ ├── helper.rs # Internal helper functions -│ └── proxywasm/ # ProxyWasm API implementations -│ ├── mod.rs -│ ├── key_value.rs -│ ├── secret.rs -│ ├── dictionary.rs -│ └── utils.rs -├── derive/ # Procedural macros -│ ├── Cargo.toml -│ └── src/ -│ └── lib.rs # #[fastedge::http] attribute macro -├── wit/ # WebAssembly Interface Types definitions -│ ├── world.wit # Main world definition -│ ├── http-handler.wit # HTTP handler interface -│ ├── http-client.wit # HTTP client interface -│ ├── key-value.wit # Key-value store interface -│ ├── secret.wit # Secret management interface -│ ├── dictionary.wit # Dictionary interface -│ └── utils.wit # Utility functions -├── examples/ # Example applications -│ ├── backend/ # Backend proxy example -│ ├── key-value/ # Key-value store usage -│ ├── secret/ # Secret access example -│ ├── markdown-render/ # Markdown to HTML converter -│ ├── api-wrapper/ # API wrapping example -│ ├── watermark/ # Image watermarking -│ ├── print/ # Simple print example -│ └── dummy/ # Minimal example -└── wasi-nn/ # WASI Neural Network interface (submodule) -``` - ---- - -## Architecture & Design Patterns - -### 1. Component Model vs ProxyWasm - -The SDK supports two runtime models: - -**Component Model (Default)**: -- Uses WIT (WebAssembly Interface Types) bindings via `wit-bindgen` -- Modern WebAssembly component model -- Type-safe interfaces -- Generated bindings in `src/lib.rs` via `wit_bindgen::generate!` macro - -**ProxyWasm (Feature Flag)**: -- Enabled with `features = ["proxywasm"]` -- Uses FFI (Foreign Function Interface) with `extern "C"` functions -- Compatible with Envoy and other proxy-wasm hosts -- Implementation in `src/proxywasm/` directory - -### 2. Core Design Patterns - -#### Attribute Macro Pattern - -The `#[fastedge::http]` macro transforms a regular Rust function into a WebAssembly component export: - -```rust -// User writes: -#[fastedge::http] -fn main(req: Request) -> Result> { ... } - -// Macro generates: -struct Component; -impl Guest for Component { - fn process(req: ::fastedge::http_handler::Request) -> ::fastedge::http_handler::Response { - // Converts bindgen types to http crate types - // Calls user function - // Converts result back to bindgen types - } -} -``` - -**Location**: `derive/src/lib.rs` - -#### Type Conversion Pattern - -The SDK bridges between three type systems: -1. Standard Rust `http` crate types (user-facing) -2. WIT-generated bindgen types (runtime interface) -3. Internal `Body` type with content-type awareness - -**Key Conversions** (`src/lib.rs` lines 200-275): -- `impl From for ::http::Method` -- `impl TryFrom for ::http::Request` -- `impl From<::http::Response> for Response` -- `impl TryFrom for ::http::Response` - -#### Body Type Pattern - -The `Body` type wraps `bytes::Bytes` and tracks content type: - -```rust -pub struct Body { - pub(crate) content_type: String, - pub(crate) inner: Bytes, -} -``` - -**Key Features**: -- Implements `Deref` to `Bytes` for transparent access -- Automatic content-type assignment based on input type -- Optional JSON support via feature flag -- Factory methods: `empty()`, `from()` conversions - --- - -## WIT Interface Definitions - -### World Definition - -**File**: `wit/world.wit` - -```wit -world reactor { - import http; // HTTP types and utilities - import http-client; // Outbound HTTP requests - import dictionary; // Fast read-only config - import secret; // Encrypted secret access - import key-value; // Persistent storage - import utils; // Diagnostics and stats - - export http-handler; // Main application entry point -} -``` - -### Key Interfaces - -#### HTTP Handler (`wit/http-handler.wit`) -```wit -interface http-handler { - use http.{request, response}; - process: func(req: request) -> response; -} -``` - -#### Key-Value Store (`wit/key-value.wit`) -- Resource-based API (`resource store`) -- Operations: `open`, `get`, `scan`, `zrange-by-score`, `zscan`, `bf-exists` -- Errors: `no-such-store`, `access-denied`, `internal-error` - -#### Secret (`wit/secret.wit`) -- `get(key: string) -> result, error>` -- `get-effective-at(key: string, at: u32) -> result, error>` -- Supports versioned secrets with time-based retrieval - +doc_type: policy +audience: bot +lang: en +tags: ['ai-agents', 'rules', 'critical', 'codex'] +last_modified: 2026-04-02T00:00:00Z +copyright: '© 2026 gcore.com' --- -## Dependencies - -### Core Dependencies - -```toml -fastedge-derive = { path = "derive", version = "0.3" } # Procedural macros -http = "1.3" # HTTP types -bytes = "1.10" # Byte buffer -wit-bindgen = "0.46" # WIT bindings generator -thiserror = "2.0" # Error derive macros -mime = "^0.3" # MIME type constants -serde_json = { version = "^1.0", optional = true } # JSON support -``` - -### Important Considerations - -1. **http crate**: Version 1.3+ required for modern HTTP types -2. **wit-bindgen**: Version 0.46 - must match Wasmtime runtime version -3. **bytes**: Used for zero-copy buffer management -4. **serde_json**: Only included with `json` feature flag - ---- - -## Feature Flags - -### Available Features - -```toml -[features] -default = ["proxywasm"] -proxywasm = [] # Enable ProxyWasm compatibility layer -json = ["serde_json"] # Enable JSON body support -``` - -### Usage Patterns - -**ProxyWasm Feature**: -```rust -#[cfg(feature = "proxywasm")] -pub mod proxywasm; // Conditionally compiled -``` - -**JSON Feature**: -```rust -#[cfg(feature = "json")] -impl TryFrom for Body { - type Error = serde_json::Error; - fn try_from(value: serde_json::Value) -> Result { - Ok(Body { - content_type: mime::APPLICATION_JSON.to_string(), - inner: Bytes::from(serde_json::to_vec(&value)?), - }) - } -} -``` +RULES FOR AI AGENTS +====================== ---- - -## Common Development Patterns - -### 1. Creating a New Example - -```bash -# Create example directory -cd examples -mkdir my-example -cd my-example - -# Create Cargo.toml -cat > Cargo.toml << 'EOF' -[package] -name = "my-example" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = { path = "../.." } -anyhow = "1.0" -EOF - -# Create src/lib.rs -mkdir src -cat > src/lib.rs << 'EOF' -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; - -#[fastedge::http] -fn main(req: Request) -> Result> { - Response::builder() - .status(StatusCode::OK) - .body(Body::from("Hello from example")) - .map_err(Into::into) -} -EOF -``` - -### 2. Building Examples - -```bash -# From workspace root -cargo build --target wasm32-wasip1 --release --package my-example - -# Output location -ls target/wasm32-wasip1/release/my_example.wasm -``` - -### 3. Adding New WIT Interfaces - -When adding new WIT interfaces: - -1. Create `.wit` file in `wit/` directory -2. Update `wit/world.wit` to import/export the interface -3. Regenerate bindings by running `cargo build` -4. Add Rust wrapper module in `src/` to expose nice API -5. Add documentation in module-level docs - -Example pattern: -```rust -// src/lib.rs -pub mod my_interface { - #[doc(inline)] - pub use crate::gcore::fastedge::my_interface::MyFunction; -} -``` - -### 4. Error Handling Pattern - -Always use `Result` types with `anyhow` for application code: - -```rust -use anyhow::{Result, anyhow, Context}; - -#[fastedge::http] -fn main(req: Request) -> Result> { - let value = some_operation() - .context("Failed to perform operation")?; - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(value))?) -} -``` - -### 5. Testing Pattern - -Unit tests should focus on business logic, not the handler: - -```rust -// lib.rs -fn process_data(input: &str) -> Result { - // Business logic here -} - -#[fastedge::http] -fn main(req: Request) -> Result> { - let input = String::from_utf8_lossy(&req.body()); - let output = process_data(&input)?; - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from(output))?) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_process_data() { - let result = process_data("input").unwrap(); - assert_eq!(result, "expected"); - } -} -``` - ---- - -## Code Generation - -### WIT Binding Generation - -The SDK uses `wit-bindgen` to generate Rust code from WIT files: - -**In `src/lib.rs`**: -```rust -wit_bindgen::generate!({ - world: "reactor", - path: "wit", - pub_export_macro: true, -}); -``` - -This generates: -- Type definitions matching WIT interfaces -- Import function signatures -- Export trait definitions -- Conversion utilities - -**Generated modules** (not in source tree): -- `gcore::fastedge::http` -- `gcore::fastedge::http_client` -- `gcore::fastedge::key_value` -- `gcore::fastedge::secret` -- `gcore::fastedge::dictionary` -- `gcore::fastedge::utils` -- `exports::gcore::fastedge::http_handler` - -### Procedural Macro Implementation - -**File**: `derive/src/lib.rs` - -The `#[fastedge::http]` macro uses `syn` for parsing and `quote` for code generation: - -```rust -#[proc_macro_attribute] -pub fn http(_attr: TokenStream, item: TokenStream) -> TokenStream { - let func = parse_macro_input!(item as ItemFn); - let func_name = &func.sig.ident; - - quote!( - // Generated code structure: - // 1. Define internal_error helper - // 2. Preserve original function with #[no_mangle] - // 3. Implement Guest trait - // 4. Export component - ).into() -} -``` - -**Key aspects**: -- Preserves original function for potential testing -- Wraps in error handling -- Converts types between user API and bindgen API -- Exports via `fastedge::export!` macro - ---- - -## ProxyWasm Implementation - -When `proxywasm` feature is enabled, the SDK provides FFI bindings to ProxyWasm host functions. - -### FFI Functions (`src/proxywasm/mod.rs`) - -```rust -extern "C" { - fn proxy_secret_get(...) -> u32; - fn proxy_secret_get_effective_at(...) -> u32; - fn proxy_dictionary_get(...) -> u32; - fn proxy_kv_store_open(...) -> u32; - fn proxy_kv_store_get(...) -> u32; - fn proxy_kv_store_zrange_by_score(...) -> u32; - fn proxy_kv_store_scan(...) -> u32; - // etc. -} -``` - -### Wrapper Pattern - -Each ProxyWasm module wraps FFI calls in safe Rust: - -```rust -// src/proxywasm/secret.rs -pub fn get(key: &str) -> Result>, Error> { - let mut return_data: *mut u8 = std::ptr::null_mut(); - let mut return_size: usize = 0; - - let status = unsafe { - proxy_secret_get( - key.as_ptr(), - key.len(), - &mut return_data, - &mut return_size, - ) - }; - - // Convert status code to Result - // Copy data from host memory - // Return safe Rust types -} -``` - ---- +TL;DR: Keep command output short. Do not take actions unless asked. +Do not waste tokens on experiments. Do only what is asked. -## Build System +COMMUNICATION STYLE +=================== -### Workspace Configuration +- Use English by default; if the user writes in another language, use that language +- Use an informal tone, avoid formal business language +- Question ideas and suggest alternatives — do not just agree with everything +- Think for yourself instead of agreeing to be polite -**Root `Cargo.toml`**: -```toml -[workspace] -members = ["derive", ".", "examples/*"] +INVARIANTS +========== -[workspace.package] -version = "0.3.2" -edition = "2021" -# ... shared metadata -``` +- NEVER do anything beyond the assigned task +- NEVER change code that was not asked to change +- NEVER "improve" or "optimize" without a clear request +- NEVER use scripts for mass code replacements +- NEVER make architecture decisions on your own +- ALWAYS keep command output short — every extra line = wasted tokens +- ALWAYS think before acting — do not repeat checks, remember context +- ALWAYS ask an expert when the solution is not clear +- ALWAYS tell apart an observation from an action request: + observation ("works oddly") → discuss, DO NOT fix + request ("fix this") → act -**Benefits**: -- Single version number across all crates -- Shared dependency resolution -- Unified build commands - -### Build Targets - -#### Development Build -```bash -cargo build --target wasm32-wasip1 -``` - -#### Release Build -```bash -cargo build --target wasm32-wasip1 --release -``` - -#### Build Specific Example -```bash -cargo build --target wasm32-wasip1 --release --package backend -``` - -#### Check Without Building -```bash -cargo check --target wasm32-wasip1 -``` - -### Output Locations - -- Debug: `target/wasm32-wasip1/debug/*.wasm` -- Release: `target/wasm32-wasip1/release/*.wasm` - ---- - -## Error Types - -### SDK Error Type (`src/lib.rs`) - -```rust -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("method `{0}` is not supported")] - UnsupportedMethod(::http::Method), - - #[error("http error: {0}")] - BindgenHttpError(#[from] HttpError), - - #[error("http error: {0}")] - HttpError(#[from] ::http::Error), - - #[error("invalid http body")] - InvalidBody, - - #[error("invalid status code {0}")] - InvalidStatusCode(u16), -} -``` - -### Module-Specific Errors - -**Key-Value Store**: -```rust -variant error { - no-such-store, - access-denied, - internal-error, -} -``` - -**Secret**: -```rust -variant error { - access-denied, - decrypt-error, - other(string), -} -``` - ---- - -## Testing Strategy - -### Unit Tests - -Test business logic separately from handlers: - -```rust -#[cfg(test)] -mod tests { - #[test] - fn test_logic() { - // Test pure functions - } -} -``` - -### Integration Tests - -Integration tests would require a FastEdge runtime environment. Consider: -- Using `wasmtime` CLI for testing -- Mocking WIT interfaces -- Testing individual functions, not the full handler - ---- - -## Documentation Standards - -### Module-Level Documentation - -```rust -//! # Module Name -//! -//! Brief description. -//! -//! ## Example -//! -//! ```rust -//! use fastedge::module::function; -//! // Example code -//! ``` -``` - -### Function Documentation - -```rust -/// Brief description of function. -/// -/// # Arguments -/// -/// * `param` - Description of parameter -/// -/// # Returns -/// -/// Description of return value -/// -/// # Errors -/// -/// When this function errors and why -/// -/// # Example -/// -/// ``` -/// let result = function(param); -/// ``` -pub fn function(param: Type) -> Result { - // Implementation -} -``` - -### Re-exports - -Use `#[doc(inline)]` for re-exported items: - -```rust -pub mod dictionary { - #[doc(inline)] - pub use crate::gcore::fastedge::dictionary::get; -} -``` - ---- - -## Common Issues & Solutions - -### Issue: WIT Binding Conflicts - -**Problem**: Changes to `.wit` files not reflected in build - -**Solution**: -```bash -cargo clean -cargo build --target wasm32-wasip1 -``` - -### Issue: Type Conversion Errors - -**Problem**: `InvalidBody` or `InvalidStatusCode` errors - -**Solution**: Ensure proper error handling when converting between types: -```rust -let response = Response::builder() - .status(StatusCode::OK) - .body(body)?; // Use ? to propagate http::Error -``` - -### Issue: ProxyWasm FFI Crashes - -**Problem**: Segfaults when using ProxyWasm features - -**Solution**: -- Check pointer validity before dereferencing -- Ensure proper memory management (host vs guest memory) -- Validate return codes from FFI calls - -### Issue: Example Won't Build - -**Problem**: `unresolved import` errors in examples - -**Solution**: Ensure example's `Cargo.toml` has correct dependencies: -```toml -[dependencies] -fastedge = { path = "../.." } -``` - ---- - -## Contribution Guidelines - -### Adding New Features - -1. **Update WIT interfaces** if adding host capabilities -2. **Implement in `src/`** following existing patterns -3. **Add ProxyWasm support** if applicable (`src/proxywasm/`) -4. **Create example** demonstrating the feature -5. **Update documentation** in module docs and README -6. **Add tests** where possible - -### Code Style - -- Follow Rust standard style (`rustfmt`) -- Use meaningful variable names -- Add inline comments for complex logic -- Keep functions focused and small -- Prefer explicit error handling over `unwrap()` - -### Commit Messages - -Follow conventional commits: -- `feat: Add new capability` -- `fix: Resolve issue with...` -- `docs: Update documentation for...` -- `refactor: Improve structure of...` -- `test: Add tests for...` - ---- - -## Deployment - -### Building for Production - -```bash -# Full release build -cargo build --target wasm32-wasip1 --release --workspace - -# Optimize WASM size (requires wasm-opt from binaryen) -wasm-opt -Oz -o output.wasm input.wasm -``` - -### Size Optimization Tips - -1. Use `opt-level = "z"` or `"s"` in `Cargo.toml`: -```toml -[profile.release] -opt-level = "z" -lto = true -codegen-units = 1 -``` - -2. Remove debug info: -```toml -[profile.release] -strip = true -``` - -3. Consider `wee_alloc` for smaller allocator (if needed) - ---- - -## Version History - -- **0.3.2** (Current): Latest stable release -- **0.3.x**: Refinements and bug fixes -- **0.2.x**: Added ProxyWasm support -- **0.1.x**: Initial release with Component Model - ---- - -## Key Contacts & Resources - -- **Repository**: https://github.com/G-Core/FastEdge-sdk-rust -- **Documentation**: https://docs.rs/fastedge -- **Platform Docs**: https://gcore.com/docs/fastedge -- **Maintainer**: FastEdge Development Team - ---- - -## Quick Reference - -### Essential Commands - -```bash -# Setup -rustup target add wasm32-wasip1 - -# Build -cargo build --target wasm32-wasip1 --release - -# Check -cargo check --target wasm32-wasip1 - -# Test (Rust tests only, no WASM) -cargo test - -# Build specific example -cargo build --target wasm32-wasip1 --release --package example-name - -# Format code -cargo fmt - -# Lint -cargo clippy --target wasm32-wasip1 -``` - -### Key Files for AI Agents - -When making changes, these files are most commonly edited: - -1. **`src/lib.rs`** - Core SDK implementation -2. **`wit/*.wit`** - Interface definitions -3. **`derive/src/lib.rs`** - Attribute macro -4. **`examples/*/src/lib.rs`** - Example applications -5. **`Cargo.toml`** - Dependencies and metadata - -### Import Patterns - -```rust -// Standard handler -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; - -// HTTP client -use fastedge::send_request; -use fastedge::http::Method; - -// Key-value store -use fastedge::key_value::{Store, Error as StoreError}; - -// Secrets -use fastedge::secret; - -// Dictionary -use fastedge::dictionary; - -// Utilities -use fastedge::utils::set_user_diag; -``` - ---- +PROJECT CONTEXT +=============== -**This document is intended for AI coding agents to understand the FastEdge Rust SDK architecture, patterns, and development practices. It should be updated whenever significant architectural changes are made.** +see CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9a6a260 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,244 @@ +# AI Agent Instructions for FastEdge Rust SDK + +## Governance (REQUIRED) + +Read `AGENTS.md` for company-wide agent rules. These are mandatory and override any conflicting behavior. Key rules: never go beyond the assigned task, never change code that was not asked to change, never "improve" or "optimize" without a clear request, always distinguish observations from action requests. + +--- + +## CRITICAL: Read Smart, Not Everything + +**DO NOT read all context files upfront.** This repository uses a **discovery-based context system** to minimize token usage while maximizing effectiveness. + +--- + +## Getting Started: Discovery Pattern + +### Step 1: Read the Index (REQUIRED — ~140 lines) + +**First action when starting work:** Read `context/CONTEXT_INDEX.md` + +This lightweight file gives you: +- Project quick start (what this repo does in 10 lines) +- Documentation map organized by topic with sizes +- Decision tree for what to read based on your task +- Search patterns for finding information + +### Step 2: Read Based on Your Task (JUST-IN-TIME) + +Use the decision tree in CONTEXT_INDEX.md to determine what to read. **Only read what's relevant to your current task.** + +**Examples:** + +**Task: "Add a new WIT interface"** +- Read: `context/architecture/RUNTIME_ARCHITECTURE.md` (WIT section + change workflow) +- Read: existing `.wit` files in `wit/` +- Grep: `context/CHANGELOG.md` for similar past changes + +**Task: "Fix the proc macro"** +- Read: `context/architecture/SDK_ARCHITECTURE.md` (attribute macro pattern) +- Read: `derive/src/lib.rs` (entry point) + +**Task: "Add ProxyWasm wrapper for new host function"** +- Read: `context/architecture/RUNTIME_ARCHITECTURE.md` (ProxyWasm FFI section) +- Read: `src/proxywasm/key_value.rs` as template +- Grep: `src/proxywasm/mod.rs` for existing FFI declarations + +**Task: "Add a new example"** +- Browse: `examples/http/wasi/` for similar existing example (**use `#[wstd::http_server]`, not `#[fastedge::http]`**) +- Read: `context/development/BUILD_AND_CI.md` (example build pattern) + +### Step 3: Search, Don't Read Everything + +**Use grep and search tools** instead of reading large files linearly: + +- **CHANGELOG.md**: Will grow over time — always grep, never read end-to-end +- **Architecture docs** (~130-170 lines): Read specific sections by heading +- **Source code**: Use module names to navigate (`src/proxywasm/`, `derive/`, etc.) + +--- + +## Decision Tree Reference + +**Quick lookup for common tasks:** + +| Task Type | What to Read | +|-----------|-------------| +| **Writing a new WASI-HTTP app** | SDK_ARCHITECTURE (wstd section) + `examples/http/wasi/` | +| **Working with basic sync handler** | SDK_ARCHITECTURE (fastedge::http section) + `examples/http/basic/` | +| **Adding a WIT interface** | RUNTIME_ARCHITECTURE (WIT + change workflow) | +| **Fixing proc macro** | SDK_ARCHITECTURE (macro section) + `derive/src/lib.rs` | +| **Adding ProxyWasm feature** | RUNTIME_ARCHITECTURE (FFI) + existing wrapper as template | +| **Adding an example** | Browse `examples/http/wasi/` (**use wstd, not fastedge::http**) | +| **Modifying HTTP client** | SDK_ARCHITECTURE (HTTP client + type conversion) | +| **Working with KV/secrets** | SDK_ARCHITECTURE (module structure) + `src/proxywasm/` | +| **Understanding the system** | PROJECT_OVERVIEW (~149 lines) | +| **Changing build/CI** | BUILD_AND_CI | +| **Modifying type conversions** | SDK_ARCHITECTURE (type conversion + body type) | +| **Updating dependencies** | PROJECT_OVERVIEW (deps table) + BUILD_AND_CI (workspace) | +| **Working with WASI-NN/ML** | RUNTIME_ARCHITECTURE (submodules) | +| **Debugging host errors** | ERROR_CODES (host codes 3100-3120) + HOST_SDK_CONTRACT (constraints) | +| **Using request properties** | PROPERTIES_REFERENCE + `examples/cdn/properties/` | +| **HTTP callout pause/resume** | REQUEST_LIFECYCLE (callout section) + HOST_SDK_CONTRACT | +| **Adding host function wrapper** | HOST_SDK_CONTRACT (FFI + memory) + RUNTIME_ARCHITECTURE (change workflow) | + +--- + +## Anti-Patterns (What NOT to Do) + +**Don't:** Read all 5 context docs upfront (~625 lines wasted if you only need one) +**Don't:** Read `src/lib.rs` (667 lines) end-to-end for a simple ProxyWasm change +**Don't:** Read DOCUMENTATION.md — it is superseded by this system +**Don't:** Read entire architecture docs when you need one specific section +**Don't:** Modify `wit/` files directly — they come from the `G-Core/FastEdge-wit` submodule + +**Do:** Read `context/CONTEXT_INDEX.md` first — always +**Do:** Use grep to search CHANGELOG and large source files +**Do:** Read `examples/` for real-world usage patterns +**Do:** Read only sections relevant to your current task +**Do:** Follow the decision tree for targeted reading + +--- + +## Critical Working Practices + +### Task Checklists (ALWAYS USE) + +When starting any non-trivial task (multi-step, multiple files, features, etc.): + +1. Use `TaskCreate` to break work into discrete steps +2. Mark tasks `in_progress` when starting, `completed` when done +3. This helps track progress and prevents missed steps + +### Parallel Agents + +For independent work, spawn parallel agents: +- Research different subsystems simultaneously +- Build multiple examples at once +- Read multiple source files concurrently + +### Documentation Maintenance + +When you make significant changes, update the relevant context docs: + +1. **After adding a feature:** Add a CHANGELOG.md entry (agent decision log) +2. **After changing architecture:** Update the relevant architecture doc +3. **After changing build config:** Update BUILD_AND_CI.md +4. **After adding a new module:** Update SDK_ARCHITECTURE.md and PROJECT_OVERVIEW.md + +**CHANGELOG entry format:** +```markdown +## [YYYY-MM-DD] — Brief Description + +### Overview +One sentence summary. + +### Decisions +- Why this approach was chosen +- What alternatives were considered + +### Changes +- Bullet list of what changed +``` + +--- + +## Context Organization + +``` +FastEdge-sdk-rust/ +├── CLAUDE.md <- YOU ARE HERE +├── context/ +│ ├── CONTEXT_INDEX.md <- Read first (discovery hub) +│ ├── PROJECT_OVERVIEW.md <- New to codebase? Start here +│ ├── CHANGELOG.md <- Agent decision log (search with grep) +│ ├── architecture/ +│ │ ├── SDK_ARCHITECTURE.md <- Core arch, types, errors, modules +│ │ ├── RUNTIME_ARCHITECTURE.md <- WIT, interfaces, ProxyWasm FFI +│ │ ├── HOST_SDK_CONTRACT.md <- ABI contract, FFI functions, constraints +│ │ └── REQUEST_LIFECYCLE.md <- Request phases, callout pause/resume +│ ├── reference/ +│ │ ├── PROPERTIES_REFERENCE.md <- Request properties (geo, IP, host, etc.) +│ │ └── ERROR_CODES.md <- Host status codes, SDK errors +│ └── development/ +│ └── BUILD_AND_CI.md <- Build system, CI, release, examples +├── src/ <- Rust source (core SDK) +│ ├── lib.rs <- Entry point, type conversions +│ ├── http_client.rs <- Outbound HTTP +│ ├── helper.rs <- Internal utilities +│ └── proxywasm/ <- ProxyWasm FFI wrappers +├── derive/ <- Proc macro crate +├── wit/ <- WIT definitions (submodule) +├── wasi-nn/ <- ML interface (submodule) +├── examples/ <- 30+ example apps +│ ├── http/basic/ <- Sync handler examples +│ ├── http/wasi/ <- Async handler examples +│ └── cdn/ <- CDN-specific examples +├── README.md <- User-facing (not agent context) +└── AGENTS.md <- Pointer to this file +``` + +--- + +## Search Tips + +**Find public API surface:** +```bash +grep -r "pub fn\|pub struct\|pub enum" src/ +``` + +**Find feature-gated code:** +```bash +grep -r "#\[cfg(feature" src/ +``` + +**Find FFI declarations:** +```bash +grep -r "extern \"C\"" src/proxywasm/ +``` + +**Find handler examples:** +```bash +grep -r "fastedge::http" examples/ +``` + +**Find WIT binding usage:** +```bash +grep -r "wit_bindgen" src/ +``` + +--- + +## Quick Reference + +**Tech Stack:** Rust (edition 2021), wit-bindgen 0.46, wasm32-wasip1 +**Crate:** `fastedge` v0.3.5 on crates.io +**Docs:** https://docs.rs/fastedge +**License:** Apache-2.0 + +**Common Commands:** + +| Command | Purpose | +|---------|---------| +| `cargo build --release` | Build (WASM, default target) | +| `cargo check` | Type-check only | +| `cargo clippy --all-targets --all-features` | Lint | +| `cargo fmt` | Format | +| `cargo test` | Run Rust-native tests | +| `cargo build --release --package ` | Build specific example | +| `cargo doc` | Generate docs | + +--- + +## Summary + +1. Read `context/CONTEXT_INDEX.md` first +2. Use the decision tree to find relevant docs +3. Read only what you need for your current task +4. Use grep for CHANGELOG and large files +5. Update context docs after significant changes +6. Use TaskCreate for multi-step work + +--- + +**Last Updated**: March 2026 diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md deleted file mode 100644 index a2e31f4..0000000 --- a/DOCUMENTATION.md +++ /dev/null @@ -1,843 +0,0 @@ -# FastEdge Rust SDK - Complete Documentation - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture](#architecture) -3. [Installation & Setup](#installation--setup) -4. [Core Concepts](#core-concepts) -5. [API Reference](#api-reference) -6. [Examples & Usage Patterns](#examples--usage-patterns) -7. [Advanced Topics](#advanced-topics) -8. [Troubleshooting](#troubleshooting) - ---- - -## Overview - -The FastEdge Rust SDK is a comprehensive toolkit for building high-performance edge computing applications using WebAssembly (WASM). It supports both the WebAssembly Component Model and the ProxyWasm API, providing flexibility for different deployment scenarios. - -### Key Features - -- **HTTP Request Handling**: Process incoming HTTP requests at the edge -- **Outbound HTTP Client**: Make HTTP requests to backend services -- **Key-Value Storage**: Persistent storage interface for application data -- **Secret Management**: Secure access to encrypted secrets and credentials -- **Dictionary Interface**: Fast key-value lookups for configuration -- **Dual Runtime Support**: Works with both Component Model and ProxyWasm -- **Machine Learning**: WASI-NN integration for ML inference at the edge - -### Version Information - -- **Current Version**: 0.3.2 -- **License**: Apache-2.0 -- **Documentation**: https://docs.rs/fastedge -- **Repository**: https://github.com/G-Core/FastEdge-sdk-rust - ---- - -## Architecture - -### Component Model Architecture - -The FastEdge SDK is built on the WebAssembly Component Model, which provides: - -``` -┌─────────────────────────────────────┐ -│ FastEdge Application │ -│ (Your Rust Code) │ -└──────────────┬──────────────────────┘ - │ #[fastedge::http] - ▼ -┌─────────────────────────────────────┐ -│ FastEdge SDK (fastedge crate) │ -│ - HTTP Handler │ -│ - HTTP Client │ -│ - Key-Value Store │ -│ - Secret Management │ -│ - Dictionary │ -└──────────────┬──────────────────────┘ - │ WIT Bindings - ▼ -┌─────────────────────────────────────┐ -│ Wasmtime Runtime │ -│ (FastEdge Platform) │ -└─────────────────────────────────────┘ -``` - -### WIT Interfaces - -The SDK uses WebAssembly Interface Types (WIT) to define the contract between your application and the runtime: - -**World Definition** (`wit/world.wit`): -```wit -package gcore:fastedge; - -world reactor { - import http; - import http-client; - import dictionary; - import secret; - import key-value; - import utils; - - export http-handler; -} -``` - -### ProxyWasm Architecture - -For environments using ProxyWasm, the SDK provides a compatibility layer: - -```rust -#[cfg(feature = "proxywasm")] -pub mod proxywasm; -``` - -This allows the same application code to work in both environments. - ---- - -## Installation & Setup - -### Prerequisites - -1. **Rust Toolchain**: Install from https://rustup.rs -2. **WASM Target**: Add the wasm32-wasip1 target - -```bash -rustup target add wasm32-wasip1 -``` - -### Adding to Your Project - -Add to your `Cargo.toml`: - -```toml -[dependencies] -fastedge = "0.3" -anyhow = "1.0" # For error handling - -# Optional features -# fastedge = { version = "0.3", features = ["json"] } -``` - -### Project Structure - -Create a new library project: - -```bash -cargo new --lib my-fastedge-app -cd my-fastedge-app -``` - -Configure `Cargo.toml`: - -```toml -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.3" -anyhow = "1.0" -``` - -### Building - -Build for the WASM target: - -```bash -cargo build --target wasm32-wasip1 --release -``` - -The compiled WASM binary will be at: -``` -target/wasm32-wasip1/release/my_fastedge_app.wasm -``` - ---- - -## Core Concepts - -### The HTTP Handler - -Every FastEdge application starts with an HTTP handler function decorated with `#[fastedge::http]`: - -```rust -use fastedge::http::{Request, Response, StatusCode}; -use fastedge::body::Body; -use anyhow::Result; - -#[fastedge::http] -fn main(req: Request) -> Result> { - // Your application logic here - Response::builder() - .status(StatusCode::OK) - .body(Body::from("Hello, World!")) - .map_err(Into::into) -} -``` - -### Request and Response Types - -The SDK uses the standard Rust `http` crate types: - -- `http::Request`: Incoming HTTP request -- `http::Response`: Outgoing HTTP response -- `fastedge::body::Body`: Request/response body with content-type handling - -### Body Type - -The `Body` type is a wrapper around `bytes::Bytes` with content-type awareness: - -```rust -// Create bodies from different types -let text_body = Body::from("Hello"); -let bytes_body = Body::from(vec![1, 2, 3]); -let empty_body = Body::empty(); - -// With JSON feature enabled -#[cfg(feature = "json")] -let json_body = Body::try_from(serde_json::json!({"key": "value"}))?; - -// Access body data -let bytes: &Bytes = &body; // Deref to Bytes -let content_type = body.content_type(); -``` - -### Error Handling - -The SDK uses the `anyhow` crate for error handling: - -```rust -use anyhow::{Result, anyhow}; - -#[fastedge::http] -fn main(req: Request) -> Result> { - let query = req.uri().query() - .ok_or(anyhow!("Missing query parameter"))?; - - // Your logic here - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::from("Success"))?) -} -``` - ---- - -## API Reference - -### HTTP Module - -#### Sending HTTP Requests - -```rust -use fastedge::send_request; -use fastedge::http::{Method, Request}; - -// Create a request -let request = Request::builder() - .method(Method::GET) - .uri("https://api.example.com/data") - .header("User-Agent", "FastEdge/1.0") - .body(Body::empty())?; - -// Send the request -let response = send_request(request)?; - -// Process the response -let status = response.status(); -let body_bytes = response.body(); -``` - -### Key-Value Storage - -The key-value store provides persistent storage with advanced features: - -```rust -use fastedge::key_value::{Store, Error}; - -// Open a store -let store = Store::open("my-store")?; - -// Basic operations -let value = store.get("key")?; -if let Some(data) = value { - // Process data -} - -// Scan with pattern matching -let keys = store.scan("user:*")?; - -// Sorted set operations -let results = store.zrange_by_score("leaderboard", 0.0, 100.0)?; -for (value, score) in results { - println!("Value: {:?}, Score: {}", value, score); -} - -// Bloom filter check -let exists = store.bf_exists("filter", "item")?; -``` - -#### Error Handling - -```rust -match Store::open("restricted-store") { - Ok(store) => { /* use store */ }, - Err(Error::AccessDenied) => { - return Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::from("Access denied")) - .map_err(Into::into); - }, - Err(Error::NoSuchStore) => { - return Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("Store not found")) - .map_err(Into::into); - }, - Err(Error::InternalError) => { - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .map_err(Into::into); - } -} -``` - -### Secret Management - -Securely access encrypted secrets: - -```rust -use fastedge::secret; - -// Get current secret value -match secret::get("API_KEY") { - Ok(Some(value)) => { - let api_key = String::from_utf8_lossy(&value); - // Use the API key - }, - Ok(None) => { - // Secret not found - }, - Err(secret::Error::AccessDenied) => { - // Access denied - }, - Err(secret::Error::DecryptError) => { - // Decryption failed - }, - Err(secret::Error::Other(msg)) => { - // Other error - } -} - -// Get secret effective at a specific time -use std::time::{SystemTime, UNIX_EPOCH}; - -let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH)? - .as_secs() as u32; - -let historical_value = secret::get_effective_at("API_KEY", timestamp)?; -``` - -### Dictionary - -Fast, read-only key-value lookups: - -```rust -use fastedge::dictionary; - -// Get a value from the dictionary -if let Some(config_value) = dictionary::get("config-key")? { - let config = String::from_utf8_lossy(&config_value); - // Use the configuration value -} -``` - -### Utilities - -Diagnostic and statistics functions: - -```rust -use fastedge::utils; - -// Set custom diagnostic information -utils::set_user_diag("Processing completed successfully"); -``` - -### WASI-NN (Machine Learning) - -Integrate machine learning models: - -```rust -use fastedge::wasi_nn; - -// Load and use ML models -// (Requires WASI-NN compatible runtime) -``` - ---- - -## Examples & Usage Patterns - -### 1. Simple HTTP Handler - -```rust -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; - -#[fastedge::http] -fn main(_req: Request) -> Result> { - Response::builder() - .status(StatusCode::OK) - .body(Body::from("Hello, FastEdge!")) - .map_err(Into::into) -} -``` - -### 2. Backend Proxy - -Forward requests to a backend service: - -```rust -use anyhow::{anyhow, Result}; -use fastedge::body::Body; -use fastedge::http::{Method, Request, Response, StatusCode}; - -#[fastedge::http] -fn main(req: Request) -> Result> { - let query = req.uri().query() - .ok_or(anyhow!("Missing query parameter"))?; - - let params = querystring::querify(query); - let url = params.iter() - .find(|(k, _)| k == &"url") - .ok_or(anyhow!("Missing url parameter"))? - .1; - - let backend_request = Request::builder() - .method(Method::GET) - .uri(urlencoding::decode(url)?.to_string()) - .body(req.into_body())?; - - let backend_response = fastedge::send_request(backend_request)?; - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!( - "Response length: {}, Content-Type: {:?}", - backend_response.body().len(), - backend_response.headers().get("Content-Type") - ))) - .map_err(Into::into) -} -``` - -### 3. Key-Value Store Operations - -```rust -use anyhow::{anyhow, Result}; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; -use fastedge::key_value::Store; - -#[fastedge::http] -fn main(req: Request) -> Result> { - let query = req.uri().query() - .ok_or(anyhow!("No query parameters"))?; - - let params = querystring::querify(query); - - let store_name = params.iter() - .find(|(k, _)| k == &"store") - .map(|(_, v)| v) - .ok_or(anyhow!("Missing 'store' parameter"))?; - - let key = params.iter() - .find(|(k, _)| k == &"key") - .map(|(_, v)| v) - .ok_or(anyhow!("Missing 'key' parameter"))?; - - let store = Store::open(store_name)?; - let value = store.get(key)?; - - match value { - Some(data) => { - Response::builder() - .status(StatusCode::OK) - .body(Body::from(data)) - .map_err(Into::into) - }, - None => { - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("Key not found")) - .map_err(Into::into) - } - } -} -``` - -### 4. Secret Access - -```rust -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; -use fastedge::secret; - -#[fastedge::http] -fn main(_req: Request) -> Result> { - match secret::get("DATABASE_URL") { - Ok(Some(secret_value)) => { - let db_url = String::from_utf8_lossy(&secret_value); - // Use the database URL to connect - - Response::builder() - .status(StatusCode::OK) - .body(Body::from("Connected successfully")) - .map_err(Into::into) - }, - Ok(None) => { - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("Secret not configured")) - .map_err(Into::into) - }, - Err(secret::Error::AccessDenied) => { - Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::from("Access denied")) - .map_err(Into::into) - }, - Err(_) => { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("Secret retrieval failed")) - .map_err(Into::into) - } - } -} -``` - -### 5. Environment Variables - -```rust -use std::env; -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; - -#[fastedge::http] -fn main(_req: Request) -> Result> { - let base_url = env::var("BASE_URL") - .unwrap_or_else(|_| "https://default.example.com".to_string()); - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!("Base URL: {}", base_url))) - .map_err(Into::into) -} -``` - -### 6. Markdown Renderer - -Transform Markdown content to HTML: - -```rust -use fastedge::body::Body; -use fastedge::http::{header, Method, Request, Response, StatusCode}; -use pulldown_cmark::{Options, Parser}; -use std::env; - -#[fastedge::http] -fn main(req: Request) -> Result, http::Error> { - if !matches!(req.method(), &Method::GET | &Method::HEAD) { - return Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .header(header::ALLOW, "GET, HEAD") - .body(Body::from("Method not allowed")); - } - - let base = env::var("BASE").unwrap_or_default(); - let path = req.uri().path(); - - // Fetch Markdown from backend - let backend_req = Request::builder() - .method(Method::GET) - .uri(format!("{}{}", base, path)) - .body(Body::empty())?; - - let response = fastedge::send_request(backend_req) - .map_err(|_| http::Error::from(()))?; - - let markdown = String::from_utf8_lossy(response.body()).to_string(); - - // Convert to HTML - let parser = Parser::new_ext( - &markdown, - Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES - ); - - let mut html = String::from(""); - pulldown_cmark::html::push_html(&mut html, parser); - html.push_str(""); - - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, mime::TEXT_HTML.to_string()) - .body(Body::from(html)) -} -``` - ---- - -## Advanced Topics - -### ProxyWasm Compatibility - -When using the `proxywasm` feature, you can access ProxyWasm-specific APIs: - -```rust -#[cfg(feature = "proxywasm")] -use fastedge::proxywasm; - -// ProxyWasm-specific implementations available -``` - -The ProxyWasm module provides: -- Key-Value Store operations -- Secret management -- Dictionary access -- Utility functions - -All through native ProxyWasm FFI calls. - -### Custom Body Types - -Implement custom conversions for your types: - -```rust -use fastedge::body::Body; -use bytes::Bytes; - -struct CustomData { - data: Vec, -} - -impl From for Body { - fn from(custom: CustomData) -> Self { - Body::from(custom.data) - } -} -``` - -### JSON Feature - -Enable JSON support in your `Cargo.toml`: - -```toml -[dependencies] -fastedge = { version = "0.3", features = ["json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -``` - -Use JSON bodies: - -```rust -use serde_json::json; - -#[fastedge::http] -fn main(_req: Request) -> Result> { - let json_response = json!({ - "status": "ok", - "message": "Success", - "data": { - "items": [1, 2, 3] - } - }); - - Response::builder() - .status(StatusCode::OK) - .body(Body::try_from(json_response)?) - .map_err(Into::into) -} -``` - -### Header Manipulation - -```rust -use fastedge::http::header; - -#[fastedge::http] -fn main(req: Request) -> Result> { - // Read request headers - let user_agent = req.headers() - .get(header::USER_AGENT) - .and_then(|v| v.to_str().ok()) - .unwrap_or("unknown"); - - // Build response with custom headers - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "application/json") - .header(header::CACHE_CONTROL, "max-age=3600") - .header("X-Custom-Header", "custom-value") - .body(Body::from(format!("User-Agent: {}", user_agent))) - .map_err(Into::into) -} -``` - -### Error Type Conversions - -The SDK provides comprehensive error types: - -```rust -use fastedge::Error; - -// Error variants: -// - UnsupportedMethod(http::Method) -// - BindgenHttpError(HttpError) -// - HttpError(http::Error) -// - InvalidBody -// - InvalidStatusCode(u16) - -fn handle_error(err: Error) -> Response { - match err { - Error::UnsupportedMethod(method) => { - Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body(Body::from(format!("Method {} not supported", method))) - .unwrap() - }, - Error::InvalidStatusCode(code) => { - Response::builder() - .status(StatusCode::BAD_GATEWAY) - .body(Body::from(format!("Invalid status code: {}", code))) - .unwrap() - }, - _ => { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("Internal error")) - .unwrap() - } - } -} -``` - ---- - -## Troubleshooting - -### Common Build Issues - -#### Missing WASM Target - -**Error**: `error[E0463]: can't find crate for 'std'` - -**Solution**: -```bash -rustup target add wasm32-wasip1 -``` - -#### Compilation Errors - -**Error**: `linking with 'rust-lld' failed` - -**Solution**: Ensure you're building with the correct target: -```bash -cargo build --target wasm32-wasip1 --release -``` - -### Runtime Issues - -#### Store Access Denied - -When getting `Error::AccessDenied` from key-value store: - -1. Verify the store name is correct -2. Check that your application has been granted access to the store -3. Ensure the store exists in your FastEdge environment - -#### Secret Decryption Errors - -When getting `Error::DecryptError`: - -1. Verify the secret name is correct -2. Check that the secret is properly encrypted in the platform -3. Ensure your application has the correct permissions - -### Performance Optimization - -#### Minimize Memory Allocations - -```rust -// Instead of creating new strings -let response_text = format!("Value: {}", value); - -// Consider using static strings when possible -const RESPONSE: &str = "Fixed response"; -``` - -#### Reuse Connections - -The HTTP client handles connection pooling internally, but ensure you're not creating unnecessary requests: - -```rust -// Good: Single request -let response = fastedge::send_request(request)?; - -// Avoid: Multiple redundant requests -``` - -#### Optimize Body Sizes - -```rust -// For large responses, consider streaming or pagination -// instead of loading everything into memory -``` - -### Debugging - -Enable debug output: - -```rust -use fastedge::utils::set_user_diag; - -set_user_diag(&format!("Processing request: {:?}", req.uri())); -``` - -Check the FastEdge platform logs for diagnostic messages. - ---- - -## Best Practices - -1. **Error Handling**: Always use `Result` types and handle all error cases -2. **Security**: Never log or expose sensitive data from secrets -3. **Performance**: Minimize allocations and avoid blocking operations -4. **Resource Management**: Close stores and resources when done -5. **Testing**: Write unit tests for your business logic separately from the handler -6. **Documentation**: Document your application's expected environment variables and configurations - ---- - -## Further Resources - -- [FastEdge Documentation](https://gcore.com/docs/fastedge) -- [WebAssembly Component Model](https://component-model.bytecodealliance.org) -- [Rust HTTP Crate](https://docs.rs/http) -- [Wasmtime Runtime](https://wasmtime.dev/) -- [FastEdge SDK Repository](https://github.com/G-Core/FastEdge-sdk-rust) - ---- - -**Last Updated**: January 2026 -**SDK Version**: 0.3.2 diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md new file mode 100644 index 0000000..a8ca7a4 --- /dev/null +++ b/context/CHANGELOG.md @@ -0,0 +1,66 @@ +# Agent Decision Log + +This file tracks agent decisions, architectural changes, and context for future agents working in this repository. It is **not** related to the root `CHANGELOG.md` which tracks release versions. + +--- + +## [2026-03-31] — Added Host-Side Context (Contract, Lifecycle, Properties, Errors) + +### Overview +Added 4 new context files documenting the host-SDK relationship from the SDK developer's perspective. Based on research of the host runtime source code (rust_host/proxywasm), distilled to high-level concepts without exposing proprietary implementation details. + +### Decisions +- Host internals are proprietary — context docs describe the **contract** (what the SDK developer needs to know), not the implementation +- Properties, error codes, and lifecycle phases documented as reference material for debugging and development +- HTTP callout pause/resume mechanism documented as a conceptual flow — this was a gap that made CDN-mode development harder to understand + +### Files Created +- `context/architecture/HOST_SDK_CONTRACT.md` — ABI contract, FFI functions, memory conventions, execution constraints +- `context/architecture/REQUEST_LIFECYCLE.md` — Handler phases, HTTP callout pause/resume, local response short-circuit +- `context/reference/PROPERTIES_REFERENCE.md` — Available request properties (geo, IP, host, tracing) +- `context/reference/ERROR_CODES.md` — Host status codes (3100-3120), SDK errors, module errors + +### Files Updated +- `context/CONTEXT_INDEX.md` — Added entries for all 4 new files + 5 new decision tree scenarios +- `context/architecture/RUNTIME_ARCHITECTURE.md` — Cross-references to HOST_SDK_CONTRACT and REQUEST_LIFECYCLE +- `context/architecture/SDK_ARCHITECTURE.md` — Added HTTP callout pause/resume mention + host constraint note +- `CLAUDE.md` — Updated context organization tree + decision tree table + +--- + +## [2026-03-31] — Documented Two Handler Patterns + +### Overview +Added documentation for `#[wstd::http_server]` (async, WASI-HTTP) alongside `#[fastedge::http]` (sync, original). + +### Decisions +- `#[wstd::http_server]` is the recommended approach for new apps — documented as the forward path +- `#[fastedge::http]` is the original basic pattern — `#[wstd::http_server]` is preferred for new apps +- New examples should use the wstd async pattern, not fastedge sync +- Updated all context docs (SDK_ARCHITECTURE, PROJECT_OVERVIEW, CONTEXT_INDEX, CLAUDE.md) to reflect both patterns + +--- + +## [2026-03-31] — Context System Created + +### Overview + +Established discovery-based context system (CLAUDE.md + context/) for AI agent discoverability, following the pattern used by FastEdge-sdk-js and fastedge-test. + +### Decisions + +- Followed the **FastEdge-sdk-js pattern** (lean SDK-style) rather than fastedge-test (full-stack app with more surface area) +- Content distilled from existing `AGENTS.md` (827 lines) and `DOCUMENTATION.md` (844 lines) into structured context/ files +- `AGENTS.md` converted to a pointer file directing agents to `CLAUDE.md` +- `DOCUMENTATION.md` removed — all content absorbed into context/ +- Context docs kept under 170 lines each for single-sitting reads + +### Files Created + +- `CLAUDE.md` — entry point for AI agents +- `context/CONTEXT_INDEX.md` — discovery hub +- `context/PROJECT_OVERVIEW.md` — lightweight overview +- `context/architecture/SDK_ARCHITECTURE.md` — core architecture +- `context/architecture/RUNTIME_ARCHITECTURE.md` — WIT + runtime +- `context/development/BUILD_AND_CI.md` — build system + CI +- `context/CHANGELOG.md` — this file diff --git a/context/CONTEXT_INDEX.md b/context/CONTEXT_INDEX.md new file mode 100644 index 0000000..8098e02 --- /dev/null +++ b/context/CONTEXT_INDEX.md @@ -0,0 +1,166 @@ +# Context Discovery Index + +## Quick Start + +- **Project:** FastEdge Rust SDK +- **Crate:** `fastedge` v0.3.5 on crates.io +- **Purpose:** Build edge computing apps in Rust that compile to WASM for Gcore's FastEdge platform +- **Workspace:** 2 crates — `fastedge` (core SDK) + `fastedge-derive` (proc macro) +- **Target:** `wasm32-wasip1` +- **APIs:** HTTP handler, outbound HTTP, key-value store, secrets, dictionary, utils, WASI-NN +- **Dual Runtime:** Component Model (WIT/wit-bindgen) + ProxyWasm (FFI) +- **Build:** `cargo build --release` (default target is WASM via `.cargo/config.toml`) +- **Test:** `cargo test` (Rust-native tests only) + +--- + +## Documentation Map + +### Architecture (read when modifying internal structure) + +| Document | Lines | Purpose | +|----------|-------|---------| +| `architecture/SDK_ARCHITECTURE.md` | ~197 | Dual API approach, type conversion pattern, Body type, error handling, module structure, import patterns. Read when working on `src/`. | +| `architecture/RUNTIME_ARCHITECTURE.md` | ~140 | WIT world definition, interface contracts, submodules, ProxyWasm FFI layer, WIT change workflow. Read when working on `wit/` or `src/proxywasm/`. | +| `architecture/HOST_SDK_CONTRACT.md` | ~130 | ABI contract between SDK and host: FFI functions, memory conventions, execution constraints. Read when adding host function wrappers or debugging host interaction. | +| `architecture/REQUEST_LIFECYCLE.md` | ~130 | Request phases (Component Model + ProxyWasm), HTTP callout pause/resume, local response short-circuit. Read when debugging handler behavior or building CDN apps. | + +### Development (read when building or deploying) + +| Document | Lines | Purpose | +|----------|-------|---------| +| `development/BUILD_AND_CI.md` | ~142 | Workspace config, build commands, CI pipeline, release pipeline, FOSSA, example build pattern, size optimization. Read when changing build or CI. | + +### Reference (search on-demand) + +| Document | Lines | Purpose | +|----------|-------|---------| +| `PROJECT_OVERVIEW.md` | ~149 | Lightweight overview — crate structure, modules, features, examples, deps, setup. Read when new to the codebase. | +| `reference/PROPERTIES_REFERENCE.md` | ~80 | Available request properties (geo, IP, host, URI, tracing) for ProxyWasm apps. Read when working with `proxy_get_property()`. | +| `reference/ERROR_CODES.md` | ~120 | Host status codes (3100-3120), SDK error enum, module errors, FFI status codes. Read when debugging failures. | +| `CHANGELOG.md` | ~29+ | Agent decision log. Use grep as this file grows. | + +### External (not in context/) + +| Resource | Location | Purpose | +|----------|----------|---------| +| API docs | https://docs.rs/fastedge | Generated Rust documentation | +| WIT definitions | `wit/` (submodule → G-Core/FastEdge-wit) | Interface contracts | +| Examples | `examples/` (30+ apps) | Real-world usage patterns | +| Release changelog | Root `CHANGELOG.md` | Version history (auto-generated) | + +--- + +## Decision Tree: What Should I Read? + +### Adding a New WIT Interface +1. Read `architecture/RUNTIME_ARCHITECTURE.md` (WIT section + change workflow) +2. Read existing `.wit` files in `wit/` +3. Add Rust wrapper in `src/`, following existing module patterns + +### Fixing the Proc Macro +1. Read `architecture/SDK_ARCHITECTURE.md` (attribute macro pattern section) +2. Read `derive/src/lib.rs` directly + +### Adding a ProxyWasm Feature +1. Read `architecture/RUNTIME_ARCHITECTURE.md` (ProxyWasm FFI section) +2. Read `architecture/SDK_ARCHITECTURE.md` (module structure) +3. Read existing wrapper as template: `src/proxywasm/key_value.rs` + +### Modifying HTTP Client +1. Read `architecture/SDK_ARCHITECTURE.md` (HTTP client + type conversion sections) +2. Read `src/http_client.rs` directly + +### Working with KV / Secrets / Dictionary +1. Read `architecture/SDK_ARCHITECTURE.md` (module structure section) +2. Read the specific module in `src/proxywasm/` + +### Writing a New WASI-HTTP App (async) +1. Read `architecture/SDK_ARCHITECTURE.md` (two handler patterns — `#[wstd::http_server]` section) +2. Browse `examples/http/wasi/hello_world/` as the simplest template +3. Use `wstd = "0.6"` — this is the **recommended** approach for new apps + +### Working with Basic Sync Handler +1. Read `architecture/SDK_ARCHITECTURE.md` (two handler patterns — `#[fastedge::http]` section) +2. Browse `examples/http/basic/` for sync examples +3. Note: `#[wstd::http_server]` is preferred for new apps, but `#[fastedge::http]` is fully supported + +### Adding an Example +1. Browse `examples/` for a similar existing example +2. **Prefer `#[wstd::http_server]` (async) for new examples** over `#[fastedge::http]` (basic sync) +3. Read `PROJECT_OVERVIEW.md` (examples section) +4. Read `development/BUILD_AND_CI.md` (example build pattern) + +### Understanding the System (New to Codebase) +1. Read `PROJECT_OVERVIEW.md` (~149 lines) +2. Skim `architecture/SDK_ARCHITECTURE.md` (two handler patterns + module structure) +3. Browse `examples/http/wasi/hello_world/` for the recommended pattern + +### Changing Build or CI +1. Read `development/BUILD_AND_CI.md` +2. Check `.github/workflows/` for specific pipeline + +### Modifying Type Conversions +1. Read `architecture/SDK_ARCHITECTURE.md` (type conversion + body type sections) +2. Read `src/lib.rs` (conversion implementations) + +### Adding Error Handling +1. Read `reference/ERROR_CODES.md` (full error catalog) +2. Read `architecture/SDK_ARCHITECTURE.md` (error handling section) +3. Check existing module error types in `src/proxywasm/` + +### Debugging Host Interaction / Status Codes +1. Read `reference/ERROR_CODES.md` (host codes 3100-3120) +2. Read `architecture/HOST_SDK_CONTRACT.md` (execution constraints) + +### Working with Request Properties (ProxyWasm) +1. Read `reference/PROPERTIES_REFERENCE.md` (available properties) +2. Browse `examples/cdn/properties/` for usage example + +### Understanding HTTP Callout Mechanism +1. Read `architecture/REQUEST_LIFECYCLE.md` (pause/resume section) +2. Read `architecture/HOST_SDK_CONTRACT.md` (host-provided functions) +3. Browse `examples/cdn/http_call/` for working example + +### Adding a Host Function Wrapper +1. Read `architecture/HOST_SDK_CONTRACT.md` (FFI functions, memory convention) +2. Read `architecture/RUNTIME_ARCHITECTURE.md` (ProxyWasm FFI + WIT change workflow) +3. Use `src/proxywasm/key_value.rs` as template + +### Working with WASI-NN / ML +1. Read `architecture/RUNTIME_ARCHITECTURE.md` (submodules section) +2. Check `wasi-nn/` submodule for interface definitions + +### Updating Dependencies +1. Read `PROJECT_OVERVIEW.md` (key dependencies table) +2. Read `development/BUILD_AND_CI.md` (workspace config) +3. Note: `wit-bindgen` version must match Wasmtime runtime version + +--- + +## Search Tips + +- **Don't** read `CHANGELOG.md` linearly — grep for keywords as it grows +- **Grep patterns:** + - `grep -r "wit_bindgen" src/` — find WIT binding usage + - `grep -r "extern \"C\"" src/` — find FFI declarations + - `grep -r "fastedge::http" examples/` — find handler examples + - `grep -r "#\[cfg(feature" src/` — find feature-gated code + - `grep -r "pub fn\|pub struct\|pub enum" src/` — find public API surface + +--- + +## Documentation Size Reference + +| Category | Documents | Total Lines | +|----------|-----------|-------------| +| Architecture | 4 docs | ~597 | +| Development | 1 doc | ~142 | +| Reference | 4 docs | ~378 | +| **Total** | **9 docs** | **~1,117** | + +All documents are designed for single-sitting reads. No doc exceeds ~200 lines. + +--- + +**Last Updated**: March 2026 diff --git a/context/PROJECT_OVERVIEW.md b/context/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..d9ed7ef --- /dev/null +++ b/context/PROJECT_OVERVIEW.md @@ -0,0 +1,151 @@ +# Project Overview + +## What Is This? + +**FastEdge Rust SDK** is a library for building edge computing applications that compile to WebAssembly and run on Gcore's FastEdge platform. It provides HTTP request handling, backend communication, key-value storage, secret management, and ML inference capabilities. + +- **Crate**: `fastedge` on crates.io +- **Docs**: https://docs.rs/fastedge +- **Repository**: https://github.com/G-Core/FastEdge-sdk-rust +- **License**: Apache-2.0 + +--- + +## Crate Structure (Workspace) + +This is a Rust workspace with 2 member crates: + +| Crate | Type | Purpose | +|-------|------|---------| +| `fastedge` | Library (`cdylib`) | Core SDK — HTTP handler, client, KV, secrets, dictionary, utils | +| `fastedge-derive` | Proc-macro | `#[fastedge::http]` attribute macro for marking HTTP handler functions | + +Both share version `0.3.5`, edition 2021. + +--- + +## Key Modules + +### Core SDK (`src/`) + +| File | Lines | Purpose | +|------|-------|---------| +| `lib.rs` | ~667 | Entry point, type conversions, module re-exports, `wit_bindgen::generate!` | +| `http_client.rs` | ~141 | `send_request()` — outbound HTTP to backend services | +| `helper.rs` | ~75 | Internal binary serialization/deserialization utilities | + +### ProxyWasm Layer (`src/proxywasm/`) + +Feature-gated behind `proxywasm` (default: enabled). Provides FFI bindings to proxy-wasm host functions. + +| File | Lines | Purpose | +|------|-------|---------| +| `mod.rs` | ~109 | `extern "C"` FFI declarations for all proxy_* functions | +| `key_value.rs` | ~292 | `Store` resource — open, get, scan, zrange, bloom filter | +| `secret.rs` | ~142 | `secret::get()`, `secret::get_effective_at()` | +| `dictionary.rs` | ~64 | `dictionary::get()` — read-only config lookups | +| `utils.rs` | ~50 | `set_user_diag()` — diagnostic reporting | + +### Derive Macro (`derive/src/lib.rs`) + +~186 lines. Transforms `#[fastedge::http] fn main(req) -> Result` into a full Component Model export with Guest trait implementation and error-to-500 conversion. + +--- + +## Feature Flags + +| Feature | Default | Purpose | +|---------|---------|---------| +| `proxywasm` | Yes | ProxyWasm compatibility layer (FFI bindings) | +| `json` | No | JSON body support via `serde_json` | + +--- + +## WIT World + +The SDK implements the `gcore:fastedge/reactor` world: + +**Imports** (platform provides): `http`, `http-client`, `dictionary`, `secret`, `key-value`, `utils` +**Exports** (app implements): `http-handler` + +WIT definitions live in the `wit/` submodule (points to `G-Core/FastEdge-wit`). + +--- + +## Examples + +30+ examples organized in three categories: + +| Category | Path | Handler | Description | +|----------|------|---------|-------------| +| HTTP WASI | `examples/http/wasi/` | `#[wstd::http_server]` (async, **recommended**) | hello_world, headers, key_value, outbound_fetch, secret_rollover, geo_redirect, variables_and_secrets, simple_fetch | +| HTTP Basic | `examples/http/basic/` | `#[fastedge::http]` (sync, **original**) | hello_world, headers, key_value, secret, backend, geo_redirect, api_wrapper, etc. | +| CDN | `examples/cdn/` | ProxyWasm | headers, body, http_call, key_value, geo_redirect, properties, variables_and_secrets, etc. | + +- `#[wstd::http_server]` is the forward path — new apps should use this pattern +- `#[fastedge::http]` is the original basic pattern and remains fully supported +- Each example has its own `Cargo.toml`, `src/lib.rs`, and `README.md` + +--- + +## Submodules + +| Submodule | Source | Purpose | +|-----------|--------|---------| +| `wit/` | `G-Core/FastEdge-wit` | WIT interface definitions for the reactor world | +| `wasi-nn/` | `WebAssembly/wasi-nn` | ML inference interface definitions | + +--- + +## Development Setup + +```bash +# Prerequisites +rustup target add wasm32-wasip1 + +# Build +cargo build --target wasm32-wasip1 --release + +# Check (no build artifacts) +cargo check --target wasm32-wasip1 + +# Lint +cargo clippy --target wasm32-wasip1 --all-targets --all-features + +# Format +cargo fmt + +# Test (Rust-native tests only, no WASM) +cargo test + +# Build a specific example +cargo build --target wasm32-wasip1 --release --package hello-world +``` + +Default build target is `wasm32-wasip1` (set in `.cargo/config.toml`). + +--- + +## Testing + +- Inline unit tests in modules (e.g., `helper.rs` has serialize/deserialize round-trip tests) +- No dedicated test directory — examples serve as integration-level validation +- CI runs: `cargo-audit`, `cargo build --release --all-features`, `cargo doc`, `cargo clippy` +- All warnings are errors in CI (`RUSTFLAGS="-Dwarnings"`) + +--- + +## Key Dependencies + +| Dependency | Version | Purpose | +|------------|---------|---------| +| `http` | 1.3 | Standard HTTP types (user-facing API) | +| `bytes` | 1.10 | Zero-copy byte buffers | +| `wit-bindgen` | 0.46 | WIT code generation (must match Wasmtime version) | +| `thiserror` | 2.0 | Error derive macros | +| `mime` | 0.3 | MIME type handling | +| `serde_json` | 1.0 | JSON support (optional, `json` feature) | + +--- + +**Last Updated**: March 2026 diff --git a/context/architecture/HOST_SDK_CONTRACT.md b/context/architecture/HOST_SDK_CONTRACT.md new file mode 100644 index 0000000..f6f9107 --- /dev/null +++ b/context/architecture/HOST_SDK_CONTRACT.md @@ -0,0 +1,143 @@ +# Host-SDK Contract + +This document describes the interface contract between the FastEdge host runtime and WASM modules built with the SDK. It covers what the SDK developer needs to know — not how the host implements it internally. + +--- + +## Overview + +SDK apps are WASM modules that run inside a host runtime. Communication happens through two mechanisms: + +1. **Component Model (WIT)** — type-safe imports/exports defined in `wit/` (see RUNTIME_ARCHITECTURE.md) +2. **ProxyWasm FFI** — `extern "C"` function calls for CDN/proxy-wasm environments + +Both expose the same capabilities (KV, secrets, dictionary, HTTP, diagnostics) through different calling conventions. + +--- + +## Memory Convention + +All FFI-based host functions pass data via pointer/size pairs in WASM linear memory: + +**Sending data to host:** +``` +(key_ptr: *const u8, key_size: usize) // SDK writes, host reads +``` + +**Receiving data from host:** +``` +(return_data: *mut *mut u8, return_size: *mut usize) // Host allocates via WASM export, SDK reads +``` + +The host calls the WASM export `proxy_on_memory_allocate(size) -> ptr` (or `malloc`) to allocate memory in the guest for return values. The SDK's FFI wrappers in `src/proxywasm/` handle all pointer management — application code never deals with raw pointers. + +**Return values:** All `proxy_*` FFI functions return `u32` status codes: +- `0` — success +- Non-zero — error (mapped to module-specific error enums by the SDK wrappers) + +--- + +## Host-Provided Functions (ProxyWasm FFI) + +These are the `extern "C"` functions the host makes available to WASM modules. The SDK wraps them in safe Rust APIs. + +### Key-Value Store + +| FFI Function | SDK Wrapper | Purpose | +|-------------|-------------|---------| +| `proxy_kv_store_open(name, len, handle)` | `Store::open(name)` | Open a named KV store, returns handle | +| `proxy_kv_store_get(handle, key, len, ret, ret_len)` | `Store::get(key)` | Retrieve value by key | +| `proxy_kv_store_scan(handle, pattern, len, ret, ret_len)` | `Store::scan(pattern)` | Glob-pattern key scan | +| `proxy_kv_store_zrange_by_score(handle, key, len, min, max, ret, ret_len)` | `Store::zrange_by_score(key, min, max)` | Sorted set range query | +| `proxy_kv_store_zscan(handle, key, len, pattern, plen, ret, ret_len)` | `Store::zscan(key, pattern)` | Sorted set pattern scan | +| `proxy_kv_store_bf_exists(handle, key, len, item, ilen, ret)` | `Store::bf_exists(key, item)` | Bloom filter membership check | + +### Secrets + +| FFI Function | SDK Wrapper | Purpose | +|-------------|-------------|---------| +| `proxy_secret_get(key, len, ret, ret_len)` | `secret::get(key)` | Retrieve current secret value | +| `proxy_secret_get_effective_at(key, len, ts, ret, ret_len)` | `secret::get_effective_at(key, ts)` | Retrieve secret valid at timestamp | + +Timestamp-based retrieval supports secret rotation — fetch the secret that was active at a specific point in time. + +### Dictionary + +| FFI Function | SDK Wrapper | Purpose | +|-------------|-------------|---------| +| `proxy_dictionary_get(key, len, ret, ret_len)` | `dictionary::get(key)` | Read-only configuration lookup | + +Dictionary values are set at deployment time and are immutable during request handling. Fast path for configuration data. + +### Diagnostics + +| FFI Function | SDK Wrapper | Purpose | +|-------------|-------------|---------| +| `stats_set_user_diag(value, len)` | `utils::set_user_diag(value)` | Set diagnostic string for monitoring | + +The diagnostic string is attached to the request and visible in platform monitoring/logs. + +--- + +## Component Model Interface + +For the WIT-based Component Model path, the same capabilities are exposed as typed interfaces rather than raw FFI. The `wit_bindgen::generate!` macro produces Rust bindings automatically. + +The WIT world (`gcore:fastedge/reactor`) imports: +- `http` + `http-client` — request/response types and outbound HTTP +- `key-value` — persistent storage (same operations as FFI above) +- `secret` — encrypted secrets (same operations as FFI above) +- `dictionary` — read-only config (same as FFI above) +- `utils` — diagnostics (same as FFI above) + +And exports: +- `http-handler` — the app's request processing function + +See `architecture/RUNTIME_ARCHITECTURE.md` for full WIT definitions. + +--- + +## WASM Module Exports (called by host) + +The host expects these exports from the WASM module: + +### Component Model Apps (`#[fastedge::http]` / `#[wstd::http_server]`) + +| Export | Purpose | +|--------|---------| +| `process(request) -> response` | Main handler — receives HTTP request, returns response | + +The `#[fastedge::http]` macro generates the `Guest` trait implementation that bridges to this export. + +### ProxyWasm Apps (CDN) + +| Export | Purpose | +|--------|---------| +| `proxy_on_memory_allocate(size) -> ptr` | Memory allocation for host-to-guest data transfer | +| `_initialize()` or `_start()` | Module initialization | +| `proxy_on_context_create(context_id, parent_id)` | Create root (parent=0) or request context | +| `proxy_on_request_headers(context_id, num_headers)` | Handle request headers phase | +| `proxy_on_request_body(context_id, body_size, end_of_stream)` | Handle request body phase | +| `proxy_on_response_headers(context_id, num_headers)` | Handle response headers phase | +| `proxy_on_response_body(context_id, body_size, end_of_stream)` | Handle response body phase | +| `proxy_on_log(context_id)` | Final logging callback | +| `proxy_on_http_call_response(ctx, call_id, h_size, b_size, t_size)` | HTTP callout response delivered | + +See `architecture/REQUEST_LIFECYCLE.md` for the order these are called. + +--- + +## Execution Constraints + +The host enforces limits on WASM execution: + +| Constraint | Behavior | +|-----------|----------| +| **Execution timeout** | Host interrupts WASM after a configured duration | +| **Memory limit** | WASM linear memory is capped; exceeding it triggers a trap | +| **Non-public hosts blocked** | Outbound HTTP calls to internal/private IPs are rejected | +| **Single-threaded** | Each request runs in its own WASM instance, no shared state between requests | + +--- + +**Last Updated**: March 2026 diff --git a/context/architecture/REQUEST_LIFECYCLE.md b/context/architecture/REQUEST_LIFECYCLE.md new file mode 100644 index 0000000..cabfa22 --- /dev/null +++ b/context/architecture/REQUEST_LIFECYCLE.md @@ -0,0 +1,164 @@ +# Request Lifecycle + +How an HTTP request flows through the FastEdge runtime from the SDK developer's perspective. Understanding this lifecycle helps when debugging handler behavior, working with HTTP callouts, or building CDN-mode apps. + +--- + +## Component Model Apps (HTTP Handler) + +For apps using `#[fastedge::http]` or `#[wstd::http_server]`, the lifecycle is straightforward: + +``` +Client Request + | + v +Host receives HTTP request + | + v +Host instantiates WASM module (fresh instance per request) + | + v +Host calls handler: process(request) -> response + | | + | [App can call send_request() for outbound HTTP] + | [App can access KV, secrets, dictionary, utils] + | + v +Host sends response to client + | + v +WASM instance is discarded +``` + +**Key points:** +- Each request gets a **fresh WASM instance** — no state carries between requests +- The handler is synchronous for `#[fastedge::http]`, async for `#[wstd::http_server]` +- `send_request()` makes outbound HTTP calls during handler execution +- Persistent state lives in the KV store, not in memory + +--- + +## ProxyWasm Apps (CDN Mode) — Phase-Based Lifecycle + +CDN-mode apps using the proxy-wasm interface have a multi-phase lifecycle. The host calls handler functions in a defined order, and the app returns an **action** after each phase. + +### Phase Order + +``` +1. Module Init _initialize() or _start() + | +2. Context Create proxy_on_context_create(root_id=1, parent=0) // root context + | +3. Context Create proxy_on_context_create(request_id, parent=1) // request context + | +4. Request Headers proxy_on_request_headers(ctx, num_headers) + | + [Action: Continue / Pause] + | +5. Request Body proxy_on_request_body(ctx, size, end_of_stream) + | (only if request has a body) + | +6. Response Headers proxy_on_response_headers(ctx, num_headers) + | +7. Response Body proxy_on_response_body(ctx, size, end_of_stream) + | +8. Log proxy_on_log(ctx) + | +9. Instance discarded +``` + +### Actions + +Each phase handler returns an action code that controls flow: + +| Action | Meaning | +|--------|---------| +| **Continue** | Proceed to next phase | +| **Pause** | Halt processing — waiting for an HTTP callout response | + +--- + +## HTTP Callout (Pause/Resume) + +The most complex part of the lifecycle. When a ProxyWasm app needs to fetch data from an external service during request processing, it uses the HTTP callout mechanism. + +### Flow + +``` +1. App calls proxy_http_call(upstream, headers, body, timeout) + | + v +2. Host returns call_id immediately + | + v +3. App returns Action::Pause from current phase handler + | + v +4. Host makes the outbound HTTP request asynchronously + | + v +5. When response arrives, host calls: + proxy_on_http_call_response(ctx, call_id, headers_size, body_size, trailers_size) + | + v +6. App reads response via: + - proxy_get_header_map_pairs(HttpCallResponseHeaders) // response headers + - proxy_get_buffer_bytes(HttpCallResponseBody, 0, size) // response body + | + v +7. App resumes — host re-invokes the phase handler + | + v +8. App returns Action::Continue to proceed +``` + +### Multiple Callouts + +An app can make multiple sequential HTTP callouts by returning Pause repeatedly. Each callout follows the same pattern: call -> pause -> response delivered -> resume -> call again or continue. + +### Key Constraints + +- **One active callout at a time** — the app pauses until the response arrives +- **Timeout enforced** — if the upstream doesn't respond in time, the callout fails +- **Non-public hosts blocked** — callouts to internal/private IP ranges are rejected +- **Response is temporary** — the callout response is only available inside `proxy_on_http_call_response` + +--- + +## Local Response (Short-Circuit) + +An app can skip upstream processing entirely by sending a local response: + +``` +proxy_send_local_response(status_code, headers, body) +``` + +This immediately sets the response and stops further phase processing. Common use cases: +- Returning cached data from KV store +- Rejecting requests based on headers or properties +- Returning error responses before reaching the backend + +--- + +## Header and Body Access by Phase + +What data is available in each phase: + +| Phase | Can Read | Can Modify | +|-------|----------|------------| +| Request Headers | Request headers, properties | Request headers | +| Request Body | Request body (buffered) | Request body | +| Response Headers | Response headers, properties | Response headers | +| Response Body | Response body (buffered) | Response body | +| HTTP Call Response | Callout response headers + body | N/A (read-only) | +| Log | Request + response metadata | Nothing | + +--- + +## Properties + +Request properties are available throughout the lifecycle via `proxy_get_property()`. These provide metadata about the request and client. See `reference/PROPERTIES_REFERENCE.md` for the full list. + +--- + +**Last Updated**: March 2026 diff --git a/context/architecture/RUNTIME_ARCHITECTURE.md b/context/architecture/RUNTIME_ARCHITECTURE.md new file mode 100644 index 0000000..39b8537 --- /dev/null +++ b/context/architecture/RUNTIME_ARCHITECTURE.md @@ -0,0 +1,142 @@ +# Runtime Architecture + +## WIT World Definition + +The SDK implements the `gcore:fastedge/reactor` world, defined in the `wit/` submodule (source: `G-Core/FastEdge-wit`). + +```wit +world reactor { + import http; // HTTP types and utilities + import http-client; // Outbound HTTP requests + import dictionary; // Fast read-only config + import secret; // Encrypted secret access + import key-value; // Persistent storage + import utils; // Diagnostics and stats + + export http-handler; // Main application entry point +} +``` + +--- + +## WIT Interfaces + +### http-handler (exported by app) + +```wit +interface http-handler { + use http.{request, response}; + process: func(req: request) -> response; +} +``` + +This is what the `#[fastedge::http]` macro implements via the `Guest` trait. + +### http-client (imported from platform) + +Provides `send-request` for outbound HTTP calls. Wrapped by `src/http_client.rs`. + +### key-value (imported from platform) + +Resource-based API: +- `store::open(name)` — open a named store +- `store::get(key)` — retrieve value +- `store::scan(pattern)` — glob-pattern scan +- `store::zrange-by-score(key, min, max)` — sorted set range query +- `store::zscan(key, pattern, cursor, count)` — sorted set scan +- `store::bf-exists(key, item)` — bloom filter membership check + +Errors: `no-such-store`, `access-denied`, `internal-error` + +### secret (imported from platform) + +- `get(key)` — retrieve current secret value +- `get-effective-at(key, timestamp)` — retrieve secret valid at a specific time + +Errors: `access-denied`, `decrypt-error`, `other(string)` + +### dictionary (imported from platform) + +- `get(key)` — fast read-only configuration lookup + +### utils (imported from platform) + +- `set-user-diag(value)` — set diagnostic string for monitoring + +--- + +## WIT Binding Generation + +In `src/lib.rs`: + +```rust +wit_bindgen::generate!({ + world: "reactor", + path: "wit", + pub_export_macro: true, +}); +``` + +This generates Rust types and traits matching all WIT interfaces. The generated code lives in `target/` (not in source tree) under namespaces like `gcore::fastedge::*` and `exports::gcore::fastedge::*`. + +--- + +## Submodules + +| Submodule | Git Source | Purpose | +|-----------|-----------|---------| +| `wit/` | `G-Core/FastEdge-wit` | WIT interface definitions — the contract between SDK and platform | +| `wasi-nn/` | `WebAssembly/wasi-nn` | ML inference interface for neural network support | + +Both are checked out via `.gitmodules`. The `wit/` submodule is required for building; `wasi-nn/` is optional. + +--- + +## Host-SDK Contract + +For the full ABI contract between the SDK and the host runtime — including all FFI functions, memory conventions, execution constraints, and WASM exports the host expects — see `architecture/HOST_SDK_CONTRACT.md`. + +For how requests flow through the handler phases (including the HTTP callout pause/resume mechanism), see `architecture/REQUEST_LIFECYCLE.md`. + +--- + +## ProxyWasm FFI Layer + +When the `proxywasm` feature is enabled (default), `src/proxywasm/mod.rs` declares FFI bindings: + +```rust +extern "C" { + fn proxy_secret_get(...) -> u32; + fn proxy_dictionary_get(...) -> u32; + fn proxy_kv_store_open(...) -> u32; + fn proxy_kv_store_get(...) -> u32; + // ... etc +} +``` + +Each wrapper module (`secret.rs`, `key_value.rs`, `dictionary.rs`, `utils.rs`) wraps these unsafe FFI calls in safe Rust APIs with proper error handling, pointer management, and type conversion. + +### FFI Wrapper Pattern + +```rust +pub fn get(key: &str) -> Result>, Error> { + let mut return_data: *mut u8 = std::ptr::null_mut(); + let mut return_size: usize = 0; + let status = unsafe { proxy_*_get(key.as_ptr(), key.len(), &mut return_data, &mut return_size) }; + // Convert status code → Result, copy data from host memory +} +``` + +--- + +## How WIT Changes Flow Through the Codebase + +1. Update `.wit` files in `wit/` submodule +2. Run `cargo build` — `wit-bindgen` regenerates Rust types +3. Update Rust wrapper module in `src/` to expose the new API +4. Add ProxyWasm FFI equivalent in `src/proxywasm/` if applicable +5. Create example demonstrating the new capability + +--- + +**Last Updated**: March 2026 diff --git a/context/architecture/SDK_ARCHITECTURE.md b/context/architecture/SDK_ARCHITECTURE.md new file mode 100644 index 0000000..406f196 --- /dev/null +++ b/context/architecture/SDK_ARCHITECTURE.md @@ -0,0 +1,203 @@ +# SDK Architecture + +## Dual API Approach + +The SDK supports two runtime models: + +### Component Model (Primary) + +- Uses WIT (WebAssembly Interface Types) via `wit-bindgen` 0.46 +- Type-safe generated bindings from `wit/` definitions +- Generated in `src/lib.rs` via `wit_bindgen::generate!` macro +- Modern WebAssembly Component Model standard + +### ProxyWasm (Secondary, feature-gated) + +- Enabled by default via `features = ["proxywasm"]` +- FFI-based using `extern "C"` declarations in `src/proxywasm/mod.rs` +- Compatible with Envoy and other proxy-wasm hosts +- Wraps unsafe FFI in safe Rust APIs + +--- + +## Two Handler Patterns + +### `#[wstd::http_server]` — Async / WASI-HTTP (Future) + +The forward path. Uses the `wstd` crate (v0.6) for async WASI-HTTP handlers. **This is the recommended approach for new apps.** + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + Ok(Response::builder() + .status(200) + .body(Body::from("Hello"))?) +} +``` + +- Uses standard WASI-HTTP interfaces (not FastEdge-specific) +- Async handler with proper HTTP client (`wstd::http::Client`) +- Examples: `examples/http/wasi/` (hello_world, headers, key_value, outbound_fetch, etc.) +- Dependency: `wstd = "0.6"` (external crate, not part of this SDK) + +### `#[fastedge::http]` — Sync / Original + +The original pattern provided by this SDK's derive macro (`derive/src/lib.rs`). Transforms sync functions into Component Model exports. + +```rust +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; + +#[fastedge::http] +fn main(req: Request) -> anyhow::Result> { + Ok(Response::builder().status(StatusCode::OK).body(Body::empty())?) +} +``` + +- Sync handler — no async support +- Macro generates Guest trait implementation with type conversions and error-to-500 handling +- Examples: `examples/http/basic/` (hello_world, headers, key_value, secret, backend, etc.) +- Dependency: `fastedge` crate (this SDK) + +**Note:** `#[wstd::http_server]` is the preferred choice for new apps, but `#[fastedge::http]` remains fully supported. + +--- + +## Type Conversion Pattern + +Three type systems are bridged: + +1. **Standard `http` crate types** — user-facing API (`http::Request`, `http::Response`) +2. **WIT-generated bindgen types** — runtime interface (`http_handler::Request`, `http_handler::Response`) +3. **Internal `Body` type** — wraps `bytes::Bytes` with content-type awareness + +Key conversions in `src/lib.rs`: + +| Conversion | Direction | +|------------|-----------| +| `From for http::Method` | bindgen → http crate | +| `TryFrom for http::Request` | bindgen → http crate | +| `From> for Response` | http crate → bindgen | +| `TryFrom for http::Response` | bindgen → http crate | + +--- + +## Body Type + +`body::Body` wraps `bytes::Bytes` with content-type metadata: + +```rust +pub struct Body { + pub(crate) content_type: String, + pub(crate) inner: Bytes, +} +``` + +- Implements `Deref` to `Bytes` for transparent access +- Auto-assigns content-type: `text/plain` for strings, `application/octet-stream` for bytes +- `Body::empty()` factory for empty responses +- Optional JSON support (`json` feature): `TryFrom` → `application/json` + +--- + +## Error Handling + +### SDK Error Type (`src/lib.rs`) + +```rust +#[derive(thiserror::Error, Debug)] +pub enum Error { + UnsupportedMethod(http::Method), // Unknown HTTP method + BindgenHttpError(HttpError), // WIT-generated error + HttpError(http::Error), // http crate error + InvalidBody, // Body conversion failure + InvalidStatusCode(u16), // Bad status code +} +``` + +### Module-Specific Errors + +Each ProxyWasm module defines its own error enum: +- **key_value**: `NoSuchStore`, `AccessDenied`, `InternalError` +- **secret**: `AccessDenied`, `DecryptError`, `Other(String)` + +### Handler Error Flow + +When a handler returns `Err(...)`, the `#[fastedge::http]` macro catches it and returns an HTTP 500 response with the error message as body. + +--- + +## Module Structure + +### Public API Surface + +```rust +// Re-exported from lib.rs +pub mod body; // Body type +pub mod dictionary; // dictionary::get() +pub mod secret; // secret::get(), secret::get_effective_at() +pub mod key_value; // Store resource +pub mod utils; // set_user_diag() +pub use http; // Re-export http crate +pub use send_request; // Outbound HTTP function +``` + +### Generated Namespaces (from WIT) + +``` +gcore::fastedge::http // HTTP types +gcore::fastedge::http_client // Outbound requests +gcore::fastedge::key_value // KV store +gcore::fastedge::secret // Secret access +gcore::fastedge::dictionary // Config lookups +gcore::fastedge::utils // Diagnostics +exports::gcore::fastedge::http_handler // Handler export +``` + +--- + +## HTTP Client (`src/http_client.rs`) + +`send_request(req: http::Request) -> Result, Error>` + +1. Converts `http::Request` → bindgen request types +2. Calls `http_client::send_request` (WIT import) +3. Converts bindgen response → `http::Response` +4. Supports all methods: GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS + +**Note:** Outbound HTTP calls to non-public (internal/private) IP addresses are blocked by the host. See `architecture/HOST_SDK_CONTRACT.md` for execution constraints. + +### HTTP Callouts in ProxyWasm (CDN Mode) + +CDN-mode apps use a different mechanism: `proxy_http_call()` which is asynchronous with a pause/resume pattern. The app pauses request processing, the host makes the outbound call, then delivers the response via `proxy_on_http_call_response()`. See `architecture/REQUEST_LIFECYCLE.md` for the full flow. + +--- + +## Import Patterns + +```rust +// Standard handler +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; + +// HTTP client +use fastedge::send_request; + +// Key-value store +use fastedge::key_value::Store; + +// Secrets & dictionary +use fastedge::secret; +use fastedge::dictionary; + +// Utilities +use fastedge::utils::set_user_diag; +``` + +--- + +**Last Updated**: March 2026 diff --git a/context/development/BUILD_AND_CI.md b/context/development/BUILD_AND_CI.md new file mode 100644 index 0000000..240cbe3 --- /dev/null +++ b/context/development/BUILD_AND_CI.md @@ -0,0 +1,146 @@ +# Build System and CI + +## Workspace Configuration + +Root `Cargo.toml` defines a workspace: + +```toml +[workspace] +members = ["derive"] + +[workspace.package] +version = "0.3.5" +edition = "2021" +license = "Apache-2.0" +``` + +Both `fastedge` and `fastedge-derive` share the workspace version. Examples are standalone crates with `path = "../.."` dependencies. + +--- + +## Build Target + +**Default target**: `wasm32-wasip1` (WebAssembly System Interface Preview 1) + +Set in `.cargo/config.toml`: +```toml +[build] +target = "wasm32-wasip1" +``` + +This means `cargo build` defaults to WASM output without needing `--target`. + +--- + +## Common Commands + +| Command | Purpose | +|---------|---------| +| `rustup target add wasm32-wasip1` | One-time setup | +| `cargo build --release` | Release build (WASM) | +| `cargo check` | Type-check without building | +| `cargo clippy --all-targets --all-features` | Lint | +| `cargo fmt` | Format code | +| `cargo test` | Run Rust-native tests (not WASM) | +| `cargo build --release --package ` | Build a specific example | +| `cargo doc` | Generate documentation | +| `cargo clean` | Clear build artifacts (useful after WIT changes) | + +--- + +## Build Outputs + +- Debug: `target/wasm32-wasip1/debug/*.wasm` +- Release: `target/wasm32-wasip1/release/*.wasm` + +Examples build as `cdylib` (dynamic library), producing `.wasm` files. + +--- + +## CI Pipeline (`.github/workflows/ci.yaml`) + +Triggered on every push. Steps: + +1. **Checkout** with submodules (recursive) +2. **Setup Rust** with `wasm32-wasip1` target +3. **Security audit**: `cargo-audit` on binary crate +4. **Build**: `cargo build --release --all-features` +5. **Documentation**: `cargo doc` +6. **Lint**: `cargo clippy --all-targets --all-features` + +Environment: `RUSTFLAGS="-Dwarnings"` — all warnings treated as errors. + +--- + +## Release Pipeline (`.github/workflows/release.yml`) + +Triggered on push to `main` or manual `workflow_dispatch`. Two jobs: + +### 1. Prepare + +- Analyzes conventional commits since last release +- Determines version bump (major/minor/patch) +- Creates a version bump PR + +### 2. Publish + +- Tags release as `v{version}` +- Creates GitHub release +- Publishes `fastedge-derive` to crates.io first (dependency) +- Publishes `fastedge` to crates.io second + +Version management uses `release-plz` (config in `release-plz.toml`). + +--- + +## FOSSA Compliance (`.github/workflows/fossa.yml`) + +License compliance checking via FOSSA. Runs on push to ensure all dependencies meet licensing requirements. + +--- + +## Example Build Pattern + +Each example is a **standalone crate** that must be independent of the root workspace. Every example `Cargo.toml` **must** start with an empty `[workspace]` table — this tells Cargo the example is its own workspace root and prevents it from being absorbed by the parent SDK workspace. + +```toml +# examples/http/basic/hello_world/Cargo.toml +[workspace] + +[package] +name = "hello-world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = { path = "../../../.." } +anyhow = "1.0" +``` + +**CRITICAL: The `[workspace]` line is required.** Without it, Cargo walks up the directory tree, finds the root `Cargo.toml`, and fails because the example isn't listed as a workspace member. This breaks standalone builds from within the example directory (e.g., from IDE extensions or `cd examples/http/basic/hello_world && cargo build`). + +Build: `cargo build --release --package hello-world` +Output: `target/wasm32-wasip1/release/hello_world.wasm` + +--- + +## Size Optimization + +For production WASM binaries: + +```toml +[profile.release] +opt-level = "z" # Optimize for size +lto = true # Link-time optimization +codegen-units = 1 # Single codegen unit +strip = true # Remove debug info +``` + +Optional post-build: `wasm-opt -Oz -o output.wasm input.wasm` (requires binaryen). + +--- + +**Last Updated**: March 2026 diff --git a/context/reference/ERROR_CODES.md b/context/reference/ERROR_CODES.md new file mode 100644 index 0000000..290bcc2 --- /dev/null +++ b/context/reference/ERROR_CODES.md @@ -0,0 +1,116 @@ +# Error Codes Reference + +Error codes encountered when developing and debugging FastEdge SDK apps. Covers both SDK-level errors (in your Rust code) and host-level status codes (returned by the platform). + +--- + +## Host Status Codes (3100-3120) + +These codes are returned by the FastEdge host runtime when something goes wrong during WASM execution. You'll see these in platform logs and monitoring — they help diagnose why a request failed. + +| Code | Name | Meaning | Common Cause | +|------|------|---------|--------------| +| `3100` | Context Error | Failed to initialize WASM module | Bad module binary, missing exports | +| `3101` | Execution Error | WASM execution failed | Unhandled panic, invalid operation | +| `3102` | Exit Error | Module called `proc_exit()` with non-zero code | Explicit error exit from app code | +| `3103` | Execution Panic | WASM trap (not timeout or OOM) | Unreachable code, division by zero, stack overflow | +| `3110` | Timeout (Interrupt) | Execution exceeded time limit (epoch interrupt) | Handler too slow, infinite loop | +| `3111` | Timeout (Elapsed) | Execution exceeded wall-clock time limit | Long-running async operation | +| `3120` | Out of Memory | WASM linear memory limit exceeded | Large allocations, unbounded data structures | + +### Debugging by Code + +**3100 — Context Error** +- Check that the WASM binary is valid and was built for `wasm32-wasip1` +- Ensure all required exports exist (`_initialize` or `_start`, handler function) + +**3103 — Execution Panic** +- Check for `unreachable!()`, `panic!()`, or `unwrap()` on `None`/`Err` in your code +- Stack overflows from deep recursion also trigger this + +**3110/3111 — Timeout** +- Profile your handler — is it doing too much work per request? +- Check for accidental infinite loops +- Consider whether outbound HTTP calls are timing out + +**3120 — Out of Memory** +- Avoid unbounded `Vec` growth or large string concatenation +- Check for memory leaks (though per-request instances limit impact) +- Use streaming approaches for large payloads where possible + +--- + +## SDK Error Enum + +The `fastedge::Error` enum in `src/lib.rs` covers errors within the SDK's type conversion and HTTP handling layer: + +```rust +pub enum Error { + UnsupportedMethod(http::Method), // HTTP method not recognized by the platform + BindgenHttpError(HttpError), // Error from WIT-generated HTTP types + HttpError(http::Error), // Error from the `http` crate + InvalidBody, // Body conversion failure + InvalidStatusCode(u16), // Status code out of valid range +} +``` + +These typically surface when: +- Building an `http::Request` or `http::Response` with invalid parameters +- Calling `send_request()` with an unsupported HTTP method +- Type conversion between SDK and bindgen types fails + +--- + +## Module-Specific Errors + +### Key-Value Store (`key_value::Error`) + +| Variant | Meaning | +|---------|---------| +| `NoSuchStore` | Store name not found — check deployment configuration | +| `AccessDenied` | App doesn't have permission to access this store | +| `InternalError` | Platform-side storage error | + +### Secrets (`secret::Error`) + +| Variant | Meaning | +|---------|---------| +| `AccessDenied` | App doesn't have permission to access this secret | +| `DecryptError` | Secret couldn't be decrypted — may be corrupted or expired | +| `Other(String)` | Platform-side error with description | + +--- + +## ProxyWasm FFI Status Codes + +The `proxy_*` FFI functions return `u32` status codes: + +| Value | Meaning | +|-------|---------| +| `0` | Success | +| `1` | Not found (key doesn't exist) | +| `2` | Bad argument | +| `3` | Not allowed | +| `6` | Internal failure | + +The SDK's ProxyWasm wrappers in `src/proxywasm/` translate these into Rust `Result` types — application code doesn't see raw status codes. + +--- + +## Handler Error Behavior + +### `#[fastedge::http]` (Sync) + +If the handler function returns `Err(...)`, the macro catches it and returns an HTTP 500 response with the error message as the response body. This is a safety net — prefer returning explicit error responses. + +### `#[wstd::http_server]` (Async) + +Similar behavior — unhandled errors result in a 500 response. Use `anyhow::Result` for ergonomic error handling. + +### ProxyWasm (CDN) + +Panics in CDN-mode handlers trigger a WASM trap, which the host reports as status code `3103`. Use proper error handling to avoid panics. + +--- + +**Last Updated**: March 2026 diff --git a/context/reference/PROPERTIES_REFERENCE.md b/context/reference/PROPERTIES_REFERENCE.md new file mode 100644 index 0000000..e3cf126 --- /dev/null +++ b/context/reference/PROPERTIES_REFERENCE.md @@ -0,0 +1,84 @@ +# Request Properties Reference + +Properties are read-only metadata about the current request and client, available via `proxy_get_property()` in ProxyWasm apps. They provide context that isn't in the HTTP headers themselves. + +--- + +## Available Properties + +### Client Information + +| Property Path | Type | Description | +|--------------|------|-------------| +| `request.x_real_ip` | string | Client's real IP address | +| `request.country` | string | Client's country (from IP geolocation) | +| `request.city` | string | Client's city (from IP geolocation) | +| `request.asn` | string | Client's Autonomous System Number | +| `request.geo.lat` | string | Client's latitude | +| `request.geo.long` | string | Client's longitude | + +### Request Metadata + +| Property Path | Type | Description | +|--------------|------|-------------| +| `request.host` | string | Request hostname (from CDN headers) | +| `request.uri` | string | Full request URI (scheme + host + path) | +| `request.scheme` | string | Request scheme (http/https) | +| `request.path` | string | Request path | + +### Tracing + +| Property Path | Type | Description | +|--------------|------|-------------| +| `request.traceparent` | string | W3C Trace Context traceparent header for distributed tracing | + +--- + +## Usage in ProxyWasm Apps + +```rust +// In an HttpContext implementation +fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + // Get client's country + if let Some(country) = self.get_property(vec!["request", "country"]) { + let country_str = String::from_utf8(country).unwrap_or_default(); + // Use for geo-routing, access control, etc. + } + + // Get client IP + if let Some(ip) = self.get_property(vec!["request", "x_real_ip"]) { + // Use for rate limiting, logging, etc. + } + + Action::Continue +} +``` + +--- + +## Usage in Component Model Apps + +In `#[fastedge::http]` or `#[wstd::http_server]` apps, some of these values are available through request headers or the `dictionary` interface rather than properties directly. The property system is primarily a ProxyWasm concept. + +For Component Model apps: +- **Client IP** — available via request headers (e.g., `X-Real-IP`) +- **Geo data** — available via `dictionary::get()` with appropriate keys +- **Host/URI** — available from the `http::Request` object directly + +--- + +## Property Caching + +Properties are cached per-request by the host after first access. Repeated lookups for the same property within a single request are fast. Geo-related properties (country, city, lat/long) may involve an IP lookup on first access. + +--- + +## Notes + +- All property values are returned as raw bytes (`Vec`). The SDK developer is responsible for parsing (usually UTF-8 strings). +- Properties not available for the current request return `None`. +- The property set may expand over time as the platform adds capabilities. + +--- + +**Last Updated**: March 2026 diff --git a/docs/HOST_SERVICES.md b/docs/HOST_SERVICES.md new file mode 100644 index 0000000..933843b --- /dev/null +++ b/docs/HOST_SERVICES.md @@ -0,0 +1,395 @@ +# FastEdge Rust SDK — Host Services + +Host-provided service modules for key-value storage, secret management, configuration dictionaries, and diagnostic utilities. + +The host-service modules documented here (`fastedge::key_value`, `fastedge::secret`, `fastedge::dictionary`, `fastedge::utils`) are part of the core FastEdge API and are available regardless of whether the `proxywasm` feature is enabled. The `proxywasm` feature only enables the `fastedge::proxywasm::*` compatibility layer. No additional Cargo.toml changes are needed beyond the standard `fastedge` dependency. + +--- + +## Key-Value Storage + +The `fastedge::key_value` module provides persistent storage with support for simple key-value pairs, glob-style key scanning, sorted sets, and bloom filters. Data is organized into named stores; access to a store must be granted via platform configuration. + +### Opening a Store + +```rust +pub fn new() -> Result +pub fn open(name: &str) -> Result +``` + +`Store::new()` opens the default store. `Store::open(name)` opens a named store. Both return `Err(Error::NoSuchStore)` if the store label is not recognized and `Err(Error::AccessDenied)` if the application is not authorized. + +```rust +use fastedge::key_value::Store; + +// Open the default store +let store = Store::new()?; + +// Open a named store +let store = Store::open("user-data")?; +``` + +### Reading Values + +```rust +pub fn get(&self, key: &str) -> Result>, Error> +``` + +Returns `Ok(Some(bytes))` if the key exists, `Ok(None)` if it does not. + +```rust +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; +use fastedge::key_value::Store; + +#[fastedge::http] +fn main(req: Request) -> Result> { + let store = Store::open("user-data")?; + + match store.get("user:123:profile")? { + Some(data) => Response::builder() + .status(StatusCode::OK) + .body(Body::from(data)) + .map_err(Into::into), + None => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .map_err(Into::into), + } +} +``` + +### Pattern Scanning + +```rust +pub fn scan(&self, pattern: &str) -> Result, Error> +``` + +Scans the store for keys matching a glob-style pattern. Returns a list of matching key names. Returns an empty `Vec` if no keys match. + +Supported glob syntax: + +| Pattern | Matches | +| ------- | ------------------------------------------- | +| `*` | Any sequence of characters within a segment | +| `?` | Any single character | +| `[abc]` | Any character in the set | + +```rust +use fastedge::key_value::Store; + +#[fastedge::http] +fn main(_req: fastedge::http::Request) -> anyhow::Result> { + let store = Store::open("user-data")?; + + let keys = store.scan("user:123:*")?; + for key in &keys { + println!("Found key: {}", key); + } + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::from(format!("{} keys found", keys.len()))) + .map_err(Into::into) +} +``` + +### Sorted Sets + +Sorted sets store members associated with a `f64` score. Members are ordered from lowest to highest score. + +```rust +pub fn zrange_by_score(&self, key: &str, min: f64, max: f64) -> Result, f64)>, Error> +pub fn zscan(&self, key: &str, pattern: &str) -> Result, f64)>, Error> +``` + +`zrange_by_score` returns all members of the sorted set stored at `key` whose score falls in the inclusive range `[min, max]`. Use `f64::NEG_INFINITY` and `f64::INFINITY` for unbounded ranges. + +`zscan` returns members of the sorted set at `key` whose member value matches the glob-style `pattern`. + +Both return an empty `Vec` when the key does not exist or no members fall within the specified range or pattern. + +```rust +use fastedge::key_value::Store; + +#[fastedge::http] +fn main(_req: fastedge::http::Request) -> anyhow::Result> { + let store = Store::open("game-data")?; + + // Retrieve all leaderboard entries with scores >= 1000 + let top_players = store.zrange_by_score("leaderboard", 1000.0, f64::INFINITY)?; + for (member, score) in &top_players { + let name = String::from_utf8_lossy(member); + println!("Player: {}, Score: {}", name, score); + } + + // Retrieve sorted set members matching a pattern + let guild_members = store.zscan("guild:42:members", "player:*")?; + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::from(format!("{} top players", top_players.len()))) + .map_err(Into::into) +} +``` + +### Bloom Filters + +```rust +pub fn bf_exists(&self, key: &str, item: &str) -> Result +``` + +Tests whether `item` is a member of the bloom filter stored at `key`. Returns `true` if the item was probably added to the filter (subject to the false-positive rate of the filter), or `false` if the key does not exist or the item was definitely not added. + +Bloom filters cannot produce false negatives: if `bf_exists` returns `false`, the item has not been added. + +```rust +use fastedge::key_value::Store; + +#[fastedge::http] +fn main(req: fastedge::http::Request) -> anyhow::Result> { + let store = Store::open("rate-limit")?; + + let client_ip = "203.0.113.42"; + + if store.bf_exists("blocked_ips", client_ip)? { + return fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::FORBIDDEN) + .body(fastedge::body::Body::from("Blocked")) + .map_err(Into::into); + } + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::empty()) + .map_err(Into::into) +} +``` + +### Error Handling + +All `Store` methods return `Result<_, Error>`. The `Error` type has three variants: + +| Variant | Description | +| ---------------------- | --------------------------------------------------------------- | +| `Error::NoSuchStore` | The requested store label is not recognized by the host | +| `Error::AccessDenied` | The application does not have permission to access the store | +| `Error::Other(String)` | An implementation-specific error (I/O or internal host failure) | + +```rust +use fastedge::key_value::{Store, Error}; + +#[fastedge::http] +fn main(_req: fastedge::http::Request) -> anyhow::Result> { + let store = match Store::open("config") { + Ok(s) => s, + Err(Error::NoSuchStore) => { + return fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::INTERNAL_SERVER_ERROR) + .body(fastedge::body::Body::from("Store not configured")) + .map_err(Into::into); + } + Err(Error::AccessDenied) => { + return fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::FORBIDDEN) + .body(fastedge::body::Body::from("Access denied")) + .map_err(Into::into); + } + Err(Error::Other(msg)) => { + return Err(anyhow::anyhow!("KV store error: {}", msg)); + } + }; + + // continue using store + let _ = store.get("key")?; + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::empty()) + .map_err(Into::into) +} +``` + +--- + +## Secret Management + +The `fastedge::secret` module provides access to encrypted secrets such as API keys, passwords, and certificates. Secrets are encrypted at rest and support versioned retrieval for rotation scenarios. + +### Reading Secrets + +```rust +pub fn get(key: &str) -> Result>, Error> +``` + +Returns the currently effective value of the named secret. Returns `Ok(None)` if no secret with that name is configured. + +```rust +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; +use fastedge::secret; + +#[fastedge::http] +fn main(_req: Request) -> Result> { + let api_key = match secret::get("UPSTREAM_API_KEY")? { + Some(key) => key, + None => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("API key not configured")) + .map_err(Into::into); + } + }; + + // Use api_key bytes for authentication — do not log or include in responses + let _ = api_key; + + Response::builder() + .status(StatusCode::OK) + .body(Body::empty()) + .map_err(Into::into) +} +``` + +### Time-Based Retrieval + +```rust +pub fn get_effective_at(key: &str, at: u32) -> Result>, Error> +``` + +Returns the value of the named secret that was effective at the given Unix timestamp (`at`, seconds since epoch). This is useful during secret rotation: you can verify that both the old and new versions of a secret are accessible before completing a rotation. + +Returns `Ok(None)` if no version of the secret was configured at that time. + +```rust +use fastedge::secret; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[fastedge::http] +fn main(_req: fastedge::http::Request) -> anyhow::Result> { + // Retrieve the secret that was valid 5 minutes ago + let five_minutes_ago = SystemTime::now() + .duration_since(UNIX_EPOCH)? + .as_secs() as u32 + - 300; + + let previous_secret = secret::get_effective_at("SIGNING_KEY", five_minutes_ago)?; + + // Retrieve the current secret + let current_secret = secret::get("SIGNING_KEY")?; + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::empty()) + .map_err(Into::into) +} +``` + +### Security Notes + +- Never include secret values in HTTP responses, log output, or diagnostic messages. +- Secret values are returned as raw bytes (`Vec`). Convert to a string only when the secret is defined as UTF-8 text, and handle the conversion error explicitly. +- Access to secrets is controlled by platform configuration. Unauthorized access returns `Err(secret::Error)`, not `Ok(None)`. `Ok(None)` indicates the secret is not configured or not found. +- Clear secret material from memory as soon as it is no longer needed. Rust's ownership model helps with this: binding a secret to a local variable ensures it is dropped at the end of its scope. + +--- + +## Dictionary + +The `fastedge::dictionary` module provides fast, read-only lookups for configuration values that do not change during the lifetime of a deployment. + +### Configuration Lookups + +```rust +pub fn get(key: &str) -> Option +``` + +Returns `Some(value)` if the key exists and its value is valid UTF-8, or `None` if the key is not found or the value cannot be decoded as UTF-8. + +```rust +use fastedge::dictionary; + +#[fastedge::http] +fn main(_req: fastedge::http::Request) -> anyhow::Result> { + let upstream = dictionary::get("upstream_origin") + .unwrap_or_else(|| "https://default.example.com".to_string()); + + let timeout_ms: u64 = dictionary::get("timeout_ms") + .and_then(|v| v.parse().ok()) + .unwrap_or(5000); + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::from(format!( + "Upstream: {}, Timeout: {}ms", + upstream, timeout_ms + ))) + .map_err(Into::into) +} +``` + +### When to Use Dictionary vs Key-Value vs Secrets + +| Criterion | `dictionary` | `key_value` | `secret` | +| ---------------------------- | ------------------------------------- | ----------------------------------------- | ------------------------------------------- | +| **Mutability** | Read-only; set at deployment time | Read-only from application code | Read-only; managed by platform | +| **Value type** | UTF-8 strings only | Arbitrary bytes | Arbitrary bytes | +| **Advanced data structures** | No | Sorted sets, bloom filters, glob scan | No | +| **Confidentiality** | Not encrypted; visible in config | Not encrypted at the application layer | Encrypted at rest; access-controlled | +| **Typical use cases** | Feature flags, routing config, tuning | Caching, counters, state, rate-limit data | API keys, tokens, certificates, credentials | +| **Versioning / rotation** | No | No | Yes, via `get_effective_at` | + +Use `dictionary` for simple, non-sensitive string configuration that is known at deployment time. Use `key_value` for larger datasets, binary values, or data that requires advanced query patterns. Use `secret` for any value that must be kept confidential. + +--- + +## Utilities + +The `fastedge::utils` module provides diagnostic functions for monitoring and debugging edge applications. + +### Diagnostics + +```rust +pub fn set_user_diag(value: &str) +``` + +Writes a diagnostic string that appears in the FastEdge platform logs associated with the current request. This is intended for debugging and operational monitoring. There is no return value; the function panics if the host rejects the call. + +```rust +use fastedge::utils::set_user_diag; + +#[fastedge::http] +fn main(req: fastedge::http::Request) -> anyhow::Result> { + set_user_diag("handler entered"); + + let store = fastedge::key_value::Store::open("cache")?; + + match store.get("config:version")? { + Some(v) => { + set_user_diag(&format!("config version: {}", String::from_utf8_lossy(&v))); + } + None => { + set_user_diag("config version: not found"); + } + } + + fastedge::http::Response::builder() + .status(fastedge::http::StatusCode::OK) + .body(fastedge::body::Body::empty()) + .map_err(Into::into) +} +``` + +One diagnostic message per request is the typical pattern. If `set_user_diag` is called multiple times, the platform may record only the last value or concatenate them depending on runtime behavior. + +Do not write sensitive values (secrets, credentials, personally identifiable information) to diagnostics, as the output appears in platform logs that may be accessible to operations personnel. + +--- + +## See Also + +- [SDK_API.md](SDK_API.md) — Core HTTP handler, `send_request`, `Body`, and the `#[fastedge::http]` macro +- [quickstart.md](quickstart.md) — Getting started guide +- [INDEX.md](INDEX.md) — Documentation index diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..81fd716 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,17 @@ +# 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. + +## Documents + +| File | Description | +| ------------------------------------ | ---------------------------------------------------------------------------- | +| [quickstart.md](quickstart.md) | Getting started: adding the dependency, writing a handler, building to WASM | +| [SDK_API.md](SDK_API.md) | Public API reference: types, traits, macros, error variants, feature flags | +| [HOST_SERVICES.md](HOST_SERVICES.md) | Host-provided services: KV store, secrets, outbound HTTP, request properties | + +## Suggested Reading Order + +1. **[quickstart.md](quickstart.md)** — Start here. Covers dependency setup, a minimal handler, and building to `wasm32-wasip1`. +2. **[SDK_API.md](SDK_API.md)** — Reference for all public types, the `#[fastedge::http]` and `#[wstd::http_server]` handler macros, the `Body` type, HTTP request/response types, and error handling. +3. **[HOST_SERVICES.md](HOST_SERVICES.md)** — Reference for runtime services your handler can call: outbound HTTP, key-value store, secrets, and request properties. diff --git a/docs/SDK_API.md b/docs/SDK_API.md new file mode 100644 index 0000000..3c33439 --- /dev/null +++ b/docs/SDK_API.md @@ -0,0 +1,390 @@ +# FastEdge Rust SDK — Core API + +Reference for the `fastedge` crate. Covers the handler macro, body type, outbound HTTP, errors, and feature flags. For host services (key-value, secrets, dictionary), see HOST_SERVICES.md. + +--- + +## Quick Start + +### Cargo.toml + +Add the following to your project's `Cargo.toml`. The crate version can be confirmed in the repository's `Cargo.toml` under `[workspace.package]`. + +```toml +[dependencies] +fastedge = "0.3" +anyhow = "1.0" + +[lib] +crate-type = ["cdylib"] +``` + +### Minimal Handler + +```rust +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; + +#[fastedge::http] +fn main(_req: Request) -> Result> { + Response::builder() + .status(StatusCode::OK) + .body(Body::from("Hello, FastEdge!")) + .map_err(Into::into) +} +``` + +### Build + +```bash +rustup target add wasm32-wasip1 +cargo build --target wasm32-wasip1 --release +``` + +The output `.wasm` file is located at `target/wasm32-wasip1/release/.wasm`. + +--- + +## Handler Macro + +### `#[fastedge::http]` + +```rust +#[proc_macro_attribute] +pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream +``` + +Applied to a function, this attribute macro registers it as the HTTP request handler for the WebAssembly component. The decorated function must match the following signature: + +```rust +fn (req: fastedge::http::Request) -> anyhow::Result> +``` + +**Requirements:** + +- Accepts exactly one parameter of type `Request`. +- Returns `Result>`. Any `Result` type whose error implements `Into>` (such as `anyhow::Result`) is accepted. +- The function name is not significant; `main` is conventional. + +**Error handling:** + +If the function returns `Err(e)`, the macro converts it to an HTTP `500 Internal Server Error` response with the error message as the body. No panic occurs. + +**Examples:** + +```rust +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; + +#[fastedge::http] +fn main(_req: Request) -> Result> { + Response::builder() + .status(StatusCode::OK) + .body(Body::from("OK")) + .map_err(Into::into) +} +``` + +```rust +use anyhow::{anyhow, Result}; +use fastedge::body::Body; +use fastedge::http::{Method, Request, Response, StatusCode}; + +#[fastedge::http] +fn main(req: Request) -> Result> { + match req.method() { + &Method::GET => Response::builder() + .status(StatusCode::OK) + .body(Body::from("GET OK")) + .map_err(Into::into), + _ => Err(anyhow!("method not allowed")), + } +} +``` + +### `#[wstd::http_server]` (Async Alternative) + +`#[wstd::http_server]` is an alternative handler macro from the [`wstd`](https://crates.io/crates/wstd) crate. It provides an async handler using the standard WASI-HTTP interface and targets `wasm32-wasip2`. + +| Aspect | `#[fastedge::http]` | `#[wstd::http_server]` | +| ------------------ | ------------------------ | ------------------------ | +| Execution model | Synchronous | Async (`async fn`) | +| HTTP client | `fastedge::send_request` | `wstd::http::Client` | +| Body type | `fastedge::body::Body` | `wstd::http::body::Body` | +| Build target | `wasm32-wasip1` | `wasm32-wasip2` | +| Interface standard | FastEdge-specific WIT | WASI-HTTP (standard) | + +```rust,no_run +// wstd async handler — requires wstd dependency and wasm32-wasip2 target +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let upstream = Request::get("https://api.example.com/data") + .header("accept", "application/json") + .body(Body::empty())?; + let response = Client::new().send(upstream).await?; + Ok(response) +} +``` + +To avoid passing `--target` on every build, add a `.cargo/config.toml` to your project: + +```toml +[build] +target = "wasm32-wasip2" +``` + +Then `cargo build --release` is sufficient. + +--- + +## Body Type + +```rust +pub struct Body { /* private fields */ } +``` + +`fastedge::body::Body` wraps [`bytes::Bytes`](https://docs.rs/bytes) and carries a MIME content-type. The content-type is set at construction time based on the input data. + +`Body` implements `Deref`, so all `Bytes` methods (`.len()`, `.is_empty()`, slicing, iteration) are available directly. + +### 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` | + +```rust +use fastedge::body::Body; + +let text = Body::from("hello"); +let owned = Body::from(String::from("hello")); +let bytes = Body::from(vec![0x48u8, 0x69]); +let empty = Body::empty(); +``` + +```rust +// json feature required +use fastedge::body::Body; +use serde_json::json; + +# fn main() -> Result<(), serde_json::Error> { +let body = Body::try_from(json!({"status": "ok"}))?; +assert_eq!(body.content_type(), "application/json"); +# Ok(()) +# } +``` + +### 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 | + +All methods from `bytes::Bytes` are available via `Deref`: + +```rust +use fastedge::body::Body; + +let body = Body::from("hello"); +assert_eq!(body.len(), 5); +assert!(!body.is_empty()); +let slice: &[u8] = &body[..]; +``` + +### Content-Type Detection + +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` | + +To send a response with a specific content-type that does not match the automatic detection, set the `Content-Type` header explicitly on the response builder: + +```rust +use fastedge::body::Body; +use fastedge::http::{Response, StatusCode}; + +let html = "

Hello

"; +let response = Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/html; charset=utf-8") + .body(Body::from(html)) + .unwrap(); +``` + +--- + +## Outbound HTTP + +### `send_request` + +```rust +pub fn send_request(req: http::Request) -> Result, Error> +``` + +Sends a synchronous outbound HTTP request to a backend service and returns the response. + +**Supported methods:** `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS`. Any other method returns `Err(Error::UnsupportedMethod)`. + +**Errors:** + +- `Error::UnsupportedMethod` — the request method is not in the supported set. +- `Error::BindgenHttpError` — the host runtime rejected or failed the request. +- `Error::InvalidBody` — the response body could not be decoded. + +```rust +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Method, Request, Response, StatusCode}; + +#[fastedge::http] +fn main(_req: Request) -> Result> { + let upstream = Request::builder() + .method(Method::GET) + .uri("https://api.example.com/data") + .header("accept", "application/json") + .body(Body::empty())?; + + let upstream_resp = fastedge::send_request(upstream)?; + + Response::builder() + .status(StatusCode::OK) + .body(upstream_resp.into_body()) + .map_err(Into::into) +} +``` + +```rust +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Method, Request, Response, StatusCode}; + +#[fastedge::http] +fn main(req: Request) -> Result> { + let payload = Body::from(r#"{"event":"click"}"#); + let upstream = Request::builder() + .method(Method::POST) + .uri("https://ingest.example.com/events") + .header("content-type", "application/json") + .body(payload)?; + + let _resp = fastedge::send_request(upstream)?; + + Response::builder() + .status(StatusCode::ACCEPTED) + .body(Body::empty()) + .map_err(Into::into) +} +``` + +--- + +## Error Handling + +### Error Enum + +```rust +#[derive(thiserror::Error, Debug)] +pub enum Error { + UnsupportedMethod(http::Method), + BindgenHttpError(/* host HTTP error */), + HttpError(http::Error), + InvalidBody, + InvalidStatusCode(u16), +} +``` + +| 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 +use fastedge::{Error, send_request}; +use fastedge::body::Body; +use fastedge::http::{Method, Request}; + +fn fetch(uri: &str) -> Result { + let req = Request::builder() + .method(Method::GET) + .uri(uri) + .body(Body::empty()) + .map_err(Error::HttpError)?; + + let resp = send_request(req)?; + Ok(format!("status: {}", resp.status())) +} +``` + +--- + +## 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 | + +Enable non-default features in `Cargo.toml`: + +```toml +[dependencies] +fastedge = { version = "0.3", features = ["json"] } +``` + +Disable the default `proxywasm` feature if you do not need it: + +```toml +[dependencies] +fastedge = { version = "0.3", default-features = false } +``` + +--- + +## Re-exports + +`fastedge` re-exports the [`http`](https://crates.io/crates/http) crate as `fastedge::http`. All standard HTTP types are available through this path without adding `http` as a direct dependency. + +```rust +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` | + +--- + +## See Also + +- [HOST_SERVICES.md](HOST_SERVICES.md) — Key-value store, secrets, and dictionary APIs +- [quickstart.md](quickstart.md) — Getting started guide +- [INDEX.md](INDEX.md) — Documentation index diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..35b4fa8 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,163 @@ +# Getting Started with FastEdge Rust SDK + +Build and deploy edge computing applications that compile to WebAssembly using the `fastedge` crate. + +## Prerequisites + +- Rust toolchain (stable) +- `wasm32-wasip1` target — required for the sync handler path +- `wasm32-wasip2` target — required for the async WASI handler path + +Install both targets: + +```bash +rustup target add wasm32-wasip1 +rustup target add wasm32-wasip2 +``` + +## Create a New Project + +Create a new library crate: + +```bash +cargo new --lib my-edge-app +cd my-edge-app +``` + +The crate must be compiled as a `cdylib`. Add to `Cargo.toml`: + +```toml +[lib] +crate-type = ["cdylib"] +``` + +## Option A: Async Handler (Recommended) + +The async handler uses the standard WASI-HTTP interface via the [`wstd`](https://crates.io/crates/wstd) crate. This path supports `async`/`await` and a full HTTP client. + +Add dependencies to `Cargo.toml`: + +```toml +[dependencies] +wstd = "0.6" +anyhow = "1.0" + +[lib] +crate-type = ["cdylib"] +``` + +Write the handler in `src/lib.rs`: + +```rust,no_run +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let url = request.uri().to_string(); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain;charset=UTF-8") + .body(Body::from(format!("Hello, you made a request to {url}")))?) +} +``` + +Build targeting `wasm32-wasip2`: + +```bash +cargo build --target wasm32-wasip2 --release +``` + +To avoid passing `--target` on every build, add a `.cargo/config.toml` to your project: + +```toml +[build] +target = "wasm32-wasip2" +``` + +Then `cargo build --release` is sufficient. + +The compiled `.wasm` file is written to `target/wasm32-wasip2/release/`. + +## Option B: Basic Sync Handler + +The sync handler uses the `fastedge` crate directly. It is synchronous and suited for simple request/response processing where `async` is not required. + +Add dependencies to `Cargo.toml`: + +```toml +[dependencies] +fastedge = "0.3" +anyhow = "1.0" + +[lib] +crate-type = ["cdylib"] +``` + +Write the handler in `src/lib.rs`: + +```rust,no_run +use anyhow::Result; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; + +#[fastedge::http] +fn main(req: Request) -> Result> { + let url = req.uri().to_string(); + + Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/plain;charset=UTF-8") + .body(Body::from(format!("Hello, you made a request to {url}"))) + .map_err(Into::into) +} +``` + +Build targeting `wasm32-wasip1`: + +```bash +cargo build --target wasm32-wasip1 --release +``` + +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` | + +Both commands produce a `.wasm` binary in the respective `target//release/` directory. + +## Feature Flags + +| Feature | Default | Description | +| ------------ | ------- | ----------------------------------------- | +| `proxywasm` | yes | Enable ProxyWasm compatibility layer | +| `json` | no | Enable JSON body support via `serde_json` | + +Enable the `json` feature in `Cargo.toml`: + +```toml +[dependencies] +fastedge = { version = "0.3", features = ["json"] } +``` + +## Next Steps + +Once your handler compiles, you can extend it with outbound HTTP and platform host services: + +- **Outbound HTTP** — call backend services using `fastedge::send_request` (sync) or `wstd::http::Client` (async) — see [SDK_API.md](SDK_API.md) +- **Key-Value Storage** — read and write persistent data via `fastedge::key_value::Store` +- **Secrets** — retrieve encrypted credentials via `fastedge::secret::get` +- **Dictionary** — read static configuration via `fastedge::dictionary::get` + +See [HOST_SERVICES.md](HOST_SERVICES.md) for key-value, secrets, dictionary, and utilities. + +## See Also + +- [SDK_API.md](SDK_API.md) — Core API: handler macros, Body type, outbound HTTP, error handling +- [HOST_SERVICES.md](HOST_SERVICES.md) — Key-value storage, secrets, dictionary, utilities +- [INDEX.md](INDEX.md) — Documentation index diff --git a/examples/http/basic/.cargo/config.toml b/examples/http/basic/.cargo/config.toml new file mode 100644 index 0000000..6b509f5 --- /dev/null +++ b/examples/http/basic/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/examples/http/basic/hello_world/src/lib.rs b/examples/http/basic/hello_world/src/lib.rs index 7474cf7..036c420 100644 --- a/examples/http/basic/hello_world/src/lib.rs +++ b/examples/http/basic/hello_world/src/lib.rs @@ -9,6 +9,8 @@ fn main(req: Request) -> Result> { Response::builder() .status(StatusCode::OK) .header("content-type", "text/plain;charset=UTF-8") - .body(Body::from(format!("Hello, you made a request to {url}"))) + .body(Body::from(format!( + "Hello, you made a basic request to {url}" + ))) .map_err(Into::into) } diff --git a/examples/http/wasi/.cargo/config.toml b/examples/http/wasi/.cargo/config.toml new file mode 100644 index 0000000..f68f33c --- /dev/null +++ b/examples/http/wasi/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip2" diff --git a/examples/http/wasi/geo_redirect/Cargo.lock b/examples/http/wasi/geo_redirect/Cargo.lock index b900fd6..80eb26f 100644 --- a/examples/http/wasi/geo_redirect/Cargo.lock +++ b/examples/http/wasi/geo_redirect/Cargo.lock @@ -32,37 +32,6 @@ 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" @@ -72,54 +41,12 @@ 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" @@ -141,76 +68,14 @@ dependencies = [ "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 = "geo_redirect_wasi" version = "0.1.0" dependencies = [ "anyhow", - "fastedge", "wstd", ] -[[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.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[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" @@ -244,24 +109,6 @@ dependencies = [ "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.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - [[package]] name = "instant" version = "0.1.13" @@ -277,36 +124,12 @@ 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" @@ -319,16 +142,6 @@ 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" @@ -347,12 +160,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -412,38 +219,12 @@ dependencies = [ "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" @@ -456,53 +237,7 @@ version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen 0.51.0", -] - -[[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", + "wit-bindgen", ] [[package]] @@ -514,85 +249,6 @@ 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" diff --git a/examples/http/wasi/hello_world/src/lib.rs b/examples/http/wasi/hello_world/src/lib.rs index f83958a..0a6227e 100644 --- a/examples/http/wasi/hello_world/src/lib.rs +++ b/examples/http/wasi/hello_world/src/lib.rs @@ -8,5 +8,7 @@ async fn main(request: Request) -> anyhow::Result> { Ok(Response::builder() .status(200) .header("content-type", "text/plain;charset=UTF-8") - .body(Body::from(format!("Hello, you made a request to {url}")))?) + .body(Body::from(format!( + "Hello, you made a wasi request to {url}" + )))?) } diff --git a/fastedge-plugin-source/.generation-config.md b/fastedge-plugin-source/.generation-config.md new file mode 100644 index 0000000..3d023a1 --- /dev/null +++ b/fastedge-plugin-source/.generation-config.md @@ -0,0 +1,289 @@ +# Generation Config — FastEdge Rust SDK + +> **What this file is**: Structured instructions for generating `docs/` from source code. +> **How to use it**: Run `./fastedge-plugin-source/generate-docs.sh` which reads this file and invokes Claude per doc file. +> **Who reads this**: The generation script + Claude CLI. The plugin pipeline does NOT read this file. + +--- + +## Global Rules + +### Audience + +These docs are for **consumers** of the `fastedge` Rust crate — developers building edge computing apps that compile to WASM. They are NOT for contributors developing this repo. + +This distinction drives what to include and exclude: +- **Include**: anything a consumer needs to use the crate (public API, types, macros, build instructions) +- **Exclude**: internal implementation details, contributor workflows, WIT binding internals +- **Test**: "Would someone adding `fastedge` to their Cargo.toml need to know this?" If no, leave it out. + +### Style +- Technical prose only — no marketing language, no superlatives, no "easily" or "simply" +- Use Rust code signatures for all types, functions, and methods +- Use fenced code blocks for all examples (language-tagged as `rust`) +- Use tables for structured data (methods, error variants, config fields) +- Tables must use padded columns aligned to the widest cell in each column. Pad every cell with spaces so columns line up visually in raw markdown. The separator row dashes must match each column's padded width. + Good: `| Method | Return Type | Description |` + Bad: `| Method | Return Type | Description |` + Good: `| --------------------- | -------------------- | ----------- |` + Bad: `|---|---|---|` +- Every code example must be self-contained and compilable (with appropriate `no_run` or `ignore` annotations) + +### Structure +- Every doc file starts with a level-1 heading and a one-line description +- Use level-2 headings for major sections, level-3 for subsections +- No table of contents — keep files navigable by heading structure alone +- End each file with a "See Also" section linking ONLY to other files in `docs/` (SDK_API.md, HOST_SERVICES.md, quickstart.md, INDEX.md). Never link to files from `context/` or `context/reference/` — those are internal developer docs, not consumer-facing. + +### Exclusions (apply to all files) +- No internal implementation details (how type conversions work internally, macro expansion details) +- No source code file paths or line numbers +- No version history or changelog entries +- No references to `context/` or `CLAUDE.md` (those are internal developer docs) +- No WIT binding internals (`wit_bindgen::generate!`, bindgen types) +- No `#[doc(hidden)]` items + +### Accuracy +- Never hardcode the crate version — instruct the generator to read it from `Cargo.toml` +- All type signatures must match actual source code declarations +- Feature flag conditions must be accurate (`#[cfg(feature = "proxywasm")]`, `#[cfg(feature = "json")]`) + +### Coverage Rule +- Every public export, type, function, method, and error variant must appear in exactly one doc file +- If something is referenced in multiple files, one file is authoritative (has the full definition) and others link to it + +--- + +## docs/SDK_API.md + +### Source Files +- `src/lib.rs` — public re-exports, `Error` enum, `body::Body` type, type conversions, module-level doc comments +- `src/http_client.rs` — `send_request` function +- `derive/src/lib.rs` — `#[fastedge::http]` attribute macro +- `Cargo.toml` — crate version, feature flags, dependencies + +### Scope +Core SDK API reference. Everything a developer needs to write a basic FastEdge handler. + +**This file does NOT cover**: Host services like key-value, secret, dictionary (→ HOST_SERVICES.md), example catalog (→ EXAMPLES.md). + +### Required Content + +**#[fastedge::http] macro:** +- Full signature and usage pattern +- Function requirements (parameter type, return type) +- Error handling behavior (Err → HTTP 500) +- Comparison with `#[wstd::http_server]` (async alternative) + +**Body type (`fastedge::body::Body`):** +- All constructors: `from(String)`, `from(&str)`, `from(Vec)`, `from(&[u8])`, `empty()` +- `TryFrom` (json feature) +- Methods: `content_type()`, `empty()` +- Content-type auto-detection rules +- `Deref` for accessing raw bytes + +**Error enum (`fastedge::Error`):** +- All variants with descriptions +- When each variant occurs + +**send_request function:** +- Signature: `pub fn send_request(req: http::Request) -> Result, Error>` +- Usage pattern with request builder + +**Feature flags:** +- `proxywasm` (default) — enables ProxyWasm compatibility +- `json` — enables `serde_json` body support + +**Re-exports:** +- `fastedge::http` (re-exported `http` crate) +- Supported HTTP methods (GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS) + +**Quick start:** +- Cargo.toml setup (read version from source, don't hardcode) +- Minimal handler example +- Build command (`cargo build --target wasm32-wasip1 --release`) + +### Structure +``` +# FastEdge Rust SDK — Core API +## Quick Start +### Cargo.toml +### Minimal Handler +### Build +## Handler Macro +### #[fastedge::http] +### #[wstd::http_server] (Async Alternative) +## Body Type +### Constructors +### Methods +### Content-Type Detection +## Outbound HTTP +### send_request +## Error Handling +### Error Enum +## Feature Flags +## Re-exports +## See Also +``` + +### DO NOT document +- `#[doc(hidden)] pub use crate::exports::gcore::fastedge::http_handler` — internal +- `mod helper` — internal +- Type conversion `impl From`/`impl TryFrom` details — internal plumbing +- `wit_bindgen::generate!` invocations — internal + +--- + +## docs/HOST_SERVICES.md + +### Source Files +- `src/lib.rs` — `key_value`, `secret`, `dictionary`, `utils` module declarations with doc comments +- `src/proxywasm/key_value.rs` — `Store` struct, `Error` enum, all methods +- `src/proxywasm/secret.rs` — `get`, `get_effective_at` functions +- `src/proxywasm/dictionary.rs` — `get` function +- `src/proxywasm/utils.rs` — `set_user_diag` function + +### Scope +All host-provided service modules. Everything beyond basic HTTP handling. + +**This file does NOT cover**: Core HTTP handling, Body type, send_request (→ SDK_API.md). + +### Required Content + +**Key-Value Storage (`fastedge::key_value`):** +- `Store` struct — all public methods: + - `new()` → open default store + - `open(name)` → open named store + - `get(key)` → read a value + - `scan(pattern)` → glob-style key scanning + - `zrange_by_score(key, min, max)` → sorted set range query + - `zscan(key, pattern)` → sorted set pattern scan + - `bf_exists(key, item)` → bloom filter membership test +- `Error` enum — all variants +- Return types (exact Rust types, not summaries) +- Usage examples for each operation category + +CRITICAL — exact method signatures from source: +```rust +pub fn new() -> Result +pub fn open(name: &str) -> Result +pub fn get(&self, key: &str) -> Result>, Error> +pub fn zrange_by_score(&self, key: &str, min: f64, max: f64) -> Result, f64)>, Error> +pub fn scan(&self, pattern: &str) -> Result, Error> +pub fn zscan(&self, key: &str, pattern: &str) -> Result, f64)>, Error> +pub fn bf_exists(&self, key: &str, item: &str) -> Result +``` + +**Secret Management (`fastedge::secret`):** +- `get(key)` — current secret value +- `get_effective_at(key, at)` — time-based retrieval for rotation +- `Error` type — the core API has a proper error type for access failures +- Security notes (don't log secrets, access controlled by platform) +- CRITICAL: `Ok(None)` means the secret is not configured/not found. Unauthorized access returns `Err(secret::Error)`, NOT `Ok(None)`. Do not conflate "not found" with "access denied". + +**Dictionary (`fastedge::dictionary`):** +- `get(key)` — read-only configuration lookup +- When to use dictionary vs key-value vs secrets + +**Utilities (`fastedge::utils`):** +- `set_user_diag(value)` — diagnostic information +- Where diagnostics appear (platform logs) + +**Feature gating — CRITICAL accuracy:** +- The host-service modules (`fastedge::key_value`, `fastedge::secret`, `fastedge::dictionary`, `fastedge::utils`) are part of the core FastEdge API. They do NOT require the `proxywasm` feature. +- The `proxywasm` feature only enables the `fastedge::proxywasm::*` compatibility layer, which is a separate concern. +- State clearly that no additional Cargo.toml feature flags are needed for these host services. + +### Structure +``` +# FastEdge Rust SDK — Host Services +## Key-Value Storage +### Opening a Store +### Reading Values +### Pattern Scanning +### Sorted Sets +### Bloom Filters +### Error Handling +## Secret Management +### Reading Secrets +### Time-Based Retrieval +### Security Notes +## Dictionary +### Configuration Lookups +### When to Use Dictionary vs Key-Value vs Secrets +## Utilities +### Diagnostics +## See Also +``` + +### DO NOT document +- ProxyWasm FFI internals (`extern "C"` declarations in `src/proxywasm/mod.rs`) +- Internal error conversion details +- `proxy_get_property`, `proxy_set_property` raw FFI functions + +--- + +## docs/quickstart.md + +### Source Files +- `Cargo.toml` — crate version, dependencies +- `src/lib.rs` — quick start doc comments (lines 38-100) +- `examples/http/wasi/hello_world/Cargo.toml` — WASI example deps (read `wstd` version from here, do NOT guess) +- `examples/http/wasi/hello_world/src/lib.rs` — recommended starter example +- `examples/http/basic/hello_world/src/lib.rs` — alternative basic example + +### Scope +Getting started guide. From zero to deployed FastEdge app. + +**This file does NOT cover**: Full API reference (→ SDK_API.md), host services (→ HOST_SERVICES.md). + +### Required Content +- Prerequisites (Rust, wasm32-wasip1 target for basic, wasm32-wasip2 target for WASI) +- Cargo.toml setup +- Two paths: basic sync handler vs async WASI handler (recommend WASI) +- Build commands: `cargo build --release` for both (no `cargo-component` needed) +- Brief mention of outbound HTTP (`send_request`) — link to SDK_API.md (NOT HOST_SERVICES.md) +- Brief mention of host services (KV, secrets, dictionary) — link to HOST_SERVICES.md + +CRITICAL — version accuracy: +- Read the `fastedge` crate version from root `Cargo.toml` `[package]` section +- Read the `wstd` version from `examples/http/wasi/hello_world/Cargo.toml` — do NOT invent a version +- `#[wstd::http_server]` apps use `wasm32-wasip2` target, NOT `wasm32-wasip1` +- Neither handler type needs `cargo-component` — plain `cargo build --release` suffices + +### Structure +``` +# Getting Started with FastEdge Rust SDK +## Prerequisites +## Create a New Project +## Option A: Async Handler (Recommended) +## Option B: Basic Sync Handler +## Build +## Next Steps +## See Also +``` + +--- + +## docs/INDEX.md + +### Source Files +- `Cargo.toml` — crate name, version, description +- `docs/SDK_API.md` — for linking +- `docs/HOST_SERVICES.md` — for linking +- `docs/quickstart.md` — for linking + +### Scope +Entry point that lists all doc files with brief descriptions. + +### Required Content +- Crate name and one-line summary +- List of all doc files with brief description +- Suggested reading order + +### Structure +``` +# FastEdge Rust SDK Documentation +## Documents +## Suggested Reading Order +``` diff --git a/fastedge-plugin-source/check-copilot-sync.sh b/fastedge-plugin-source/check-copilot-sync.sh new file mode 100755 index 0000000..28d1153 --- /dev/null +++ b/fastedge-plugin-source/check-copilot-sync.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Validates copilot-instructions.md stays in sync with the codebase: +# 1. All doc files in manifest.json are referenced in the mapping table +# 2. All doc files in the mapping table actually exist on disk +# +# This script is part of the fastedge-plugin pipeline contract. +# Canonical template: fastedge-plugin/scripts/sync/templates/check-copilot-sync-template.sh +# Each source repo gets a copy at: fastedge-plugin-source/check-copilot-sync.sh +# +# Exits 0 if in sync, 1 if drift detected. + +set -euo pipefail + +MANIFEST="fastedge-plugin-source/manifest.json" +COPILOT=".github/copilot-instructions.md" +errors=0 + +if [ ! -f "$COPILOT" ]; then + echo "FAIL: $COPILOT does not exist" + exit 1 +fi + +# --- Check 1: manifest doc files appear in the mapping table --- + +# Extract doc/schema paths that appear in mapping table rows (lines starting with '|') +# Use POSIX-compatible awk instead of grep -P so this works on macOS/BSD grep too +mapping_table_docs=$(awk ' + /^\|/ { + line = $0 + while (match(line, /`(docs|schemas)\/[^`]+`/)) { + print substr(line, RSTART + 1, RLENGTH - 2) + line = substr(line, RSTART + RLENGTH) + } + } +' "$COPILOT" | sort -u) + +if [ -z "$mapping_table_docs" ]; then + echo "FAIL: No doc/schema paths found in $COPILOT mapping table — expected backticked docs/ or schemas/ paths in table rows" + exit 1 +fi + +if [ -f "$MANIFEST" ]; then + if ! command -v jq &>/dev/null; then + echo "Error: jq is required but not installed" + exit 1 + fi + + doc_files=$(jq -r '.sources[].files[]' "$MANIFEST" | grep -E '^(docs|schemas)/' | sort -u) + + missing=() + for doc in $doc_files; do + if ! echo "$mapping_table_docs" | grep -qF "$doc"; then + missing+=("$doc") + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + echo "FAIL: Doc files from $MANIFEST missing from $COPILOT:" + for f in "${missing[@]}"; do + echo " - $f" + done + errors=1 + else + echo "OK: All manifest doc files are referenced in copilot-instructions.md" + fi +else + echo "SKIP: No manifest found at $MANIFEST" +fi + +# --- Check 2: doc files referenced in mapping table exist on disk --- + +# Reuse mapping_table_docs extracted above for check 2 +stale=() +while IFS= read -r doc_path; do + [ -z "$doc_path" ] && continue + if [ ! -f "$doc_path" ]; then + stale+=("$doc_path") + fi +done <<< "$mapping_table_docs" + +if [ ${#stale[@]} -gt 0 ]; then + echo "FAIL: Doc files referenced in $COPILOT mapping table do not exist:" + for f in "${stale[@]}"; do + echo " - $f" + done + errors=1 +else + echo "OK: All doc files in copilot-instructions mapping table exist on disk" +fi + +# --- Result --- + +if [ $errors -ne 0 ]; then + echo "" + echo "Fix the issues above and re-run this check." + exit 1 +fi diff --git a/fastedge-plugin-source/generate-docs.sh b/fastedge-plugin-source/generate-docs.sh new file mode 100755 index 0000000..ed8055f --- /dev/null +++ b/fastedge-plugin-source/generate-docs.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate docs/ from source code using .generation-config.md +# +# Usage: +# ./fastedge-plugin-source/generate-docs.sh # all files (parallel where possible) +# ./fastedge-plugin-source/generate-docs.sh SDK_API.md # specific file +# ./fastedge-plugin-source/generate-docs.sh SDK_API.md HOST_SERVICES.md # multiple files +# +# Reference implementation: fastedge-test/fastedge-plugin-source/generate-docs.sh + +# Model to use for generation (sonnet is recommended for cost efficiency) +MODEL="sonnet" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_FILE="$SCRIPT_DIR/.generation-config.md" +DOCS_DIR="$REPO_ROOT/docs" + +# --- Cleanup on interrupt --- +# Two mechanisms work together to ensure clean shutdown: +# +# 1. kill_tree() recursively kills each background subshell AND its children +# (the `claude` processes). Plain `kill $pid` only kills the subshell, +# leaving `claude` orphaned. +# +# 2. INTERRUPT_FLAG file signals subshells to stop retrying. Bash variables +# don't cross process boundaries, so a temp file is the reliable way to +# communicate "stop" to background jobs before their next retry iteration. +ALL_PIDS=() +INTERRUPT_FLAG=$(mktemp /tmp/.generate-docs-interrupt.XXXXXX) +rm -f "$INTERRUPT_FLAG" # absent = running; present = stop + +kill_tree() { + local pid=$1 + local children + children=$(pgrep -P "$pid" 2>/dev/null || true) + for child in $children; do + kill_tree "$child" + done + kill "$pid" 2>/dev/null || true +} + +cleanup() { + echo "" + echo "Interrupted — killing background processes..." + touch "$INTERRUPT_FLAG" # tell subshells to stop retrying + trap - INT TERM # prevent re-entry + + for pid in "${ALL_PIDS[@]}"; do + kill_tree "$pid" + done + + # Clean up temp files left by killed generate_file subshells + # Must happen BEFORE kill -- -$$ which kills this script too + rm -f "$DOCS_DIR"/.*.md.[a-zA-Z0-9]* 2>/dev/null || true + rm -f "$INTERRUPT_FLAG" + + # Belt-and-suspenders: kill entire process group (including this script) + kill -- -$$ 2>/dev/null || true + exit 130 +} + +trap cleanup INT TERM + +# ============================================================================= +# === CUSTOMIZE: Define your doc files and their dependency tiers === +# +# Tier 1: Independent files (generated in parallel — read from source code only) +# Tier 2: Files that reference tier 1 docs (e.g. quickstart, getting-started) +# Tier 3: Files that summarize all other docs (e.g. INDEX.md) +# ============================================================================= + +TIER1_FILES=("SDK_API.md" "HOST_SERVICES.md") +TIER2_FILES=("quickstart.md") +TIER3_FILES=("INDEX.md") + +ALL_FILES=("${TIER1_FILES[@]}" "${TIER2_FILES[@]}" "${TIER3_FILES[@]}") + +# ============================================================================= +# === CUSTOMIZE: Map each doc file to its source files === +# +# Keys must match the filenames in the tier arrays above. +# Values are space-separated paths relative to the repo root. +# The script reads each file and passes its content to the generation prompt. +# ============================================================================= + +declare -A SOURCE_FILES +SOURCE_FILES[SDK_API.md]="src/lib.rs src/http_client.rs derive/src/lib.rs Cargo.toml" +SOURCE_FILES[HOST_SERVICES.md]="src/lib.rs src/proxywasm/key_value.rs src/proxywasm/secret.rs src/proxywasm/dictionary.rs src/proxywasm/utils.rs" +SOURCE_FILES[quickstart.md]="Cargo.toml src/lib.rs examples/http/wasi/hello_world/Cargo.toml examples/http/wasi/hello_world/src/lib.rs examples/http/basic/hello_world/src/lib.rs" +SOURCE_FILES[INDEX.md]="Cargo.toml" + +# ============================================================================= +# === CUSTOMIZE: Package name for the generation prompt === +# ============================================================================= + +PACKAGE_NAME="fastedge" + +# ============================================================================= +# === END CUSTOMIZATION — everything below is the reusable engine === +# ============================================================================= + +if [ ! -f "$CONFIG_FILE" ]; then + echo "Error: $CONFIG_FILE not found" + exit 1 +fi + +# Determine which files to generate +if [ $# -eq 0 ]; then + targets=("${ALL_FILES[@]}") + run_all=true +else + targets=("$@") + run_all=false + # Validate targets + for target in "${targets[@]}"; do + found=false + for valid in "${ALL_FILES[@]}"; do + if [ "$target" = "$valid" ]; then + found=true + break + fi + done + if [ "$found" = false ]; then + echo "Error: unknown doc file '$target'" + echo "Valid files: ${ALL_FILES[*]}" + exit 1 + fi + done +fi + +mkdir -p "$DOCS_DIR" + +# Clean up stale temp files from previous interrupted runs +rm -f "$DOCS_DIR"/.*.md.[a-zA-Z0-9]* 2>/dev/null || true + +generate_file() { + local target="$1" + + # Validate that SOURCE_FILES has an entry for this target + if [ -z "${SOURCE_FILES[$target]+set}" ] || [ -z "${SOURCE_FILES[$target]}" ]; then + echo " ERROR: no SOURCE_FILES entry for '$target' — add one to the CUSTOMIZE section" + return 1 + fi + + local sources="${SOURCE_FILES[$target]}" + + # Build the source files content block + local source_content="" + local loaded=0 + for src in $sources; do + local full_path="$REPO_ROOT/$src" + if [ ! -f "$full_path" ]; then + echo " Warning: source file $src not found, skipping" + continue + fi + source_content+=" +--- FILE: $src --- +$(cat "$full_path") +--- END FILE --- +" + loaded=$((loaded + 1)) + done + + if [ "$loaded" -eq 0 ]; then + echo " ERROR: all source files for '$target' are missing (expected: $sources)" + return 1 + fi + + # Extract the section for this target from generation-config + # Use awk variable to avoid regex delimiter issues with / + local section + local escaped_target="docs/$target" + section=$(awk -v start="## $escaped_target" ' + $0 == start { found=1; next } + found && /^## docs\// { exit } + found { print } + ' "$CONFIG_FILE") + + # Validate that the config section exists and has content + if [ -z "$(echo "$section" | tr -d '[:space:]')" ]; then + echo " ERROR: no instructions found for '$target' — add a '## docs/$target' section to $CONFIG_FILE" + return 1 + fi + + # Check for existing doc to enable incremental updates + # When docs/ already exists, it is passed as context so the model + # preserves accurate content and manual additions, only changing what is + # incorrect, incomplete, or missing per the source code. + local existing_doc="" + local existing_path="$DOCS_DIR/$target" + local mode="Generate" + if [ -f "$existing_path" ]; then + existing_doc=$(cat "$existing_path") + mode="Update" + fi + + if [ "$mode" = "Update" ]; then + echo "Updating docs/$target ..." + else + echo "Generating docs/$target ..." + fi + + local existing_section="" + if [ -n "$existing_doc" ]; then + existing_section=" +# 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. + + +$existing_doc + +" + fi + + # Build prompt with sandwich output constraint: + # The OUTPUT CONSTRAINT appears at both the start and end of the prompt. + # This is critical for large prompts where the model may lose track of + # the instruction to output only raw markdown. Without it, the model + # sometimes produces conversational preamble or asks for permission. + local prompt + prompt="$(cat < "$tmpfile" + + # 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" + echo " Done: docs/$target" + return 0 + fi + + echo " Attempt $attempt/$max_attempts failed for $target (got conversational output), retrying..." + attempt=$((attempt + 1)) + done + + rm -f "$tmpfile" + echo " FAILED after $max_attempts attempts: docs/$target" + return 1 +} + +# Run a tier of files in parallel, wait for all to complete +run_tier() { + local tier_name="$1" + shift + local files=("$@") + local pids=() + local failed=() + + # Skip empty tiers + if [ ${#files[@]} -eq 0 ]; then + return 0 + fi + + echo "--- $tier_name (${#files[@]} files in parallel) ---" + + for target in "${files[@]}"; do + generate_file "$target" & + pids+=($!) + ALL_PIDS+=($!) + done + + # Wait for all and collect failures + for i in "${!pids[@]}"; do + if ! wait "${pids[$i]}"; then + failed+=("${files[$i]}") + fi + done + + if [ ${#failed[@]} -gt 0 ]; then + echo " FAILED in $tier_name: ${failed[*]}" + return 1 + fi + return 0 +} + +# --- Main execution --- + +if [ "$run_all" = true ]; then + # Parallel tiered execution + tier_failed=false + + run_tier "Tier 1: Core reference" "${TIER1_FILES[@]}" || tier_failed=true + + if [ "$tier_failed" = false ]; then + run_tier "Tier 2: Quickstart" "${TIER2_FILES[@]}" || tier_failed=true + else + echo "Skipping Tier 2 due to Tier 1 failures" + fi + + if [ "$tier_failed" = false ]; then + run_tier "Tier 3: Index" "${TIER3_FILES[@]}" || tier_failed=true + else + echo "Skipping Tier 3 due to earlier failures" + fi + + echo "" + echo "=== Generation Complete ===" + echo "Generated: ${#ALL_FILES[@]} file(s) in docs/" + if [ "$tier_failed" = true ]; then + echo "Some files failed — check output above" + exit 1 + fi + + # Regenerate llms.txt from docs/ contents (if the script is installed) + if [ -x "$SCRIPT_DIR/generate-llms-txt.sh" ]; then + "$SCRIPT_DIR/generate-llms-txt.sh" + fi +else + # Specific files: run sequentially (user chose explicit order) + failed=() + for target in "${targets[@]}"; do + if ! generate_file "$target"; then + failed+=("$target") + echo " FAILED: docs/$target" + fi + done + + echo "" + echo "=== Generation Complete ===" + echo "Generated: ${#targets[@]} file(s) in docs/" + if [ ${#failed[@]} -gt 0 ]; then + echo "Failed: ${failed[*]}" + exit 1 + fi +fi diff --git a/fastedge-plugin-source/generate-llms-txt.sh b/fastedge-plugin-source/generate-llms-txt.sh new file mode 100755 index 0000000..616dbd3 --- /dev/null +++ b/fastedge-plugin-source/generate-llms-txt.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate llms.txt from docs/ contents +# +# Produces an llms.txt file at the repo root that indexes all documentation +# files in docs/. This follows the llms.txt proposal (llmstxt.org) to help +# LLM agents discover and navigate package documentation. +# +# Usage: +# ./fastedge-plugin-source/generate-llms-txt.sh # standalone +# ./fastedge-plugin-source/generate-docs.sh # calls this automatically after a full run +# +# Requirements: bash 4+, jq (only if package.json is the name source) +# No customization needed — package name and docs are discovered at runtime. +# Supports: package.json (Node), Cargo.toml (Rust), pyproject.toml (Python), +# or falls back to the directory name. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DOCS_DIR="$REPO_ROOT/docs" +OUTPUT="$REPO_ROOT/llms.txt" + +# --- Validate prerequisites --- + +if [ ! -d "$DOCS_DIR" ]; then + echo "Error: docs/ directory not found" + exit 1 +fi + +if [ ! -f "$DOCS_DIR/INDEX.md" ]; then + echo "Error: docs/INDEX.md not found — required for llms.txt summary" + exit 1 +fi + +# --- Extract package name (language-agnostic) --- +# Tries in order: package.json (Node), Cargo.toml (Rust), pyproject.toml (Python), dirname fallback + +detect_package_name() { + if [ -f "$REPO_ROOT/package.json" ]; then + if command -v jq &>/dev/null; then + jq -r '.name' "$REPO_ROOT/package.json" + return + fi + fi + + if [ -f "$REPO_ROOT/Cargo.toml" ]; then + sed -n '/^\[package\]/,/^\[/{ s/^name *= *"\(.*\)"/\1/p; }' "$REPO_ROOT/Cargo.toml" | head -1 + return + fi + + if [ -f "$REPO_ROOT/pyproject.toml" ]; then + sed -n '/^\[project\]/,/^\[/{ s/^name *= *"\(.*\)"/\1/p; }' "$REPO_ROOT/pyproject.toml" | head -1 + return + fi + + basename "$REPO_ROOT" +} + +PACKAGE_NAME=$(detect_package_name) + +if [ -z "$PACKAGE_NAME" ]; then + PACKAGE_NAME=$(basename "$REPO_ROOT") + echo "Warning: could not detect package name, using directory name: $PACKAGE_NAME" +fi + +# Extract summary from INDEX.md line 3 (expected format: blockquote or plain text after H1 + blank line) +# Strips leading "> " if present +SUMMARY=$(sed -n '3p' "$DOCS_DIR/INDEX.md" | sed 's/^> //') + +if [ -z "$SUMMARY" ]; then + echo "Warning: could not extract summary from docs/INDEX.md line 3, using package name" + SUMMARY="Documentation for $PACKAGE_NAME" +fi + +# --- Build llms.txt --- + +{ + echo "# $PACKAGE_NAME" + echo "" + echo "> $SUMMARY" + echo "" + echo "## Documentation" + echo "" + + # Curated order: INDEX first (entry point), then quickstart, then rest alphabetically. + # This keeps the most useful docs near the top rather than relying on glob order + # (which puts lowercase filenames like quickstart.md last). + PRIORITY_FILES=("INDEX.md" "quickstart.md") + + for pfile in "${PRIORITY_FILES[@]}"; do + if [ -f "$DOCS_DIR/$pfile" ]; then + heading=$(head -1 "$DOCS_DIR/$pfile" | sed 's/^#\+ //') + [ -z "$heading" ] && heading="${pfile%.md}" + echo "- [$heading](docs/$pfile)" + fi + done + + # Remaining docs alphabetically, skip priority files + for doc in "$DOCS_DIR"/*.md; do + filename=$(basename "$doc") + skip=false + for pfile in "${PRIORITY_FILES[@]}"; do + [ "$filename" = "$pfile" ] && skip=true && break + done + [ "$skip" = true ] && continue + + heading=$(head -1 "$doc" | sed 's/^#\+ //') + if [ -z "$heading" ]; then + heading="${filename%.md}" + fi + + echo "- [$heading](docs/$filename)" + done +} > "$OUTPUT" + +echo " Done: llms.txt ($(wc -l < "$OUTPUT") lines)" diff --git a/fastedge-plugin-source/manifest.json b/fastedge-plugin-source/manifest.json new file mode 100644 index 0000000..2eeb777 --- /dev/null +++ b/fastedge-plugin-source/manifest.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://fastedge-plugin-source/manifest/v1", + "repo_id": "fastedge-sdk-rust", + "version": "1.0.0", + "sources": { + "sdk-api": { + "files": ["docs/SDK_API.md"], + "required": true, + "description": "Core SDK API — #[fastedge::http] macro, Body type, Error enum, send_request, type conversions" + }, + "host-services": { + "files": ["docs/HOST_SERVICES.md"], + "required": true, + "description": "Host service modules — key_value::Store, secret, dictionary, utils" + } + }, + "target_mapping": { + "sdk-api": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference.md", + "section": "Rust SDK" + }, + "host-services": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference.md", + "section": "Rust SDK" + } + }, + "validation": { + "mode": "advisory", + "strict_fields": ["sdk-api", "host-services"] + } +} diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..17f7deb --- /dev/null +++ b/llms.txt @@ -0,0 +1,10 @@ +# 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 + +- [FastEdge Rust SDK Documentation](docs/INDEX.md) +- [Getting Started with FastEdge Rust SDK](docs/quickstart.md) +- [FastEdge Rust SDK — Host Services](docs/HOST_SERVICES.md) +- [FastEdge Rust SDK — Core API](docs/SDK_API.md) diff --git a/src/lib.rs b/src/lib.rs index 5972382..5358dda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,18 +89,21 @@ //! } //! ``` //! -//! Build with [`cargo-component`] instead of `cargo build`: +//! Build for WebAssembly using the `wasm32-wasip2` target: //! //! ```bash -//! cargo install cargo-component -//! cargo component build --release +//! rustup target add wasm32-wasip2 +//! cargo build --target wasm32-wasip2 --release //! ``` //! -//! See the [fetch example] for a complete working app and a side-by-side comparison -//! with the FastEdge SDK approach. +//! Tip: add a `.cargo/config.toml` to your project to avoid passing `--target` every time: //! -//! [`cargo-component`]: https://github.com/bytecodealliance/cargo-component -//! [fetch example]: https://github.com/G-Core/FastEdge-sdk-rust/tree/main/examples/fetch +//! ```toml +//! [build] +//! target = "wasm32-wasip2" +//! ``` +//! +//! Then `cargo build --release` is all you need. //! //! ## Feature Flags //!