From f7855ef4d68b7c6c85b6f014b1c29d196df88707 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 17 Feb 2026 10:36:24 -0600 Subject: [PATCH 1/4] initial spin implementation --- Cargo.lock | 123 +++- Cargo.toml | 3 + crates/edgezero-adapter-spin/Cargo.toml | 27 + crates/edgezero-adapter-spin/src/cli.rs | 358 +++++++++++ crates/edgezero-adapter-spin/src/context.rs | 26 + crates/edgezero-adapter-spin/src/lib.rs | 45 ++ crates/edgezero-adapter-spin/src/proxy.rs | 92 +++ crates/edgezero-adapter-spin/src/request.rs | 102 ++++ crates/edgezero-adapter-spin/src/response.rs | 39 ++ .../src/templates/Cargo.toml.hbs | 23 + .../src/templates/spin.toml.hbs | 16 + .../src/templates/src/lib.rs.hbs | 12 + crates/edgezero-cli/Cargo.toml | 2 + examples/app-demo/Cargo.lock | 569 +++++++++++++----- examples/app-demo/Cargo.toml | 3 + .../crates/app-demo-adapter-spin/Cargo.toml | 24 + .../crates/app-demo-adapter-spin/spin.toml | 16 + .../crates/app-demo-adapter-spin/src/lib.rs | 12 + examples/app-demo/edgezero.toml | 29 +- 19 files changed, 1376 insertions(+), 145 deletions(-) create mode 100644 crates/edgezero-adapter-spin/Cargo.toml create mode 100644 crates/edgezero-adapter-spin/src/cli.rs create mode 100644 crates/edgezero-adapter-spin/src/context.rs create mode 100644 crates/edgezero-adapter-spin/src/lib.rs create mode 100644 crates/edgezero-adapter-spin/src/proxy.rs create mode 100644 crates/edgezero-adapter-spin/src/request.rs create mode 100644 crates/edgezero-adapter-spin/src/response.rs create mode 100644 crates/edgezero-adapter-spin/src/templates/Cargo.toml.hbs create mode 100644 crates/edgezero-adapter-spin/src/templates/spin.toml.hbs create mode 100644 crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs create mode 100644 examples/app-demo/crates/app-demo-adapter-spin/Cargo.toml create mode 100644 examples/app-demo/crates/app-demo-adapter-spin/spin.toml create mode 100644 examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4faf62a..6004f24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,6 +731,24 @@ dependencies = [ "walkdir", ] +[[package]] +name = "edgezero-adapter-spin" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "ctor", + "edgezero-adapter", + "edgezero-core", + "futures", + "futures-util", + "log", + "spin-sdk", + "tempfile", + "walkdir", +] + [[package]] name = "edgezero-cli" version = "0.1.0" @@ -741,6 +759,7 @@ dependencies = [ "edgezero-adapter-axum", "edgezero-adapter-cloudflare", "edgezero-adapter-fastly", + "edgezero-adapter-spin", "edgezero-core", "futures", "handlebars", @@ -1053,7 +1072,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1572,7 +1591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -1979,6 +1998,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "routefinder" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0971d3c8943a6267d6bd0d782fdc4afa7593e7381a92a3df950ff58897e066b5" +dependencies = [ + "smartcow", + "smartstring", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -2299,6 +2328,26 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smartcow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" +dependencies = [ + "smartstring", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "socket2" version = "0.6.2" @@ -2309,12 +2358,64 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin-executor" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba409d00af758cd5de128da4a801e891af0545138f66a688f025f6d4e33870b" +dependencies = [ + "futures", + "once_cell", + "wasi 0.13.1+wasi-0.2.0", +] + +[[package]] +name = "spin-macro" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f959f16928e3c023468e41da9ebb77442e2ce22315e8dab11508fe76b3567ee1" +dependencies = [ + "anyhow", + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "spin-sdk" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8951c7c4ab7f87f332d497789eeed9631c8116988b628b4851eb2fa999ead019" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "form_urlencoded", + "futures", + "http", + "once_cell", + "routefinder", + "spin-executor", + "spin-macro", + "thiserror 2.0.18", + "wasi 0.13.1+wasi-0.2.0", + "wit-bindgen 0.51.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2755,6 +2856,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.13.1+wasi-0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f43d1c36145feb89a3e61aa0ba3e582d976a8ab77f1474aa0adb80800fe0cf8" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -3273,6 +3383,15 @@ dependencies = [ "wit-parser", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0780cf7046630ed70f689a098cd8d56c5c3b22f2a7379bbdb088879963ff96" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "wit-bindgen-rust" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 5a95130..2d46770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/edgezero-adapter-axum", "crates/edgezero-adapter-cloudflare", "crates/edgezero-adapter-fastly", + "crates/edgezero-adapter-spin", "crates/edgezero-adapter", "crates/edgezero-cli", "crates/edgezero-core", @@ -35,6 +36,7 @@ edgezero-adapter = { path = "crates/edgezero-adapter" } edgezero-adapter-axum = { path = "crates/edgezero-adapter-axum", default-features = false } edgezero-adapter-cloudflare = { path = "crates/edgezero-adapter-cloudflare", default-features = false } edgezero-adapter-fastly = { path = "crates/edgezero-adapter-fastly", default-features = false } +edgezero-adapter-spin = { path = "crates/edgezero-adapter-spin", default-features = false } edgezero-core = { path = "crates/edgezero-core", default-features = false } fastly = "0.11" fern = "0.7" @@ -53,6 +55,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" simple_logger = "5" +spin-sdk = { version = "5.2", default-features = false } tempfile = "3" thiserror = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } diff --git a/crates/edgezero-adapter-spin/Cargo.toml b/crates/edgezero-adapter-spin/Cargo.toml new file mode 100644 index 0000000..be28c52 --- /dev/null +++ b/crates/edgezero-adapter-spin/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "edgezero-adapter-spin" +edition = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[features] +default = [] +spin = ["dep:spin-sdk"] +cli = ["dep:edgezero-adapter", "edgezero-adapter/cli", "dep:ctor", "dep:walkdir"] + +[dependencies] +edgezero-core = { path = "../edgezero-core" } +edgezero-adapter = { path = "../edgezero-adapter", optional = true, features = ["cli"] } +anyhow = { workspace = true } +async-trait = { workspace = true } +bytes = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } +log = { workspace = true } +spin-sdk = { workspace = true, optional = true } +ctor = { workspace = true, optional = true } +walkdir = { workspace = true, optional = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/crates/edgezero-adapter-spin/src/cli.rs b/crates/edgezero-adapter-spin/src/cli.rs new file mode 100644 index 0000000..4893252 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/cli.rs @@ -0,0 +1,358 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use ctor::ctor; +use edgezero_adapter::cli_support::{ + find_manifest_upwards, find_workspace_root, path_distance, read_package_name, +}; +use edgezero_adapter::scaffold::{ + register_adapter_blueprint, AdapterBlueprint, AdapterFileSpec, CommandTemplates, + DependencySpec, LoggingDefaults, ManifestSpec, ReadmeInfo, TemplateRegistration, +}; +use edgezero_adapter::{register_adapter, Adapter, AdapterAction}; +use walkdir::WalkDir; + +const TARGET_TRIPLE: &str = "wasm32-wasip1"; + +pub fn build(extra_args: &[String]) -> Result { + let manifest = find_spin_manifest( + std::env::current_dir() + .map_err(|e| e.to_string())? + .as_path(), + )?; + let manifest_dir = manifest + .parent() + .ok_or_else(|| "spin manifest has no parent directory".to_string())?; + let cargo_manifest = manifest_dir.join("Cargo.toml"); + let crate_name = read_package_name(&cargo_manifest)?; + + let status = Command::new("cargo") + .args([ + "build", + "--release", + "--target", + TARGET_TRIPLE, + "--manifest-path", + cargo_manifest + .to_str() + .ok_or("invalid Cargo manifest path")?, + ]) + .args(extra_args) + .status() + .map_err(|e| format!("failed to run cargo build: {e}"))?; + if !status.success() { + return Err(format!("cargo build failed with status {status}")); + } + + let workspace_root = find_workspace_root(manifest_dir); + let artifact = locate_artifact(&workspace_root, manifest_dir, &crate_name)?; + let pkg_dir = workspace_root.join("pkg"); + fs::create_dir_all(&pkg_dir) + .map_err(|e| format!("failed to create {}: {e}", pkg_dir.display()))?; + let dest = pkg_dir.join(format!("{crate_name}.wasm")); + fs::copy(&artifact, &dest) + .map_err(|e| format!("failed to copy artifact to {}: {e}", dest.display()))?; + + Ok(dest) +} + +pub fn deploy(extra_args: &[String]) -> Result<(), String> { + let manifest = find_spin_manifest( + std::env::current_dir() + .map_err(|e| e.to_string())? + .as_path(), + )?; + let manifest_dir = manifest + .parent() + .ok_or_else(|| "spin manifest has no parent directory".to_string())?; + + let status = Command::new("spin") + .args(["deploy"]) + .args(extra_args) + .current_dir(manifest_dir) + .status() + .map_err(|e| format!("failed to run spin CLI: {e}"))?; + if !status.success() { + return Err(format!("spin deploy failed with status {status}")); + } + + Ok(()) +} + +pub fn serve(extra_args: &[String]) -> Result<(), String> { + let manifest = find_spin_manifest( + std::env::current_dir() + .map_err(|e| e.to_string())? + .as_path(), + )?; + let manifest_dir = manifest + .parent() + .ok_or_else(|| "spin manifest has no parent directory".to_string())?; + + let status = Command::new("spin") + .args(["up"]) + .args(extra_args) + .current_dir(manifest_dir) + .status() + .map_err(|e| format!("failed to run spin CLI: {e}"))?; + if !status.success() { + return Err(format!("spin up failed with status {status}")); + } + + Ok(()) +} + +struct SpinCliAdapter; + +static SPIN_TEMPLATE_REGISTRATIONS: &[TemplateRegistration] = &[ + TemplateRegistration { + name: "spin_Cargo_toml", + contents: include_str!("templates/Cargo.toml.hbs"), + }, + TemplateRegistration { + name: "spin_src_lib_rs", + contents: include_str!("templates/src/lib.rs.hbs"), + }, + TemplateRegistration { + name: "spin_spin_toml", + contents: include_str!("templates/spin.toml.hbs"), + }, +]; + +static SPIN_FILE_SPECS: &[AdapterFileSpec] = &[ + AdapterFileSpec { + template: "spin_Cargo_toml", + output: "Cargo.toml", + }, + AdapterFileSpec { + template: "spin_src_lib_rs", + output: "src/lib.rs", + }, + AdapterFileSpec { + template: "spin_spin_toml", + output: "spin.toml", + }, +]; + +static SPIN_DEPENDENCIES: &[DependencySpec] = &[ + DependencySpec { + key: "dep_edgezero_core_spin", + repo_crate: "crates/edgezero-core", + fallback: "edgezero-core = { git = \"https://git@github.com/stackpop/edgezero.git\", package = \"edgezero-core\", default-features = false }", + features: &[], + }, + DependencySpec { + key: "dep_edgezero_adapter_spin", + repo_crate: "crates/edgezero-adapter-spin", + fallback: + "edgezero-adapter-spin = { git = \"https://git@github.com/stackpop/edgezero.git\", package = \"edgezero-adapter-spin\", default-features = false }", + features: &[], + }, + DependencySpec { + key: "dep_edgezero_adapter_spin_wasm", + repo_crate: "crates/edgezero-adapter-spin", + fallback: + "edgezero-adapter-spin = { git = \"https://git@github.com/stackpop/edgezero.git\", package = \"edgezero-adapter-spin\", default-features = false, features = [\"spin\"] }", + features: &["spin"], + }, +]; + +static SPIN_BLUEPRINT: AdapterBlueprint = AdapterBlueprint { + id: "spin", + display_name: "Spin (Fermyon)", + crate_suffix: "adapter-spin", + dependency_crate: "edgezero-adapter-spin", + dependency_repo_path: "crates/edgezero-adapter-spin", + template_registrations: SPIN_TEMPLATE_REGISTRATIONS, + files: SPIN_FILE_SPECS, + extra_dirs: &["src"], + dependencies: SPIN_DEPENDENCIES, + manifest: ManifestSpec { + manifest_filename: "spin.toml", + build_target: "wasm32-wasip1", + build_profile: "release", + build_features: &["spin"], + }, + commands: CommandTemplates { + build: "cargo build --target wasm32-wasip1 --release -p {crate_name}", + deploy: "spin deploy --from {crate_dir}", + serve: "spin up --from {crate_dir}", + }, + logging: LoggingDefaults { + endpoint: None, + level: "info", + echo_stdout: None, + }, + readme: ReadmeInfo { + description: "{display} entrypoint.", + dev_heading: "{display} (local)", + dev_steps: &["`edgezero-cli serve --adapter spin`"], + }, + run_module: "edgezero_adapter_spin", +}; + +static SPIN_ADAPTER: SpinCliAdapter = SpinCliAdapter; + +impl Adapter for SpinCliAdapter { + fn name(&self) -> &'static str { + "spin" + } + + fn execute(&self, action: AdapterAction, args: &[String]) -> Result<(), String> { + match action { + AdapterAction::Build => { + let artifact = build(args)?; + println!("[edgezero] Spin build complete -> {}", artifact.display()); + Ok(()) + } + AdapterAction::Deploy => deploy(args), + AdapterAction::Serve => serve(args), + } + } +} + +pub fn register() { + register_adapter(&SPIN_ADAPTER); + register_adapter_blueprint(&SPIN_BLUEPRINT); +} + +#[ctor] +fn register_ctor() { + register(); +} + +fn find_spin_manifest(start: &Path) -> Result { + if let Some(found) = find_manifest_upwards(start, "spin.toml") { + return Ok(found); + } + + let root = find_workspace_root(start); + let mut candidates: Vec = WalkDir::new(&root) + .follow_links(true) + .max_depth(8) + .into_iter() + .filter_map(Result::ok) + .map(|entry| entry.path().to_path_buf()) + .filter(|path| { + path.file_name().map(|n| n == "spin.toml").unwrap_or(false) + && path + .parent() + .map(|dir| dir.join("Cargo.toml").exists()) + .unwrap_or(false) + }) + .collect(); + + if candidates.is_empty() { + return Err("could not locate spin.toml".to_string()); + } + + candidates.sort_by_key(|path| { + let parent = path.parent().unwrap_or(Path::new("")); + path_distance(start, parent) + }); + + Ok(candidates.remove(0)) +} + +fn locate_artifact( + workspace_root: &Path, + manifest_dir: &Path, + crate_name: &str, +) -> Result { + let release_name = format!("{crate_name}.wasm"); + + if let Some(custom) = std::env::var_os("CARGO_TARGET_DIR") { + let candidate = PathBuf::from(custom) + .join(TARGET_TRIPLE) + .join("release") + .join(&release_name); + if candidate.exists() { + return Ok(candidate); + } + } + + let manifest_target = manifest_dir + .join("target") + .join(TARGET_TRIPLE) + .join("release") + .join(&release_name); + if manifest_target.exists() { + return Ok(manifest_target); + } + + let workspace_target = workspace_root + .join("target") + .join(TARGET_TRIPLE) + .join("release") + .join(&release_name); + if workspace_target.exists() { + return Ok(workspace_target); + } + + Err(format!( + "compiled artifact not found (looked in {} and workspace target)", + manifest_dir.display() + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_adapter::cli_support::read_package_name; + use tempfile::tempdir; + + #[test] + fn finds_manifest_in_current_directory() { + let dir = tempdir().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); + fs::write(root.join("spin.toml"), "spin_manifest_version = 2").unwrap(); + + let manifest = find_spin_manifest(root).expect("should find manifest"); + assert_eq!(manifest, root.join("spin.toml")); + } + + #[test] + fn read_package_prefers_package_table() { + let dir = tempdir().unwrap(); + let manifest = dir.path().join("Cargo.toml"); + fs::write(&manifest, "[package]\nname = \"demo\"\n").unwrap(); + let name = read_package_name(&manifest).unwrap(); + assert_eq!(name, "demo"); + } + + #[test] + fn locate_artifact_considers_workspace_target() { + let dir = tempdir().unwrap(); + let workspace = dir.path(); + let manifest_dir = workspace.join("service"); + fs::create_dir_all(manifest_dir.join("target/wasm32-wasip1/release")).unwrap(); + let artifact = workspace.join("target/wasm32-wasip1/release/demo.wasm"); + fs::create_dir_all(artifact.parent().unwrap()).unwrap(); + fs::write(&artifact, "wasm").unwrap(); + + let located = locate_artifact(workspace, &manifest_dir, "demo").unwrap(); + assert_eq!(located, artifact); + } + + #[test] + fn finds_closest_manifest_when_multiple_exist() { + let dir = tempdir().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); + + let first = root.join("crates/first"); + fs::create_dir_all(&first).unwrap(); + fs::write(first.join("Cargo.toml"), "[package]\nname=\"first\"").unwrap(); + fs::write(first.join("spin.toml"), "spin_manifest_version = 2").unwrap(); + + let second = root.join("examples/second"); + fs::create_dir_all(&second).unwrap(); + fs::write(second.join("Cargo.toml"), "[package]\nname=\"second\"").unwrap(); + fs::write(second.join("spin.toml"), "spin_manifest_version = 2").unwrap(); + + let found = find_spin_manifest(&second).unwrap(); + assert_eq!(found, second.join("spin.toml")); + } +} diff --git a/crates/edgezero-adapter-spin/src/context.rs b/crates/edgezero-adapter-spin/src/context.rs new file mode 100644 index 0000000..ed3e667 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/context.rs @@ -0,0 +1,26 @@ +use edgezero_core::http::Request; + +/// Platform-specific request context for Spin. +/// +/// Spin exposes client information via special headers +/// (`spin-client-addr`, `spin-full-url`, etc.) rather than +/// a separate runtime context object. +#[derive(Debug, Clone)] +pub struct SpinRequestContext { + /// The client IP address, extracted from the `spin-client-addr` header. + pub client_addr: Option, + /// The full URL of the incoming request. + pub full_url: Option, +} + +impl SpinRequestContext { + /// Store this context in the request's extensions. + pub fn insert(request: &mut Request, context: SpinRequestContext) { + request.extensions_mut().insert(context); + } + + /// Retrieve a previously-inserted context from request extensions. + pub fn get(request: &Request) -> Option<&SpinRequestContext> { + request.extensions().get::() + } +} diff --git a/crates/edgezero-adapter-spin/src/lib.rs b/crates/edgezero-adapter-spin/src/lib.rs new file mode 100644 index 0000000..ee108a3 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/lib.rs @@ -0,0 +1,45 @@ +//! Adapter helpers for Spin (Fermyon). + +#[cfg(feature = "cli")] +pub mod cli; + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +mod context; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +mod proxy; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +mod request; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +mod response; + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub use context::SpinRequestContext; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub use proxy::SpinProxyClient; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub use request::{dispatch, into_core_request}; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub use response::from_core_response; + +/// Convenience entry point: build the app from `Hooks`, dispatch the +/// incoming Spin request through the EdgeZero router, and return the +/// response. +/// +/// Usage in a Spin component: +/// +/// ```ignore +/// use spin_sdk::http_component; +/// use my_core::App; +/// +/// #[http_component] +/// async fn handle(req: spin_sdk::http::IncomingRequest) -> anyhow::Result { +/// edgezero_adapter_spin::run_app::(req).await +/// } +/// ``` +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub async fn run_app( + req: spin_sdk::http::IncomingRequest, +) -> anyhow::Result { + let app = A::build_app(); + dispatch(&app, req).await +} diff --git a/crates/edgezero-adapter-spin/src/proxy.rs b/crates/edgezero-adapter-spin/src/proxy.rs new file mode 100644 index 0000000..ce3cf88 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/proxy.rs @@ -0,0 +1,92 @@ +use async_trait::async_trait; +use edgezero_core::body::Body; +use edgezero_core::error::EdgeError; +use edgezero_core::http::StatusCode; +use edgezero_core::proxy::{ProxyClient, ProxyRequest, ProxyResponse}; +use futures_util::StreamExt; + +/// A proxy client that uses Spin's outbound HTTP (`spin_sdk::http::send`) +/// to forward requests to upstream services. +pub struct SpinProxyClient; + +#[async_trait(?Send)] +impl ProxyClient for SpinProxyClient { + async fn send(&self, request: ProxyRequest) -> Result { + let (method, uri, headers, body, _extensions) = request.into_parts(); + + let mut builder = spin_sdk::http::Request::builder(); + builder + .method(into_spin_method(&method)) + .uri(uri.to_string()); + + for (name, value) in headers.iter() { + if let Ok(v) = value.to_str() { + builder.header(name.as_str(), v); + } + } + + let body_bytes = match body { + Body::Once(bytes) => bytes.to_vec(), + Body::Stream(mut stream) => { + // Spin doesn't support streaming outbound bodies; collect into bytes. + let mut collected = Vec::new(); + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => collected.extend_from_slice(&bytes), + Err(err) => return Err(EdgeError::internal(err)), + } + } + collected + } + }; + + builder.body(body_bytes); + let spin_request = builder.build(); + + let spin_response: spin_sdk::http::Response = spin_sdk::http::send(spin_request) + .await + .map_err(|e| EdgeError::internal(anyhow::anyhow!("Spin outbound HTTP error: {}", e)))?; + + let status = StatusCode::from_u16(*spin_response.status()) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + // Collect response headers before consuming the body. + let response_headers: Vec<_> = spin_response + .headers() + .filter_map(|(name, value)| { + let v = value.as_str()?; + let hname = edgezero_core::http::HeaderName::from_bytes(name.as_bytes()).ok()?; + let hval: edgezero_core::http::HeaderValue = v.parse().ok()?; + Some((hname, hval)) + }) + .collect(); + + let response_body = spin_response.into_body(); + let mut proxy_response = ProxyResponse::new(status, Body::from(response_body)); + + for (name, value) in response_headers { + proxy_response.headers_mut().insert(name, value); + } + + proxy_response + .headers_mut() + .insert("x-edgezero-proxy", "spin".parse().unwrap()); + + Ok(proxy_response) + } +} + +fn into_spin_method(method: &edgezero_core::http::Method) -> spin_sdk::http::Method { + match *method { + edgezero_core::http::Method::GET => spin_sdk::http::Method::Get, + edgezero_core::http::Method::POST => spin_sdk::http::Method::Post, + edgezero_core::http::Method::PUT => spin_sdk::http::Method::Put, + edgezero_core::http::Method::DELETE => spin_sdk::http::Method::Delete, + edgezero_core::http::Method::PATCH => spin_sdk::http::Method::Patch, + edgezero_core::http::Method::HEAD => spin_sdk::http::Method::Head, + edgezero_core::http::Method::OPTIONS => spin_sdk::http::Method::Options, + edgezero_core::http::Method::CONNECT => spin_sdk::http::Method::Connect, + edgezero_core::http::Method::TRACE => spin_sdk::http::Method::Trace, + ref other => spin_sdk::http::Method::Other(other.to_string()), + } +} diff --git a/crates/edgezero-adapter-spin/src/request.rs b/crates/edgezero-adapter-spin/src/request.rs new file mode 100644 index 0000000..7f68051 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/request.rs @@ -0,0 +1,102 @@ +use crate::context::SpinRequestContext; +use crate::proxy::SpinProxyClient; +use crate::response::from_core_response; +use edgezero_core::app::App; +use edgezero_core::body::Body; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{request_builder, Request, Uri}; +use edgezero_core::proxy::ProxyHandle; +use spin_sdk::http::{IncomingRequest, IntoResponse}; + +/// Convert a Spin `IncomingRequest` into an EdgeZero core `Request`. +/// +/// Reads the full body into a buffered `Body::Once`, inserts +/// `SpinRequestContext` and a `ProxyHandle` into extensions. +pub async fn into_core_request(req: IncomingRequest) -> Result { + let method = req.method(); + let path_with_query = req.path_with_query().unwrap_or_else(|| "/".to_string()); + + let uri: Uri = path_with_query + .parse() + .map_err(|err| EdgeError::bad_request(format!("invalid URI: {}", err)))?; + + // Extract headers before consuming the request body. The WASI `headers()` + // handle borrows the request and must be dropped before `into_body()`. + let headers = req.headers(); + let header_entries = headers.entries(); + + let mut builder = request_builder() + .method(into_core_method(&method)) + .uri(uri); + + for (name, value) in &header_entries { + if let Ok(value_str) = std::str::from_utf8(value) { + builder = builder.header(name.as_str(), value_str); + } + } + + let client_addr = find_header_string(&header_entries, "spin-client-addr"); + let full_url = find_header_string(&header_entries, "spin-full-url"); + + // Drop the WASI resource handle before consuming the body. + drop(headers); + + let body_bytes = req + .into_body() + .await + .map_err(|e| EdgeError::bad_request(format!("failed to read request body: {}", e)))?; + + let mut request = builder + .body(Body::from(body_bytes)) + .map_err(|e| EdgeError::bad_request(format!("failed to build request: {}", e)))?; + + SpinRequestContext::insert( + &mut request, + SpinRequestContext { + client_addr, + full_url, + }, + ); + request + .extensions_mut() + .insert(ProxyHandle::with_client(SpinProxyClient)); + + Ok(request) +} + +/// Find a header value by name from pre-extracted header entries. +fn find_header_string(entries: &[(String, Vec)], name: &str) -> Option { + entries + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(name)) + .and_then(|(_, v)| String::from_utf8(v.clone()).ok()) +} + +/// Dispatch a Spin request through the EdgeZero router and return +/// a Spin-compatible response. +pub async fn dispatch( + app: &App, + req: IncomingRequest, +) -> anyhow::Result { + let core_request = into_core_request(req).await?; + let response = app.router().oneshot(core_request).await; + Ok(from_core_response(response).await?) +} + +fn into_core_method(method: &spin_sdk::http::Method) -> edgezero_core::http::Method { + match method { + spin_sdk::http::Method::Get => edgezero_core::http::Method::GET, + spin_sdk::http::Method::Post => edgezero_core::http::Method::POST, + spin_sdk::http::Method::Put => edgezero_core::http::Method::PUT, + spin_sdk::http::Method::Delete => edgezero_core::http::Method::DELETE, + spin_sdk::http::Method::Patch => edgezero_core::http::Method::PATCH, + spin_sdk::http::Method::Head => edgezero_core::http::Method::HEAD, + spin_sdk::http::Method::Options => edgezero_core::http::Method::OPTIONS, + spin_sdk::http::Method::Connect => edgezero_core::http::Method::CONNECT, + spin_sdk::http::Method::Trace => edgezero_core::http::Method::TRACE, + spin_sdk::http::Method::Other(s) => { + edgezero_core::http::Method::from_bytes(s.as_bytes()) + .unwrap_or(edgezero_core::http::Method::GET) + } + } +} diff --git a/crates/edgezero-adapter-spin/src/response.rs b/crates/edgezero-adapter-spin/src/response.rs new file mode 100644 index 0000000..72cc014 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/response.rs @@ -0,0 +1,39 @@ +use edgezero_core::body::Body; +use edgezero_core::error::EdgeError; +use edgezero_core::http::Response; +use futures_util::StreamExt; +use spin_sdk::http as spin_http; + +/// Convert an EdgeZero core `Response` into a Spin SDK `Response`. +/// +/// Both `Body::Once` and `Body::Stream` are converted to a buffered +/// byte body. Streaming bodies are collected into a single `Vec`. +pub async fn from_core_response(response: Response) -> Result { + let (parts, body) = response.into_parts(); + + let mut builder = spin_http::Response::builder(); + builder.status(parts.status.as_u16()); + + for (name, value) in parts.headers.iter() { + if let Ok(v) = value.to_str() { + builder.header(name.as_str(), v); + } + } + + let body_bytes = match body { + Body::Once(bytes) => bytes.to_vec(), + Body::Stream(mut stream) => { + let mut collected = Vec::new(); + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => collected.extend_from_slice(&bytes), + Err(err) => return Err(EdgeError::internal(err)), + } + } + collected + } + }; + + builder.body(body_bytes); + Ok(builder.build()) +} diff --git a/crates/edgezero-adapter-spin/src/templates/Cargo.toml.hbs b/crates/edgezero-adapter-spin/src/templates/Cargo.toml.hbs new file mode 100644 index 0000000..03ba534 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/templates/Cargo.toml.hbs @@ -0,0 +1,23 @@ +[package] +name = "{{proj_spin}}" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[features] +default = ["spin"] +spin = ["edgezero-adapter-spin/spin"] + +[dependencies] +{{proj_core}} = { path = "../{{proj_core}}" } +{{{dep_edgezero_adapter_spin}}} +anyhow = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +{{{dep_edgezero_adapter_spin_wasm}}} +{{{dep_edgezero_core_spin}}} +spin-sdk = { workspace = true } diff --git a/crates/edgezero-adapter-spin/src/templates/spin.toml.hbs b/crates/edgezero-adapter-spin/src/templates/spin.toml.hbs new file mode 100644 index 0000000..1baeb37 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/templates/spin.toml.hbs @@ -0,0 +1,16 @@ +spin_manifest_version = 2 + +[application] +name = "{{proj_spin}}" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "{{proj_spin}}" + +[component.{{proj_spin}}] +source = "target/wasm32-wasip1/release/{{proj_spin_underscored}}.wasm" +allowed_outbound_hosts = ["https://*:*"] +[component.{{proj_spin}}.build] +command = "cargo build --target wasm32-wasip1 --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs b/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs new file mode 100644 index 0000000..64b0fa2 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs @@ -0,0 +1,12 @@ +#[cfg(target_arch = "wasm32")] +use spin_sdk::http::{IncomingRequest, IntoResponse}; +#[cfg(target_arch = "wasm32")] +use spin_sdk::http_component; +#[cfg(target_arch = "wasm32")] +use {{proj_core_mod}}::App; + +#[cfg(target_arch = "wasm32")] +#[http_component] +async fn handle(req: IncomingRequest) -> anyhow::Result { + edgezero_adapter_spin::run_app::(req).await +} diff --git a/crates/edgezero-cli/Cargo.toml b/crates/edgezero-cli/Cargo.toml index 99bed31..5aa07e7 100644 --- a/crates/edgezero-cli/Cargo.toml +++ b/crates/edgezero-cli/Cargo.toml @@ -11,6 +11,7 @@ edgezero-adapter = { path = "../edgezero-adapter" } edgezero-adapter-axum = { workspace = true, features = ["cli", "axum"], optional = true } edgezero-adapter-cloudflare = { workspace = true, features = ["cli"], optional = true } edgezero-adapter-fastly = { workspace = true, features = ["cli"], optional = true } +edgezero-adapter-spin = { workspace = true, features = ["cli"], optional = true } app-demo-core = { path = "../../examples/app-demo/crates/app-demo-core", package = "app-demo-core", optional = true } clap = { version = "4", features = ["derive"], optional = true } futures = { workspace = true } @@ -32,6 +33,7 @@ default = [ "edgezero-adapter-axum", "edgezero-adapter-fastly", "edgezero-adapter-cloudflare", + "edgezero-adapter-spin", ] cli = ["dep:clap"] dev-example = ["dep:app-demo-core"] diff --git a/examples/app-demo/Cargo.lock b/examples/app-demo/Cargo.lock index e4cbc2f..26d76d0 100644 --- a/examples/app-demo/Cargo.lock +++ b/examples/app-demo/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "app-demo-adapter-axum" @@ -84,6 +84,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "app-demo-adapter-spin" +version = "0.1.0" +dependencies = [ + "anyhow", + "app-demo-core", + "edgezero-adapter-spin", + "edgezero-core", + "spin-sdk", +] + [[package]] name = "app-demo-core" version = "0.1.0" @@ -98,9 +109,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -127,7 +138,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -138,7 +149,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -155,9 +166,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.4" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -165,9 +176,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -241,9 +252,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -277,9 +288,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -295,9 +306,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -325,9 +336,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -376,9 +387,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" dependencies = [ "brotli", "compression-core", @@ -447,7 +458,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -458,14 +469,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -488,7 +499,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -517,7 +528,7 @@ dependencies = [ "http", "log", "reqwest", - "simple_logger 5.1.0", + "simple_logger 5.2.0", "thiserror 2.0.18", "tokio", "tower", @@ -559,6 +570,20 @@ dependencies = [ "log-fastly", ] +[[package]] +name = "edgezero-adapter-spin" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "edgezero-core", + "futures", + "futures-util", + "log", + "spin-sdk", +] + [[package]] name = "edgezero-core" version = "0.1.0" @@ -594,7 +619,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.114", + "syn 2.0.117", "toml", "validator", ] @@ -723,6 +748,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -740,9 +771,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -755,9 +786,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -765,15 +796,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -782,38 +813,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -823,7 +854,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -846,7 +876,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -864,12 +894,27 @@ dependencies = [ "wasm-bindgen", ] +[[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" @@ -1081,6 +1126,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1115,14 +1166,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -1183,9 +1236,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1197,11 +1250,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -1295,7 +1354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -1365,29 +1424,29 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1419,6 +1478,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1438,7 +1507,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1508,9 +1577,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1575,9 +1644,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -1628,6 +1697,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "routefinder" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0971d3c8943a6267d6bd0d782fdc4afa7593e7381a92a3df950ff58897e066b5" +dependencies = [ + "smartcow", + "smartstring", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1636,9 +1715,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "once_cell", @@ -1741,11 +1820,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -1754,14 +1833,20 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] +[[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" @@ -1800,7 +1885,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1835,7 +1920,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1908,9 +1993,9 @@ dependencies = [ [[package]] name = "simple_logger" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291bee647ce7310b0ea721bfd7e0525517b4468eb7c7e15eb8bd774343179702" +checksum = "c7038d0e96661bf9ce647e1a6f6ef6d6f3663f66d9bf741abf14ba4876071c17" dependencies = [ "colored 3.1.1", "log", @@ -1930,6 +2015,26 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smartcow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" +dependencies = [ + "smartstring", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "socket2" version = "0.6.2" @@ -1940,12 +2045,64 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin-executor" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba409d00af758cd5de128da4a801e891af0545138f66a688f025f6d4e33870b" +dependencies = [ + "futures", + "once_cell", + "wasi 0.13.1+wasi-0.2.0", +] + +[[package]] +name = "spin-macro" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f959f16928e3c023468e41da9ebb77442e2ce22315e8dab11508fe76b3567ee1" +dependencies = [ + "anyhow", + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "spin-sdk" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8951c7c4ab7f87f332d497789eeed9631c8116988b628b4851eb2fa999ead019" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "form_urlencoded", + "futures", + "http", + "once_cell", + "routefinder", + "spin-executor", + "spin-macro", + "thiserror 2.0.18", + "wasi 0.13.1+wasi-0.2.0", + "wit-bindgen 0.51.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1971,9 +2128,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1997,7 +2154,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2026,7 +2183,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2037,7 +2194,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2100,9 +2257,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2116,13 +2273,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2196,7 +2353,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -2240,7 +2397,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2266,9 +2423,15 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -2321,7 +2484,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2355,6 +2518,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.13.1+wasi-0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f43d1c36145feb89a3e61aa0ba3e582d976a8ab77f1474aa0adb80800fe0cf8" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -2366,9 +2538,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2379,9 +2551,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -2393,9 +2565,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2403,31 +2575,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.58" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" dependencies = [ "async-trait", "cast", @@ -2447,26 +2619,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.58" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.108" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -2475,11 +2669,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -2534,7 +2740,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2545,7 +2751,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2881,7 +3087,7 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2890,14 +3096,103 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0780cf7046630ed70f689a098cd8d56c5c3b22f2a7379bbdb088879963ff96" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "worker" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244647fd7673893058f91f22a0eabd0f45bb50298e995688cb0c4b9837081b19" +checksum = "7267f3baa986254a8dace6f6a7c6ab88aef59f00c03aaad6749e048b5faaf6f6" dependencies = [ "async-trait", "bytes", @@ -2925,14 +3220,14 @@ dependencies = [ [[package]] name = "worker-macros" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7e73ffb164183b57bb67d3efb881681fcd93ef5515ba32a4d022c4a6acc2ce" +checksum = "7410081121531ec2fa111ab17b911efc601d7b6d590c0a92b847874ebeff0030" dependencies = [ "async-trait", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-macro-support", @@ -2941,9 +3236,9 @@ dependencies = [ [[package]] name = "worker-sys" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2b96254fcaa9229fd82d886f04be99c4ee8e59c8d80438724aa70039dca838" +checksum = "4777582bf8a04174a034cb336f3702eb0e5cb444a67fdaa4fd44454ff7e2dd95" dependencies = [ "cfg-if", "js-sys", @@ -2976,28 +3271,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3017,7 +3312,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -3057,11 +3352,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/app-demo/Cargo.toml b/examples/app-demo/Cargo.toml index 12a794b..8518668 100644 --- a/examples/app-demo/Cargo.toml +++ b/examples/app-demo/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/app-demo-adapter-axum", "crates/app-demo-adapter-cloudflare", "crates/app-demo-adapter-fastly", + "crates/app-demo-adapter-spin", ] resolver = "2" @@ -18,7 +19,9 @@ bytes = "1" edgezero-adapter-axum = { path = "../../crates/edgezero-adapter-axum" } edgezero-adapter-cloudflare = { path = "../../crates/edgezero-adapter-cloudflare" } edgezero-adapter-fastly = { path = "../../crates/edgezero-adapter-fastly" } +edgezero-adapter-spin = { path = "../../crates/edgezero-adapter-spin" } edgezero-core = { path = "../../crates/edgezero-core" } +spin-sdk = { version = "5.2", default-features = false } fastly = "0.11" futures = { version = "0.3", default-features = false, features = ["std", "executor"] } log = "0.4" diff --git a/examples/app-demo/crates/app-demo-adapter-spin/Cargo.toml b/examples/app-demo/crates/app-demo-adapter-spin/Cargo.toml new file mode 100644 index 0000000..b18a924 --- /dev/null +++ b/examples/app-demo/crates/app-demo-adapter-spin/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "app-demo-adapter-spin" +version = "0.1.0" +edition = "2021" +license.workspace = true +publish = false + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[features] +default = ["spin"] +spin = ["edgezero-adapter-spin/spin"] + +[dependencies] +app-demo-core = { path = "../app-demo-core" } +edgezero-adapter-spin = { workspace = true } +anyhow = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +edgezero-adapter-spin = { workspace = true, features = ["spin"] } +edgezero-core = { workspace = true } +spin-sdk = { workspace = true } diff --git a/examples/app-demo/crates/app-demo-adapter-spin/spin.toml b/examples/app-demo/crates/app-demo-adapter-spin/spin.toml new file mode 100644 index 0000000..ed4152f --- /dev/null +++ b/examples/app-demo/crates/app-demo-adapter-spin/spin.toml @@ -0,0 +1,16 @@ +spin_manifest_version = 2 + +[application] +name = "app-demo-adapter-spin" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "app-demo" + +[component.app-demo] +source = "target/wasm32-wasip1/release/app_demo_adapter_spin.wasm" +allowed_outbound_hosts = ["https://*:*"] +[component.app-demo.build] +command = "cargo build --target wasm32-wasip1 --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs b/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs new file mode 100644 index 0000000..8c68a33 --- /dev/null +++ b/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs @@ -0,0 +1,12 @@ +#[cfg(target_arch = "wasm32")] +use app_demo_core::App; +#[cfg(target_arch = "wasm32")] +use spin_sdk::http::{IncomingRequest, IntoResponse}; +#[cfg(target_arch = "wasm32")] +use spin_sdk::http_component; + +#[cfg(target_arch = "wasm32")] +#[http_component] +async fn handle(req: IncomingRequest) -> anyhow::Result { + edgezero_adapter_spin::run_app::(req).await +} diff --git a/examples/app-demo/edgezero.toml b/examples/app-demo/edgezero.toml index dd320ac..572f77a 100644 --- a/examples/app-demo/edgezero.toml +++ b/examples/app-demo/edgezero.toml @@ -12,7 +12,7 @@ id = "root" path = "/" methods = ["GET"] handler = "app_demo_core::handlers::root" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] description = "Default health-check endpoint" [[triggers.http]] @@ -20,14 +20,14 @@ id = "echo" path = "/echo/{name}" methods = ["GET"] handler = "app_demo_core::handlers::echo" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "stream" path = "/stream" methods = ["GET"] handler = "app_demo_core::handlers::stream" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] body-mode = "stream" [[triggers.http]] @@ -35,14 +35,14 @@ id = "headers" path = "/headers" methods = ["GET"] handler = "app_demo_core::handlers::headers" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "echo_json" path = "/echo" methods = ["POST"] handler = "app_demo_core::handlers::echo_json" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] @@ -50,7 +50,7 @@ id = "proxy_demo" path = "/proxy/{*rest}" methods = ["GET", "POST"] handler = "app_demo_core::handlers::proxy_demo" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] # [environment] # @@ -119,3 +119,20 @@ serve = "fastly compute serve -C crates/app-demo-adapter-fastly" endpoint = "app_demo_log" level = "info" echo_stdout = true + +[adapters.spin.adapter] +crate = "crates/app-demo-adapter-spin" +manifest = "crates/app-demo-adapter-spin/spin.toml" + +[adapters.spin.build] +target = "wasm32-wasip1" +profile = "release" +features = ["spin"] + +[adapters.spin.commands] +build = "cargo build --target wasm32-wasip1 --release -p app-demo-adapter-spin" +deploy = "spin deploy --from crates/app-demo-adapter-spin" +serve = "spin up --from crates/app-demo-adapter-spin" + +[adapters.spin.logging] +level = "info" From 58aa63c04546dbe318512b0f6ec7ba1ce519a5f0 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 4 Mar 2026 19:35:11 -0500 Subject: [PATCH 2/4] Address PR review findings for Spin adapter - Fix cargo fmt formatting issues - Return EdgeError for unsupported HTTP methods instead of silently defaulting to GET - Add init_logger() matching Cloudflare's no-op pattern, call in run_app - Expose SpinRequestContext unconditionally (pure Rust, no WASM deps) - Add AppExt trait on App for consistent adapter API - Add contract test scaffold and context unit tests - Replace bare .unwrap() with .expect() in proxy header insert - Simplify anyhow wrapping in proxy error path to format!() --- crates/edgezero-adapter-spin/src/context.rs | 38 ++++++++++++++++++ crates/edgezero-adapter-spin/src/lib.rs | 40 ++++++++++++++++++- crates/edgezero-adapter-spin/src/proxy.rs | 9 +++-- crates/edgezero-adapter-spin/src/request.rs | 35 ++++++++-------- .../edgezero-adapter-spin/tests/contract.rs | 36 +++++++++++++++++ 5 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 crates/edgezero-adapter-spin/tests/contract.rs diff --git a/crates/edgezero-adapter-spin/src/context.rs b/crates/edgezero-adapter-spin/src/context.rs index ed3e667..bee17c8 100644 --- a/crates/edgezero-adapter-spin/src/context.rs +++ b/crates/edgezero-adapter-spin/src/context.rs @@ -24,3 +24,41 @@ impl SpinRequestContext { request.extensions().get::() } } + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::body::Body; + use edgezero_core::http::request_builder; + + #[test] + fn inserts_and_retrieves_context() { + let mut request = request_builder() + .uri("https://example.com") + .body(Body::empty()) + .expect("request"); + + let context = SpinRequestContext { + client_addr: Some("127.0.0.1:12345".to_string()), + full_url: Some("https://example.com/path".to_string()), + }; + SpinRequestContext::insert(&mut request, context); + + let retrieved = SpinRequestContext::get(&request).expect("context"); + assert_eq!(retrieved.client_addr.as_deref(), Some("127.0.0.1:12345")); + assert_eq!( + retrieved.full_url.as_deref(), + Some("https://example.com/path") + ); + } + + #[test] + fn get_returns_none_when_missing() { + let request = request_builder() + .uri("https://example.com") + .body(Body::empty()) + .expect("request"); + + assert!(SpinRequestContext::get(&request).is_none()); + } +} diff --git a/crates/edgezero-adapter-spin/src/lib.rs b/crates/edgezero-adapter-spin/src/lib.rs index ee108a3..865657c 100644 --- a/crates/edgezero-adapter-spin/src/lib.rs +++ b/crates/edgezero-adapter-spin/src/lib.rs @@ -3,7 +3,6 @@ #[cfg(feature = "cli")] pub mod cli; -#[cfg(all(feature = "spin", target_arch = "wasm32"))] mod context; #[cfg(all(feature = "spin", target_arch = "wasm32"))] mod proxy; @@ -12,7 +11,6 @@ mod request; #[cfg(all(feature = "spin", target_arch = "wasm32"))] mod response; -#[cfg(all(feature = "spin", target_arch = "wasm32"))] pub use context::SpinRequestContext; #[cfg(all(feature = "spin", target_arch = "wasm32"))] pub use proxy::SpinProxyClient; @@ -21,6 +19,43 @@ pub use request::{dispatch, into_core_request}; #[cfg(all(feature = "spin", target_arch = "wasm32"))] pub use response::from_core_response; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub fn init_logger() -> Result<(), log::SetLoggerError> { + Ok(()) +} + +#[cfg(not(all(feature = "spin", target_arch = "wasm32")))] +pub fn init_logger() -> Result<(), log::SetLoggerError> { + Ok(()) +} + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub trait AppExt { + fn dispatch<'a>( + &'a self, + req: spin_sdk::http::IncomingRequest, + ) -> ::core::pin::Pin< + Box> + 'a>, + >; +} + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +impl AppExt for edgezero_core::app::App { + fn dispatch<'a>( + &'a self, + req: spin_sdk::http::IncomingRequest, + ) -> ::core::pin::Pin< + Box> + 'a>, + > { + Box::pin(async move { + let core_request = into_core_request(req).await?; + let response = self.router().oneshot(core_request).await; + let spin_response = from_core_response(response).await?; + Ok(spin_response) + }) + } +} + /// Convenience entry point: build the app from `Hooks`, dispatch the /// incoming Spin request through the EdgeZero router, and return the /// response. @@ -40,6 +75,7 @@ pub use response::from_core_response; pub async fn run_app( req: spin_sdk::http::IncomingRequest, ) -> anyhow::Result { + init_logger().expect("init spin logger"); let app = A::build_app(); dispatch(&app, req).await } diff --git a/crates/edgezero-adapter-spin/src/proxy.rs b/crates/edgezero-adapter-spin/src/proxy.rs index ce3cf88..d44931b 100644 --- a/crates/edgezero-adapter-spin/src/proxy.rs +++ b/crates/edgezero-adapter-spin/src/proxy.rs @@ -45,7 +45,7 @@ impl ProxyClient for SpinProxyClient { let spin_response: spin_sdk::http::Response = spin_sdk::http::send(spin_request) .await - .map_err(|e| EdgeError::internal(anyhow::anyhow!("Spin outbound HTTP error: {}", e)))?; + .map_err(|e| EdgeError::internal(format!("Spin outbound HTTP error: {e}")))?; let status = StatusCode::from_u16(*spin_response.status()) .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); @@ -68,9 +68,10 @@ impl ProxyClient for SpinProxyClient { proxy_response.headers_mut().insert(name, value); } - proxy_response - .headers_mut() - .insert("x-edgezero-proxy", "spin".parse().unwrap()); + proxy_response.headers_mut().insert( + "x-edgezero-proxy", + "spin".parse().expect("static header value should parse"), + ); Ok(proxy_response) } diff --git a/crates/edgezero-adapter-spin/src/request.rs b/crates/edgezero-adapter-spin/src/request.rs index 7f68051..07ae1e4 100644 --- a/crates/edgezero-adapter-spin/src/request.rs +++ b/crates/edgezero-adapter-spin/src/request.rs @@ -26,7 +26,7 @@ pub async fn into_core_request(req: IncomingRequest) -> Result)], name: &str) -> Option anyhow::Result { +pub async fn dispatch(app: &App, req: IncomingRequest) -> anyhow::Result { let core_request = into_core_request(req).await?; let response = app.router().oneshot(core_request).await; Ok(from_core_response(response).await?) } -fn into_core_method(method: &spin_sdk::http::Method) -> edgezero_core::http::Method { +fn into_core_method( + method: &spin_sdk::http::Method, +) -> Result { match method { - spin_sdk::http::Method::Get => edgezero_core::http::Method::GET, - spin_sdk::http::Method::Post => edgezero_core::http::Method::POST, - spin_sdk::http::Method::Put => edgezero_core::http::Method::PUT, - spin_sdk::http::Method::Delete => edgezero_core::http::Method::DELETE, - spin_sdk::http::Method::Patch => edgezero_core::http::Method::PATCH, - spin_sdk::http::Method::Head => edgezero_core::http::Method::HEAD, - spin_sdk::http::Method::Options => edgezero_core::http::Method::OPTIONS, - spin_sdk::http::Method::Connect => edgezero_core::http::Method::CONNECT, - spin_sdk::http::Method::Trace => edgezero_core::http::Method::TRACE, - spin_sdk::http::Method::Other(s) => { - edgezero_core::http::Method::from_bytes(s.as_bytes()) - .unwrap_or(edgezero_core::http::Method::GET) - } + spin_sdk::http::Method::Get => Ok(edgezero_core::http::Method::GET), + spin_sdk::http::Method::Post => Ok(edgezero_core::http::Method::POST), + spin_sdk::http::Method::Put => Ok(edgezero_core::http::Method::PUT), + spin_sdk::http::Method::Delete => Ok(edgezero_core::http::Method::DELETE), + spin_sdk::http::Method::Patch => Ok(edgezero_core::http::Method::PATCH), + spin_sdk::http::Method::Head => Ok(edgezero_core::http::Method::HEAD), + spin_sdk::http::Method::Options => Ok(edgezero_core::http::Method::OPTIONS), + spin_sdk::http::Method::Connect => Ok(edgezero_core::http::Method::CONNECT), + spin_sdk::http::Method::Trace => Ok(edgezero_core::http::Method::TRACE), + spin_sdk::http::Method::Other(s) => edgezero_core::http::Method::from_bytes(s.as_bytes()) + .map_err(|_| EdgeError::bad_request(format!("unsupported HTTP method: {s}"))), } } diff --git a/crates/edgezero-adapter-spin/tests/contract.rs b/crates/edgezero-adapter-spin/tests/contract.rs new file mode 100644 index 0000000..1a5ca2f --- /dev/null +++ b/crates/edgezero-adapter-spin/tests/contract.rs @@ -0,0 +1,36 @@ +#![cfg(all(feature = "spin", target_arch = "wasm32"))] + +use edgezero_adapter_spin::{dispatch, from_core_response, into_core_request, SpinRequestContext}; +use edgezero_core::app::App; +use edgezero_core::body::Body; +use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{response_builder, Method, Response, StatusCode}; +use edgezero_core::router::RouterService; + +fn build_test_app() -> App { + async fn capture_uri(ctx: RequestContext) -> Result { + let body = Body::text(ctx.request().uri().to_string()); + let response = response_builder() + .status(StatusCode::OK) + .body(body) + .expect("response"); + Ok(response) + } + + async fn mirror_body(ctx: RequestContext) -> Result { + let bytes = ctx.request().body().as_bytes().to_vec(); + let response = response_builder() + .status(StatusCode::OK) + .body(Body::from(bytes)) + .expect("response"); + Ok(response) + } + + let router = RouterService::builder() + .get("/uri", capture_uri) + .post("/mirror", mirror_body) + .build(); + + App::new(router) +} From 7c2a2f9f66392e0857c0eb817ad0373a90043259 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 6 Mar 2026 11:03:44 -0500 Subject: [PATCH 3/4] Address PR #166 review: fix wasm32 compile, scaffolding, tests, and docs - Fix wasm32 compile error: format!() -> anyhow::anyhow!() in proxy.rs - Fix scaffold {crate_name} -> {crate} placeholder in Spin build command - Add proj_{id}_underscored template var for spin.toml wasm filename - Add spin-sdk to seed_workspace_dependencies for generated projects - Delegate AppExt::dispatch to request::dispatch (matches CF pattern) - Change dispatch() return to concrete spin_sdk::http::Response type - Parse client_addr as Option instead of Option - Extract shared collect_body_bytes helper to deduplicate proxy/response - Add log::warn for silently dropped non-UTF-8 headers - Simplify init_logger by removing redundant cfg gates - Add contract tests (2 native, 3 wasm32-gated) with proper cfg split - Add spin to CI feature check and wasm32-wasip1 compilation step - Update CLAUDE.md and README.md with Spin adapter documentation --- .github/workflows/test.yml | 5 +- CLAUDE.md | 26 +++-- README.md | 3 +- crates/edgezero-adapter-spin/src/cli.rs | 2 +- crates/edgezero-adapter-spin/src/context.rs | 58 +++++++++- crates/edgezero-adapter-spin/src/lib.rs | 13 +-- crates/edgezero-adapter-spin/src/proxy.rs | 46 ++++---- crates/edgezero-adapter-spin/src/request.rs | 9 +- crates/edgezero-adapter-spin/src/response.rs | 33 +++--- .../edgezero-adapter-spin/tests/contract.rs | 109 +++++++++++++++++- crates/edgezero-cli/src/generator.rs | 8 ++ 11 files changed, 239 insertions(+), 73 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0a9a4e9..6a6216f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,4 +56,7 @@ jobs: run: cargo test --workspace --all-targets - name: Check feature compilation - run: cargo check --workspace --all-targets --features "fastly cloudflare" + run: cargo check --workspace --all-targets --features "fastly cloudflare spin" + + - name: Check Spin wasm32 compilation + run: cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin diff --git a/CLAUDE.md b/CLAUDE.md index 4b805d3..c857a6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,10 +3,10 @@ ## Project Overview EdgeZero is a portable HTTP workload toolkit in Rust. Write once, deploy to -Fastly Compute, Cloudflare Workers, or native Axum servers. The codebase is a -Cargo workspace with 7 crates under `crates/`, an example app under -`examples/app-demo/`, a VitePress documentation site under `docs/`, and CI -workflows under `.github/workflows/`. +Fastly Compute, Cloudflare Workers, Fermyon Spin, or native Axum servers. The +codebase is a Cargo workspace with 8 crates under `crates/`, an example app +under `examples/app-demo/`, a VitePress documentation site under `docs/`, and +CI workflows under `.github/workflows/`. ## Workspace Layout @@ -17,6 +17,7 @@ crates/ edgezero-adapter/ # Adapter registry and traits edgezero-adapter-fastly/ # Fastly Compute bridge (wasm32-wasip1) edgezero-adapter-cloudflare/# Cloudflare Workers bridge (wasm32-unknown-unknown) + edgezero-adapter-spin/ # Fermyon Spin bridge (wasm32-wasip1) edgezero-adapter-axum/ # Axum/Tokio bridge (native, dev server) edgezero-cli/ # CLI: new, build, deploy, dev, serve examples/app-demo/ # Reference app with all 3 adapters (excluded from workspace) @@ -49,7 +50,10 @@ cargo fmt --all -- --check cargo clippy --workspace --all-targets --all-features -- -D warnings # Feature compilation check -cargo check --workspace --all-targets --features "fastly cloudflare" +cargo check --workspace --all-targets --features "fastly cloudflare spin" + +# Spin wasm32 compilation check +cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin # Run the demo dev server cargo run -p edgezero-cli --features dev-example -- dev @@ -67,6 +71,7 @@ faster iteration on a single crate. | ---------- | ------------------------ | ---------------------------------- | | Fastly | `wasm32-wasip1` | Requires Viceroy for local testing | | Cloudflare | `wasm32-unknown-unknown` | Requires `wrangler` for dev/deploy | +| Spin | `wasm32-wasip1` | Requires `spin` CLI for dev/deploy | | Axum | Native (host triple) | Standard Tokio runtime | ## Coding Conventions @@ -132,12 +137,14 @@ impl Middleware for MyMiddleware { ### Proxy Use `ProxyService` with adapter-specific clients (`FastlyProxyClient`, -`CloudflareProxyClient`). Keep proxy logic provider-agnostic in core. +`CloudflareProxyClient`, `SpinProxyClient`). Keep proxy logic provider-agnostic +in core. ### Logging - Adapter-specific init: `edgezero_adapter_fastly::init_logger()`, - `edgezero_adapter_cloudflare::init_logger()`. + `edgezero_adapter_cloudflare::init_logger()`, + `edgezero_adapter_spin::init_logger()`. - Use `simple_logger` for local/Axum builds. - Use the `log` / `tracing` facade, not direct dependencies. @@ -151,7 +158,7 @@ Use `ProxyService` with adapter-specific clients (`FastlyProxyClient`, - **Minimal changes**: every change should impact as little code as possible. Avoid unnecessary refactoring, docstrings on untouched code, or premature abstractions. - **Feature gates**: platform-specific code goes behind `fastly`, `cloudflare`, - or `axum` features. Core stays `default-features = false` for WASM targets. + `spin`, or `axum` features. Core stays `default-features = false` for WASM targets. - **No direct `http` crate imports** in application code — use `edgezero_core` re-exports. ## Adapter Pattern @@ -179,7 +186,8 @@ Every PR must pass: 1. `cargo fmt --all -- --check` 2. `cargo clippy --workspace --all-targets --all-features -- -D warnings` 3. `cargo test --workspace --all-targets` -4. `cargo check --workspace --all-targets --features "fastly cloudflare"` +4. `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +5. `cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin` Docs CI additionally runs ESLint + Prettier on the `docs/` directory. diff --git a/README.md b/README.md index bd59b59..af7d6f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # EdgeZero -Production-ready toolkit for portable edge HTTP workloads. Write once, deploy to Fastly Compute, Cloudflare Workers, or native Axum servers. +Production-ready toolkit for portable edge HTTP workloads. Write once, deploy to Fastly Compute, Cloudflare Workers, Fermyon Spin, or native Axum servers. ## Quick Start @@ -34,6 +34,7 @@ Full documentation is available at **[stackpop.github.io/edgezero](https://stack | ------------------ | ------------------------ | ------ | | Fastly Compute | `wasm32-wasip1` | Stable | | Cloudflare Workers | `wasm32-unknown-unknown` | Stable | +| Fermyon Spin | `wasm32-wasip1` | Stable | | Axum (Native) | Host | Stable | ## License diff --git a/crates/edgezero-adapter-spin/src/cli.rs b/crates/edgezero-adapter-spin/src/cli.rs index 4893252..9069ac7 100644 --- a/crates/edgezero-adapter-spin/src/cli.rs +++ b/crates/edgezero-adapter-spin/src/cli.rs @@ -175,7 +175,7 @@ static SPIN_BLUEPRINT: AdapterBlueprint = AdapterBlueprint { build_features: &["spin"], }, commands: CommandTemplates { - build: "cargo build --target wasm32-wasip1 --release -p {crate_name}", + build: "cargo build --target wasm32-wasip1 --release -p {crate}", deploy: "spin deploy --from {crate_dir}", serve: "spin up --from {crate_dir}", }, diff --git a/crates/edgezero-adapter-spin/src/context.rs b/crates/edgezero-adapter-spin/src/context.rs index bee17c8..98d1dd1 100644 --- a/crates/edgezero-adapter-spin/src/context.rs +++ b/crates/edgezero-adapter-spin/src/context.rs @@ -1,3 +1,5 @@ +use std::net::IpAddr; + use edgezero_core::http::Request; /// Platform-specific request context for Spin. @@ -7,12 +9,27 @@ use edgezero_core::http::Request; /// a separate runtime context object. #[derive(Debug, Clone)] pub struct SpinRequestContext { - /// The client IP address, extracted from the `spin-client-addr` header. - pub client_addr: Option, + /// The client IP address, parsed from the `spin-client-addr` header. + /// The header value has the format `ip:port`; only the IP is retained. + pub client_addr: Option, /// The full URL of the incoming request. pub full_url: Option, } +/// Parse an IP address from a `host:port` string. +/// +/// Falls back to parsing the raw value as a bare IP (no port) and also +/// handles IPv6 bracket notation (`[::1]:port`). +#[cfg(any(test, all(feature = "spin", target_arch = "wasm32")))] +pub(crate) fn parse_client_addr(raw: &str) -> Option { + // Try `ip:port` (IPv4) or `[ip]:port` (IPv6 bracket notation). + if let Ok(sock) = raw.parse::() { + return Some(sock.ip()); + } + // Bare IP with no port. + raw.parse::().ok() +} + impl SpinRequestContext { /// Store this context in the request's extensions. pub fn insert(request: &mut Request, context: SpinRequestContext) { @@ -30,6 +47,7 @@ mod tests { use super::*; use edgezero_core::body::Body; use edgezero_core::http::request_builder; + use std::str::FromStr; #[test] fn inserts_and_retrieves_context() { @@ -39,13 +57,16 @@ mod tests { .expect("request"); let context = SpinRequestContext { - client_addr: Some("127.0.0.1:12345".to_string()), + client_addr: Some(IpAddr::from_str("127.0.0.1").unwrap()), full_url: Some("https://example.com/path".to_string()), }; SpinRequestContext::insert(&mut request, context); let retrieved = SpinRequestContext::get(&request).expect("context"); - assert_eq!(retrieved.client_addr.as_deref(), Some("127.0.0.1:12345")); + assert_eq!( + retrieved.client_addr, + Some(IpAddr::from_str("127.0.0.1").unwrap()) + ); assert_eq!( retrieved.full_url.as_deref(), Some("https://example.com/path") @@ -61,4 +82,33 @@ mod tests { assert!(SpinRequestContext::get(&request).is_none()); } + + #[test] + fn parse_client_addr_ipv4_with_port() { + let ip = parse_client_addr("192.168.1.1:8080").unwrap(); + assert_eq!(ip, IpAddr::from_str("192.168.1.1").unwrap()); + } + + #[test] + fn parse_client_addr_ipv4_bare() { + let ip = parse_client_addr("10.0.0.1").unwrap(); + assert_eq!(ip, IpAddr::from_str("10.0.0.1").unwrap()); + } + + #[test] + fn parse_client_addr_ipv6_bracket() { + let ip = parse_client_addr("[::1]:3000").unwrap(); + assert_eq!(ip, IpAddr::from_str("::1").unwrap()); + } + + #[test] + fn parse_client_addr_ipv6_bare() { + let ip = parse_client_addr("::1").unwrap(); + assert_eq!(ip, IpAddr::from_str("::1").unwrap()); + } + + #[test] + fn parse_client_addr_invalid() { + assert!(parse_client_addr("not-an-ip").is_none()); + } } diff --git a/crates/edgezero-adapter-spin/src/lib.rs b/crates/edgezero-adapter-spin/src/lib.rs index 865657c..9795986 100644 --- a/crates/edgezero-adapter-spin/src/lib.rs +++ b/crates/edgezero-adapter-spin/src/lib.rs @@ -19,12 +19,6 @@ pub use request::{dispatch, into_core_request}; #[cfg(all(feature = "spin", target_arch = "wasm32"))] pub use response::from_core_response; -#[cfg(all(feature = "spin", target_arch = "wasm32"))] -pub fn init_logger() -> Result<(), log::SetLoggerError> { - Ok(()) -} - -#[cfg(not(all(feature = "spin", target_arch = "wasm32")))] pub fn init_logger() -> Result<(), log::SetLoggerError> { Ok(()) } @@ -47,12 +41,7 @@ impl AppExt for edgezero_core::app::App { ) -> ::core::pin::Pin< Box> + 'a>, > { - Box::pin(async move { - let core_request = into_core_request(req).await?; - let response = self.router().oneshot(core_request).await; - let spin_response = from_core_response(response).await?; - Ok(spin_response) - }) + Box::pin(request::dispatch(self, req)) } } diff --git a/crates/edgezero-adapter-spin/src/proxy.rs b/crates/edgezero-adapter-spin/src/proxy.rs index d44931b..6eee0f1 100644 --- a/crates/edgezero-adapter-spin/src/proxy.rs +++ b/crates/edgezero-adapter-spin/src/proxy.rs @@ -1,9 +1,9 @@ +use crate::response::collect_body_bytes; use async_trait::async_trait; use edgezero_core::body::Body; use edgezero_core::error::EdgeError; use edgezero_core::http::StatusCode; use edgezero_core::proxy::{ProxyClient, ProxyRequest, ProxyResponse}; -use futures_util::StreamExt; /// A proxy client that uses Spin's outbound HTTP (`spin_sdk::http::send`) /// to forward requests to upstream services. @@ -22,44 +22,40 @@ impl ProxyClient for SpinProxyClient { for (name, value) in headers.iter() { if let Ok(v) = value.to_str() { builder.header(name.as_str(), v); + } else { + log::warn!("dropping non-UTF-8 proxy request header: {}", name); } } - let body_bytes = match body { - Body::Once(bytes) => bytes.to_vec(), - Body::Stream(mut stream) => { - // Spin doesn't support streaming outbound bodies; collect into bytes. - let mut collected = Vec::new(); - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => collected.extend_from_slice(&bytes), - Err(err) => return Err(EdgeError::internal(err)), - } - } - collected - } - }; + let body_bytes = collect_body_bytes(body).await?; builder.body(body_bytes); let spin_request = builder.build(); let spin_response: spin_sdk::http::Response = spin_sdk::http::send(spin_request) .await - .map_err(|e| EdgeError::internal(format!("Spin outbound HTTP error: {e}")))?; + .map_err(|e| EdgeError::internal(anyhow::anyhow!("Spin outbound HTTP error: {e}")))?; let status = StatusCode::from_u16(*spin_response.status()) .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); // Collect response headers before consuming the body. - let response_headers: Vec<_> = spin_response - .headers() - .filter_map(|(name, value)| { - let v = value.as_str()?; - let hname = edgezero_core::http::HeaderName::from_bytes(name.as_bytes()).ok()?; - let hval: edgezero_core::http::HeaderValue = v.parse().ok()?; - Some((hname, hval)) - }) - .collect(); + let mut response_headers = Vec::new(); + for (name, value) in spin_response.headers() { + let Some(v) = value.as_str() else { + log::warn!("dropping non-UTF-8 proxy response header: {}", name); + continue; + }; + let Ok(hname) = edgezero_core::http::HeaderName::from_bytes(name.as_bytes()) else { + log::warn!("dropping invalid proxy response header name: {}", name); + continue; + }; + let Ok(hval) = v.parse::() else { + log::warn!("dropping invalid proxy response header value for: {}", name); + continue; + }; + response_headers.push((hname, hval)); + } let response_body = spin_response.into_body(); let mut proxy_response = ProxyResponse::new(status, Body::from(response_body)); diff --git a/crates/edgezero-adapter-spin/src/request.rs b/crates/edgezero-adapter-spin/src/request.rs index 07ae1e4..1bb86a2 100644 --- a/crates/edgezero-adapter-spin/src/request.rs +++ b/crates/edgezero-adapter-spin/src/request.rs @@ -6,7 +6,7 @@ use edgezero_core::body::Body; use edgezero_core::error::EdgeError; use edgezero_core::http::{request_builder, Request, Uri}; use edgezero_core::proxy::ProxyHandle; -use spin_sdk::http::{IncomingRequest, IntoResponse}; +use spin_sdk::http::IncomingRequest; /// Convert a Spin `IncomingRequest` into an EdgeZero core `Request`. /// @@ -32,10 +32,13 @@ pub async fn into_core_request(req: IncomingRequest) -> Result)], name: &str) -> Option anyhow::Result { +pub async fn dispatch(app: &App, req: IncomingRequest) -> anyhow::Result { let core_request = into_core_request(req).await?; let response = app.router().oneshot(core_request).await; Ok(from_core_response(response).await?) diff --git a/crates/edgezero-adapter-spin/src/response.rs b/crates/edgezero-adapter-spin/src/response.rs index 72cc014..abde846 100644 --- a/crates/edgezero-adapter-spin/src/response.rs +++ b/crates/edgezero-adapter-spin/src/response.rs @@ -4,6 +4,23 @@ use edgezero_core::http::Response; use futures_util::StreamExt; use spin_sdk::http as spin_http; +/// Collect a `Body` into a `Vec`, consuming streamed chunks if necessary. +pub(crate) async fn collect_body_bytes(body: Body) -> Result, EdgeError> { + match body { + Body::Once(bytes) => Ok(bytes.to_vec()), + Body::Stream(mut stream) => { + let mut collected = Vec::new(); + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => collected.extend_from_slice(&bytes), + Err(err) => return Err(EdgeError::internal(err)), + } + } + Ok(collected) + } + } +} + /// Convert an EdgeZero core `Response` into a Spin SDK `Response`. /// /// Both `Body::Once` and `Body::Stream` are converted to a buffered @@ -17,22 +34,12 @@ pub async fn from_core_response(response: Response) -> Result bytes.to_vec(), - Body::Stream(mut stream) => { - let mut collected = Vec::new(); - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => collected.extend_from_slice(&bytes), - Err(err) => return Err(EdgeError::internal(err)), - } - } - collected - } - }; + let body_bytes = collect_body_bytes(body).await?; builder.body(body_bytes); Ok(builder.build()) diff --git a/crates/edgezero-adapter-spin/tests/contract.rs b/crates/edgezero-adapter-spin/tests/contract.rs index 1a5ca2f..87d4d90 100644 --- a/crates/edgezero-adapter-spin/tests/contract.rs +++ b/crates/edgezero-adapter-spin/tests/contract.rs @@ -1,12 +1,12 @@ -#![cfg(all(feature = "spin", target_arch = "wasm32"))] - -use edgezero_adapter_spin::{dispatch, from_core_response, into_core_request, SpinRequestContext}; +use bytes::Bytes; +use edgezero_adapter_spin::SpinRequestContext; use edgezero_core::app::App; use edgezero_core::body::Body; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; -use edgezero_core::http::{response_builder, Method, Response, StatusCode}; +use edgezero_core::http::{response_builder, Response, StatusCode}; use edgezero_core::router::RouterService; +use futures::stream; fn build_test_app() -> App { async fn capture_uri(ctx: RequestContext) -> Result { @@ -27,10 +27,111 @@ fn build_test_app() -> App { Ok(response) } + async fn stream_response(_ctx: RequestContext) -> Result { + let chunks = stream::iter(vec![ + Bytes::from_static(b"chunk-1"), + Bytes::from_static(b"chunk-2"), + ]); + + let response = response_builder() + .status(StatusCode::OK) + .body(Body::stream(chunks)) + .expect("response"); + Ok(response) + } + let router = RouterService::builder() .get("/uri", capture_uri) .post("/mirror", mirror_body) + .get("/stream", stream_response) .build(); App::new(router) } + +// --------------------------------------------------------------------------- +// Tests that run on the host (no WASI runtime required) +// --------------------------------------------------------------------------- + +#[test] +fn context_default_is_empty() { + let ctx = SpinRequestContext { + client_addr: None, + full_url: None, + }; + assert!(ctx.client_addr.is_none()); + assert!(ctx.full_url.is_none()); +} + +#[test] +fn build_test_app_creates_valid_router() { + // Smoke test: ensure the router builds without panicking and that + // the test helpers are usable for future integration tests. + let _app = build_test_app(); +} + +// --------------------------------------------------------------------------- +// Tests that require `spin_sdk` types (wasm32 + spin feature only) +// +// `from_core_response` returns `spin_sdk::http::Response` which is only +// available on wasm32. `into_core_request` and `dispatch` additionally +// require a WASI `IncomingRequest` handle from the Spin runtime. +// --------------------------------------------------------------------------- + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +mod wasm { + use super::*; + use edgezero_adapter_spin::from_core_response; + + #[test] + fn from_core_response_translates_status_and_headers() { + futures::executor::block_on(async { + let response = response_builder() + .status(StatusCode::CREATED) + .header("x-edgezero-res", "1") + .body(Body::from(b"hello".to_vec())) + .expect("response"); + + let spin_response = from_core_response(response).await.expect("spin response"); + + assert_eq!(*spin_response.status(), 201); + let header = spin_response + .headers() + .find(|(name, _)| name == "x-edgezero-res"); + assert!(header.is_some()); + }); + } + + #[test] + fn from_core_response_collects_streaming_body() { + futures::executor::block_on(async { + let response = response_builder() + .status(StatusCode::OK) + .body(Body::stream(stream::iter(vec![ + Bytes::from_static(b"chunk-1"), + Bytes::from_static(b"chunk-2"), + ]))) + .expect("response"); + + let spin_response = from_core_response(response).await.expect("spin response"); + + assert_eq!(*spin_response.status(), 200); + assert_eq!(spin_response.into_body(), b"chunk-1chunk-2"); + }); + } + + #[test] + fn from_core_response_handles_empty_body() { + futures::executor::block_on(async { + let response = response_builder() + .status(StatusCode::NO_CONTENT) + .body(Body::from(Vec::new())) + .expect("response"); + + let spin_response = from_core_response(response).await.expect("spin response"); + + assert_eq!(*spin_response.status(), 204); + assert!(spin_response.into_body().is_empty()); + }); + } +} diff --git a/crates/edgezero-cli/src/generator.rs b/crates/edgezero-cli/src/generator.rs index 590177b..e36a6ea 100644 --- a/crates/edgezero-cli/src/generator.rs +++ b/crates/edgezero-cli/src/generator.rs @@ -137,6 +137,10 @@ fn seed_workspace_dependencies() -> BTreeMap { "tokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\"] }".to_string(), ); deps.insert("tracing".to_string(), "tracing = \"0.1\"".to_string()); + deps.insert( + "spin-sdk".to_string(), + "spin-sdk = { version = \"5.2\", default-features = false }".to_string(), + ); deps } @@ -185,6 +189,10 @@ fn collect_adapter_data( let mut data_entries: Vec<(String, String)> = Vec::new(); data_entries.push((format!("proj_{}", blueprint.id), crate_name.clone())); + data_entries.push(( + format!("proj_{}_underscored", blueprint.id), + crate_name.replace('-', "_"), + )); for dep in blueprint.dependencies { let ResolvedDependency { From e9f454853641da1f7a3599e83f541beb2230d85b Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 12 Mar 2026 13:11:41 -0500 Subject: [PATCH 4/4] Address PR review feedback Fix artifact lookup hyphen-to-underscore bug in all three adapter CLIs, stop find_workspace_root at [workspace] tables for nested workspaces, compute correct target dir in spin.toml template for workspace members, and use HeaderValue::from_bytes for inbound request/proxy headers. Adds tests for hyphenated crate names and nested workspace resolution. --- crates/edgezero-adapter-cloudflare/src/cli.rs | 4 +- crates/edgezero-adapter-fastly/src/cli.rs | 4 +- crates/edgezero-adapter-spin/src/cli.rs | 20 +++++++- crates/edgezero-adapter-spin/src/proxy.rs | 22 +++++---- crates/edgezero-adapter-spin/src/request.rs | 11 +++-- crates/edgezero-adapter-spin/src/response.rs | 7 ++- .../src/templates/spin.toml.hbs | 2 +- crates/edgezero-adapter/src/cli_support.rs | 48 ++++++++++++++++++- crates/edgezero-cli/src/generator.rs | 8 ++++ .../crates/app-demo-adapter-spin/spin.toml | 2 +- 10 files changed, 103 insertions(+), 25 deletions(-) diff --git a/crates/edgezero-adapter-cloudflare/src/cli.rs b/crates/edgezero-adapter-cloudflare/src/cli.rs index ced3f10..a84eaa4 100644 --- a/crates/edgezero-adapter-cloudflare/src/cli.rs +++ b/crates/edgezero-adapter-cloudflare/src/cli.rs @@ -49,7 +49,7 @@ pub fn build() -> Result { let pkg_dir = workspace_root.join("pkg"); fs::create_dir_all(&pkg_dir) .map_err(|e| format!("failed to create {}: {e}", pkg_dir.display()))?; - let dest = pkg_dir.join(format!("{crate_name}.wasm")); + let dest = pkg_dir.join(format!("{}.wasm", crate_name.replace('-', "_"))); fs::copy(&artifact, &dest) .map_err(|e| format!("failed to copy artifact to {}: {e}", dest.display()))?; @@ -284,7 +284,7 @@ fn locate_artifact( manifest_dir: &Path, crate_name: &str, ) -> Result { - let release_name = format!("{crate_name}.wasm"); + let release_name = format!("{}.wasm", crate_name.replace('-', "_")); if let Some(custom) = std::env::var_os("CARGO_TARGET_DIR") { let candidate = PathBuf::from(custom) diff --git a/crates/edgezero-adapter-fastly/src/cli.rs b/crates/edgezero-adapter-fastly/src/cli.rs index f75a5b7..8678780 100644 --- a/crates/edgezero-adapter-fastly/src/cli.rs +++ b/crates/edgezero-adapter-fastly/src/cli.rs @@ -48,7 +48,7 @@ pub fn build(extra_args: &[String]) -> Result { let pkg_dir = workspace_root.join("pkg"); fs::create_dir_all(&pkg_dir) .map_err(|e| format!("failed to create {}: {e}", pkg_dir.display()))?; - let dest = pkg_dir.join(format!("{crate_name}.wasm")); + let dest = pkg_dir.join(format!("{}.wasm", crate_name.replace('-', "_"))); fs::copy(&artifact, &dest) .map_err(|e| format!("failed to copy artifact to {}: {e}", dest.display()))?; @@ -269,7 +269,7 @@ fn locate_artifact( crate_name: &str, ) -> Result { let target_triple = "wasm32-wasip1"; - let release_name = format!("{crate_name}.wasm"); + let release_name = format!("{}.wasm", crate_name.replace('-', "_")); if let Some(custom) = std::env::var_os("CARGO_TARGET_DIR") { let candidate = PathBuf::from(custom) diff --git a/crates/edgezero-adapter-spin/src/cli.rs b/crates/edgezero-adapter-spin/src/cli.rs index 9069ac7..bd6dc17 100644 --- a/crates/edgezero-adapter-spin/src/cli.rs +++ b/crates/edgezero-adapter-spin/src/cli.rs @@ -50,7 +50,7 @@ pub fn build(extra_args: &[String]) -> Result { let pkg_dir = workspace_root.join("pkg"); fs::create_dir_all(&pkg_dir) .map_err(|e| format!("failed to create {}: {e}", pkg_dir.display()))?; - let dest = pkg_dir.join(format!("{crate_name}.wasm")); + let dest = pkg_dir.join(format!("{}.wasm", crate_name.replace('-', "_"))); fs::copy(&artifact, &dest) .map_err(|e| format!("failed to copy artifact to {}: {e}", dest.display()))?; @@ -260,7 +260,7 @@ fn locate_artifact( manifest_dir: &Path, crate_name: &str, ) -> Result { - let release_name = format!("{crate_name}.wasm"); + let release_name = format!("{}.wasm", crate_name.replace('-', "_")); if let Some(custom) = std::env::var_os("CARGO_TARGET_DIR") { let candidate = PathBuf::from(custom) @@ -336,6 +336,22 @@ mod tests { assert_eq!(located, artifact); } + #[test] + fn locate_artifact_converts_hyphens_to_underscores() { + let dir = tempdir().unwrap(); + let workspace = dir.path(); + let manifest_dir = workspace.join("crates/my-cool-crate"); + fs::create_dir_all(&manifest_dir).unwrap(); + + // Cargo emits underscored filenames for hyphenated crate names. + let artifact = workspace.join("target/wasm32-wasip1/release/my_cool_crate.wasm"); + fs::create_dir_all(artifact.parent().unwrap()).unwrap(); + fs::write(&artifact, "wasm").unwrap(); + + let located = locate_artifact(workspace, &manifest_dir, "my-cool-crate").unwrap(); + assert_eq!(located, artifact); + } + #[test] fn finds_closest_manifest_when_multiple_exist() { let dir = tempdir().unwrap(); diff --git a/crates/edgezero-adapter-spin/src/proxy.rs b/crates/edgezero-adapter-spin/src/proxy.rs index 6eee0f1..4615896 100644 --- a/crates/edgezero-adapter-spin/src/proxy.rs +++ b/crates/edgezero-adapter-spin/src/proxy.rs @@ -19,11 +19,16 @@ impl ProxyClient for SpinProxyClient { .method(into_spin_method(&method)) .uri(uri.to_string()); + // Spin's WASI HTTP interface requires string-typed header values, + // so non-UTF-8 values cannot be forwarded and are dropped with a warning. for (name, value) in headers.iter() { if let Ok(v) = value.to_str() { builder.header(name.as_str(), v); } else { - log::warn!("dropping non-UTF-8 proxy request header: {}", name); + log::warn!( + "dropping non-UTF-8 proxy request header (Spin WASI limitation): {}", + name + ); } } @@ -42,19 +47,16 @@ impl ProxyClient for SpinProxyClient { // Collect response headers before consuming the body. let mut response_headers = Vec::new(); for (name, value) in spin_response.headers() { - let Some(v) = value.as_str() else { - log::warn!("dropping non-UTF-8 proxy response header: {}", name); - continue; - }; let Ok(hname) = edgezero_core::http::HeaderName::from_bytes(name.as_bytes()) else { log::warn!("dropping invalid proxy response header name: {}", name); continue; }; - let Ok(hval) = v.parse::() else { - log::warn!("dropping invalid proxy response header value for: {}", name); - continue; - }; - response_headers.push((hname, hval)); + match edgezero_core::http::HeaderValue::from_bytes(value.as_bytes()) { + Ok(hval) => response_headers.push((hname, hval)), + Err(_) => { + log::warn!("dropping invalid proxy response header value for: {}", name); + } + } } let response_body = spin_response.into_body(); diff --git a/crates/edgezero-adapter-spin/src/request.rs b/crates/edgezero-adapter-spin/src/request.rs index 1bb86a2..5ef004c 100644 --- a/crates/edgezero-adapter-spin/src/request.rs +++ b/crates/edgezero-adapter-spin/src/request.rs @@ -30,10 +30,13 @@ pub async fn into_core_request(req: IncomingRequest) -> Result { + builder = builder.header(name.as_str(), hval); + } + Err(_) => { + log::warn!("dropping invalid request header value: {}", name); + } } } diff --git a/crates/edgezero-adapter-spin/src/response.rs b/crates/edgezero-adapter-spin/src/response.rs index abde846..ddac000 100644 --- a/crates/edgezero-adapter-spin/src/response.rs +++ b/crates/edgezero-adapter-spin/src/response.rs @@ -31,11 +31,16 @@ pub async fn from_core_response(response: Response) -> Result Option PathBuf { let mut current: Option<&Path> = Some(dir); let mut candidate: Option = None; while let Some(path) = current { - if path.join("Cargo.toml").exists() { + let cargo = path.join("Cargo.toml"); + if cargo.exists() { candidate = Some(path.to_path_buf()); + if fs::read_to_string(&cargo) + .map(|s| s.contains("[workspace]")) + .unwrap_or(false) + { + break; + } } current = path.parent(); } @@ -99,6 +109,40 @@ mod tests { assert_eq!(find_workspace_root(&child), root); } + #[test] + fn workspace_root_stops_at_workspace_table() { + let dir = tempdir().unwrap(); + let outer = dir.path(); + + // Outer repo root with a Cargo.toml + fs::write( + outer.join("Cargo.toml"), + "[workspace]\nmembers = [\"examples/*\"]", + ) + .unwrap(); + + // Inner workspace (e.g. examples/app-demo) + let inner = outer.join("examples/app-demo"); + fs::create_dir_all(&inner).unwrap(); + fs::write( + inner.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/*\"]", + ) + .unwrap(); + + // Crate inside the inner workspace + let crate_dir = inner.join("crates/my-adapter"); + fs::create_dir_all(&crate_dir).unwrap(); + fs::write( + crate_dir.join("Cargo.toml"), + "[package]\nname = \"my-adapter\"", + ) + .unwrap(); + + // Should resolve to the inner workspace, not the outer repo root. + assert_eq!(find_workspace_root(&crate_dir), inner); + } + #[test] fn path_distance_counts_divergence() { let a = Path::new("/a/b/c"); diff --git a/crates/edgezero-cli/src/generator.rs b/crates/edgezero-cli/src/generator.rs index e36a6ea..baed752 100644 --- a/crates/edgezero-cli/src/generator.rs +++ b/crates/edgezero-cli/src/generator.rs @@ -212,6 +212,14 @@ fn collect_adapter_data( let crate_dir_rel = format!("crates/{}", crate_name); + // Compute the relative path from the adapter crate to the workspace + // target directory so templates can reference build artifacts. + let depth = crate_dir_rel.matches('/').count() + 1; + data_entries.push(( + format!("target_dir_{}", blueprint.id), + format!("{}target", "../".repeat(depth)), + )); + let build_cmd = blueprint .commands .build diff --git a/examples/app-demo/crates/app-demo-adapter-spin/spin.toml b/examples/app-demo/crates/app-demo-adapter-spin/spin.toml index ed4152f..d9aa7b6 100644 --- a/examples/app-demo/crates/app-demo-adapter-spin/spin.toml +++ b/examples/app-demo/crates/app-demo-adapter-spin/spin.toml @@ -9,7 +9,7 @@ route = "/..." component = "app-demo" [component.app-demo] -source = "target/wasm32-wasip1/release/app_demo_adapter_spin.wasm" +source = "../../target/wasm32-wasip1/release/app_demo_adapter_spin.wasm" allowed_outbound_hosts = ["https://*:*"] [component.app-demo.build] command = "cargo build --target wasm32-wasip1 --release"