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 supportsScope(prefix)with correct prefix semantics. -
CapParams
The driver supports canonical{name}parameters in patterns, and can return values viaParam(r, "name"). -
CapParamSuffix
The driver supports in-segment literals around a single param, such as/files/{id}.jsonor/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")thensc.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:
- The behavior is not universal across frameworks, and faking it would be surprising.
- The behavior has clear semantic rules (what must match, what must not, precedence rules, error cases).
- 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:
CapScopeonly (or even none) - v1: add
CapParamsonce param extraction is correct - v2: add
CapAnyMethodonce precedence is proven - v3: add
CapParamSuffixonly if you can preserve canonicalidnaming (noid.jsonsurprises)
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
- Custom drivers — checklist for implementing a new driver
- Adapters overview — what application code should use