# `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** | Name-only hooks (`before-call`, `after-call`, `should-block-call`): middleware sees function names but not types or data | [`wit/tier1/world.wit`](wit/tier1/world.wit) | **Supported** |
| **Tier 2** | Read-only reflection: middleware can inspect the types and serialized data being passed to/from the target, but cannot modify it | `wit/tier2/world.wit` (planned) | Planned |
| **Tier 3** | Read-write interception: middleware can inspect AND modify the data flowing through | `wit/tier3/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).
---
# Installation
From source:
```bash
cargo build --release
```
Binary will be located at:
```
target/release/splicer
```
---
# Usage
```bash
splicer <JSON_GRAPH> <SPLICE_CFG> [--output <FILE>]
```
### Arguments
| `JSON_GRAPH` | Path to the composition graph in JSON format |
| `SPLICE_CFG` | Path to the splice configuration YAML file |
| `--output` | Optional output file (defaults to stdout) |
---
# 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.