Adding a framework
A framework is a Go package under internal/gen/server/framework/<name>. It holds two things:
- an implementation of the
framework.Frameworkinterface - the router template,
templates/router.tmpl
Everything else is shared by all frameworks: the service contract, the adapter, the errors and the scaffolds. The adapter writes its handlers in the shape the framework asks for, and the router template registers them.
The interface is meant to stay as it is. If your framework needs something it does not offer, open an issue first, so we can find a way that works for the other frameworks too.
The interface
type Framework interface {
Name() string
Imports() []gomodel.Import
RoutePattern(method, path string) (string, error)
Conflicts(routes []Route) ([]Route, []Conflict)
Handler(s *gocode.Scope) Handler
PathParam(s *gocode.Scope, name string) string
Templates() fs.FS
}
| Method | Answers |
|---|---|
Name |
the value of server.framework, in kebab case: std-http |
Imports |
every package the framework's templates write, its own module first. The templates get the first as .Framework and each as .Packages.<name>. |
RoutePattern |
the route as the framework writes it, for a method and an OpenAPI path. It can fail, see below. |
Conflicts |
the routes the framework holds together, and the ones it cannot. See below. |
Handler |
the shape of the adapter's handlers, see Handler shape |
PathParam |
the expression that reads a path parameter in a handler |
Templates |
an fs.FS whose templates folder holds router.tmpl. A framework that serves in its own way adds scaffold-main.tmpl. |
What RoutePattern returns
RoutePattern fails in two cases:
| Error | When |
|---|---|
framework.ErrPattern, wrapped with the reason |
the framework rejects the path |
framework.ErrMethod |
the framework has no way to register the method |
For a router with one function per method, framework.CheckMethod gives the second answer.
What Conflicts returns
- the routes the framework holds together, in the order they are registered
- each route it cannot hold next to an earlier one, with the reason
A framework may rewrite the Pattern of a kept route and set its Names (see gin).
Route rules
RoutePattern and Conflicts hold the rules of the framework. Together they make two promises
about the generated router:
- it never panics
- it never answers a request with the wrong operation
A spec path the framework rejects, or a route it cannot hold next to an earlier one, is left out
with a route-dropped warning.
To find the rules, write down what the router panics on and what it replaces without a word. Then
test both sides: the patterns it takes, and, for a router that panics, that it panics on each route
you drop (see stdhttp).
The paths example holds path shapes found in real-world specs:
- a parameter with a literal after it
- two parameters in one segment
- a name with a colon, a literal colon, a dollar sign
- a trailing slash
- the TRACE method
- the same position named differently in two paths
- escaped values
Generate it for the new framework. Look at what panics, what is replaced without a word, and what the parameter values look like. Every rule in server came from such a run.
stdhttp
stdhttp is the one framework whose Conflicts holds no rules of its own. Its router is the
standard library, so it registers the routes on a ServeMux and drops each one that panics. The
generator does not import the other routers, so their rules are written out.
Helpers
The framework package holds what the rules share:
| Helper | Does |
|---|---|
Check |
fails on what no router takes: no leading slash, an unclosed brace, a * that is not last |
CheckClean |
fails on a path that is not clean, for a router that cleans paths |
CheckMethod |
fails on a method that a router with one function per method has no function for |
Colon |
writes the routes of a router that takes parameters as :name. Its fields Literal, Name, Wildcard, IsWildcardNamed and IsPrefixAllowed cover what differs. Echo, gin, hertz, fiber, beego and go-zero use it. |
Brace |
writes the routes of a router that takes {name} too. You pass it the functions that write the names and the wildcard. Gorilla-mux, kratos and fasthttp use it. |
Rename |
writes each {name} as the router names it, and fails on two names it makes one |
RestName |
names the wildcard so that no parameter has its name |
Escaping, EscapingColon |
Literal functions that escape the characters the router reads as a parameter |
Rejecting |
a Literal function that rejects those characters |
Same, Identifier, Unmarked |
Name functions: keep a name, make it an identifier, or write a colon and a star in it as underscores |
Params |
the parameters of a path |
Shape |
the path with its parameter names blanked |
ConflictsByShape, ConflictsByKey |
drop every route that repeats the shape, or the key, of an earlier one. They fit a router that replaces or shadows such a route without a word. |
StaticFirst |
orders routes for a router that takes the first match. At each position the order is: a literal, then a parameter next to a literal (the longer literal first), then a parameter alone, then the wildcard. |
HTTPHandler, ContextHandler |
the two handler shapes, see below |
Handler shape
type Handler struct {
Signature string // after the name: "(w http.ResponseWriter, r *http.Request)" or "(c echo.Context) error"
Writer string // the handler's response writer: "w" or "c.Response()"
Request string // the handler's request: "r" or "c.Request()"
Epilogue string // ends the handler: "" or "return nil"
ServeSignature string // the serve method's shape: "(w http.ResponseWriter, r *http.Request)"
Context string // the framework's context: "" or "c"
ContextServeSignature string // the same, with Context first: "(c echo.Context, w http.ResponseWriter, ...)"
}
The adapter template writes two methods per operation:
func (a *HTTPAdapter) GetPet(c echo.Context) error {
a.operations.Serve(c.Response(), c.Request(), "GetPet", func(w http.ResponseWriter, r *http.Request) {
a.serveGetPet(c, w, r)
})
return nil
}
func (a *HTTPAdapter) serveGetPet(c echo.Context, w http.ResponseWriter, r *http.Request) {
// decodes the request, calls the service, writes the response
}
Serve runs the serve method inside the operation middleware.
An operation without path parameters does not need the context, so its serve method takes
ServeSignature and the handler passes a.serveListPets as it is.
Names in a handler
The serve method always names the response writer w, the request r and the framework's
context c. So PathParam may use them.
PathParam has to give the value unescaped once, whatever the router hands over. For a router that
cuts values from the raw path, use runtime.UnescapePath.
Two shapes
- A framework whose handlers are
http.HandlerFuncs returnsframework.HTTPHandler(s). - A framework whose handlers take its context returns
framework.ContextHandler(s, ctxType), with the context's type spelled throughs.Import.
Handlers of the second kind read their errors from the error handler. They write every failed request themselves and return nil, so an operation answers the same under every framework.
When a handler takes the framework's context
A handler that takes the framework's context fits a framework when both hold:
- its context gives an
http.ResponseWriterand an*http.Request - its middleware can wrap that writer, as echo's
WrapMiddlewareand kratos's filters do
Every other framework
Every other framework takes http.HandlerFuncs. Its router template serves the handler from the
framework's context with a small function, handle. handle does two things:
- It puts the path parameters on the request with
r.SetPathValue, soPathParamreadsr.PathValue(name). - It calls the handler with the framework's writer and request. For a framework that is not built on net/http (fasthttp, fiber, hertz), it calls the handler with a request and a writer made from the framework's own.
This keeps the middleware contract whole: a middleware that wraps the writer, to log the status for example, sees every write.
Two more cases:
- A server that reuses the memory of a request for the next one on its connection, as fasthttp
does, gets copies (
httpserver.DetachRequest,httpserver.DetachPathValue). - A server that ends the process on a panic recovers in
wrap(httpserver.Recover).
The router template
router.tmpl renders the router part with a server.RouterView:
| Field | Holds |
|---|---|
.Framework |
the name the framework's first import is written under in the file |
.Packages |
the name each import of Imports is written under, looked up by the package's own name: .Packages.http for net/http, .Packages.bcontext for an import with that alias |
.Runtime |
the name of the runtime package, when the template writes it |
.Service, .Option, .Options, .NewOptions, .NewAdapter |
the shared names, as the file spells them |
.Routes |
one RouteView per operation, see below |
.User |
the config's user-context |
A RouteView has these fields:
| Field | Holds |
|---|---|
.Method |
the method as Get |
.HTTPMethod |
the method as GET |
.Pattern |
the route, quoted |
.Operation |
the operation, the name of its handler on the adapter |
.IsShortcut |
true for a method routers have a function of their own for (all but TRACE and the methods a spec adds) |
.Names |
the route's Names, quoted |
A package of Imports is imported into the file when the template, or the config's
server.router-extra override, writes it as .Packages.<name>. So a file imports what it uses and
nothing else.
What the template declares
WithRoutertakes a router of the framework and stores it inServerOptions.Router.NewRouterregisters every route on the router given, or on a new one, and returns it.
Middleware
Keep the contract of the other frameworks. WithMiddleware takes func(http.Handler) http.Handler
values, outermost first.
- On a new router, the middleware wraps everything, unknown paths too.
- On a given router, it wraps the generated routes and nothing else.
Most templates keep a wrap closure that folds the middleware over an http.Handler. They pass it
to the register closure as route. On a new router they also give the framework's not-found and
method-not-allowed handlers wrap(http.NotFoundHandler()) and the like, so that a middleware sees
those answers.
The register closure
The routes sit in the register closure. {{- override "server.router-extra" .}} comes right after
them. It writes the text a config gives that block, on lines of its own, so a config can add routes
with the same names.
Logic in the template
Templates hold no logic beyond range and if on the view's fields. Anything else is computed in
Go, or is plain Go in the template, such as handle.
If the template needs an import the view does not offer, add it to Imports.
The main scaffold
The shared scaffold-main.tmpl serves the router with an http.Server. Every router that is an
http.Handler fits it.
A framework whose server is served in its own way, such as fiber, fasthttp, hertz and goframe,
brings a scaffold-main.tmpl of its own in Templates. It replaces the shared one.
It renders with the server.ScaffoldMainView of the shared template. Its .Framework and
.Packages are those of the router view. Two more fields name an option:
| Field | Names |
|---|---|
.WithRouter |
the option that gives NewRouter a server the main built with its address and timeout |
.WithConfig |
the option fiber and hertz declare instead. It makes the new server from the main's settings, so that the middleware wraps unknown paths too. |
Keep the shape of the shared one: run serves until the process is told to stop, then lets the
requests in flight finish within .Timeout.
Wiring
- Add the framework to
server.Frameworks()ininternal/gen/server/server.go, and to theenumtag ofServer.Frameworkinpkg/config/blocks.go. Then runmake schema. internal/gen/server/server_test.gorenders these golden files. Write them withUPDATE=1.testdata/server.router.<name>.goldenfor every frameworkserver.adapter.<name>.goldenfor one whose handlers take its contextserver.scaffold.main.<name>.goldenfor one with a main of its own
- Add a variant to
serversintest/integration/integration_test.go, with the packages its code imports. ItsInitbuilds the router, which must not panic on any spec. - Add examples under
examples/server/<name>:- Add
basic,bodies,errors,params,paths,scaffoldsandsplit, with the specs and configs of an existing framework andframework: <name>. - Their tests run the shared cases of
examples/server/internal/servertest. basicadds the framework's own cases:WithRouterwith middleware, middleware on a new router, the adapter called alone, method not allowed.pathsrunsservertest.Pathsand lists what the framework answers to the rest, route by route.- A server that reuses memory between requests also runs
servertest.KeepAliveon a real listener. - A router that is not an
http.Handlergets a helper package underexamples/server/<name>/internalthat serves it as one, asfasthttptest,hertztestandgoframetestdo. - Add the module to
examples/go.mod, then runmake examples.
- Add
- Document the framework in server: its row in the router table and a section with
its pattern rules, what it drops and how it differs from the others. Give it a row in the
routes table of templates, with a
server.router-extraline that you have built and served. - Run
make check, thenmake test-integration FRAMEWORKS=<name>on the spec corpus. At least 95% of it should pass.