From f58f56701d047398cc8d7a6433719de1b6070242 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sun, 17 Aug 2025 17:47:38 +0800 Subject: [PATCH] Refactor handlers structure and add BaseData --- forged/internal/incoming/web/handler.go | 20 +++++++++++++------- forged/internal/incoming/web/handlers/index.go | 15 +++++++++++++++ forged/internal/incoming/web/handlers/repo/index.go | 20 ++++++++++++++++++++ forged/internal/incoming/web/router.go | 131 ++++++++++++++++++++++++++++++----------------------- forged/internal/incoming/web/stub.go | 38 ++++++++++++++++---------------------- forged/internal/incoming/web/types/types.go | 39 +++++++++++++++++++++++++++++++++++++++ diff --git a/forged/internal/incoming/web/handler.go b/forged/internal/incoming/web/handler.go index 9018547e81b69ab6489b18b2a1c46a9491c1c1f1..6341a9382507cc3c4af6cc943e508a2ac3c2b2a4 100644 --- a/forged/internal/incoming/web/handler.go +++ b/forged/internal/incoming/web/handler.go @@ -1,9 +1,11 @@ -// internal/incoming/web/handler.go package web import ( "net/http" "path/filepath" + + handlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers" + repoHandlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers/repo" ) type handler struct { @@ -21,24 +23,28 @@ http.StripPrefix("/-/static/", staticFS), WithDirIfEmpty("rest"), ) + // Feature handler instances + indexHTTP := handlers.NewIndexHTTP() + repoHTTP := repoHandlers.NewHTTP() + // Index - h.r.GET("/", h.index) + h.r.GET("/", indexHTTP.Index) // Top-level utilities h.r.ANY("-/login", h.notImplemented) h.r.ANY("-/users", h.notImplemented) - // Group index + // Group index (kept local for now; migrate later) h.r.GET("@group/", h.groupIndex) - // Repo index - h.r.GET("@group/-/repos/:repo/", h.repoIndex) + // Repo index (handled by repoHTTP) + h.r.GET("@group/-/repos/:repo/", repoHTTP.Index) - // Repo + // Repo (kept local for now) h.r.ANY("@group/-/repos/:repo/info", h.notImplemented) h.r.ANY("@group/-/repos/:repo/git-upload-pack", h.notImplemented) - // Repo features + // Repo features (kept local for now) h.r.GET("@group/-/repos/:repo/branches/", h.notImplemented) h.r.GET("@group/-/repos/:repo/log/", h.notImplemented) h.r.GET("@group/-/repos/:repo/commit/:commit", h.notImplemented) diff --git a/forged/internal/incoming/web/handlers/index.go b/forged/internal/incoming/web/handlers/index.go new file mode 100644 index 0000000000000000000000000000000000000000..773a0c64eac0c4c47812b7b79278330fe9271d45 --- /dev/null +++ b/forged/internal/incoming/web/handlers/index.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "net/http" + + wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" +) + +type IndexHTTP struct{} + +func NewIndexHTTP() *IndexHTTP { return &IndexHTTP{} } + +func (h *IndexHTTP) Index(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) { + _, _ = w.Write([]byte("index: replace with template render")) +} diff --git a/forged/internal/incoming/web/handlers/repo/index.go b/forged/internal/incoming/web/handlers/repo/index.go new file mode 100644 index 0000000000000000000000000000000000000000..3a6d7ea6840a72d26e2095858cc94c1a866ba854 --- /dev/null +++ b/forged/internal/incoming/web/handlers/repo/index.go @@ -0,0 +1,20 @@ +package repo + +import ( + "fmt" + "net/http" + "strings" + + wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" +) + +type HTTP struct{} + +func NewHTTP() *HTTP { return &HTTP{} } + +func (h *HTTP) Index(w http.ResponseWriter, r *http.Request, v wtypes.Vars) { + base := wtypes.Base(r) + repo := v["repo"] + _, _ = w.Write([]byte(fmt.Sprintf("repo index: group=%q repo=%q", + "/"+strings.Join(base.GroupPath, "/")+"/", repo))) +} diff --git a/forged/internal/incoming/web/router.go b/forged/internal/incoming/web/router.go index 59b04d5f9a0fb7a8f7a9d70d021db953afc4ec2b..46eb93561e6307532a1dd91aa703b9fa0173cfae 100644 --- a/forged/internal/incoming/web/router.go +++ b/forged/internal/incoming/web/router.go @@ -4,22 +4,18 @@ import ( "net/http" "net/url" "sort" - "strconv" "strings" -) -type ( - Params map[string]any - HandlerFunc func(http.ResponseWriter, *http.Request, Params) + wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" ) type UserResolver func(*http.Request) (id int, username string, err error) type ErrorRenderers struct { - BadRequest func(http.ResponseWriter, Params, string) - BadRequestColon func(http.ResponseWriter, Params) - NotFound func(http.ResponseWriter, Params) - ServerError func(http.ResponseWriter, Params, string) + BadRequest func(http.ResponseWriter, *wtypes.BaseData, string) + BadRequestColon func(http.ResponseWriter, *wtypes.BaseData) + NotFound func(http.ResponseWriter, *wtypes.BaseData) + ServerError func(http.ResponseWriter, *wtypes.BaseData, string) } type dirPolicy int @@ -52,7 +48,7 @@ rawPattern string wantDir dirPolicy ifEmptyKey string segs []patSeg - h HandlerFunc + h wtypes.HandlerFunc hh http.Handler priority int } @@ -80,15 +76,15 @@ func WithDirIfEmpty(param string) RouteOption { return func(rt *route) { rt.wantDir = dirRequireIfEmpty; rt.ifEmptyKey = param } } -func (r *Router) GET(pattern string, f HandlerFunc, opts ...RouteOption) { +func (r *Router) GET(pattern string, f wtypes.HandlerFunc, opts ...RouteOption) { r.handle("GET", pattern, f, nil, opts...) } -func (r *Router) POST(pattern string, f HandlerFunc, opts ...RouteOption) { +func (r *Router) POST(pattern string, f wtypes.HandlerFunc, opts ...RouteOption) { r.handle("POST", pattern, f, nil, opts...) } -func (r *Router) ANY(pattern string, f HandlerFunc, opts ...RouteOption) { +func (r *Router) ANY(pattern string, f wtypes.HandlerFunc, opts ...RouteOption) { r.handle("", pattern, f, nil, opts...) } @@ -96,7 +92,7 @@ func (r *Router) ANYHTTP(pattern string, hh http.Handler, opts ...RouteOption) { r.handle("", pattern, nil, hh, opts...) } -func (r *Router) handle(method, pattern string, f HandlerFunc, hh http.Handler, opts ...RouteOption) { +func (r *Router) handle(method, pattern string, f wtypes.HandlerFunc, hh http.Handler, opts ...RouteOption) { want := dirIgnore if strings.HasSuffix(pattern, "/") { want = dirRequire @@ -127,50 +123,43 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { segments, dirMode, err := splitAndUnescapePath(req.URL.EscapedPath()) if err != nil { - r.err400(w, Params{"global": r.global}, "Error parsing request URI: "+err.Error()) + r.err400(w, &wtypes.BaseData{Global: r.global}, "Error parsing request URI: "+err.Error()) return } for _, s := range segments { if strings.Contains(s, ":") { - r.err400Colon(w, Params{"global": r.global}) + r.err400Colon(w, &wtypes.BaseData{Global: r.global}) return } } - p := Params{ - "url_segments": segments, - "dir_mode": dirMode, - "global": r.global, + // Prepare base data; vars are attached per-route below. + bd := &wtypes.BaseData{ + Global: r.global, + URLSegments: segments, + DirMode: dirMode, } if r.user != nil { uid, uname, uerr := r.user(req) if uerr != nil { - r.err500(w, p, "Error getting user info from request: "+uerr.Error()) - // TODO: Revamp error handling again... + r.err500(w, bd, "Error getting user info from request: "+uerr.Error()) return } - p["user_id"] = uid - p["username"] = uname - if uid == 0 { - p["user_id_string"] = "" - } else { - p["user_id_string"] = strconv.Itoa(uid) - } + bd.UserID = uid + bd.Username = uname } method := req.Method + var pathMatched bool // for 405 detection for _, rt := range r.routes { - if rt.method != "" && - !(rt.method == method || (method == http.MethodHead && rt.method == http.MethodGet)) { - continue - } - // TODO: Consider returning 405 on POST/GET mismatches and the like. ok, vars, sepIdx := match(rt.segs, segments) if !ok { continue } + pathMatched = true + switch rt.wantDir { case dirRequire: if !dirMode && redirectAddSlash(w, req) { @@ -181,33 +170,46 @@ if dirMode && redirectDropSlash(w, req) { return } case dirRequireIfEmpty: - if v, _ := vars[rt.ifEmptyKey]; v == "" && !dirMode && redirectAddSlash(w, req) { + if v := vars[rt.ifEmptyKey]; v == "" && !dirMode && redirectAddSlash(w, req) { return } } - for k, v := range vars { - p[k] = v + + // Derive group path and separator index on the matched request. + bd.SeparatorIndex = sepIdx + if g := vars["group"]; g == "" { + bd.GroupPath = []string{} + } else { + bd.GroupPath = strings.Split(g, "/") } - // convert "group" (joined) into []string group_path - if g, ok := p["group"].(string); ok { - if g == "" { - p["group_path"] = []string{} - } else { - p["group_path"] = strings.Split(g, "/") - } + + // Attach BaseData to request context. + req = req.WithContext(wtypes.WithBaseData(req.Context(), bd)) + + // Enforce method now. + if rt.method != "" && + !(rt.method == method || (method == http.MethodHead && rt.method == http.MethodGet)) { + // 405 for a path that matched but wrong method + w.Header().Set("Allow", allowForPattern(r.routes, rt.rawPattern)) + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return } - p["separator_index"] = sepIdx if rt.h != nil { - rt.h(w, req, p) + rt.h(w, req, wtypes.Vars(vars)) } else if rt.hh != nil { rt.hh.ServeHTTP(w, req) } else { - r.err500(w, p, "route has no handler") + r.err500(w, bd, "route has no handler") } return } - r.err404(w, p) + if pathMatched { + // Safety; normally handled above, but keep semantics. + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + r.err404(w, bd) } func compilePattern(pat string) ([]patSeg, int) { @@ -329,33 +331,50 @@ http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) return true } -func (r *Router) err400(w http.ResponseWriter, p Params, msg string) { +func allowForPattern(routes []route, raw string) string { + seen := map[string]struct{}{} + out := make([]string, 0, 4) + for _, rt := range routes { + if rt.rawPattern != raw || rt.method == "" { + continue + } + if _, ok := seen[rt.method]; ok { + continue + } + seen[rt.method] = struct{}{} + out = append(out, rt.method) + } + sort.Strings(out) + return strings.Join(out, ", ") +} + +func (r *Router) err400(w http.ResponseWriter, b *wtypes.BaseData, msg string) { if r.errors.BadRequest != nil { - r.errors.BadRequest(w, p, msg) + r.errors.BadRequest(w, b, msg) return } http.Error(w, msg, http.StatusBadRequest) } -func (r *Router) err400Colon(w http.ResponseWriter, p Params) { +func (r *Router) err400Colon(w http.ResponseWriter, b *wtypes.BaseData) { if r.errors.BadRequestColon != nil { - r.errors.BadRequestColon(w, p) + r.errors.BadRequestColon(w, b) return } http.Error(w, "bad request", http.StatusBadRequest) } -func (r *Router) err404(w http.ResponseWriter, p Params) { +func (r *Router) err404(w http.ResponseWriter, b *wtypes.BaseData) { if r.errors.NotFound != nil { - r.errors.NotFound(w, p) + r.errors.NotFound(w, b) return } http.NotFound(w, nil) } -func (r *Router) err500(w http.ResponseWriter, p Params, msg string) { +func (r *Router) err500(w http.ResponseWriter, b *wtypes.BaseData, msg string) { if r.errors.ServerError != nil { - r.errors.ServerError(w, p, msg) + r.errors.ServerError(w, b, msg) return } http.Error(w, msg, http.StatusInternalServerError) diff --git a/forged/internal/incoming/web/stub.go b/forged/internal/incoming/web/stub.go index 72077563cf3378d3f265d4af01e6e81a894ee781..4fffd7395432a140e19d648edd7d22f8318601aa 100644 --- a/forged/internal/incoming/web/stub.go +++ b/forged/internal/incoming/web/stub.go @@ -4,41 +4,35 @@ import ( "fmt" "net/http" "strings" + + wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" ) -func (h *handler) index(w http.ResponseWriter, r *http.Request, p Params) { - _, _ = w.Write([]byte("index: replace with template render")) +func (h *handler) groupIndex(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) { + base := wtypes.Base(r) + _, _ = w.Write([]byte("group index for: /" + strings.Join(base.GroupPath, "/") + "/")) } -func (h *handler) groupIndex(w http.ResponseWriter, r *http.Request, p Params) { - g := p["group_path"].([]string) // captured by @group - _, _ = w.Write([]byte("group index for: /" + strings.Join(g, "/") + "/")) -} - -func (h *handler) repoIndex(w http.ResponseWriter, r *http.Request, p Params) { - repo := p["repo"].(string) - g := p["group_path"].([]string) - _, _ = w.Write([]byte(fmt.Sprintf("repo index: group=%q repo=%q", "/"+strings.Join(g, "/")+"/", repo))) -} - -func (h *handler) repoTree(w http.ResponseWriter, r *http.Request, p Params) { - repo := p["repo"].(string) - rest := p["rest"].(string) // may be "" - if p["dir_mode"].(bool) && rest != "" && !strings.HasSuffix(rest, "/") { +func (h *handler) repoTree(w http.ResponseWriter, r *http.Request, v wtypes.Vars) { + base := wtypes.Base(r) + repo := v["repo"] + rest := v["rest"] // may be "" + if base.DirMode && rest != "" && !strings.HasSuffix(rest, "/") { rest += "/" } _, _ = w.Write([]byte(fmt.Sprintf("tree: repo=%q path=%q", repo, rest))) } -func (h *handler) repoRaw(w http.ResponseWriter, r *http.Request, p Params) { - repo := p["repo"].(string) - rest := p["rest"].(string) - if p["dir_mode"].(bool) && rest != "" && !strings.HasSuffix(rest, "/") { +func (h *handler) repoRaw(w http.ResponseWriter, r *http.Request, v wtypes.Vars) { + base := wtypes.Base(r) + repo := v["repo"] + rest := v["rest"] + if base.DirMode && rest != "" && !strings.HasSuffix(rest, "/") { rest += "/" } _, _ = w.Write([]byte(fmt.Sprintf("raw: repo=%q path=%q", repo, rest))) } -func (h *handler) notImplemented(w http.ResponseWriter, _ *http.Request, _ Params) { +func (h *handler) notImplemented(w http.ResponseWriter, _ *http.Request, _ wtypes.Vars) { http.Error(w, "not implemented", http.StatusNotImplemented) } diff --git a/forged/internal/incoming/web/types/types.go b/forged/internal/incoming/web/types/types.go new file mode 100644 index 0000000000000000000000000000000000000000..d47b13ab5965146d6c100e7f85f2ab50c6d6eff3 --- /dev/null +++ b/forged/internal/incoming/web/types/types.go @@ -0,0 +1,39 @@ +package types + +import ( + "context" + "net/http" +) + +// BaseData is per-request context computed by the router and read by handlers. +// Keep it small and stable; page-specific data should live in view models. +type BaseData struct { + Global any + UserID int + Username string + URLSegments []string + DirMode bool + GroupPath []string + SeparatorIndex int +} + +type ctxKey struct{} + +// WithBaseData attaches BaseData to a context. +func WithBaseData(ctx context.Context, b *BaseData) context.Context { + return context.WithValue(ctx, ctxKey{}, b) +} + +// Base retrieves BaseData from the request (never nil). +func Base(r *http.Request) *BaseData { + if v, ok := r.Context().Value(ctxKey{}).(*BaseData); ok && v != nil { + return v + } + return &BaseData{} +} + +// Vars are route variables captured by the router (e.g., :repo, *rest). +type Vars map[string]string + +// HandlerFunc is the router↔handler function contract. +type HandlerFunc func(http.ResponseWriter, *http.Request, Vars) -- 2.48.1