Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. capability claims must be honest
  2. bad input must become ordinary errors, not panics
  3. canonical {name} semantics must be preserved
  4. 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:

  • ServeHTTP
  • Kind
  • Caps
  • Scope
  • Handle
  • Param
  • Engine
  • IsNil

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:

  • CapParamSuffix
  • CapAnyMethod
  • CapNativeScopeMW

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 scoping
  • adapter.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.RunDriver
  • suite.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.


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-empty
  • IsNil() is safe on a nil receiver
  • Handle(...) never panics
  • Scope(...) never panics
  • Param(...) is defensive and uses canonical keys
  • MethodAny precedence is correct if claimed
  • suffix/prefix param semantics are correct if claimed
  • native middleware support is claimed only if it actually exists
  • suite.RunDriver passes
  • suite.RunDriverSmoke passes
  • suite.RunAdapter passes if you ship an adapter module

See also