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 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)