Files
kandev/apps/backend/internal/webapp/routes.go
T
2026-06-18 21:30:38 +01:00

160 lines
4.2 KiB
Go

package webapp
import (
pathpkg "path"
"strings"
)
// RouteKind describes how the backend should handle an incoming request path.
type RouteKind string
const (
RouteKindSPA RouteKind = "spa"
RouteKindAPI RouteKind = "api"
RouteKindWebSocket RouteKind = "websocket"
RouteKindStatic RouteKind = "static"
RouteKindHealth RouteKind = "health"
)
// RouteName identifies SPA routes that need route-aware boot data.
type RouteName string
const (
RouteUnknown RouteName = "unknown"
RouteHome RouteName = "home"
RouteTasks RouteName = "tasks"
RouteTaskDetail RouteName = "taskDetail"
RouteOffice RouteName = "office"
RouteSettings RouteName = "settings"
RouteGitHub RouteName = "github"
RouteGitLab RouteName = "gitlab"
RouteJira RouteName = "jira"
RouteLinear RouteName = "linear"
RouteStats RouteName = "stats"
)
// RouteClassification is the backend contract for SPA-vs-backend routing.
type RouteClassification struct {
Kind RouteKind `json:"kind"`
Route RouteName `json:"route,omitempty"`
Path string `json:"path"`
Params map[string]string `json:"params,omitempty"`
}
// ClassifyRoute returns the future webapp handling class for a request path.
func ClassifyRoute(rawPath string) RouteClassification {
requestPath := normalizePath(rawPath)
if kind, ok := classifyNonSPA(requestPath); ok {
return RouteClassification{Kind: kind, Path: requestPath}
}
route, params := classifySPARoute(requestPath)
return RouteClassification{
Kind: RouteKindSPA,
Route: route,
Path: requestPath,
Params: params,
}
}
// IsSPARoute reports whether rawPath should serve the SPA shell.
func IsSPARoute(rawPath string) bool {
return ClassifyRoute(rawPath).Kind == RouteKindSPA
}
func classifyNonSPA(requestPath string) (RouteKind, bool) {
switch {
case requestPath == "/api" || strings.HasPrefix(requestPath, "/api/"):
return RouteKindAPI, true
case requestPath == "/ws" || strings.HasPrefix(requestPath, "/ws/"):
return RouteKindWebSocket, true
case requestPath == "/health":
return RouteKindHealth, true
case isStaticPath(requestPath):
return RouteKindStatic, true
default:
return "", false
}
}
func classifySPARoute(requestPath string) (RouteName, map[string]string) {
switch {
case requestPath == "/":
return RouteHome, nil
case requestPath == "/tasks":
return RouteTasks, nil
case requestPath == "/github":
return RouteGitHub, nil
case requestPath == "/gitlab":
return RouteGitLab, nil
case requestPath == "/jira":
return RouteJira, nil
case requestPath == "/linear":
return RouteLinear, nil
case requestPath == "/stats":
return RouteStats, nil
case requestPath == "/office" || strings.HasPrefix(requestPath, "/office/"):
return RouteOffice, nil
case requestPath == "/settings" || strings.HasPrefix(requestPath, "/settings/"):
return RouteSettings, nil
default:
return classifyTaskRoute(requestPath)
}
}
func classifyTaskRoute(requestPath string) (RouteName, map[string]string) {
taskID, ok := cutSingleSegment(requestPath, "/t/")
if !ok {
taskID, ok = cutSingleSegment(requestPath, "/tasks/")
}
if !ok {
return RouteUnknown, nil
}
return RouteTaskDetail, map[string]string{"taskId": taskID}
}
func cutSingleSegment(requestPath, prefix string) (string, bool) {
value, ok := strings.CutPrefix(requestPath, prefix)
if !ok || value == "" || strings.Contains(value, "/") {
return "", false
}
return value, true
}
func isStaticPath(requestPath string) bool {
if strings.HasPrefix(requestPath, "/assets/") || strings.HasPrefix(requestPath, "/_next/") {
return true
}
switch requestPath {
case "/apple-touch-icon.png",
"/favicon.ico",
"/icon.svg",
"/manifest.webmanifest",
"/robots.txt",
"/vite.svg":
return true
default:
return false
}
}
func normalizePath(rawPath string) string {
if rawPath == "" {
return "/"
}
cleanInput := stripSuffix(rawPath, "?")
cleanInput = stripSuffix(cleanInput, "#")
if !strings.HasPrefix(cleanInput, "/") {
cleanInput = "/" + cleanInput
}
return pathpkg.Clean(cleanInput)
}
func stripSuffix(value, marker string) string {
if idx := strings.Index(value, marker); idx >= 0 {
return value[:idx]
}
return value
}