Forráskód Böngészése

refactor(core): rename HttpHandler to RequestHandler and add pattern-based routing

- Rename HttpHandler interface to RequestHandler
- Rename handle() method to handle_request() for clarity
- Implement pattern-based routing with named segment support (e.g., /users/{id})
- Add HTTP method-specific route registration (get, post, put, delete, patch)
- Add RouteContext for accessing matched route segments
- Add RouteHandler and RouteErrorHandler interfaces
- Add DefaultNotFoundHandler and DefaultErrorHandler implementations
- Remove DelegateHttpHandler class
- Remove debug print statements from Server and AsyncPipe

BREAKING CHANGE: HttpHandler interface renamed to RequestHandler and handle() method renamed to handle_request(). All existing handlers must be updated to implement RequestHandler and use handle_request().
Billy Barrow 4 hete
szülő
commit
581ef7abc9

+ 6 - 6
examples/FormData.vala

@@ -10,7 +10,7 @@ using Invercargill.DataStructures;
  */
 
 // HTML form page handler
-class FormPageHandler : Object, HttpHandler {
+class FormPageHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) throws Error {
         var headers = new Catalogue<string, string>();
         headers.add("Content-Type", "text/html");
@@ -128,7 +128,7 @@ class FormPageHandler : Object, HttpHandler {
 }
 
 // Simple form submission handler
-class SimpleFormHandler : Object, HttpHandler {
+class SimpleFormHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) throws Error {
         if (!context.request.is_post()) {
             return new BufferedHttpResult.from_string(
@@ -168,7 +168,7 @@ class SimpleFormHandler : Object, HttpHandler {
 }
 
 // Registration form submission handler
-class RegisterFormHandler : Object, HttpHandler {
+class RegisterFormHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) throws Error {
         if (!context.request.is_post()) {
             return new BufferedHttpResult.from_string(
@@ -245,7 +245,7 @@ class RegisterFormHandler : Object, HttpHandler {
 }
 
 // Search form submission handler
-class SearchFormHandler : Object, HttpHandler {
+class SearchFormHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) throws Error {
         if (!context.request.is_post()) {
             return new BufferedHttpResult.from_string(
@@ -318,7 +318,7 @@ class SearchFormHandler : Object, HttpHandler {
 }
 
 // File upload handler (multipart/form-data)
-class FileUploadHandler : Object, HttpHandler {
+class FileUploadHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) throws Error {
         if (!context.request.is_post()) {
             return new BufferedHttpResult.from_string(
@@ -358,7 +358,7 @@ class FileUploadHandler : Object, HttpHandler {
 }
 
 // Form debug tool handler
-class FormDebugHandler : Object, HttpHandler {
+class FormDebugHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) throws Error {
         FormData? form_data = null;
         string? body_text = null;

+ 1 - 1
examples/RemoteAddress.vala

@@ -16,7 +16,7 @@ public class RemoteAddressExample : Object {
 }
 
 // Simple handler that responds with client information
-public class SimpleHttpHandler : Object, HttpHandler {
+public class SimpleHttpHandler : Object, RequestHandler {
     public async HttpResult handle(HttpContext context) {
         var request = context.request;
         

+ 0 - 1
src/Core/AsyncPipe.vala

@@ -35,7 +35,6 @@ namespace Astralis {
         }
 
         private void write(uint8[] data) {
-            print(@"$(data.length) bytes added to chunks\n");
             chunks.add(new ByteBuffer.from_byte_array(data));
             write_occurred = true;
         }

+ 3 - 17
src/Core/RequestHandler.vala

@@ -1,22 +1,8 @@
 
 namespace Astralis {
 
-    public interface HttpHandler : Object {
-        public abstract async HttpResult handle(HttpContext context) throws Error;
+    public interface RequestHandler : Object {
+        public abstract async HttpResult handle_request(HttpContext context) throws Error;
     }
-
-    public delegate HttpResult HttpHandlerDelegate(HttpContext context) throws Error;
-    public class DelegateHttpHandler : Object, HttpHandler {
-        private HttpHandlerDelegate callback;
-
-        public DelegateHttpHandler(owned HttpHandlerDelegate func) {
-            callback = (owned)func;
-        }
-
-        public async HttpResult handle(HttpContext context) throws Error {
-            return callback(context);
-        }
-
-    }
-
+    
 }

+ 324 - 16
src/Handlers/Router.vala

@@ -1,34 +1,342 @@
+using Invercargill;
 using Invercargill.DataStructures;
 
 namespace Astralis {
 
-    public class RouteEntry {
-        public HttpHandler handler;
-        public RouteEntry(HttpHandler handler) {
-            this.handler = handler;
+    /// <summary>
+    /// HTTP router that maps request paths to handlers with support for named segments.
+    /// </summary>
+    /// <example>
+    /// var router = new Router();
+    /// router.map("/users", users_handler);
+    /// router.map("/users/{id}", user_detail_handler);
+    /// router.map("/files/{filename}/details", file_details_handler);
+    /// router.not_found_handler = new NotFoundHandler();
+    /// router.error_handler = new ErrorHandler();
+    /// </example>
+    public class Router : RequestHandler, Object {
+
+        private Vector<Route> _routes = new Vector<Route>();
+
+        /// <summary>
+        /// Handler invoked when no route matches the request path.
+        /// </summary>
+        public RequestHandler not_found_handler { get; set; }
+
+        /// <summary>
+        /// Handler invoked when a route handler throws an error.
+        /// </summary>
+        public RouteErrorHandler error_handler { get; set; }
+
+        /// <summary>
+        /// Registers a route handler for the specified path pattern.
+        /// </summary>
+        /// <param name="pattern">The path pattern to match. Supports named segments like "/users/{id}"</param>
+        /// <param name="handler">The handler to invoke when the pattern matches</param>
+        /// <param name="methods">Optional HTTP methods to restrict this route to (e.g., "GET", "POST")</param>
+        public void map(string pattern, owned RouteHandler handler, string[]? methods = null) {
+            var route = new Route(pattern, handler, methods);
+            _routes.add(route);
+        }
+
+        /// <summary>
+        /// Registers a route handler for GET requests.
+        /// </summary>
+        public new void get(string pattern, owned RouteHandler handler) {
+            map(pattern, handler, { "GET" });
+        }
+
+        /// <summary>
+        /// Registers a route handler for POST requests.
+        /// </summary>
+        public void post(string pattern, owned RouteHandler handler) {
+            map(pattern, handler, { "POST" });
+        }
+
+        /// <summary>
+        /// Registers a route handler for PUT requests.
+        /// </summary>
+        public void put(string pattern, owned RouteHandler handler) {
+            map(pattern, handler, { "PUT" });
+        }
+
+        /// <summary>
+        /// Registers a route handler for DELETE requests.
+        /// </summary>
+        public void delete(string pattern, owned RouteHandler handler) {
+            map(pattern, handler, { "DELETE" });
+        }
+
+        /// <summary>
+        /// Registers a route handler for PATCH requests.
+        /// </summary>
+        public void patch(string pattern, owned RouteHandler handler) {
+            map(pattern, handler, { "PATCH" });
+        }
+
+        /// <summary>
+        /// Removes all registered routes.
+        /// </summary>
+        public void clear() {
+            _routes.clear();
+        }
+
+        /// <summary>
+        /// Handles an incoming HTTP request by matching against registered routes.
+        /// </summary>
+        public async HttpResult handle_request(HttpContext context) throws Error {
+            var request_path = context.request.raw_path.split("?")[0];
+            
+            var routes_array = _routes.to_array();
+            foreach (var route in routes_array) {
+                var match_result = route.match(request_path, context.request.method);
+                if (match_result != null) {
+                    try {
+                        return yield route.handler.handle_route(context, match_result);
+                    } catch (Error e) {
+                        if (error_handler != null) {
+                            return yield error_handler.handle_route_error(context, match_result, e);
+                        }
+                        throw e;
+                    }
+                }
+            }
+
+            // No route matched - use not found handler or return default 404
+            if (not_found_handler != null) {
+                return yield not_found_handler.handle_request(context);
+            }
+
+            var headers = new Catalogue<string, string>();
+            headers.add("Content-Type", "text/plain");
+            return new BufferedHttpResult.from_string(
+                "Not Found",
+                StatusCode.NOT_FOUND,
+                headers
+            );
         }
     }
 
-    public class Router : HttpHandler, Object {
-        private Dictionary<string, RouteEntry> routes = new Dictionary<string, RouteEntry>();
+    /// <summary>
+    /// Represents a single route with its pattern, handler, and allowed methods.
+    /// </summary>
+    internal class Route : Object {
+
+        private string _pattern;
+        private string[] _pattern_segments;
+        private string[] _named_segment_names;
+        public RouteHandler handler { get; private set; }
+        private string[]? _allowed_methods;
 
-        public void map_func(string path, owned HttpHandlerDelegate handler) {
-            routes.set(path, new RouteEntry(new DelegateHttpHandler((owned) handler)));
+        public Route(string pattern, owned RouteHandler handler, string[]? methods = null) {
+            _pattern = pattern;
+            _handler = handler;
+            _allowed_methods = methods != null && methods.length > 0 ? methods : null;
+            
+            // Parse pattern into segments
+            var segments = pattern.split("/");
+            var pattern_segments = new string[0];
+            var named_segments = new string[0];
+            
+            foreach (var segment in segments) {
+                if (segment != "") {
+                    pattern_segments += segment;
+                    
+                    // Check if this is a named segment {name}
+                    if (segment.length >= 3 && segment[0] == '{' && segment[segment.length - 1] == '}') {
+                        named_segments += segment.substring(1, segment.length - 2);
+                    } else {
+                        named_segments += ""; // Empty string indicates literal segment
+                    }
+                }
+            }
+            
+            _pattern_segments = pattern_segments;
+            _named_segment_names = named_segments;
         }
 
-        public void map(string path, owned HttpHandler handler) {
-            routes.set(path, new RouteEntry(handler));
+        /// <summary>
+        /// Attempts to match the given path and method against this route.
+        /// </summary>
+        /// <returns>A RouteContext if matched, null otherwise</returns>
+        public RouteContext? match(string path, string method) {
+            // Check method first
+            if (_allowed_methods != null) {
+                bool method_allowed = false;
+                foreach (var allowed in _allowed_methods) {
+                    if (allowed == method) {
+                        method_allowed = true;
+                        break;
+                    }
+                }
+                if (!method_allowed) {
+                    return null;
+                }
+            }
+
+            // Parse path into segments
+            var path_segments = path.split("/");
+            var path_segs = new string[0];
+            foreach (var segment in path_segments) {
+                if (segment != "") {
+                    path_segs += Uri.unescape_string(segment) ?? segment;
+                }
+            }
+
+            // Check segment count
+            if (path_segs.length != _pattern_segments.length) {
+                return null;
+            }
+
+            // Match each segment
+            var named_segments = new Dictionary<string, string>();
+            
+            for (int i = 0; i < _pattern_segments.length; i++) {
+                var pattern_seg = _pattern_segments[i];
+                var path_seg = path_segs[i];
+                
+                if (_named_segment_names[i] != "") {
+                    // This is a named segment - capture the value
+                    named_segments[_named_segment_names[i]] = path_seg;
+                } else if (pattern_seg != path_seg) {
+                    // Literal segment doesn't match
+                    return null;
+                }
+            }
+
+            // Build route context
+            var context = new RouteContext();
+            context.segments = Wrap.array<string>(path_segs).to_series();
+            context.named_segments = named_segments;
+            
+            return context;
         }
+    }
+
+    /// <summary>
+    /// Interface for route handlers that process matched routes.
+    /// </summary>
+    public interface RouteHandler : Object {
+        /// <summary>
+        /// Handles a matched route.
+        /// </summary>
+        /// <param name="http_context">The HTTP context containing request information</param>
+        /// <param name="route_context">The route context containing matched segments</param>
+        /// <returns>The HTTP result to send back to the client</returns>
+        public abstract async HttpResult handle_route(HttpContext http_context, RouteContext route_context) throws Error;
+    }
+
+    /// <summary>
+    /// Interface for error handlers that process exceptions from route handlers.
+    /// </summary>
+    public interface RouteErrorHandler : Object {
+        /// <summary>
+        /// Handles an error that occurred during route processing.
+        /// </summary>
+        /// <param name="http_context">The HTTP context containing request information</param>
+        /// <param name="route_context">The route context for the matched route</param>
+        /// <param name="error">The error that was thrown</param>
+        /// <returns>The HTTP result to send back to the client</returns>
+        public abstract async HttpResult handle_route_error(HttpContext http_context, RouteContext route_context, Error error) throws Error;
+    }
+
+    /// <summary>
+    /// Context information for a matched route, containing path segments and named captures.
+    /// </summary>
+    public class RouteContext : Object {
 
-        public async HttpResult handle(HttpContext context) {
+        /// <summary>
+        /// The path segments from the matched URL.
+        /// </summary>
+        public Series<string> segments { get; set; }
+
+        /// <summary>
+        /// Named segment values captured from the route pattern.
+        /// For pattern "/users/{id}/posts/{post_id}" matching "/users/123/posts/456",
+        /// this would contain {"id": "123", "post_id": "456"}.
+        /// </summary>
+        public Dictionary<string, string> named_segments { get; set; }
+
+        /// <summary>
+        /// Gets a named segment value by name.
+        /// </summary>
+        /// <param name="name">The name of the segment to retrieve</param>
+        /// <returns>The segment value, or null if not found</returns>
+        public string? get_segment(string name) {
+            string? value = null;
             try {
-                return yield routes.get(context.request.raw_path).handler.handle(context);
-            } catch (Invercargill.IndexError e) {
-                return new BufferedHttpResult.from_string("Not Found", StatusCode.NOT_FOUND);
+                value = named_segments.get(name);
+            } catch (Error e) {
+                // Key not found
             }
-            catch {
-                return new BufferedHttpResult.from_string("Internal Server Error", StatusCode.INTERNAL_SERVER_ERROR);
+            return value;
+        }
+
+        /// <summary>
+        /// Gets a named segment value by name, with a default fallback.
+        /// </summary>
+        /// <param name="name">The name of the segment to retrieve</param>
+        /// <param name="default_value">The default value if not found</param>
+        /// <returns>The segment value, or the default if not found</returns>
+        public string get_segment_or_default(string name, string default_value) {
+            string? value = null;
+            try {
+                value = named_segments.get(name);
+            } catch (Error e) {
+                // Key not found
             }
+            return value ?? default_value;
+        }
+
+        /// <summary>
+        /// Checks if a named segment exists.
+        /// </summary>
+        /// <param name="name">The name of the segment to check</param>
+        /// <returns>True if the segment exists, false otherwise</returns>
+        public bool has_segment(string name) {
+            return named_segments.has(name);
+        }
+    }
+
+    /// <summary>
+    /// A simple not found handler that returns a 404 response.
+    /// </summary>
+    public class DefaultNotFoundHandler : RequestHandler, Object {
+        
+        public async HttpResult handle_request(HttpContext http_context) throws Error {
+            var headers = new Catalogue<string, string>();
+            headers.add("Content-Type", "text/plain");
+            return new BufferedHttpResult.from_string(
+                "404 Not Found",
+                StatusCode.NOT_FOUND,
+                headers
+            );
+        }
+    }
+
+    /// <summary>
+    /// A simple error handler that returns a 500 response with error details.
+    /// </summary>
+    public class DefaultErrorHandler : RouteErrorHandler, Object {
+        
+        public bool include_details { get; set; }
+
+        public DefaultErrorHandler(bool include_details = false) {
+            this.include_details = include_details;
+        }
+
+        public async HttpResult handle_route_error(HttpContext http_context, RouteContext route_context, Error error) throws Error {
+            var message = include_details 
+                ? @"Internal Server Error: $(error.message)"
+                : "Internal Server Error";
+            
+            var headers = new Catalogue<string, string>();
+            headers.add("Content-Type", "text/plain");
+            return new BufferedHttpResult.from_string(
+                message,
+                StatusCode.INTERNAL_SERVER_ERROR,
+                headers
+            );
         }
     }
 }

+ 4 - 12
src/Server/Server.vala

@@ -6,11 +6,11 @@ namespace Astralis {
 
     public class Server : Object {
         private Daemon daemon;
-        private HttpHandler handler;
+        private RequestHandler handler;
         private int port;
         private HashSet<RequestContext> request_contexts;
 
-        public Server(int port, HttpHandler handler) {
+        public Server(int port, RequestHandler handler) {
             this.port = port;
             this.handler = handler;
             this.request_contexts = new HashSet<RequestContext>();
@@ -18,7 +18,6 @@ namespace Astralis {
 
         private int access_handler (Connection connection, string? url, string? method, string? version, string? upload_data, size_t* upload_data_size, void** con_cls) {
 
-            printerr(@"upload_data_size = $(upload_data_size[0])\n");
             // On initial call for request, simply set up the context
             if (con_cls[0] == null) {
                 var context = new RequestContext();
@@ -65,15 +64,13 @@ namespace Astralis {
                 context.handler_context = http_context;
 
                 // Kick off the handler
-                printerr("begin handler...\n");
-                handler.handle.begin(http_context, (obj, res) => {
+                handler.handle_request.begin(http_context, (obj, res) => {
                     try {
-                        context.handler_result = handler.handle.end(res);
+                        context.handler_result = handler.handle_request.end(res);
                     }
                     catch(Error e) {
                         context.handler_error = e;
                     }
-                    printerr("handler finished...\n");
                     if(context.handler_finished()) {
                         respond(connection, context);
                         MHD.resume_connection(connection);
@@ -83,22 +80,18 @@ namespace Astralis {
 
             // On the second, and all subsequent calls - read the request body:
             if (upload_data_size[0] != 0) {
-                printerr("load chunk...\n");
                 var data = new uint8[upload_data_size[0]];
                 Memory.copy(data, upload_data, upload_data_size[0]);
                 context.request_body_pipe.write(data);
                 upload_data_size[0] = 0;
-                printerr("Return to server loop...\n");
                 return Result.YES;
             }
             // End of request body data
             else {
-                printerr("chunks done innit...\n");
                 context.request_body_pipe.complete();
                 if(context.request_reception_finished()) {
                     return respond(connection, context);
                 }
-                printerr("Suspending for now...\n");
                 MHD.suspend_connection(connection);
                 return Result.YES;
             }
@@ -120,7 +113,6 @@ namespace Astralis {
                 error("Failed to start daemon");
             }
 
-            print(@"Server running on port $port\nPress Ctrl+C to stop...\n");
             new MainLoop().run();
         }