add explict routing example to `middleware` examples. updated readme to reference it

Commit 570b24b · patx · 2025-06-04T18:21:20-04:00

Changeset
570b24b0d429465af5497b733dddb899341a9713
Parents
343b2a8187b69dd6a83175c41368da25352c3fee

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 109c7c2..a37432d 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,7 @@ class MyApp(App):
 ```
 
 By default, MicroPie's route handlers can accept any request method, it's up to you how to handle any incoming requests! You can check the request method (and an number of other things specific to the current request state) in the handler with`self.request.method`. You can see how to handle POST JSON data at [examples/api](https://github.com/patx/micropie/tree/main/examples/api).
+You can use [middlware](https://github.com/patx/micropie#8-middleware) to add explicit routing when needed (often needed for complex APIs). See the [middleware router](https://github.com/patx/micropie/blob/main/examples/middleware/router.py) example.
 
 ### **3. Real-Time Communication with Socket.IO**
 Because of its designed simplicity, MicroPie does not handle WebSockets out of the box. While the underlying ASGI interface can theoretically handle WebSocket connections, MicroPie’s routing and request-handling logic is designed primarily for HTTP. While MicroPie does not natively support WebSockets, you can easily integrate dedicated Websockets libraries like **Socket.IO** alongside Uvicorn to handle real-time, bidirectional communication. Check out [examples/socketio](https://github.com/patx/micropie/tree/main/examples/socketio) to see this in action.
@@ -170,7 +171,7 @@ class MyApp(App):
 You also can use the `SessionBackend` class to create your own session backend. You can see an example of this in [examples/sessions](https://github.com/patx/micropie/tree/main/examples/sessions).
 
 ### **8. Middleware**
-MicroPie allows you to create pluggable middleware to hook into the request lifecycle. Take a look a trivial example using `HttpMiddleware` to send the console messages before and after the request is processed.
+MicroPie allows you to create pluggable middleware to hook into the request lifecycle. Take a look a trivial example using `HttpMiddleware` to send the console messages before and after the request is processed. Check out [examples/middleware](https://github.com/patx/micropie/tree/main/examples/middleware) to see more.
 ```python
 from MicroPie import App, HttpMiddleware
 
@@ -202,12 +203,12 @@ The best way to get an idea of how MicroPie works is to see it in action! Check
 - Template rendering
 - Custom HTTP request handling
 - File uploads
-- Serving static content with ServeStatic
+- Serving static content
 - Session usage
 - JSON Requests and Responses
-- Websockets with Socket.io
+- Socket.io Integration
 - Async Streaming
-- Middleware
+- Middleware, including rate limiting and explicit routing
 - Form handling and POST requests
 - And more
 
