diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3759cc3..8d8031c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +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" playwright: name: playwright tests diff --git a/.gitignore b/.gitignore index a147ad9..380c166 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ **/bin/ **/pkg/ +# Spin runtime logs +.spin/logs/ +**/.spin/logs/ + # env .env diff --git a/CLAUDE.md b/CLAUDE.md index f405629..1b3a5ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,10 +5,10 @@ Mocktioneer is a deterministic OpenRTB banner bidder for edge platforms. It lets you test client integrations (Prebid.js, Prebid Server, custom SDKs) without depending on third-party bidders or origin backends. Write once, deploy to -Fastly Compute, Cloudflare Workers, or native Axum servers. The codebase is a -Cargo workspace with 4 crates under `crates/`, a VitePress documentation site -under `docs/`, Playwright e2e tests under `tests/playwright/`, and CI workflows -under `.github/workflows/`. +Fastly Compute, Cloudflare Workers, Fermyon Spin, or native Axum servers. The +codebase is a Cargo workspace with 5 crates under `crates/`, a VitePress +documentation site under `docs/`, Playwright e2e tests under +`tests/playwright/`, and CI workflows under `.github/workflows/`. ## Workspace Layout @@ -18,6 +18,7 @@ crates/ mocktioneer-adapter-axum/ # Native Axum HTTP server mocktioneer-adapter-cloudflare/ # Cloudflare Workers bridge (wasm32-unknown-unknown) mocktioneer-adapter-fastly/ # Fastly Compute bridge (wasm32-wasip1) + mocktioneer-adapter-spin/ # Fermyon Spin bridge (wasm32-wasip2) docs/ # VitePress documentation site (Node.js) examples/ # curl/shell scripts for endpoint demos tests/playwright/ # Playwright e2e tests (creative visibility, sizes) @@ -64,11 +65,12 @@ faster iteration since nearly all business logic lives there. ## Compilation Targets -| Adapter | Target | Notes | -| ---------- | ------------------------ | ---------------------------------- | -| Fastly | `wasm32-wasip1` | Requires Viceroy for local testing | -| Cloudflare | `wasm32-unknown-unknown` | Requires `wrangler` for dev/deploy | -| Axum | Native (host triple) | Standard Tokio runtime | +| Adapter | Target | Notes | +| ---------- | ------------------------ | ------------------------------------- | +| Fastly | `wasm32-wasip1` | Requires Viceroy for local testing | +| Cloudflare | `wasm32-unknown-unknown` | Requires `wrangler` for dev/deploy | +| Spin | `wasm32-wasip2` | Requires `spin` CLI for local/deploy | +| Axum | Native (host triple) | Standard Tokio runtime | ## Coding Conventions @@ -161,7 +163,7 @@ 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. Playwright e2e tests (`tests/playwright/`) 6. ESLint + Prettier on `docs/` @@ -290,6 +292,7 @@ Custom commands live in `.claude/commands/`: | Axum adapter entry | `crates/mocktioneer-adapter-axum/src/main.rs` | | Cloudflare adapter | `crates/mocktioneer-adapter-cloudflare/src/lib.rs` | | Fastly adapter | `crates/mocktioneer-adapter-fastly/src/main.rs` | +| Spin adapter | `crates/mocktioneer-adapter-spin/src/lib.rs` | | Playwright tests | `tests/playwright/` | | Example scripts | `examples/` | | CI tests | `.github/workflows/test.yml` | diff --git a/Cargo.lock b/Cargo.lock index 076dae3..c513bdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,7 +580,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-axum" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7595003f8334f9e8a09f7cadfbc1907bef865bab" +source = "git+https://github.com/stackpop/edgezero.git?branch=main#4664bee77090908dde546239a37266261dd50119" dependencies = [ "anyhow", "async-trait", @@ -591,6 +591,7 @@ dependencies = [ "futures-util", "http", "log", + "redb", "reqwest", "simple_logger", "thiserror 2.0.18", @@ -602,8 +603,9 @@ dependencies = [ [[package]] name = "edgezero-adapter-cloudflare" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7595003f8334f9e8a09f7cadfbc1907bef865bab" +source = "git+https://github.com/stackpop/edgezero.git?branch=main#4664bee77090908dde546239a37266261dd50119" dependencies = [ + "anyhow", "async-trait", "brotli", "bytes", @@ -619,8 +621,9 @@ dependencies = [ [[package]] name = "edgezero-adapter-fastly" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7595003f8334f9e8a09f7cadfbc1907bef865bab" +source = "git+https://github.com/stackpop/edgezero.git?branch=main#4664bee77090908dde546239a37266261dd50119" dependencies = [ + "anyhow", "async-stream", "async-trait", "brotli", @@ -636,10 +639,27 @@ dependencies = [ "log-fastly", ] +[[package]] +name = "edgezero-adapter-spin" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero.git?branch=main#4664bee77090908dde546239a37266261dd50119" +dependencies = [ + "anyhow", + "async-trait", + "brotli", + "bytes", + "edgezero-core", + "flate2", + "futures", + "futures-util", + "log", + "spin-sdk", +] + [[package]] name = "edgezero-core" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7595003f8334f9e8a09f7cadfbc1907bef865bab" +source = "git+https://github.com/stackpop/edgezero.git?branch=main#4664bee77090908dde546239a37266261dd50119" dependencies = [ "anyhow", "async-compression", @@ -667,7 +687,7 @@ dependencies = [ [[package]] name = "edgezero-macros" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7595003f8334f9e8a09f7cadfbc1907bef865bab" +source = "git+https://github.com/stackpop/edgezero.git?branch=main#4664bee77090908dde546239a37266261dd50119" dependencies = [ "log", "proc-macro2", @@ -937,7 +957,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1447,7 +1467,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", ] @@ -1487,6 +1507,17 @@ dependencies = [ "mocktioneer-core", ] +[[package]] +name = "mocktioneer-adapter-spin" +version = "0.1.0" +dependencies = [ + "anyhow", + "edgezero-adapter-spin", + "edgezero-core", + "mocktioneer-core", + "spin-sdk", +] + [[package]] name = "mocktioneer-core" version = "0.1.0" @@ -1899,6 +1930,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redb" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b440f171acaaddcd7e65eefc2ce4f4d4e05c1dffe2afe23f81af2a87cbfc4e0d" +dependencies = [ + "libc", +] + [[package]] name = "regex" version = "1.12.3" @@ -1977,6 +2017,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" @@ -2308,6 +2358,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" @@ -2318,6 +2388,52 @@ 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 = "spki" version = "0.7.3" @@ -2334,6 +2450,12 @@ 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" @@ -2766,6 +2888,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" @@ -3284,6 +3415,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 05e4399..d31340b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/mocktioneer-adapter-axum", "crates/mocktioneer-adapter-cloudflare", "crates/mocktioneer-adapter-fastly", + "crates/mocktioneer-adapter-spin", ] resolver = "2" @@ -24,9 +25,13 @@ ed25519-dalek = "2.1" edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-axum", default-features = false } edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-cloudflare", default-features = false } edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-fastly", default-features = false } +edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-spin", default-features = false } edgezero-cli = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-cli" } edgezero-core = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-core" } fastly = "0.11.9" +# Keep the Spin SDK surface minimal; the adapter only uses `#[http_component]` +# and HTTP request/response types. +spin-sdk = { version = "5.2", default-features = false } futures = { version = "0.3", features = ["std", "executor"] } futures-util = "0.3.31" handlebars = "6" diff --git a/crates/mocktioneer-adapter-spin/Cargo.toml b/crates/mocktioneer-adapter-spin/Cargo.toml new file mode 100644 index 0000000..991a803 --- /dev/null +++ b/crates/mocktioneer-adapter-spin/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "mocktioneer-adapter-spin" +version = "0.1.0" +edition = "2021" +publish = false +license.workspace = true + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[features] +default = [] +spin = ["edgezero-adapter-spin/spin"] + +[dependencies] +edgezero-adapter-spin = { workspace = true } +edgezero-core = { workspace = true } +mocktioneer-core = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +anyhow = { workspace = true } +edgezero-adapter-spin = { workspace = true, features = ["spin"] } +spin-sdk = { workspace = true } diff --git a/crates/mocktioneer-adapter-spin/spin.toml b/crates/mocktioneer-adapter-spin/spin.toml new file mode 100644 index 0000000..e731d36 --- /dev/null +++ b/crates/mocktioneer-adapter-spin/spin.toml @@ -0,0 +1,16 @@ +spin_manifest_version = 3 + +[application] +name = "mocktioneer-adapter-spin" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "mocktioneer" + +[component.mocktioneer] +source = "../../target/wasm32-wasip2/release/mocktioneer_adapter_spin.wasm" +allowed_outbound_hosts = [] +[component.mocktioneer.build] +command = "cargo build --release --target wasm32-wasip2 -p mocktioneer-adapter-spin" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/crates/mocktioneer-adapter-spin/src/lib.rs b/crates/mocktioneer-adapter-spin/src/lib.rs new file mode 100644 index 0000000..bd71e31 --- /dev/null +++ b/crates/mocktioneer-adapter-spin/src/lib.rs @@ -0,0 +1,21 @@ +//! Spin adapter for Mocktioneer. +//! +//! This crate is a `cdylib` targeting `wasm32-wasip2`. All runtime code is +//! gated behind `#[cfg(target_arch = "wasm32")]` so the crate compiles (but is +//! empty) on the host target, allowing `cargo fmt`, `cargo clippy`, and +//! `cargo test` to run across the whole workspace without pulling in WASM-only +//! dependencies. +#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] + +#[cfg(target_arch = "wasm32")] +use mocktioneer_core::MocktioneerApp; +#[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/edgezero.toml b/edgezero.toml index 720d62d..9634b7f 100644 --- a/edgezero.toml +++ b/edgezero.toml @@ -11,140 +11,140 @@ id = "root" path = "/" methods = ["GET"] handler = "mocktioneer_core::routes::handle_root" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "root_options" path = "/" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "openrtb_auction" path = "/openrtb2/auction" methods = ["POST"] handler = "mocktioneer_core::routes::handle_openrtb_auction" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "openrtb_options" path = "/openrtb2/auction" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "static_img" path = "/static/img/{size}" methods = ["GET"] handler = "mocktioneer_core::routes::handle_static_img" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "static_img_options" path = "/static/img/{size}" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "static_creatives" path = "/static/creatives/{size}" methods = ["GET"] handler = "mocktioneer_core::routes::handle_static_creatives" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "static_creatives_options" path = "/static/creatives/{size}" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "click" path = "/click" methods = ["GET"] handler = "mocktioneer_core::routes::handle_click" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "click_options" path = "/click" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "pixel" path = "/pixel" methods = ["GET"] handler = "mocktioneer_core::routes::handle_pixel" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "pixel_options" path = "/pixel" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "aps_bid" path = "/e/dtb/bid" methods = ["POST"] handler = "mocktioneer_core::routes::handle_aps_bid" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "aps_bid_options" path = "/e/dtb/bid" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "aps_win" path = "/aps/win" methods = ["GET"] handler = "mocktioneer_core::routes::handle_aps_win" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "aps_win_options" path = "/aps/win" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "adserver_mediate" path = "/adserver/mediate" methods = ["POST"] handler = "mocktioneer_core::routes::handle_adserver_mediate" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "adserver_mediate_options" path = "/adserver/mediate" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "sizes" path = "/_/sizes" methods = ["GET"] handler = "mocktioneer_core::routes::handle_sizes" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [[triggers.http]] id = "sizes_options" path = "/_/sizes" methods = ["OPTIONS"] handler = "mocktioneer_core::routes::handle_options" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] [adapters.axum.adapter] crate = "crates/mocktioneer-adapter-axum" @@ -199,3 +199,21 @@ serve = "fastly compute serve -C crates/mocktioneer-adapter-fastly" endpoint = "mocktioneerlog" level = "info" echo_stdout = false + +[adapters.spin.adapter] +crate = "crates/mocktioneer-adapter-spin" +manifest = "crates/mocktioneer-adapter-spin/spin.toml" + +[adapters.spin.build] +target = "wasm32-wasip2" +profile = "release" +features = ["spin"] + +[adapters.spin.commands] +build = "cargo build --release --target wasm32-wasip2 -p mocktioneer-adapter-spin" +deploy = "spin deploy --from crates/mocktioneer-adapter-spin" +serve = "spin up --from crates/mocktioneer-adapter-spin" + +[adapters.spin.logging] +level = "info" +echo_stdout = true