Writing a new driver
This guide is the practical checklist for implementing a drv.Drv and getting it to the point where the suite can trust it.
If you want the design rationale first, read Custom drivers. This page stays deliberately focused on the build-and-verify path.
Before you start
A good jetwarp driver is not “a thin wrapper that seems to work.” It is a translation layer that preserves the portable contract on top of a concrete router.
That means four things matter more than anything else:
- capability claims must be honest
- bad input must become ordinary errors, not panics
- canonical
{name}semantics must be preserved - the driver must stay usable after a failed registration
If that sounds strict, good — the whole point of jetwarp is that portability should be testable, not aspirational.
The minimum contract
Your driver must implement drv.Drv:
ServeHTTPKindCapsScopeHandleParamEngineIsNil
Add the compile-time guard immediately:
var _ drv.Drv = (*Driver)(nil)
That catches interface drift early and cheaply.
Capability rules
Only claim capabilities that the driver can pass with correct semantics.
A useful rule of thumb
If you are thinking:
“the underlying router sort of supports this, if you squint”
the correct answer is usually:
do not claim the capability yet
That is especially true for:
CapParamSuffixCapAnyMethodCapNativeScopeMW
Native middleware in v1.2
As of v1.2, native middleware support is real — but still capability-driven.
If your driver supports framework-native middleware through a corresponding mw/<driver> package, then:
- claim
CapNativeScopeMW - expose the matching optional driver hook
- keep the hook outside
drv.Drv - preserve the portable middleware ordering contract
Examples from the built-in drivers:
- gin:
UseGinHandler(...) - echo:
UseEchoMiddleware(...) - chi:
UseChiMiddleware(...) - fiber:
UseFiberMiddleware(...)
If the driver does not support native middleware, do not claim the capability. In that case the wrapper module should
surface ErrNativeMWUnsupported.
Portable ordering remains contractual:
Use → Group → With → per-route → handler
Native ordering is driver-specific and best-effort.
Pattern normalization and validation
If your driver lives in a submodule or outside this repo, import:
import "codeberg.org/iaconlabs/jetwarp/compat/routingpath"
Do not import internal/routingpath directly; Go’s internal/ visibility rules do not allow that outside the root
module.
compat/routingpath is the stable façade intended for drivers, adapters, and related tooling that need canonical path
normalization and validation.
Registration hardening
Every registration path must be panic-safe.
In practice that means:
- validate nil handlers up front
- normalize patterns before translation
- reject unsupported canonical forms loudly
- wrap underlying router registration in a
recover()boundary
A tiny safeRegister(...) helper is almost always worth it.
The suite will explicitly test that:
- an invalid pattern returns an error
- no panic escapes
- the driver still works for later good registrations
Scoping semantics
If you claim CapScope, Scope(prefix) must return a derived driver whose registrations are mounted under that prefix.
A subtle but important reminder:
drv.Scope()is router-native scopingadapter.Group()is logical scoping managed by core
Native middleware support in v1.2 does not change this split.
Conformance requirements
At minimum, drivers should pass:
suite.RunDriversuite.RunDriverSmoke
If you also ship an adapter wrapper, it should pass:
suite.RunAdapter
That is the real acceptance gate. If the suite disagrees with the driver, the suite wins.
Recommended file layout
Inside this repo, the standard structure is:
drivers/v1/myrouter/
go.mod
myrouter.go
driver_conformance_test.go
myrouter_smoke_test.go
myrouter_regression_test.go (optional, but recommended)
If you also ship an adapter wrapper:
adapter/myrouter/
go.mod
myrouter.go
adapter_conformance_test.go
The adapter wrapper should stay boring:
func New() core.Adapter { return core.New(driver.New()) }
If construction needs custom logic, that logic probably belongs in the driver, not the wrapper.
Final checklist
Before opening a PR or publishing a driver, verify all of the following:
- capability bits are honest
Kind()is stable and non-emptyIsNil()is safe on a nil receiverHandle(...)never panicsScope(...)never panicsParam(...)is defensive and uses canonical keysMethodAnyprecedence is correct if claimed- suffix/prefix param semantics are correct if claimed
- native middleware support is claimed only if it actually exists
suite.RunDriverpassessuite.RunDriverSmokepassessuite.RunAdapterpasses if you ship an adapter module