Billy Barrow 4 недель назад
Родитель
Сommit
04d5b51467

+ 14 - 8
examples/SimpleApi.vala

@@ -3,16 +3,20 @@ using Invercargill;
 using Invercargill.DataStructures;
 
 // Simple handler for /hello endpoint
-class HelloHandler : Object, RouteHandler {
-    public async HttpResult handle_route(HttpContext http_context, RouteContext route_context) throws Error {
+class HelloEndpoint : Object, Endpoint {
+    public string route { get { return "/hello"; } }
+    public Enumerable<Method> methods { owned get { return Iterate.these(Method.GET);} }
+    public async HttpResult handle_request(HttpContext http_context, RouteInformation route) throws Error {
         print("Handling /hello\n");
         return new HttpStringResult("Hello from Astralis!");
     }
 }
 
 // Simple handler for /json endpoint
-class JsonHandler : Object, RouteHandler {
-    public async HttpResult handle_route(HttpContext http_context, RouteContext route_context) throws Error {
+class JsonEndpoint : Object, Endpoint {
+    public string route { get { return "/json"; } }
+    public Enumerable<Method> methods { owned get { return Iterate.these(Method.GET);} }
+    public async HttpResult handle_request(HttpContext http_context, RouteInformation route) throws Error {
         print("Handling /json\n");
         return new HttpStringResult("{ \"message\": \"Hello JSON\" }")
             .set_header("Content-Type", "application/json");
@@ -20,11 +24,13 @@ class JsonHandler : Object, RouteHandler {
 }
 
 void main() {
-    var router = new Router();
-    var server = new Server(8080, router);
+    var router = new EndpointRouter()
+        .add_endpoint(new HelloEndpoint())
+        .add_endpoint(new JsonEndpoint());
     
-    router.map("/hello", new HelloHandler());
-    router.map("/json", new JsonHandler());
+    var pipeline = new Pipeline()
+        .add_component(router);
 
+    var server = new Server(8080, pipeline);
     server.run();
 }

+ 55 - 55
examples/meson.build

@@ -1,69 +1,69 @@
-# Query Parameters Example
-executable('query-parameters',
-    'QueryParameters.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Query Parameters Example
+# executable('query-parameters',
+#     'QueryParameters.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Path Routing Example
-executable('path-routing',
-    'PathRouting.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Path Routing Example
+# executable('path-routing',
+#     'PathRouting.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Headers and Cookies Example
-executable('headers-and-cookies',
-    'HeadersAndCookies.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Headers and Cookies Example
+# executable('headers-and-cookies',
+#     'HeadersAndCookies.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Form Data Example
-executable('form-data',
-    'FormData.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Form Data Example
+# executable('form-data',
+#     'FormData.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# JSON API Example
-executable('json-api',
-    'JsonApi.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # JSON API Example
+# executable('json-api',
+#     'JsonApi.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Data Structures Demo Example
-executable('data-structures-demo',
-    'DataStructuresDemo.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Data Structures Demo Example
+# executable('data-structures-demo',
+#     'DataStructuresDemo.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Enumerable Operations Example
-executable('enumerable-operations',
-    'EnumerableOperations.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Enumerable Operations Example
+# executable('enumerable-operations',
+#     'EnumerableOperations.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Error Handling Example
-executable('error-handling',
-    'ErrorHandling.vala',
-    dependencies: [astralis_dep, invercargill_dep],
-    install: false
-)
+# # Error Handling Example
+# executable('error-handling',
+#     'ErrorHandling.vala',
+#     dependencies: [astralis_dep, invercargill_dep],
+#     install: false
+# )
 
-# Original Simple API Example
+# # Original Simple API Example
 executable('simple-api',
     'SimpleApi.vala',
     dependencies: [astralis_dep, invercargill_dep],
     install: false
 )
 
-# Remote Address Example
-executable('remote-address',
-    'RemoteAddress.vala',
-    dependencies: [astralis_dep, invercargill_dep, json_glib_dep],
-    install: false
-)
+# # Remote Address Example
+# executable('remote-address',
+#     'RemoteAddress.vala',
+#     dependencies: [astralis_dep, invercargill_dep, json_glib_dep],
+#     install: false
+# )

+ 2 - 11
src/Core/Context.vala → src/Core/HttpContext.vala

@@ -7,7 +7,7 @@ namespace Astralis {
     public class HttpRequest : Object {
         // Basic request information
         public string raw_path { get; private set; }
-        public string method { get; private set; }
+        public Method method { get; private set; }
         public string version { get; private set; }
 
         // Path and routing
@@ -43,7 +43,7 @@ namespace Astralis {
             string? remote_address
         ) {
             this.raw_path = url;
-            this.method = method;
+            this.method = Method.from_string(method);
             this.version = version;
             this.headers = headers;
             this.request_body = request_body;
@@ -144,15 +144,6 @@ namespace Astralis {
             return cookies.has(name);
         }
 
-        // Utility methods
-        public bool is_get() { return method == "GET"; }
-        public bool is_post() { return method == "POST"; }
-        public bool is_put() { return method == "PUT"; }
-        public bool is_delete() { return method == "DELETE"; }
-        public bool is_patch() { return method == "PATCH"; }
-        public bool is_head() { return method == "HEAD"; }
-        public bool is_options() { return method == "OPTIONS"; }
-
         public bool is_form_urlencoded() {
             return content_type.contains("application/x-www-form-urlencoded");
         }

+ 0 - 0
src/Core/Response.vala → src/Core/HttpResult.vala


+ 27 - 1
src/Core/HttpValues.vala

@@ -10,7 +10,33 @@ namespace Astralis {
         DELETE,
         POST,
         PATCH,
-        CONNECT
+        CONNECT,
+        INVALID;
+
+        public static Method? from_string(string str) {
+            switch(str) {
+                case "GET":
+                    return Method.GET;
+                case "HEAD":
+                    return Method.HEAD;
+                case "OPTIONS":
+                    return Method.OPTIONS;
+                case "TRACE":
+                    return Method.TRACE;
+                case "PUT":
+                    return Method.PUT;
+                case "DELETE":
+                    return Method.DELETE;
+                case "POST":
+                    return Method.POST;
+                case "PATCH":
+                    return Method.PATCH;
+                case "CONNECT":
+                    return Method.CONNECT;
+                default:
+                    return Method.INVALID;
+            }
+        }
     }
 
     public enum StatusCode {

+ 84 - 0
src/Core/Pipeline.vala

@@ -0,0 +1,84 @@
+using Invercargill.DataStructures;
+namespace Astralis {
+
+    public class Pipeline {
+
+        private Series<PipelineComponentSource> component_sources =  new Series<PipelineComponentSource>();
+
+        public Pipeline add_component_source(PipelineComponentSource component_source) {
+            component_sources.add(component_source);
+            return this;
+        }
+
+        public Pipeline add_component(PipelineComponent component) {
+            var source = new SingletonPipelineComponentSource(component);
+            component_sources.add(source);
+            return this;
+        }
+
+        public async HttpResult run(HttpContext context) throws Error {
+            var components = component_sources
+                .attempt_select<PipelineComponent>(s => s.get_component())
+                .to_buffer();
+
+            if(components.length == 0) {
+                throw new PipelineError.NO_COMPONENTS("No components in pipeline");
+            }
+
+            var current_context = new PipelineContext(context, null, null);
+            for(uint i = components.length; i > 0; i--) {
+                current_context = new PipelineContext(context, components[i-1], current_context); 
+            }
+
+            var result = yield components[0].process_request(context, current_context);
+            return result;
+        }
+    }
+
+    public class PipelineContext {
+
+        private PipelineComponent? next_component;
+        private PipelineContext? next_context;
+        private HttpContext http_context;
+
+        internal PipelineContext(HttpContext http_context, PipelineComponent? next_component, PipelineContext? next_context) {
+            this.next_component = next_component;
+            this.http_context = http_context;
+            this.next_context = next_context;
+        }
+
+        public async HttpResult next() throws Error {
+            if(next_component == null) {
+                throw new PipelineError.END_OF_PIPELINE_REACHED("No more components in pipeline");
+            }
+            return yield next_component.process_request(http_context, next_context);
+        }
+
+    }
+
+    public interface PipelineComponent : Object {
+        public abstract async HttpResult process_request(HttpContext http_context, PipelineContext pipeline_context) throws Error;
+    }
+
+    public interface PipelineComponentSource : Object {
+        public abstract PipelineComponent get_component() throws Error;
+    }
+
+    public class SingletonPipelineComponentSource : Object, PipelineComponentSource {
+        public PipelineComponent instance { get; private set; }
+
+        public SingletonPipelineComponentSource(PipelineComponent instance) {
+            this.instance = instance;
+        }
+
+        public PipelineComponent get_component() {
+            return this.instance;
+        }
+    }
+
+    public errordomain PipelineError {
+        END_OF_PIPELINE_REACHED,
+        NO_COMPONENTS
+    }
+
+}

+ 0 - 8
src/Core/RequestHandler.vala

@@ -1,8 +0,0 @@
-
-namespace Astralis {
-
-    public interface RequestHandler : Object {
-        public abstract async HttpResult handle_request(HttpContext context) throws Error;
-    }
-    
-}

+ 127 - 0
src/Handlers/EndpointRouter.vala

@@ -0,0 +1,127 @@
+using Invercargill;
+using Invercargill.DataStructures;
+
+namespace Astralis {
+
+    public class EndpointRouter : Object, PipelineComponent {
+
+        private Series<EndpointSource> endpoint_sources = new Series<EndpointSource>();
+
+        public async HttpResult process_request(HttpContext http_context, PipelineContext pipeline_context) throws Error {
+            var path_components = http_context.request.path_components.to_vector();
+            var endpoint = endpoint_sources
+                .attempt_select<Endpoint>(s => s.get_endpoint())
+                .where(e => matches_endpoint(path_components, e))
+                .where(e => e.methods.any(m => m == http_context.request.method))
+                .first_or_default()
+                .unwrap();
+
+            if(endpoint == null) {
+                throw new EndpointError.ROUTE_NOT_FOUND(@"No route found for /$(path_components.to_string(null, "/"))");
+            }
+
+            var info = new RouteInformation() {
+                path_components = path_components,
+                named_components = extract_named_components(path_components, endpoint)
+            };
+            return yield endpoint.handle_request(http_context, info);
+        }
+
+        public EndpointRouter add_endpoint(Endpoint endpoint) {
+            this.endpoint_sources.add(new SingletonEndpointSource(endpoint));
+            return this;
+        }
+
+        public EndpointRouter add_endpoint_source(EndpointSource endpoint_source) {
+            this.endpoint_sources.add(endpoint_source);
+            return this;
+        }
+
+        private bool matches_endpoint(ReadOnlyAddressable<string> path_components, Endpoint endpoint) {
+            var endpoint_components = Wrap.array<string>(endpoint.route.split("/")).where(c => c.length != 0);
+            return path_components
+                .pair_up<string>(endpoint_components)
+                .all(p => {
+                    if(p.value1_is_set != p.value2_is_set) {
+                        return false;
+                    }
+                    if(p.value1.has_prefix("{") && p.value1.has_suffix("}")) {
+                        return true;
+                    }
+                    if(p.value1 == "*") {
+                        return true;
+                    }
+                    if(p.value1.contains("*")) {
+                        var parts = p.value1.split("*");
+                        if(!p.value2.has_prefix(parts[0]) || !p.value2.has_suffix(parts[parts.length-1])) {
+                            return false;
+                        }
+                        if(parts.length == 2) {
+                            return true;
+                        }
+                        var str_pos = parts[0].length;
+                        for(int i = 1; i < parts.length-1; i++) {
+                            var index = p.value2.index_of(parts[i], str_pos);
+                            if(index < 0) {
+                                return false;
+                            }
+                            str_pos += index + parts[i].length;
+                        }
+                        return true;
+                    }
+                    return p.value1 == p.value2;
+                });
+        }
+
+        private Dictionary<string, string> extract_named_components(ReadOnlyAddressable<string> path_components, Endpoint endpoint) throws IndexError {
+            var endpoint_components = endpoint.route.split("/");
+            var dictionary = new Dictionary<string, string>();
+            for(int i = 0; i < endpoint_components.length; i++) {
+                if(!endpoint_components[i].has_prefix("{") || !endpoint_components[i].has_suffix("}")) {
+                    continue;
+                }
+                dictionary.add(endpoint_components[i][1:-1], path_components[i]);
+            }
+            return dictionary;
+        }
+
+    }
+
+    public class RouteInformation {
+
+        public Vector<string> path_components { get; set; }
+        public Dictionary<string, string> named_components { get; set; }
+
+    }
+
+    public interface Endpoint : Object {
+
+        public abstract string route { get; }
+        public abstract Enumerable<Method> methods { owned get; }
+        public abstract async HttpResult handle_request(HttpContext http_context, RouteInformation route_context) throws Error;
+
+    }
+
+    public interface EndpointSource : Object {
+        public abstract Endpoint get_endpoint() throws Error;
+    }
+
+    public class SingletonEndpointSource : Object, EndpointSource {
+        public Endpoint instance { get; private set; }
+
+        public SingletonEndpointSource(Endpoint instance) {
+            this.instance = instance;
+        }
+
+        public Endpoint get_endpoint() {
+            return this.instance;
+        }
+    }
+
+    public errordomain EndpointError {
+        ROUTE_NOT_FOUND,
+        METHOD_NOT_ALLOWED
+    }
+
+
+}

+ 0 - 562
src/Handlers/Router.vala

@@ -1,562 +0,0 @@
-using Invercargill;
-using Invercargill.DataStructures;
-
-namespace Astralis {
-
-    /// <summary>
-    /// HTTP router that maps request paths to handlers with support for named segments and wildcards.
-    /// </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.map("/files/*.jpg", jpg_handler);              // Single wildcard with suffix
-    /// router.map("/files/*", file_handler);                  // Single wildcard (one segment)
-    /// router.map("/downloads/**", download_handler);         // Greedy wildcard (any depth)
-    /// router.map("/images/**.png", png_handler);             // Greedy wildcard with suffix
-    /// 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; }
-
-        public Router() {
-            not_found_handler = new DefaultNotFoundHandler();
-            error_handler = new DefaultErrorHandler();
-        }
-
-        /// <summary>
-        /// Registers a route handler for the specified path pattern.
-        /// 
-        /// Pattern syntax:
-        /// - Literal segments: "/users" matches exactly "/users"
-        /// - Named segments: "/users/{id}" captures the segment value as "id"
-        /// - Single wildcard (*): "/files/*" matches exactly one segment (e.g., "/files/test")
-        /// - Single wildcard with suffix (*.ext): "/files/*.jpg" matches one segment ending in .jpg
-        /// - Greedy wildcard (**): "/files/**" matches zero or more segments (e.g., "/files/a/b/c")
-        /// - Greedy wildcard with suffix (**.ext): "/files/**.jpg" matches paths ending in .jpg
-        /// </summary>
-        /// <param name="pattern">The path pattern to match</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
-            return yield not_found_handler.handle_request(context);
-        }
-    }
-
-    /// <summary>
-    /// Represents the type of a route segment.
-    /// </summary>
-    internal enum SegmentType {
-        /// <summary>Exact literal match required (e.g., "users")</summary>
-        LITERAL,
-        /// <summary>Named segment that captures its value (e.g., "{id}")</summary>
-        NAMED,
-        /// <summary>Single wildcard matching exactly one segment (e.g., "*")</summary>
-        SINGLE_WILDCARD,
-        /// <summary>Single wildcard with suffix requirement (e.g., "*.jpg")</summary>
-        SINGLE_WILDCARD_SUFFIX,
-        /// <summary>Greedy wildcard matching zero or more segments (e.g., "**")</summary>
-        GREEDY_WILDCARD,
-        /// <summary>Greedy wildcard with suffix requirement (e.g., "**.jpg")</summary>
-        GREEDY_WILDCARD_SUFFIX
-    }
-
-    /// <summary>
-    /// Represents a parsed route segment with its type and metadata.
-    /// </summary>
-    internal class ParsedSegment : Object {
-        public SegmentType segment_type;
-        public string value;
-        public string suffix;
-        
-        public ParsedSegment(SegmentType type, string value, string suffix = "") {
-            this.segment_type = type;
-            this.value = value;
-            this.suffix = suffix;
-        }
-    }
-
-    /// <summary>
-    /// Represents a single route with its pattern, handler, and allowed methods.
-    /// Supports wildcards: "*" for single segment, "**" for greedy multi-segment matching.
-    /// </summary>
-    internal class Route : Object {
-
-        private string _pattern;
-        private ParsedSegment[] _parsed_segments;
-        public RouteHandler handler { get; private set; }
-        private string[]? _allowed_methods;
-        
-        /// <summary>True if this route contains a greedy wildcard (**)</summary>
-        private bool _has_greedy_wildcard;
-        /// <summary>The suffix for greedy wildcard, if any (e.g., ".jpg" for "**.jpg")</summary>
-        private string _greedy_suffix = "";
-
-        public Route(string pattern, owned RouteHandler handler, string[]? methods = null) {
-            _pattern = pattern;
-            _handler = handler;
-            _allowed_methods = methods != null && methods.length > 0 ? methods : null;
-            _has_greedy_wildcard = false;
-            
-            // Parse pattern into segments
-            var segments = pattern.split("/");
-            _parsed_segments = new ParsedSegment[0];
-            
-            foreach (var segment in segments) {
-                if (segment != "") {
-                    var parsed = parse_segment(segment);
-                    _parsed_segments += parsed;
-                    
-                    if (parsed.segment_type == SegmentType.GREEDY_WILDCARD || 
-                        parsed.segment_type == SegmentType.GREEDY_WILDCARD_SUFFIX) {
-                        _has_greedy_wildcard = true;
-                        _greedy_suffix = parsed.suffix;
-                    }
-                }
-            }
-        }
-        
-        /// <summary>
-        /// Parses a single segment into its type and components.
-        /// </summary>
-        private ParsedSegment parse_segment(string segment) {
-            // Check for named segment {name}
-            if (segment.length >= 3 && segment[0] == '{' && segment[segment.length - 1] == '}') {
-                return new ParsedSegment(SegmentType.NAMED, segment.substring(1, segment.length - 2));
-            }
-            
-            // Check for greedy wildcard ** or **.suffix
-            if (segment.has_prefix("**")) {
-                string suffix = segment.substring(2);
-                if (suffix.length > 0) {
-                    return new ParsedSegment(SegmentType.GREEDY_WILDCARD_SUFFIX, "**", suffix);
-                }
-                return new ParsedSegment(SegmentType.GREEDY_WILDCARD, "**");
-            }
-            
-            // Check for single wildcard * or *.suffix
-            if (segment.has_prefix("*")) {
-                string suffix = segment.substring(1);
-                if (suffix.length > 0) {
-                    return new ParsedSegment(SegmentType.SINGLE_WILDCARD_SUFFIX, "*", suffix);
-                }
-                return new ParsedSegment(SegmentType.SINGLE_WILDCARD, "*");
-            }
-            
-            // Literal segment
-            return new ParsedSegment(SegmentType.LITERAL, segment);
-        }
-
-        /// <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;
-                }
-            }
-
-            // Use different matching strategy based on whether we have greedy wildcards
-            if (_has_greedy_wildcard) {
-                return match_with_greedy(path_segs);
-            } else {
-                return match_simple(path_segs);
-            }
-        }
-        
-        /// <summary>
-        /// Simple matching for routes without greedy wildcards.
-        /// Requires exact segment count match.
-        /// </summary>
-        private RouteContext? match_simple(string[] path_segs) {
-            // Check segment count - must match exactly
-            if (path_segs.length != _parsed_segments.length) {
-                return null;
-            }
-
-            // Match each segment
-            var named_segments = new Dictionary<string, string>();
-            var wildcard_captures = new Vector<string>();
-            
-            for (int i = 0; i < _parsed_segments.length; i++) {
-                var parsed = _parsed_segments[i];
-                var path_seg = path_segs[i];
-                
-                if (!match_segment(parsed, path_seg, named_segments, wildcard_captures)) {
-                    return null;
-                }
-            }
-
-            // Build route context
-            return build_context(path_segs, named_segments, wildcard_captures);
-        }
-        
-        /// <summary>
-        /// Matching for routes with greedy wildcards (**).
-        /// Allows variable segment count.
-        /// </summary>
-        private RouteContext? match_with_greedy(string[] path_segs) {
-            var named_segments = new Dictionary<string, string>();
-            var wildcard_captures = new Vector<string>();
-            
-            int path_index = 0;
-            int pattern_index = 0;
-            
-            while (pattern_index < _parsed_segments.length && path_index <= path_segs.length) {
-                var parsed = _parsed_segments[pattern_index];
-                
-                if (parsed.segment_type == SegmentType.GREEDY_WILDCARD || 
-                    parsed.segment_type == SegmentType.GREEDY_WILDCARD_SUFFIX) {
-                    // Find how many segments this greedy wildcard should consume
-                    int remaining_patterns = _parsed_segments.length - pattern_index - 1;
-                    int min_path_needed = remaining_patterns; // Minimum segments needed for remaining patterns
-                    
-                    // Check if we have enough path segments
-                    if (path_segs.length - path_index < min_path_needed) {
-                        return null;
-                    }
-                    
-                    // Calculate how many segments the greedy wildcard consumes
-                    int greedy_count = path_segs.length - path_index - min_path_needed;
-                    
-                    // For greedy with suffix, the last consumed segment must match the suffix
-                    if (parsed.segment_type == SegmentType.GREEDY_WILDCARD_SUFFIX) {
-                        if (greedy_count == 0) {
-                            // Need at least one segment to match the suffix
-                            return null;
-                        }
-                        int last_greedy_index = path_index + greedy_count - 1;
-                        if (last_greedy_index >= path_segs.length) {
-                            return null;
-                        }
-                        string last_seg = path_segs[last_greedy_index];
-                        if (!last_seg.has_suffix(parsed.suffix)) {
-                            return null;
-                        }
-                    }
-                    
-                    // Capture all segments consumed by greedy wildcard
-                    var greedy_segments = new string[0];
-                    for (int i = 0; i < greedy_count; i++) {
-                        string seg = path_segs[path_index + i];
-                        greedy_segments += seg;
-                        wildcard_captures.add(seg);
-                    }
-                    
-                    // Store the greedy capture as a single string with "/" separator
-                    if (greedy_segments.length > 0) {
-                        named_segments["*"] = string.joinv("/", greedy_segments);
-                    } else {
-                        named_segments["*"] = "";
-                    }
-                    
-                    path_index += greedy_count;
-                    pattern_index++;
-                } else {
-                    // Non-greedy segment - match normally
-                    if (path_index >= path_segs.length) {
-                        return null;
-                    }
-                    
-                    if (!match_segment(parsed, path_segs[path_index], named_segments, wildcard_captures)) {
-                        return null;
-                    }
-                    path_index++;
-                    pattern_index++;
-                }
-            }
-            
-            // Check if we've consumed all path segments and all patterns
-            if (path_index != path_segs.length || pattern_index != _parsed_segments.length) {
-                return null;
-            }
-
-            return build_context(path_segs, named_segments, wildcard_captures);
-        }
-        
-        /// <summary>
-        /// Matches a single path segment against a parsed pattern segment.
-        /// </summary>
-        private bool match_segment(ParsedSegment parsed, string path_seg, 
-                                   Dictionary<string, string> named_segments,
-                                   Vector<string> wildcard_captures) {
-            switch (parsed.segment_type) {
-                case SegmentType.LITERAL:
-                    return path_seg == parsed.value;
-                    
-                case SegmentType.NAMED:
-                    named_segments[parsed.value] = path_seg;
-                    return true;
-                    
-                case SegmentType.SINGLE_WILDCARD:
-                    // Matches any non-empty segment
-                    wildcard_captures.add(path_seg);
-                    return true;
-                    
-                case SegmentType.SINGLE_WILDCARD_SUFFIX:
-                    // Must end with the suffix
-                    if (path_seg.has_suffix(parsed.suffix)) {
-                        wildcard_captures.add(path_seg);
-                        return true;
-                    }
-                    return false;
-                    
-                default:
-                    return false;
-            }
-        }
-        
-        /// <summary>
-        /// Builds a RouteContext from the match results.
-        /// </summary>
-        private RouteContext build_context(string[] path_segs, 
-                                           Dictionary<string, string> named_segments,
-                                           Vector<string> wildcard_captures) {
-            var context = new RouteContext();
-            context.segments = Wrap.array<string>(path_segs).to_series();
-            context.named_segments = named_segments;
-            context.wildcard_captures = wildcard_captures.to_series();
-            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, named captures, and wildcard captures.
-    /// </summary>
-    public class RouteContext : Object {
-
-        /// <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"}.
-        /// 
-        /// For greedy wildcards (**), the key "*" contains the matched path segments
-        /// joined with "/" (e.g., for "/files/**" matching "/files/a/b/c", this would
-        /// contain {"*": "a/b/c"}).
-        /// </summary>
-        public Dictionary<string, string> named_segments { get; set; }
-
-        /// <summary>
-        /// All values captured by wildcard segments (* or **).
-        /// Each wildcard match adds its captured value(s) to this series.
-        /// 
-        /// For single wildcards (*), each matching segment is added individually.
-        /// For greedy wildcards (**), all consumed segments are added individually.
-        /// 
-        /// Example: Pattern "/files/*/*.jpg" matching "/files/images/photo.jpg"
-        /// would have wildcard_captures = ["images", "photo.jpg"]
-        /// </summary>
-        public Series<string> wildcard_captures { 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 {
-                value = named_segments.get(name);
-            } catch (Error e) {
-                // Key not found
-            }
-            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>
-        /// Gets the greedy wildcard capture value (from ** patterns).
-        /// This is a convenience method for getting the "*" key from named_segments.
-        /// </summary>
-        /// <returns>The greedy wildcard capture as a path string, or null if not present</returns>
-        public string? get_greedy_capture() {
-            return get_segment("*");
-        }
-    }
-
-    /// <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 {
-            return new HttpStringResult("Not Found", StatusCode.NOT_FOUND);
-        }
-    }
-
-    /// <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";
-            
-            return new HttpStringResult(message, StatusCode.INTERNAL_SERVER_ERROR);
-        }
-    }
-}

+ 5 - 5
src/Server/Server.vala

@@ -6,14 +6,14 @@ namespace Astralis {
 
     public class Server : Object {
         private Daemon daemon;
-        private RequestHandler handler;
+        private Pipeline pipeline;
         private int port;
         private HashSet<RequestContext> request_contexts;
         private HashSet<ResponseContext> response_contexts;
 
-        public Server(int port, RequestHandler handler) {
+        public Server(int port, Pipeline pipeline) {
             this.port = port;
-            this.handler = handler;
+            this.pipeline = pipeline;
             this.request_contexts = new HashSet<RequestContext>();
             this.response_contexts = new HashSet<ResponseContext>();
         }
@@ -66,9 +66,9 @@ namespace Astralis {
                 context.handler_context = http_context;
 
                 // Kick off the handler
-                handler.handle_request.begin(http_context, (obj, res) => {
+                pipeline.run.begin(http_context, (obj, res) => {
                     try {
-                        context.handler_result = handler.handle_request.end(res);
+                        context.handler_result = pipeline.run.end(res);
                     }
                     catch(Error e) {
                         context.handler_error = e;

+ 4 - 4
src/meson.build

@@ -1,12 +1,12 @@
 sources = files(
-    'Core/Context.vala',
+    'Core/HttpContext.vala',
     'Core/HttpValues.vala',
-    'Core/RequestHandler.vala',
-    'Core/Response.vala',
+    'Core/HttpResult.vala',
     'Core/AsyncInput.vala',
     'Core/AsyncOutput.vala',
+    'Core/Pipeline.vala',
     'Data/FormDataParser.vala',
-    'Handlers/Router.vala',
+    'Handlers/EndpointRouter.vala',
     'Server/Server.vala',
     'Server/RequestContext.vala',
     'Server/ResponseContext.vala',