From 705d6fef16ad0a8d05cbd60d38096b2612634e47 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 17 Feb 2026 10:36:24 -0600 Subject: [PATCH] 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 | 314 ++++++++++++++- 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, 1249 insertions(+), 17 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 baf6a78..7cbc565 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", @@ -1054,7 +1073,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1573,7 +1592,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", ] @@ -1980,6 +1999,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" @@ -2300,6 +2329,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" @@ -2310,12 +2359,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" @@ -2756,6 +2857,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" @@ -3274,6 +3384,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 6900053..eeb5f7a 100644 --- a/examples/app-demo/Cargo.lock +++ b/examples/app-demo/Cargo.lock @@ -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" @@ -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" @@ -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" @@ -846,7 +877,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -864,12 +895,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 +1127,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,7 +1167,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1197,6 +1251,12 @@ 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" @@ -1295,7 +1355,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", ] @@ -1419,6 +1479,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.114", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1628,6 +1698,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" @@ -1762,6 +1842,12 @@ dependencies = [ "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" @@ -1930,6 +2016,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 +2046,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" @@ -2137,9 +2295,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220" dependencies = [ "indexmap", "serde_core", @@ -2152,18 +2310,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.7+spec-1.1.0" +version = "1.0.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" dependencies = [ "winnow", ] @@ -2270,6 +2428,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2355,6 +2519,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" @@ -2462,6 +2635,28 @@ version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea" +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" @@ -2475,6 +2670,18 @@ 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.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -2891,6 +3098,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ "bitflags 2.10.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.10.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.114", + "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.114", + "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.10.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]] 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"