Writing a New Driver
This page covers what you need to implement, how to structure your module, and how to verify correctness with the conformance suite. It applies both to drivers added inside this repo and to drivers maintained externally.
For a deeper reference (with full code examples), see also docs/driver_authors.md in the repo root.
When to write a driver
- You want to support a router framework that jetwarp does not currently ship with.
- You have an in-house router and want jetwarp’s portability guarantees and OpenAPI tooling.
- You want to explore a new backend while keeping application code untouched.
The interface
type Drv interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
Kind() Kind
Caps() Capability
Scope(prefix string) (Drv, error)
Handle(method string, pattern string, h http.Handler) error
Param(r *http.Request, key string) string
Engine() any
IsNil() bool
}
Implement all nine methods. None of them may panic. See the Drivers overview for the full contract of each method.
Module layout (in-repo drivers)
drivers/v1/<n>/
go.mod ← module: codeberg.org/iaconlabs/jetwarp/drivers/v1/<n>
<n>.go ← Driver struct + drv.Drv implementation
driver_conformance_test.go ← RunDriver
<n>_smoke_test.go ← RunDriverSmoke
<n>_regression_test.go ← driver-specific edge cases (recommended)
adapter/<n>/
go.mod
<n>.go ← func New() adapter.Adapter { return core.New(driver.New()) }
adapter_conformance_test.go ← RunAdapter
External drivers can use any module path and layout; the test expectations remain the same.
Key implementation rules
1) No panics during registration
Drivers must catch panics from the underlying framework and convert them to returned errors. The driver must remain usable after a failed registration.
func (d *Driver) safeRegister(op string, fn func()) (err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("mydriver: panic during %s: %v", op, rec)
}
}()
fn()
return nil
}
This applies to Handle and Scope.
2) Capabilities are promises
Only claim a capability if the driver passes the full conformance suite for it. If uncertain, leave it out — the suite skips gated tests automatically.
CapParamSuffix requires CapParams. The suite validates this in newDriver() and fatals
immediately if violated.
3) Pattern translation
jetwarp patterns use {name} syntax. Translate to your framework’s syntax before registering:
// gin/echo example: {id} → :id
func translate(p string) string {
p = strings.ReplaceAll(p, "{", ":")
return strings.ReplaceAll(p, "}", "")
}
Param(r, key) must still work with the original jetwarp key ("id", not ":id"). The adapter
always calls Param(r, "id") regardless of internal syntax.
4) Import path for normalization helpers
Submodule drivers cannot import internal/routingpath directly. Use the public façade:
import "codeberg.org/iaconlabs/jetwarp/compat/routingpath"
5) Compile guard
var _ drv.Drv = (*Driver)(nil)
6) IsNil must be safe on nil receiver
func (d *Driver) IsNil() bool { return d == nil }
7) CapNativeScopeMW — do not implement in v1
This bit is reserved for v1.1. Do not implement native scope hooks based on it.
MethodAny ("*") implementation
Many routers do not support a true method wildcard with deterministic precedence. The standard approach: two-engine pattern.
- Primary engine: method-specific routes (
GET,POST, …) - Any engine: MethodAny routes (
"*")
ServeHTTP tries primary first, then falls back to any. This guarantees method-specific beats
MethodAny for the same path, regardless of registration order. The chi, gin, echo, and fiber
drivers all use this pattern.
Running the conformance suite
// driver_conformance_test.go
func TestDriverConformance(t *testing.T) {
suite.RunDriver(t, suite.DriverFactory{
Name: "myrouter",
New: func(t *testing.T) drv.Drv { t.Helper(); return myrouter.New() },
})
}
func TestDriverSmoke(t *testing.T) {
suite.RunDriverSmoke(t, suite.DriverFactory{
Name: "myrouter",
New: func(t *testing.T) drv.Drv { return myrouter.New() },
})
}
RunDriver covers routing, method handling, and the no-panic contract.
RunDriverSmoke covers a practical route tree with concurrency.
RunAdapter (for your adapter wrapper) additionally runs logical scoping contract tests,
which require adapter.RegistryProvider. Adapters built with core.New(d) satisfy this automatically.
Checklist before opening a PR
[ ] var _ drv.Drv = (*Driver)(nil) compile guard present
[ ] Kind() returns a stable, non-empty string
[ ] Caps() only claims capabilities that pass the suite
[ ] CapParamSuffix not claimed without CapParams
[ ] Handle() never panics; panics caught and returned as errors
[ ] Scope() never panics; returns error for empty or root-only prefix
[ ] Param() returns "" for nil request or empty key
[ ] Param() uses jetwarp key ("id") regardless of internal framework syntax
[ ] IsNil() safe on nil receiver
[ ] suite.RunDriver passes
[ ] suite.RunDriverSmoke passes
[ ] suite.RunAdapter passes (if adapter wrapper included)
[ ] CapNativeScopeMW NOT implemented (reserved v1.1)
[ ] Uses compat/routingpath (not internal/routingpath)
[ ] go.mod declares go 1.26.0
[ ] go.mod dependency on codeberg.org/iaconlabs/jetwarp is versioned (not a replace directive)