|
|
@@ -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
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
}
|