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

Drivers (overview)

In jetwarp, drivers are the pluggable backends that actually talk to a concrete router framework (stdlib, chi, gin, echo, fiber, …).

Most application code never touches drivers directly. You choose an adapter (e.g. adapter/chi) and work with the portable adapter.Adapter API. Under the hood, the adapter delegates to a driver that implements the contract in codeberg.org/iaconlabs/jetwarp/drv.

Drivers matter when you:

  • want to understand what is portable vs framework-specific
  • add support for a new router framework
  • debug a capability-dependent behavior (params, scopes, MethodAny, …)

The driver contract: drv.Drv

A driver is a net/http handler plus a small registration API:

  • it serves requests (ServeHTTP)
  • it registers routes (Handle)
  • it can optionally create scopes (Scope)
  • it can read path params (Param)
  • it advertises capabilities (Caps) and a stable kind (Kind)
  • it exposes an escape hatch to the underlying engine (Engine)
  • it must support safe typed-nil detection (IsNil)

The interface lives in drv/driver.go and is intentionally small. The adapter layer does the higher-level work: pattern normalization, middleware composition and ordering, registry recording, error accumulation, and so on.

Why drivers serve net/http even for non-net/http routers?

Jetwarp is “net/http-first”: application handlers are always http.Handler / http.HandlerFunc.

Some frameworks are not net/http-native (notably Fiber, which is based on fasthttp). In that case the driver performs a bridging step inside ServeHTTP: convert the incoming request into a standard *http.Request, call your handler, and write the response back to the underlying engine.

This design keeps application code and middleware portable and testable, and keeps the adapter layer framework-agnostic.


Capability bits: how portability is enforced

Capabilities are promises, not wishes

A driver’s Caps() method returns a bitmask of features the driver is claiming it can support.

The wording is important: a capability means core can rely on this behavior — not “the underlying router kinda does something similar”.

If a feature is not claimed, core code must not assume it works, and tests that require it should be skipped.

Why a bitmask?

A bitmask is:

  • cheap to check at runtime (Has / Any)
  • composable (drivers can advertise multiple capabilities)
  • stable for diagnostics (CapScope|CapParams|...)
  • friendly to “progressive enhancement” (a driver can start small and add caps over time)

The current capability set

Jetwarp currently defines these capability flags:

  • CapScope
    The driver supports Scope(prefix) with correct prefix semantics.

  • CapParams
    The driver supports canonical {name} parameters in patterns, and can return values via Param(r, "name").

  • CapParamSuffix
    The driver supports in-segment literals around a single param, such as /files/{id}.json or /pre-{id}. Drivers may support this natively or via careful driver-side emulation.

  • CapAnyMethod
    The driver supports method-agnostic registration (Handle("*", pattern, h)), and it must truly match regardless of HTTP method.

  • CapNativeScopeMW (reserved — v1.1+)
    Not functional in v1. This bit is reserved for a future mechanism that will let drivers accept native middleware per scope. No adapter uses this capability in v1, and drivers must not implement native scope hooks based on it. The interface contract and ordering semantics will be defined in v1.1.

How to read a capability check

The drv.Capability type provides helpers:

if d.Caps().Has(drv.CapParams) {
    // safe to use {name} patterns and Param(...)
}

if d.Caps().Has(drv.CapScope | drv.CapAnyMethod) {
    // safe to rely on both scope semantics and MethodAny behavior
}

if d.Caps().Any(drv.CapParams | drv.CapAnyMethod) {
    // at least one of these features is present
}

Capabilities and the conformance suite

Capabilities are enforced by the shared conformance suite (tests/suite):

  • tests that require a capability are skipped if the driver does not claim it
  • claiming a capability is a contract: the driver must pass all tests for that capability

This lets drivers be honest and incremental:

  • a minimal driver can claim only CapParams (or even none)
  • a mature driver can add CapScope, CapAnyMethod, etc., once it can guarantee the semantics

Design decisions behind specific capabilities

This section explains “why it’s done this way” — these constraints exist because different frameworks have different routing models, and jetwarp chooses correctness and portability over surprising behavior.

CapScope: why scoping must be explicit

Jetwarp’s adapter builds Group("/prefix") and nested groups on top of driver scoping.

Some frameworks have real “sub-routers”; others don’t. If a driver can correctly model a scope under a prefix, it can claim CapScope. If it can’t, it should not pretend it can.

When CapScope is present, core can rely on this mental model:

sc, _ := d.Scope("/v1") then sc.Handle("GET", "/x", ...) must be reachable at /v1/x.

CapParams: why {name} is canonical

Different routers use different param syntax (:id, {id}, *, regex segments, …). Jetwarp picks one canonical format ({name}) and requires drivers to translate.

This keeps application code consistent and makes tooling possible (e.g. OpenAPI generation from the registry snapshot).

The driver must also be able to read the param value back via Param(r, key).

CapParamSuffix: why it’s optional (and often rejected)

In-segment patterns like /files/{id}.json look simple, but many routers interpret them differently.

For example, in “colon param” routers, :id.json is often parsed as a parameter named id.json, not a parameter named id with a .json suffix. That breaks canonical semantics (Param("id")).

