staticrouter

This commit is contained in:
Sky Johnson 2025-03-05 19:32:23 -06:00
parent 440f0a8378
commit 6e3fe5e9ef
2 changed files with 238 additions and 0 deletions

View File

@ -0,0 +1,88 @@
package routers
import (
"errors"
"os"
"path/filepath"
"strings"
"sync"
)
// StaticRouter is a filesystem-based router for static files
type StaticRouter struct {
rootDir string // Root directory containing files
routes map[string]string // Direct mapping from URL path to file path
mu sync.RWMutex // Lock for concurrent access to routes
}
// NewStaticRouter creates a new StaticRouter instance
func NewStaticRouter(rootDir string) (*StaticRouter, error) {
// Verify root directory exists
info, err := os.Stat(rootDir)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, errors.New("root path is not a directory")
}
r := &StaticRouter{
rootDir: rootDir,
routes: make(map[string]string),
}
// Build routes
if err := r.buildRoutes(); err != nil {
return nil, err
}
return r, nil
}
// buildRoutes scans the root directory and builds the routing map
func (r *StaticRouter) buildRoutes() error {
return filepath.Walk(r.rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories
if info.IsDir() {
return nil
}
// Get relative path for URL
relPath, err := filepath.Rel(r.rootDir, path)
if err != nil {
return err
}
// Convert to URL path with forward slashes for consistency
urlPath := "/" + strings.ReplaceAll(relPath, "\\", "/")
// Add to routes map
r.routes[urlPath] = path
return nil
})
}
// Match finds a file path for the given URL path
func (r *StaticRouter) Match(path string) (string, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
filePath, found := r.routes[path]
return filePath, found
}
// Refresh rebuilds the router by rescanning the root directory
func (r *StaticRouter) Refresh() error {
r.mu.Lock()
defer r.mu.Unlock()
// Clear routes
r.routes = make(map[string]string)
// Rebuild routes
return r.buildRoutes()
}

View File

@ -0,0 +1,150 @@
package routers
import (
"os"
"path/filepath"
"testing"
)
func setupStaticFiles(t *testing.T) (string, func()) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "staticrouter-test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
// Create file structure
files := map[string]string{
"index.html": "<html>Home</html>",
"about.html": "<html>About</html>",
"api/index.json": `{"version": "1.0"}`,
"users/index.html": "<html>Users</html>",
"users/123/profile.html": "<html>User Profile</html>",
"posts/hello-world/comments.html": "<html>Post Comments</html>",
"docs/v1/api.html": "<html>API Docs</html>",
}
for path, content := range files {
filePath := filepath.Join(tempDir, path)
// Create directories
err := os.MkdirAll(filepath.Dir(filePath), 0755)
if err != nil {
t.Fatalf("Failed to create directory %s: %v", filepath.Dir(filePath), err)
}
// Create file
err = os.WriteFile(filePath, []byte(content), 0644)
if err != nil {
t.Fatalf("Failed to create file %s: %v", filePath, err)
}
}
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
}
return tempDir, cleanup
}
func TestStaticRouterInitialization(t *testing.T) {
rootDir, cleanup := setupStaticFiles(t)
defer cleanup()
router, err := NewStaticRouter(rootDir)
if err != nil {
t.Fatalf("Failed to create router: %v", err)
}
if router == nil {
t.Fatal("Router is nil")
}
}
func TestStaticRouteMatching(t *testing.T) {
rootDir, cleanup := setupStaticFiles(t)
defer cleanup()
router, err := NewStaticRouter(rootDir)
if err != nil {
t.Fatalf("Failed to create router: %v", err)
}
tests := []struct {
path string
wantFound bool
wantHandler string
}{
{"/index.html", true, filepath.Join(rootDir, "index.html")},
{"/about.html", true, filepath.Join(rootDir, "about.html")},
{"/api/index.json", true, filepath.Join(rootDir, "api/index.json")},
{"/users/index.html", true, filepath.Join(rootDir, "users/index.html")},
{"/users/123/profile.html", true, filepath.Join(rootDir, "users/123/profile.html")},
{"/posts/hello-world/comments.html", true, filepath.Join(rootDir, "posts/hello-world/comments.html")},
{"/docs/v1/api.html", true, filepath.Join(rootDir, "docs/v1/api.html")},
// Non-existent routes
{"/nonexistent.html", false, ""},
{"/api/nonexistent.json", false, ""},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
filePath, found := router.Match(tt.path)
if found != tt.wantFound {
t.Errorf("Match() found = %v, want %v", found, tt.wantFound)
}
if !found {
return
}
if filePath != tt.wantHandler {
t.Errorf("Match() handler = %v, want %v", filePath, tt.wantHandler)
}
})
}
}
//TestStaticParamExtraction has been removed since we no longer extract parameters
func TestStaticRefresh(t *testing.T) {
rootDir, cleanup := setupStaticFiles(t)
defer cleanup()
router, err := NewStaticRouter(rootDir)
if err != nil {
t.Fatalf("Failed to create router: %v", err)
}
// Add a new file
newFilePath := filepath.Join(rootDir, "new.html")
err = os.WriteFile(newFilePath, []byte("<html>New</html>"), 0644)
if err != nil {
t.Fatalf("Failed to create file: %v", err)
}
// Before refresh, file should not be found
_, found := router.Match("/new.html")
if found {
t.Errorf("New file should not be found before refresh")
}
// Refresh router
err = router.Refresh()
if err != nil {
t.Fatalf("Failed to refresh router: %v", err)
}
// After refresh, file should be found
filePath, found := router.Match("/new.html")
if !found {
t.Errorf("New file should be found after refresh")
}
if filePath != newFilePath {
t.Errorf("Expected path %s, got %s", newFilePath, filePath)
}
}