AtlatestRepositorysigil-web

sigil-web / tree / src / sigil / webroutes.sgl

1;;; (sigil web routes) - Path-based Request Routing
2;;;
3;;; Provides routing functionality for web applications.
4;;; Routes are matched in order; first match wins.
5;;;
6;;; Example:
7;;; (define app
8;;; (router
9;;; (route method: GET pattern: "/" handler: home-handler)
10;;; (route method: GET pattern: "/users/:id" handler: user-handler)
11;;; (route method: POST pattern: "/api/login" handler: login-handler)
12;;; (route method: ANY pattern: "/static/*path" handler: static-handler)))
13;;;
14;;; `method:` defaults to POST when omitted.
16(define-library (sigil web routes)
17 (import (sigil core)
18 (sigil string)
19 (sigil struct)
20 (sigil http request))
22 (export
23 ;; HTTP method symbols
24 GET POST PUT DELETE PATCH OPTIONS HEAD ANY
26 ;; Route creation
27 route
28 route-method
29 route-pattern
30 route-handler
32 ;; Router creation
33 router
34 make-router
36 ;; Path parameter extraction
37 path-param
38 path-params
40 ;; Route combination
41 routes)
43 (begin
45 ;; ============================================================
46 ;; Route Record
47 ;; ============================================================
49 ;;; A route consists of a method, pattern, and handler.
50 ;;;
51 ;;; `method:` is an HTTP-method symbol (`GET`, `POST`, `ANY`, ...) and
52 ;;; defaults to `POST` when omitted. A `"post"` string is accepted too and
53 ;;; normalized at match time.
54 (define-struct route
55 (method default: 'POST) ; Symbol: 'GET, 'POST, 'ANY, etc. (or string)
56 (pattern) ; String: "/users/:id" or "/static/*path"
57 (handler)) ; Procedure: (request) -> response
59 ;; ============================================================
60 ;; Pattern Matching
61 ;; ============================================================
63 ;;; Parse a route pattern into segments
64 ;;; Returns list of: string (literal), (:param name), (*splat name)
65 (define (parse-pattern pattern)
66 (let ((segments (string-split pattern "/")))
67 ;; Filter empty strings first, then parse each segment
68 (map parse-segment
69 (filter (lambda (s) (not (string=? s ""))) segments))))
71 ;;; Parse a single path segment
72 (define (parse-segment seg)
73 (cond
74 ((and (> (string-length seg) 0)
75 (char=? (string-ref seg 0) #\:))
76 ;; Named parameter :id -> (:param "id")
77 (list ':param (substring seg 1 (string-length seg))))
78 ((and (> (string-length seg) 0)
79 (char=? (string-ref seg 0) #\*))
80 ;; Splat parameter *path -> (*splat "path")
81 (list '*splat (substring seg 1 (string-length seg))))
82 (else
83 ;; Literal segment
84 seg)))
86 ;;; Match a path against a parsed pattern
87 ;;; Returns alist of params on match, #f on no match
88 (define (match-pattern parsed-pattern path)
89 (let ((path-segments (filter (lambda (s) (not (string=? s "")))
90 (string-split path "/"))))
91 (match-segments parsed-pattern path-segments '())))
93 ;;; Match path segments against pattern segments
94 (define (match-segments pattern-segs path-segs params)
95 (cond
96 ;; Both empty - match!
97 ((and (null? pattern-segs) (null? path-segs))
98 params)
99 ;; Pattern empty but path has more - no match
100 ((null? pattern-segs)
101 #f)
102 ;; Check first pattern segment
103 (else
104 (let ((pat (car pattern-segs)))
105 (cond
106 ;; Splat - captures rest of path
107 ((and (pair? pat) (eq? (car pat) '*splat))
108 (let ((name (cadr pat))
109 (rest-path (string-join path-segs "/")))
110 (cons (cons name rest-path) params)))
111 ;; Path empty but pattern wants more - no match (unless splat handled above)
112 ((null? path-segs)
113 #f)
114 ;; Named parameter - captures one segment
115 ((and (pair? pat) (eq? (car pat) ':param))
116 (let ((name (cadr pat)))
117 (match-segments (cdr pattern-segs)
118 (cdr path-segs)
119 (cons (cons name (car path-segs)) params))))
120 ;; Literal - must match exactly
121 ((string? pat)
122 (if (string=? pat (car path-segs))
123 (match-segments (cdr pattern-segs)
124 (cdr path-segs)
125 params)
126 #f))
127 ;; Unknown pattern type
128 (else #f))))))
130 ;; ============================================================
131 ;; Router
132 ;; ============================================================
134 ;;; Normalize an HTTP method to an upcased symbol.
135 ;;; Accepts a symbol ('GET, 'post) or string ("get", "POST").
136 (define (normalize-method m)
137 (cond
138 ((symbol? m) (string->symbol (string-upcase (symbol->string m))))
139 ((string? m) (string->symbol (string-upcase m)))
140 (else m)))
142 ;;; Does a route's method match a request's method?
143 ;;; 'ANY matches everything; otherwise compare normalized methods so
144 ;;; symbol/string and case differences don't matter.
145 (define (method-matches? route-meth req-meth)
146 (or (eq? route-meth 'ANY)
147 (eq? (normalize-method route-meth)
148 (normalize-method req-meth))))
150 ;;; Create a router from a list of routes
151 ;;; Returns a handler function: (request) -> response or #f
152 (define (make-router routes)
153 (: list? -> procedure?)
154 (lambda (request)
155 (let ((method (http-request-method request))
156 (path (http-request-path request)))
157 (let loop ((routes routes))
158 (if (null? routes)
159 #f ; No route matched
160 (let* ((r (car routes))
161 (route-meth (route-method r))
162 (pattern (route-pattern r))
163 (handler (route-handler r)))
164 (if (method-matches? route-meth method)
165 (let ((params (match-pattern (parse-pattern pattern) path)))
166 (if params
167 ;; Match! Add params to request context and call handler
168 (let ((req-with-params
169 (http-request-with-context
170 request
171 path-params:
172 params)))
173 (handler req-with-params))
174 ;; Pattern didn't match, try next route
175 (loop (cdr routes))))
176 ;; Method didn't match, try next route
177 (loop (cdr routes)))))))))
179 ;;; Convenience function for creating routers.
180 ;;; Usage: (router (route method: GET pattern: "/" handler: handler1)
181 ;;; (route method: POST pattern: "/api" handler: handler2))
182 (define (router . routes)
183 (: any? ... -> procedure?)
184 (make-router routes))
186 ;; ============================================================
187 ;; Path Parameter Access
188 ;; ============================================================
190 ;;; Get a path parameter from request
191 ;;; Returns value or #f if not found
192 (define (path-param request name)
193 (: any? string? -> any?)
194 (let ((params (http-request-context-ref request path-params: '())))
195 (let ((pair (assoc name params)))
196 (if pair (cdr pair) #f))))
198 ;;; Get all path parameters from request
199 (define (path-params request)
200 (: any? -> list?)
201 (http-request-context-ref request path-params: '()))
203 ;; ============================================================
204 ;; HTTP Method Symbols
205 ;; ============================================================
207 (define GET 'GET)
208 (define POST 'POST)
209 (define PUT 'PUT)
210 (define DELETE 'DELETE)
211 (define PATCH 'PATCH)
212 (define OPTIONS 'OPTIONS)
213 (define HEAD 'HEAD)
214 (define ANY 'ANY)
216 ;; ============================================================
217 ;; Route Combination
218 ;; ============================================================
220 ;;; Combine multiple handlers into a single handler.
221 ;;;
222 ;;; Returns the first non-#f response. Use with the threading macro
223 ;;; to build middleware chains:
224 ;;;
225 ;;; ```scheme
226 ;;; (-> (routes api-routes page-routes static-handler)
227 ;;; (with-logging)
228 ;;; (with-not-found))
229 ;;; ```
230 (define (routes . handlers)
231 (: procedure? ... -> procedure?)
232 (lambda (request)
233 (let loop ((handlers handlers))
234 (if (null? handlers)
235 #f
236 (let ((response ((car handlers) request)))
237 (if response
238 response
239 (loop (cdr handlers))))))))
241 ))