# `splicer` 🔍✂️🪡
**Plan and generate middleware splice operations for WebAssembly component composition graphs.**
`splicer` reads:
* A **composition graph** (JSON)
* A **splice configuration** (YAML)
It produces a modified plan that injects middleware components according to declarative rules.
This tool is designed to work with component-based systems such as WASI HTTP services, but is interface-agnostic and can splice across any interface edge in a component graph.
---
# Why splicer?
When building component-based systems, middleware insertion often requires:
* Rewriting instantiation chains
* Re-threading handler references
* Maintaining correct edge ordering
* Traversing nested provider chains
`splicer` automates that planning step.
Instead of manually restructuring component wiring, you define:
* What interface to target
* Where to inject middleware
* What middleware components to insert
And `splicer` generates the modified composition plan.
A demo of `splicer` can be run using: `cargo run --example demo`
A more in-depth usage of `splicer` is done in the external [`component-interposition`](https://github.com/ejrgilbert/component-interposition) repo.
---
# Adapter Components
Most middleware doesn't need to match the exact type signature of the interface
it's being placed on. A logging middleware that prints "before" and "after"
around every call works the same whether the target interface is
`wasi:http/handler` or `my:service/adder`, it only needs the function name.
Splicer generates **adapter components** that bridge between a generic
middleware WIT interface and the specific target interface. The middleware author
writes against a simple contract; splicer handles all the type plumbing at
composition time.
### Middleware Tiers
| **Tier 1** | Hook (name only) — `on-call`, `on-return`, `should-block`: middleware sees the call identity but not types or data | [`wit/tier1/world.wit`](wit/tier1/world.wit) | **Supported** |
| **Tier 2** | Observe — middleware sees the typed values flowing through (lifted into a structural attribute tree); cannot modify | `wit/tier2/world.wit` (planned) | Planned |
| **Tier 3** | Transform — middleware sees AND modifies the values; downstream is still called | `wit/tier3/world.wit` (planned) | Planned |
| **Tier 4** | Virtualize — middleware replaces the downstream entirely (mocks, virts, replayers) | `wit/tier4/world.wit` (planned) | Planned |
Each tier strictly adds one capability. Middleware written for a lower tier
works unchanged when higher tiers become available.
To write a tier-1 middleware, your component exports one or more of the
interfaces defined in [`wit/tier1/world.wit`](wit/tier1/world.wit).
When `splicer splice` detects that a middleware exports these interfaces (instead
of the target interface directly), it automatically generates an adapter
component and wires it into the composition.
For the full guide — including how to write a tier-1 middleware, how adapter
detection works, and what the generated adapter does internally — see
[docs/adapter-components.md](docs/adapter-components.md).
### Builtin Middleware
Splicer ships pre-built middleware components embedded in the binary.
Reference one from a splice config without supplying a path:
```yaml
inject:
- builtin: hello-tier1
```
The `bare` prefix marks tier-1 builtins that see only the call-id —
no payload-derived data on the emitted signal. Value-aware tier-2
siblings — `otel-spans`, `otel-metrics`, `otel-logs` (unprefixed) —
are **planned but not yet implemented**; they'll arrive once tier-2
codegen lands.
| Name | Tier | Description |
|---------------------|------|----------------------------------------------------------------------------|
| `hello-tier1` | 1 | `println!`s every wrapped call. Verifies splice rules fire. |
| `otel-bare-spans` | 1 | Emits a `wasi:otel` span per call (timing + call-id attrs, no payload). |
| `otel-bare-metrics` | 1 | Emits `wasi:otel` count + duration metrics per call (no payload). |
| `otel-bare-logs` | 1 | Emits a structured `wasi:otel` log per call (severity `INFO`, no payload). |
Source crates live under [`builtins/`](builtins/); rebuild artifacts
with `make build-builtins`.
See [docs/splice-config.md](docs/splice-config.md#inject-entry-shapes)
for the full `builtin:` schema (short + long forms).
---
# Installation
From source:
```bash
cargo build --release
```
Binary will be located at:
```
target/release/splicer
```
---
# Usage
Splicer has two subcommands. Both produce a composed `.wasm` directly.
### `splicer splice`: inject middleware into an existing composition
```bash
splicer splice <SPLICE_CFG> <COMP_WASM> [-o composed.wasm]
```
Reads splice rules from `SPLICE_CFG` (YAML), splits `COMP_WASM` into
its sub-components, injects middleware per the rules, and writes the
result to `composed.wasm`.
### `splicer compose`: synthesize a composition from N components
```bash
splicer compose <COMP_WASM>... [-o composed.wasm]
```
Discovers the composition graph by matching the components'
import/export surfaces and writes the composed result.
### Common flags
| `-o, --output <PATH>` | Where to write the composed `.wasm` (default: `composed.wasm`). |
| `--emit-wac [<PATH>]` | Also persist the intermediate WAC source (default: `./output.wac`). Useful for auditing. |
| `--plan` | Skip in-process compose; persist WAC + splits and print the `wac compose ...` command. |
| `--splits-dir <DIR>` | (`splice` only) Persist split sub-components on disk instead of in a tempdir. |
| `--package <NAME>` | Package name written to the generated WAC. |
| `--skip-type-check` | (`splice` only) Demote contract type-check errors to warnings. |
### Library usage
The same pipeline is available as a Rust library:
```rust
let bundle = splicer::splice(splicer::SpliceRequest { /* ... */ })?;
let composed: Vec<u8> = bundle.to_wasm()?;
```
See `examples/wac_compose.rs` for a runnable end-to-end demo.
---
# Configuration Format
Splicing behavior is defined in a YAML configuration file.
See full specification:
```
docs/splice-config.md
```
---
# Example Configuration
```yaml
version: 1
rules:
- before:
interface: wasi:http/handler
provider:
name: auth
inject:
- middleware-a
- middleware-b
- between:
interface: wasi:http/handler
inner:
name: auth
outer:
name: handler
inject:
- tracing
```
---
# Splice Semantics
`splicer` operates on interface edges in the graph.
If no matches are found, the generated `wac` will produce an identity component (roundtrips to same component).
Two matching modes are supported:
## 1. Single-Target Injection
Inject middleware for a given interface, optionally scoped to a specific provider.
```yaml
before:
interface: wasi:http/handler
provider:
name: auth
```
If `provider.name` is omitted, all providers of that interface are matched.
---
## 2. Between Injection
Inject middleware between two specific components connected via an interface edge.
```yaml
between:
interface: wasi:http/handler
inner:
name: auth
outer:
name: handler
```
This replaces:
```
handler → auth
```
With:
```
handler → middleware → auth
```
Middleware chains are traversed in reverse order during injection to preserve declared ordering.
---
# Rule Ordering
Rules are applied in file order.
Later rules operate on the graph after earlier modifications.
This allows intentional stacking:
```
auth → logging → metrics → handler
```
---
# Validation
The configuration will fail if:
* `version` is missing or unsupported
* Required fields are absent
* Middleware list is empty
---
# Testing
In-process unit tests live under `src/` and exercise the adapter
generator, WAC emitter, and composition planner directly:
```bash
cargo test --lib
```
## End-to-end fuzz + run harness
`tests/fuzz_and_run.rs` scaffolds provider, consumer, and middleware
crates in a tempdir, drives them through the full splicer pipeline
(compose + splice for both `between` and `before` rules), and invokes
the result under `wasmtime` to check the composition actually executes.
Two entry points, both `#[ignore]`'d (they build real crates — slow):
* `test_canned` — a hardcoded catalog of 22 value-type shapes * 2
async modes * 2 split-kind pipelines = 88 combos. Same shapes every
run. Quick-to-bisect canary for regressions in a known shape.
* `test_fuzz` — `arbitrary`-driven random shapes. Reproducible via
`SPLICER_FUZZ_SEED` so any failure can be replayed. Each iter
prints `[i/N]` progress (requires `--nocapture`).
```bash
# Canned catalog — 88 combos, ~2 min
cargo test --test fuzz_and_run -- --ignored --nocapture test_canned
# Fuzz at PR CI config (25 iters × 2 modes × depth 5, ~2 min)
SPLICER_FUZZ_ITERS=25 SPLICER_FUZZ_DEPTH=5 \
cargo test --test fuzz_and_run -- --ignored --nocapture test_fuzz
# Replay a single failing iteration
SPLICER_FUZZ_SEED=<seed_from_output> SPLICER_FUZZ_ITERS=1 \
cargo test --test fuzz_and_run -- --ignored --nocapture test_fuzz
```
Env knobs:
| `SPLICER_FUZZ_SEED` | `0xDEADBEEF` | base RNG seed; each iter's shape uses `seed + iter_idx` |
| `SPLICER_FUZZ_ITERS` | 30 | iterations per async mode (sync + async both run) |
| `SPLICER_FUZZ_DEPTH` | 4 | max recursion depth for compound shapes |
| `SPLICER_KEEP_TMPDIR` | unset | preserve the tempdir for post-mortem inspection |
---
# Project Structure
```
splicer/
├── src/
├── docs/
│ └── splice-config.md
├── README.md
```
---
# Design Principles
* **Declarative configuration**
* **Deterministic ordering**
* **Interface-driven matching**
* **Graph-aware edge replacement**
* **Middleware-agnostic**
`splicer` does not assume HTTP semantics — it operates on generic interface edges.
---
# Future Evolution
The configuration format is versioned:
```yaml
version: 1
```
Breaking changes will increment the version number.