Files
rose-ash/shared/sx/ref/router.sx
giles 2f42e8826c Add :effects annotations to all spec files and update bootstrappers
Bootstrappers (bootstrap_py.py, js.sx) now skip :effects keyword in
define forms, enabling effect annotations throughout the spec without
changing generated output.

Annotated 180+ functions across 14 spec files:
- signals.sx: signal/deref [] pure, reset!/swap!/effect/batch [mutation]
- engine.sx: parse-* [] pure, morph-*/swap-* [mutation io]
- orchestration.sx: all [mutation io] (browser event binding)
- adapter-html.sx: render-* [render]
- adapter-dom.sx: render-* [render], reactive-* [render mutation]
- adapter-sx.sx: aser-* [render]
- adapter-async.sx: async-render-*/async-aser-* [render io]
- parser.sx: all [] pure
- render.sx: predicates [] pure, process-bindings [mutation]
- boot.sx: all [mutation io] (browser init)
- deps.sx: scan-*/transitive-* [] pure, compute-all-* [mutation]
- router.sx: all [] pure (URL matching)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:22:34 +00:00

127 lines
5.0 KiB
Plaintext

;; ==========================================================================
;; router.sx — Client-side route matching specification
;;
;; Pure functions for matching URL paths against Flask-style route patterns.
;; Used by client-side routing to determine if a page can be rendered
;; locally without a server roundtrip.
;;
;; All functions are pure — no IO, no platform-specific operations.
;; Uses only primitives from primitives.sx (string ops, list ops).
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Split path into segments
;; --------------------------------------------------------------------------
;; "/docs/hello" → ("docs" "hello")
;; "/" → ()
;; "/docs/" → ("docs")
(define split-path-segments :effects []
(fn ((path :as string))
(let ((trimmed (if (starts-with? path "/") (slice path 1) path)))
(let ((trimmed2 (if (and (not (empty? trimmed))
(ends-with? trimmed "/"))
(slice trimmed 0 (- (len trimmed) 1))
trimmed)))
(if (empty? trimmed2)
(list)
(split trimmed2 "/"))))))
;; --------------------------------------------------------------------------
;; 2. Parse Flask-style route pattern into segment descriptors
;; --------------------------------------------------------------------------
;; "/docs/<slug>" → ({"type" "literal" "value" "docs"}
;; {"type" "param" "value" "slug"})
(define make-route-segment :effects []
(fn ((seg :as string))
(if (and (starts-with? seg "<") (ends-with? seg ">"))
(let ((param-name (slice seg 1 (- (len seg) 1))))
(let ((d {}))
(dict-set! d "type" "param")
(dict-set! d "value" param-name)
d))
(let ((d {}))
(dict-set! d "type" "literal")
(dict-set! d "value" seg)
d))))
(define parse-route-pattern :effects []
(fn ((pattern :as string))
(let ((segments (split-path-segments pattern)))
(map make-route-segment segments))))
;; --------------------------------------------------------------------------
;; 3. Match path segments against parsed pattern
;; --------------------------------------------------------------------------
;; Returns params dict if match, nil if no match.
(define match-route-segments :effects []
(fn ((path-segs :as list) (parsed-segs :as list))
(if (not (= (len path-segs) (len parsed-segs)))
nil
(let ((params {})
(matched true))
(for-each-indexed
(fn ((i :as number) (parsed-seg :as dict))
(when matched
(let ((path-seg (nth path-segs i))
(seg-type (get parsed-seg "type")))
(cond
(= seg-type "literal")
(when (not (= path-seg (get parsed-seg "value")))
(set! matched false))
(= seg-type "param")
(dict-set! params (get parsed-seg "value") path-seg)
:else
(set! matched false)))))
parsed-segs)
(if matched params nil)))))
;; --------------------------------------------------------------------------
;; 4. Public API: match a URL path against a pattern string
;; --------------------------------------------------------------------------
;; Returns params dict (may be empty for exact matches) or nil.
(define match-route :effects []
(fn ((path :as string) (pattern :as string))
(let ((path-segs (split-path-segments path))
(parsed-segs (parse-route-pattern pattern)))
(match-route-segments path-segs parsed-segs))))
;; --------------------------------------------------------------------------
;; 5. Search a list of route entries for first match
;; --------------------------------------------------------------------------
;; Each entry: {"pattern" "/docs/<slug>" "parsed" [...] "name" "docs-page" ...}
;; Returns matching entry with "params" added, or nil.
(define find-matching-route :effects []
(fn ((path :as string) (routes :as list))
(let ((path-segs (split-path-segments path))
(result nil))
(for-each
(fn ((route :as dict))
(when (nil? result)
(let ((params (match-route-segments path-segs (get route "parsed"))))
(when (not (nil? params))
(let ((matched (merge route {})))
(dict-set! matched "params" params)
(set! result matched))))))
routes)
result)))
;; --------------------------------------------------------------------------
;; Platform interface — none required
;; --------------------------------------------------------------------------
;; All functions use only pure primitives:
;; split, slice, starts-with?, ends-with?, len, empty?,
;; map, for-each, for-each-indexed, nth, get, dict-set!, merge,
;; list, nil?, not, =
;; --------------------------------------------------------------------------