Jetwarp’s stance is conservative:

  • if a driver can guarantee correct semantics, it may claim CapParamSuffix
  • if it cannot, it should reject these patterns during registration (recorded in Err()), rather than silently do the wrong thing

This is why the Fiber and Gin adapters call it out as a gotcha.

CapAnyMethod: why "*" must behave like a real wildcard

Some frameworks support an explicit “ANY” route; others don’t. Some support it but resolve conflicts differently.

Jetwarp requires a clear rule:

Explicit method routes must win over MethodAny for the same path, regardless of registration order.

Drivers often implement this with a two-router approach (a primary router for explicit methods and a secondary router for "*"), and a deterministic dispatch rule (try primary first, then fallback to any).

If a driver cannot guarantee this, it must not claim CapAnyMethod.

CapNativeScopeMW: reserved — do not implement in v1

This capability bit is defined but intentionally unused in v1. It marks a slot for a future mechanism that will allow drivers to accept native middleware per scope without changing the ordering contract.

No adapter mechanism reads or acts on this bit in v1. Drivers must not implement native scope hooks based on it. The interface contract and ordering semantics will be specified in v1.1.

Why capabilities exist (and how to use them)

Jetwarp runs on many router frameworks, and they do not share the same routing model. Some have true sub-routers, some don’t. Some can express {id}.json safely, others would silently treat it as a param named id.json. Some have a real “ANY” route with deterministic precedence, others don’t.

Capabilities are jetwarp’s way to make portability explicit and testable:

  • A capability is a promise: “core and tooling may rely on this behavior”.
  • If a capability is not advertised, jetwarp must not assume it works.
  • The conformance suite is capability-driven: claiming a capability means you must pass the tests for it.

How to think about adding a capability

Create or claim a capability when:

  1. The behavior is not universal across frameworks, and faking it would be surprising.
  2. The behavior has clear semantic rules (what must match, what must not, precedence rules, error cases).
  3. You can enforce it with repeatable conformance tests, not “it seems to work”.

Capabilities should reduce ambiguity in both code and docs.

Use cases for capabilities

1) Progressive driver development
Start a new driver with a small set of features and expand over time:

  • v0: CapScope only (or even none)
  • v1: add CapParams once param extraction is correct
  • v2: add CapAnyMethod once precedence is proven
  • v3: add CapParamSuffix only if you can preserve canonical id naming (no id.json surprises)

2) Safe feature gating in core/tooling
Core code and downstream tooling (like OpenAPI generation) can do:

if d.Caps().Has(drv.CapParamSuffix) {
    // allow /files/{id}.json patterns
} else {
    // reject or downgrade to portable alternatives
}

This prevents “works on my framework” bugs.

**3) Honest documentation

Docs can say: “This adapter supports in-segment suffix params” only when the driver advertises CapParamSuffix.

**4) Conformance-driven guarantees

When you add a new portability rule, you typically:

  • add/extend a capability (if the feature isn’t universal), and/or

  • add a suite contract test that every driver claiming that capability must pass

This is how jetwarp turns design intent into enforced behavior.

How a driver should claim capabilities

A driver should only claim a capability when it can prove it:

  • You’ve implemented the semantics in the driver.

  • You’ve added/updated conformance tests to cover the feature.

  • All drivers claiming the capability pass the suite in CI.

If any of those are missing, the correct move is to not claim the capability yet (and reject the feature loudly via registration errors rather than silently misbehaving).


Kind vs capabilities

Drivers also report a Kind() (a stable string identifier like chi, gin, …).

  • Capabilities exist for behavior decisions (“can I rely on X?”).
  • Kind exists for diagnostics and compatibility checks (especially for framework-native middleware wrappers).

As a rule: prefer Caps() in code; use Kind() for humans and debugging.


Error model: no panics, and typed-nil safety

No panics during registration

A driver must never let framework panics escape during registration (Handle, Scope). If the underlying framework panics (duplicate route registration, invalid patterns, internal assertions, …), the driver should recover and return an error.

Jetwarp’s higher-level contract is “no panics on bad registrations”: errors accumulate and are surfaced via Err() on the adapter.

Typed-nil (IsNil) requirement

Go interfaces can hold a typed-nil pointer value while the interface itself is non-nil. If core stores that interface and later calls methods on it, it can panic.

To prevent this, the driver contract includes:

  • IsNil() bool — and it must be safe to call even on a nil receiver

The adapter constructor uses d == nil || d.IsNil() to guarantee it never stores a typed-nil driver.


The Engine() escape hatch

Drivers may expose the underlying router engine via Engine() any.

This is intentionally an “escape hatch”:

  • it is useful for debugging and narrow integrations
  • it is not part of the portability contract
  • registering routes directly on the engine will usually bypass jetwarp’s registry and tooling (including OpenAPI)

If you need this often, consider whether that service should be treated as framework-specific.


Where drivers live in the repo

Built-in drivers are in drivers/v1/<name> (for example drivers/v1/chi, drivers/v1/stdlib, …).

The adapter packages are thin shims that return:

  • adapter.New(driver.New())

So drivers are the “real” integration work; adapters are packaging and dependency boundaries.


Next steps