aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-08-17 17:47:38 +0800
committerRunxi Yu <me@runxiyu.org>2025-08-17 18:04:34 +0800
commitf58f56701d047398cc8d7a6433719de1b6070242 (patch)
treeeddba41fc84549f8532af4da040cb227a0304f65
parentgofumpt (diff)
downloadforge-f58f56701d047398cc8d7a6433719de1b6070242.tar.gz
forge-f58f56701d047398cc8d7a6433719de1b6070242.tar.zst
forge-f58f56701d047398cc8d7a6433719de1b6070242.zip
Refactor handlers structure and add BaseData
-rw-r--r--forged/internal/incoming/web/handler.go20
-rw-r--r--forged/internal/incoming/web/handlers/index.go15
-rw-r--r--forged/internal/incoming/web/handlers/repo/index.go20
-rw-r--r--forged/internal/incoming/web/router.go131
-rw-r--r--forged/internal/incoming/web/stub.go38
-rw-r--r--forged/internal/incoming/web/types/types.go39
6 files changed, 178 insertions, 85 deletions
diff --git a/forged/internal/incoming/web/handler.go b/forged/internal/incoming/web/handler.go
index 9018547..6341a93 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 @@ func NewHandler(cfg Config) http.Handler {
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 0000000..773a0c6
--- /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 0000000..3a6d7ea
--- /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 59b04d5..46eb935 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 @@ type route struct {
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) handle(method, pattern string, f HandlerFunc, hh http.Handler,
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 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
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 @@ func redirectDropSlash(w http.ResponseWriter, r *http.Request) bool {
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 7207756..4fffd73 100644
--- a/forged/internal/incoming/web/stub.go
+++ b/forged/internal/incoming/web/stub.go
@@ -4,41 +4,35 @@ import (
"fmt"
"net/http"
"strings"
-)
-
-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, p Params) {
- g := p["group_path"].([]string) // captured by @group
- _, _ = w.Write([]byte("group index for: /" + strings.Join(g, "/") + "/"))
-}
+ wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types"
+)
-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) 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) 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 0000000..d47b13a
--- /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)