From 6e3fe5e9ef428d04dc448c5607fbf7ef085e71af Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Wed, 5 Mar 2025 19:32:23 -0600 Subject: [PATCH] staticrouter --- core/routers/staticrouter.go | 88 ++++++++++++++++++ core/routers/staticrouter_test.go | 150 ++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 core/routers/staticrouter.go create mode 100644 core/routers/staticrouter_test.go diff --git a/core/routers/staticrouter.go b/core/routers/staticrouter.go new file mode 100644 index 0000000..763de0a --- /dev/null +++ b/core/routers/staticrouter.go @@ -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() +} diff --git a/core/routers/staticrouter_test.go b/core/routers/staticrouter_test.go new file mode 100644 index 0000000..f265c47 --- /dev/null +++ b/core/routers/staticrouter_test.go @@ -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": "Home", + "about.html": "About", + "api/index.json": `{"version": "1.0"}`, + "users/index.html": "Users", + "users/123/profile.html": "User Profile", + "posts/hello-world/comments.html": "Post Comments", + "docs/v1/api.html": "API Docs", + } + + 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("New"), 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) + } +}