package routers import ( "errors" "os" "path/filepath" "strings" "sync" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) // Maximum number of URL parameters per route const maxParams = 20 // LuaRouter is a filesystem-based HTTP router for Lua files type LuaRouter struct { routesDir string // Root directory containing route files routes map[string]*node // Method -> route tree failedRoutes map[string]*RouteError // Track failed routes mu sync.RWMutex // Lock for concurrent access to routes } // node represents a node in the routing trie type node struct { handler string // Path to Lua file (empty if not an endpoint) bytecode []byte // Pre-compiled Lua bytecode paramName string // Parameter name (if this is a parameter node) staticChild map[string]*node // Static children by segment name paramChild *node // Parameter/wildcard child err error // Compilation error if any } // Params holds URL parameters with fixed-size arrays to avoid allocations type Params struct { Keys [maxParams]string Values [maxParams]string Count int } // Get returns a parameter value by name func (p *Params) Get(name string) string { for i := 0; i < p.Count; i++ { if p.Keys[i] == name { return p.Values[i] } } return "" } // NewLuaRouter creates a new LuaRouter instance func NewLuaRouter(routesDir string) (*LuaRouter, error) { // Verify routes directory exists info, err := os.Stat(routesDir) if err != nil { return nil, err } if !info.IsDir() { return nil, errors.New("routes path is not a directory") } r := &LuaRouter{ routesDir: routesDir, routes: make(map[string]*node), failedRoutes: make(map[string]*RouteError), } // Initialize method trees methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} for _, method := range methods { r.routes[method] = &node{ staticChild: make(map[string]*node), } } // Build routes err = r.buildRoutes() // If some routes failed to compile, return the router with a warning error // This allows the server to continue running with the routes that did compile if len(r.failedRoutes) > 0 { return r, ErrRoutesCompilationErrors } return r, err } // buildRoutes scans the routes directory and builds the routing tree func (r *LuaRouter) buildRoutes() error { // Clear failed routes map r.failedRoutes = make(map[string]*RouteError) return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Skip directories if info.IsDir() { return nil } // Only process .lua files if !strings.HasSuffix(info.Name(), ".lua") { return nil } // Extract method from filename method := strings.ToUpper(strings.TrimSuffix(info.Name(), ".lua")) // Check if valid method root, exists := r.routes[method] if !exists { return nil // Skip invalid methods } // Get relative path for URL relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) if err != nil { return err } // Build URL path urlPath := "/" if relDir != "." { urlPath = "/" + strings.ReplaceAll(relDir, "\\", "/") } // Add route to tree - continue even if there are errors r.addRoute(root, urlPath, path) return nil }) } // addRoute adds a route to the routing tree and compiles the Lua file to bytecode func (r *LuaRouter) addRoute(root *node, urlPath, handlerPath string) error { segments := strings.Split(strings.Trim(urlPath, "/"), "/") current := root for _, segment := range segments { if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' { if current.paramChild == nil { current.paramChild = &node{ paramName: segment[1 : len(segment)-1], staticChild: make(map[string]*node), } } current = current.paramChild } else { // Create or get static child child, exists := current.staticChild[segment] if !exists { child = &node{ staticChild: make(map[string]*node), } current.staticChild[segment] = child } current = child } } // Set handler path current.handler = handlerPath // Compile Lua file to bytecode if err := r.compileHandler(current, urlPath); err != nil { // Track the failure but don't fail the entire process routeKey := getRouteKey(urlPath, handlerPath) r.failedRoutes[routeKey] = &RouteError{ Path: urlPath, ScriptPath: handlerPath, Err: err, } } return nil } // getRouteKey generates a unique key for a route func getRouteKey(path, handler string) string { return path + ":" + handler } // Match finds a handler for the given method and path // Uses the pre-allocated params struct to avoid allocations func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { // Reset params params.Count = 0 // Get route tree for method r.mu.RLock() root, exists := r.routes[method] r.mu.RUnlock() if !exists { return nil, false } // Split path segments := strings.Split(strings.Trim(path, "/"), "/") // Match path return r.matchPath(root, segments, params, 0) } // matchPath recursively matches a path against the routing tree func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) { // Base case: no more segments if len(segments) == 0 { if current.handler != "" { return current, true } return nil, false } segment := segments[0] remaining := segments[1:] // Try static child first (exact match takes precedence) if child, exists := current.staticChild[segment]; exists { if node, found := r.matchPath(child, remaining, params, depth+1); found { return node, true } } // Try parameter child if current.paramChild != nil { // Store parameter if params.Count < maxParams { params.Keys[params.Count] = current.paramChild.paramName params.Values[params.Count] = segment params.Count++ } if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found { return node, true } // Backtrack: remove parameter if no match params.Count-- } return nil, false } // compileHandler compiles a Lua file to bytecode func (r *LuaRouter) compileHandler(n *node, urlPath string) error { if n.handler == "" { return nil } // Read the Lua file content, err := os.ReadFile(n.handler) if err != nil { n.err = err // Store the error in the node return err } // Compile to bytecode state := luajit.New() if state == nil { compileErr := errors.New("failed to create Lua state") n.err = compileErr // Store the error in the node return compileErr } defer state.Close() bytecode, err := state.CompileBytecode(string(content), n.handler) if err != nil { n.err = err // Store the error in the node return err } // Store bytecode in the node n.bytecode = bytecode n.err = nil // Clear any previous error return nil } // GetBytecode returns the compiled bytecode for a matched route // If a route exists but failed to compile, returns nil bytecode with found=true func (r *LuaRouter) GetBytecode(method, path string, params *Params) ([]byte, string, bool) { node, found := r.Match(method, path, params) if !found { return nil, "", false } // If the route exists but has a compilation error if node.err != nil { return nil, node.handler, true } return node.bytecode, node.handler, true } // Refresh rebuilds the router by rescanning the routes directory func (r *LuaRouter) Refresh() error { r.mu.Lock() defer r.mu.Unlock() // Reset routes for method := range r.routes { r.routes[method] = &node{ staticChild: make(map[string]*node), } } // Clear failed routes r.failedRoutes = make(map[string]*RouteError) // Rebuild routes err := r.buildRoutes() // If some routes failed to compile, return a warning error if len(r.failedRoutes) > 0 { return ErrRoutesCompilationErrors } return err } // ReportFailedRoutes returns a list of routes that failed to compile func (r *LuaRouter) ReportFailedRoutes() []*RouteError { r.mu.RLock() defer r.mu.RUnlock() result := make([]*RouteError, 0, len(r.failedRoutes)) for _, re := range r.failedRoutes { result = append(result, re) } return result } type NodeWithError struct { ScriptPath string Error error } // GetNodeWithError returns the node with its error for a given path func (r *LuaRouter) GetNodeWithError(method, path string, params *Params) (*NodeWithError, bool) { node, found := r.Match(method, path, params) if !found { return nil, false } return &NodeWithError{ ScriptPath: node.handler, Error: node.err, }, true } // GetRouteStats returns statistics about the router func (r *LuaRouter) GetRouteStats() (int, int64) { r.mu.RLock() defer r.mu.RUnlock() routeCount := 0 bytecodeBytes := int64(0) // Count routes and bytecode size for _, root := range r.routes { count, bytes := countNodesAndBytecode(root) routeCount += count bytecodeBytes += bytes } return routeCount, bytecodeBytes } // countNodesAndBytecode traverses the tree and counts nodes and bytecode size func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { if n == nil { return 0, 0 } // Count this node if it has a handler if n.handler != "" { count = 1 bytecodeBytes = int64(len(n.bytecode)) } // Count static children for _, child := range n.staticChild { childCount, childBytes := countNodesAndBytecode(child) count += childCount bytecodeBytes += childBytes } // Count parameter child if n.paramChild != nil { childCount, childBytes := countNodesAndBytecode(n.paramChild) count += childCount bytecodeBytes += childBytes } return count, bytecodeBytes }