mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
zip's registration is a verb method on the group now, so an operation reads as the group answering it rather than a function taking the group, and a route that is not a typed operation says which method it answers with. The package-level form is gone, along with the Router interface it existed to accept any target through. Co-authored-by: Hanzo Dev <[email protected]>
187 lines
6.1 KiB
Go
187 lines
6.1 KiB
Go
package router
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// BuildApp constructs a zip [zip.App] from the current router configurations,
|
|
// with one zip route per registered route, so a composer mounts this router by
|
|
// calling Use on it and every route this router serves is an address in the
|
|
// composed router rather than one wildcard.
|
|
//
|
|
// app.Use(baseApp)
|
|
//
|
|
// It reads the same configuration [Router.BuildMux] reads, through the same
|
|
// walk, and answers with the same handler: the event factory, the route's hook
|
|
// chain, and the error handler.
|
|
//
|
|
// WHICH route answers is still decided by [http.ServeMux], one pattern at a
|
|
// time — see [mount]. zip's router matches a superset: it folds case, it reads
|
|
// "/x" and "/x/" as one address, and it performs none of ServeMux's path
|
|
// cleaning. So a request zip carries to a route is offered to that route's own
|
|
// pattern first, and a pattern that does not name it hands the request on to
|
|
// the next route zip matched, which is the walk ServeMux performs internally.
|
|
func (r *Router[T]) BuildApp() (*zip.App, error) {
|
|
r.bindCatchAll()
|
|
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
|
|
// Routes gathered by the address zip spells them at, in registration order.
|
|
// A route that names no method matches every method, so it cannot be
|
|
// registered beside a route that names one: two claims on GET /* is a
|
|
// composition zip refuses. ServeMux settles that pair by specificity, so
|
|
// such an address takes every method and its handler offers the request to
|
|
// the named method first.
|
|
var addresses []string
|
|
routes := map[string][]leaf{}
|
|
|
|
err := r.walk(r.RouterGroup, nil, func(method, path string, serve http.HandlerFunc) error {
|
|
address, err := spell(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, seen := routes[address]; !seen {
|
|
addresses = append(addresses, address)
|
|
}
|
|
routes[address] = append(routes[address], mount(method, path, serve))
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, address := range addresses {
|
|
at := routes[address]
|
|
|
|
// A named method is more specific than none, which is the order the
|
|
// offer below has to follow. Stable, so two named methods keep the
|
|
// order they were registered in.
|
|
sort.SliceStable(at, func(i, j int) bool { return at[i].method != "" && at[j].method == "" })
|
|
|
|
if at[len(at)-1].method == "" {
|
|
app.Raw(zip.MethodAll, address, offer(at))
|
|
continue
|
|
}
|
|
for _, l := range at {
|
|
if !methods[l.method] {
|
|
return nil, fmt.Errorf("cannot register %s %s: %s is not a method this router serves", l.method, address, l.method)
|
|
}
|
|
app.Raw(l.method, address, offer([]leaf{l}))
|
|
}
|
|
}
|
|
|
|
return app, nil
|
|
}
|
|
|
|
// leaf is one registered route as zip serves it: whether a request belongs to
|
|
// the route, and the handler that answers when it does.
|
|
type leaf struct {
|
|
method string
|
|
claims func(*zip.Ctx) bool
|
|
serve zip.Handler
|
|
}
|
|
|
|
// mount turns one registered route into that pair, and both halves are one
|
|
// [http.ServeMux] holding exactly this route's pattern.
|
|
//
|
|
// So nothing here restates ServeMux's rules. The matcher [Router.BuildMux]
|
|
// installs decides the method, the case, the trailing slash, the path values a
|
|
// handler reads back with [http.Request.PathValue], and the redirect a path
|
|
// that needs cleaning earns — one pattern at a time, from the same request the
|
|
// handler will be given. A pattern that names the request answers it; a pattern
|
|
// that names nothing leaves the request for the next route.
|
|
func mount(method, path string, serve http.HandlerFunc) leaf {
|
|
pattern := path
|
|
if method != "" {
|
|
pattern = method + " " + pattern
|
|
}
|
|
|
|
one := http.NewServeMux()
|
|
one.Handle(pattern, serve)
|
|
|
|
return leaf{
|
|
method: method,
|
|
claims: func(c *zip.Ctx) bool {
|
|
u, err := url.ParseRequestURI(string(c.Fiber().RequestCtx().RequestURI()))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// The same three fields ServeMux reads, off the same raw request
|
|
// URI the adapter parses the handler's own request from.
|
|
_, named := one.Handler(&http.Request{Method: c.Method(), Host: c.Host(), URL: u})
|
|
return named != ""
|
|
},
|
|
serve: zip.AdaptNetHTTP(one),
|
|
}
|
|
}
|
|
|
|
// offer hands a request to the first route at an address whose pattern names
|
|
// it, and to the next route zip matched when none of them does.
|
|
func offer(at []leaf) zip.Handler {
|
|
return func(c *zip.Ctx) error {
|
|
for _, l := range at {
|
|
if l.claims(c) {
|
|
return l.serve(c)
|
|
}
|
|
}
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// methods are the ones a route may name. A method outside this set is refused
|
|
// rather than widened into All, which would claim every other method at that
|
|
// address as well.
|
|
var methods = map[string]bool{
|
|
http.MethodGet: true,
|
|
http.MethodHead: true,
|
|
http.MethodPost: true,
|
|
http.MethodPut: true,
|
|
http.MethodPatch: true,
|
|
http.MethodDelete: true,
|
|
http.MethodOptions: true,
|
|
}
|
|
|
|
// spell writes a ServeMux path the way zip's router writes it: "{name}" is one
|
|
// segment, "{name...}" is the rest of the path, and a path ending in "/" is the
|
|
// subtree it stands for in a ServeMux pattern.
|
|
//
|
|
// The address zip matches on is therefore wider than the pattern in every case
|
|
// where the two spellings differ, which is what makes the offer above sound:
|
|
// zip carries a request to the route, and the pattern decides.
|
|
func spell(path string) (string, error) {
|
|
if path == "" || path[0] != '/' {
|
|
return "", fmt.Errorf("cannot register %q: zip addresses start at /, so a pattern naming a host has no address", path)
|
|
}
|
|
|
|
segments := strings.Split(path, "/")
|
|
for i, s := range segments {
|
|
if len(s) < 3 || s[0] != '{' || s[len(s)-1] != '}' {
|
|
continue
|
|
}
|
|
name := s[1 : len(s)-1]
|
|
switch {
|
|
case name == "$":
|
|
// "{$}" ends a ServeMux pattern at that exact path, and zip reads
|
|
// "/x" and "/x/" as one address, so no address expresses it.
|
|
return "", fmt.Errorf("cannot register %q: zip has no address that ends where {$} ends", path)
|
|
case strings.HasSuffix(name, "..."):
|
|
segments[i] = "*"
|
|
default:
|
|
segments[i] = ":" + name
|
|
}
|
|
}
|
|
|
|
address := strings.Join(segments, "/")
|
|
if strings.HasSuffix(address, "/") {
|
|
address += "*"
|
|
}
|
|
|
|
return address, nil
|
|
}
|