diff --git a/examples/middleware/app.py b/examples/middleware/rate_limit.py
similarity index 100%
rename from examples/middleware/app.py
rename to examples/middleware/rate_limit.py
diff --git a/examples/middleware/router.py b/examples/middleware/router.py
new file mode 100644
index 0000000..e6bb02d
--- /dev/null
+++ b/examples/middleware/router.py
@@ -0,0 +1,163 @@
+import re
+import inspect
+import json
+from typing import Callable, Dict, List, Optional, Tuple, Any
+from MicroPie import App, HttpMiddleware, Request
+
+class ExplicitRoutingMiddleware(HttpMiddleware):
+    def __init__(self):
+        # Registry to map route patterns to handler callables and HTTP methods
+        self.routes: Dict[str, Tuple[Callable, str, str]] = {}
+    
+    def add_route(self, path: str, handler: Callable, method: str = "GET") -> None:
+        """
+        Register an explicit route with its handler callable and HTTP method.
+        
+        Args:
+            path: The route pattern (e.g., "/api/users/{user}/records/{record}")
+            handler: The handler method callable (e.g., app.get_record)
+            method: The HTTP method (e.g., "GET", "POST")
+        """
+        # Convert path pattern to regex (e.g., "/api/users/{user}/records/{record}" -> "^/api/users/([^/]+)/records/([^/]+)$")
+        pattern = re.sub(r"{([^}]+)}", r"([^/]+)", path)
+        pattern = f"^{pattern}$"
+        self.routes[path] = (handler, method, pattern)
+    
+    async def before_request(self, request: Request) -> Optional[Dict]:
+        """
+        Match the request path against registered routes and dispatch to the handler.
+        
+        Args:
+            request: The MicroPie Request object
+        
+        Returns:
+            Optional response dict if handled, None to continue to implicit routing
+        """
+        path = request.scope["path"]
+        request_method = request.method
+        
+        for route_path, (handler, method, pattern) in self.routes.items():
+            if method != request_method:
+                continue
+            match = re.match(pattern, path)
+            if match:
+                # Extract path parameters
+                path_params = list(match.groups())
+                
+                # Build arguments based on handler signature
+                sig = inspect.signature(handler)
+                func_args = []
+                path_params_copy = path_params[:]
+                for param in sig.parameters.values():
+                    if param.name == "self":
+                        continue
+                    if param.kind == inspect.Parameter.VAR_POSITIONAL:
+                        func_args.extend(path_params_copy)
+                        path_params_copy = []
+                        continue
+                    if path_params_copy:
+                        func_args.append(path_params_copy.pop(0))
+                    elif param.default is not param.empty:
+                        func_args.append(param.default)
+                    else:
+                        return {
+                            "status_code": 400,
+                            "body": f"400 Bad Request: Missing required parameter '{param.name}'",
+                            "headers": []
+                        }
+                
+                try:
+                    # Call handler with path parameters
+                    result = await handler(*func_args) if inspect.iscoroutinefunction(handler) else handler(*func_args)
+                    status_code = 200
+                    response_body = result
+                    extra_headers = []
+                    
+                    # Handle tuple response (body, status_code, headers)
+                    if isinstance(result, tuple):
+                        status_code, response_body = result[0], result[1]
+                        extra_headers = result[2] if len(result) > 2 else []
+                    
+                    # Convert dict/list to JSON if needed
+                    if isinstance(response_body, (dict, list)):
+                        response_body = json.dumps(response_body)
+                        extra_headers.append(("Content-Type", "application/json"))
+                    
+                    return {
+                        "status_code": status_code,
+                        "body": response_body,
+                        "headers": extra_headers
+                    }
+                except Exception as e:
+                    print(f"Handler error: {e}")
+                    return {
+                        "status_code": 500,
+                        "body": "500 Internal Server Error",
+                        "headers": []
+                    }
+        
+        # No matching explicit route, proceed to implicit routing
+        return None
+    
+    async def after_request(
+        self,
+        request: Request,
+        status_code: int,
+        response_body: Any,
+        extra_headers: List[Tuple[str, str]]
+    ) -> Optional[Dict]:
+        """
+        Pass through the response unchanged.
+        
+        Args:
+            request: The MicroPie Request object
+            status_code: HTTP status code
+            response_body: Response body
+            extra_headers: List of response headers
+        
+        Returns:
+            None to pass through the response unchanged
+        """
+        return None
+
+# Example usage
+class MyApp(App):
+    def __init__(self):
+        super().__init__()
+        self.router = ExplicitRoutingMiddleware()
+        self.middlewares.append(self.router)
+        
+        # Register explicit routes
+        self.router.add_route("/api/users/{user}/records/{record}", self.get_record, "GET")
+        self.router.add_route("/api/users/{user}/records", self.create_record, "POST")
+        self.router.add_route("/api/users/{user}/records/{record}/details/subdetails", self.get_record_subdetails, "GET")
+        # Implicit route handled by MicroPie's default routing
+        # Note: /records/{user}/{record} will use implicit routing since not explicitly defined
+    
+    async def get_record(self, user: str, record: str):
+        try:
+            record_id = int(record)
+            # Access request via self.request if needed
+            return {"user": user, "record": record_id}
+        except ValueError:
+            return {"error": "Record must be an integer"}, 400
+    
+    async def create_record(self, user: str):
+        data = self.request.get_json
+        return {"user": user, "record": data.get("record_id"), "created": True}, 201
+    
+    async def get_record_subdetails(self, user: str, record: str):
+        try:
+            record_id = int(record)
+            return {"user": user, "record": record_id, "subdetails": "more detailed info"}
+        except ValueError:
+            return {"error": "Record must be an integer"}, 400
+    
+    async def records(self, user: str, record: str):
+        try:
+            record_id = int(record)
+            return {"user": user, "record": record_id, "implicit": True}
+        except ValueError:
+            return {"error": "Record must be an integer"}, 400
+
+app = MyApp()
diff --git a/examples/requests/app.py b/examples/requests/app.py
deleted file mode 100644
index 2698eba..0000000
--- a/examples/requests/app.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from MicroPie import App
-
-# Request handlers defined outside the class
-
-def get_handler():
-    return b"Hello, GET request received!"
-
-def post_handler():
-    return b"Hello, POST request received!"
-
-def put_handler():
-    return b"Hello, PUT request received!"
-
-def patch_handler():
-    return b"Hello, PATCH request received!"
-
-def delete_handler():
-    return b"Hello, DELETE request received!"
-
-def head_handler():
-    # Return status 200 with empty body and proper headers
-    return 200, b"", [("Content-Type", "text/html")]
-
-def options_handler():
-    # Return the allowed methods in the response header and message in bytes
-    return 200, b"Allowed methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD", [
-        ("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD")
-    ]
-
-# Define the custom server application
-class Root(App):
-
-    # Handle root URL requests and delegate based on HTTP method
-    def index(self):
-        return self.handle_request()
-
-    def handle_request(self):
-        # Map request methods to their corresponding handler functions
-        method_map = {
-            "GET": get_handler,
-            "POST": post_handler,
-            "PUT": put_handler,
-            "PATCH": patch_handler,
-            "DELETE": delete_handler,
-            "HEAD": head_handler,
-            "OPTIONS": options_handler,
-        }
-
-        # Check if the request method is supported and call the handler
-        if self.request.method in method_map:
-            response = method_map[self.scope['method']]()
-
-            # Ensure response is formatted correctly for WSGI
-            if isinstance(response, tuple):
-                status_code, response_body, headers = response
-            else:
-                status_code, response_body, headers = 200, response, [("Content-Type", "text/html")]
-
-            return status_code, response_body, headers
-
-        # Return 405 if the request method is not supported
-        return 405, b"405 Method Not Allowed", [("Content-Type", "text/html")]
-
-
-
-app = Root()