1 # Prepared HTTP routes with exact paths, validated registration, and path-first method selection. Source: https://github.com/paymog/bend-kit/tree/main/router 2 import Base 3 import ./target.bend as Target 4 5 type Segment is Data: 6 Literal{text: String} 7 Parameter{name: String} 8 9 type Entry<-A: Data> is Data: 10 Entry{method: String, pattern: String, action: A, groups: List<&2, U32>} 11 12 type Handler<-A: Data> is Data: 13 Handler{method: String, pattern: String, segments: List<&2, Segment>, action: A} 14 15 type Path<-A: Data> is Data: 16 Path{segments: List<&2, Segment>, handlers: List<&2, Handler>, groups: List<&2, U32>, allow: String} 17 18 type Table<-A: Data> is Data: 19 Table{paths: List<&2, Path>, allow: String} 20 21 type Error is Data: 22 InvalidPattern{pattern: String} 23 InvalidMethod{method: String} 24 Duplicate{method: String, pattern: String} 25 GroupConflict{pattern: String} 26 BadTarget{} 27 28 type Choice<-A: Data> is Data: 29 Found{action: A, params: Map<&2, String>, groups: List<&2, U32>, method: String, pattern: String} 30 MethodMissing{groups: List<&2, U32>, allow: String} 31 Options{groups: List<&2, U32>, allow: String} 32 NotFound{} 33 34 type Resolved<-A: Data> is Data: 35 Resolved{target: Target.Target, choice: Choice} 36 37 type Hidden is Data: 38 Hidden{pattern: String, methods: String} 39 40 type Description is Data: 41 Description{method: String, pattern: String, groups: List<&2, U32>, hidden: List<&2, Hidden>} 42 43 def scalar(+c: U32) -> Bool: 44 Bool.and(U32.is_le(c, 1114111), Bool.not(Target.between(c, 55296, 57343))) 45 46 def literal.valid(s: String) -> Bool: 47 match s: 48 case SNil{}: 49 True{} 50 case SCon{Chr{+c}, t}: 51 Bool.and(Bool.and(scalar(c), Bool.and(U32.is_ne(c, 0), U32.is_ne(c, 47))), literal.valid(t)) 52 53 def name.tail(s: String) -> Bool: 54 match s: 55 case SNil{}: 56 True{} 57 case SCon{Chr{+c}, t}: 58 Bool.and(Bool.or(Target.alpha(c), Bool.or(Target.between(c, 48, 57), U32.is_eq(c, 95))), name.tail(t)) 59 60 def name.valid(s: String) -> Bool: 61 match s: 62 case SNil{}: 63 False{} 64 case SCon{Chr{+c}, t}: 65 Bool.and(Bool.or(Target.alpha(c), U32.is_eq(c, 95)), name.tail(t)) 66 67 def member(xs: List<&2, String>, +s: String) -> Bool: 68 match xs: 69 case Nil{}: 70 False{} 71 case h <> t: 72 Bool.or(String.eq(h, s), member(t, s)) 73 74 def pattern.segment(s: String, names: List<&2, String>) -> Maybe<&2, Segment>: 75 match s: 76 case SCon{Chr{58}, +t}: 77 Bool.pick(Maybe<&2, Segment>, Bool.and(name.valid(t), Bool.not(member(names, t))), Some{Parameter{t}}, None{}) 78 case SNil{}: 79 Some{Literal{""}} 80 case SCon{+h, +t}: 81 +lit = {SCon{h, t} : String} 82 Bool.pick(Maybe<&2, Segment>, literal.valid(lit), Some{Literal{lit}}, None{}) 83 84 def pattern.names(segment: Segment, names: List<&2, String>) -> List<&2, String>: 85 match segment: 86 case Literal{text}: 87 names 88 case Parameter{name}: 89 name <> names 90 91 def pattern.parts(xs: List<&2, String>, +names: List<&2, String>, acc: List<&2, Segment>) -> Maybe<&2, List<&2, Segment>>: 92 match xs: 93 case Nil{}: 94 Some{List.reverse(&2, Segment, acc)} 95 case "" <> Nil{}: 96 Some{List.reverse(&2, Segment, Literal{""} <> acc)} 97 case "" <> t: 98 None{} 99 case h <> t: 100 do Maybe<&2, List<&2, Segment>>: 101 seg : Segment <- pattern.segment(h, names) 102 +part : Segment = seg 103 pattern.parts(t, pattern.names(part, names), part <> acc) 104 105 def pattern(s: String) -> Maybe<&2, List<&2, Segment>>: 106 match s: 107 case "/": 108 Some{Nil{}} 109 case SCon{Chr{47}, t}: 110 pattern.parts(String.split(t, '/'), Nil{}, Nil{}) 111 case _: 112 None{} 113 114 def token(+c: U32) -> Bool: 115 Bool.or(Target.alpha(c), Bool.or(Target.between(c, 48, 57), Bool.or(U32.is_eq(c, 33), Bool.or(Target.between(c, 35, 39), Bool.or(Target.between(c, 42, 43), Bool.or(Target.between(c, 45, 46), Bool.or(Target.between(c, 94, 96), Bool.or(U32.is_eq(c, 124), U32.is_eq(c, 126))))))))) 116 117 def method.tail(s: String) -> Bool: 118 match s: 119 case SNil{}: 120 True{} 121 case SCon{Chr{c}, t}: 122 Bool.and(token(c), method.tail(t)) 123 124 def method.valid(s: String) -> Bool: 125 match s: 126 case SNil{}: 127 False{} 128 case SCon{Chr{c}, t}: 129 Bool.and(token(c), method.tail(t)) 130 131 def continue(ok: Bool, later: Unit -> Bool) -> Bool: 132 match ok: 133 case False{}: 134 False{} 135 case True{}: 136 later(Unit{}) 137 138 def matches(pat: List<&2, Segment>, got: List<&2, String>) -> Bool: 139 match pat got: 140 case Nil{} Nil{}: 141 True{} 142 case Literal{p} <> pt g <> gt: 143 continue(String.eq(p, g), u => matches(pt, gt)) 144 case Parameter{name} <> pt +g <> gt: 145 continue(Bool.not(String.is_empty(g)), u => matches(pt, gt)) 146 case _ _: 147 False{} 148 149 def capture(pat: List<&2, Segment>, got: List<&2, String>, acc: Map<&2, String>) -> Map<&2, String>: 150 match pat got: 151 case Parameter{name} <> pt g <> gt: 152 capture(pt, gt, Map.set(&2, String, acc, name, g)) 153 case Literal{text} <> pt g <> gt: 154 capture(pt, gt, acc) 155 case _ _: 156 acc 157 158 def shape(a: List<&2, Segment>, b: List<&2, Segment>) -> Bool: 159 match a b: 160 case Nil{} Nil{}: 161 True{} 162 case Literal{x} <> at Literal{y} <> bt: 163 continue(String.eq(x, y), u => shape(at, bt)) 164 case Parameter{x} <> at Parameter{y} <> bt: 165 shape(at, bt) 166 case _ _: 167 False{} 168 169 def specific(a: List<&2, Segment>, b: List<&2, Segment>) -> Bool: 170 match a b: 171 case Literal{x} <> at Parameter{y} <> bt: 172 True{} 173 case Parameter{x} <> at Literal{y} <> bt: 174 False{} 175 case Literal{x} <> at Literal{y} <> bt: 176 specific(at, bt) 177 case Parameter{x} <> at Parameter{y} <> bt: 178 specific(at, bt) 179 case _ _: 180 False{} 181 182 def groups.eq(a: List<&2, U32>, b: List<&2, U32>) -> Bool: 183 match a b: 184 case Nil{} Nil{}: 185 True{} 186 case x <> at y <> bt: 187 continue(U32.is_eq(x, y), u => groups.eq(at, bt)) 188 case _ _: 189 False{} 190 191 def handler.has(~A: Data, xs: List<&2, Handler>, +method: String) -> Bool: 192 match xs: 193 case Nil{}: 194 False{} 195 case Handler{want, pattern, segments, action} <> t: 196 Bool.or(String.eq(want, method), handler.has(~A, t, method)) 197 198 def insert.merge(~A: Data, duplicate: Bool, same_groups: Bool, entry: Handler, segments: List<&2, Segment>, handlers: List<&2, Handler>, groups: List<&2, U32>, rest: List<&2, Path>) -> Result<&2, &2, Error, List<&2, Path>>: 199 match duplicate same_groups: 200 case True{} _: 201 Handler{method, pattern, ss, action} = entry 202 Fail{Duplicate{method, pattern}} 203 case False{} False{}: 204 Handler{method, pattern, ss, action} = entry 205 Fail{GroupConflict{pattern}} 206 case False{} True{}: 207 Done{Path{segments, entry <> handlers, groups, ""} <> rest} 208 209 def insert.branch(~A: Data, same: Bool, segments: List<&2, Segment>, +handlers: List<&2, Handler>, +old_groups: List<&2, U32>, allowed: String, tail: List<&2, Path>, entry: Handler, groups: List<&2, U32>, rest: Unit -> Result<&2, &2, Error, List<&2, Path>>) -> Result<&2, &2, Error, List<&2, Path>>: 210 match same: 211 case True{}: 212 Handler{+method, pattern, ss, action} = entry 213 insert.merge(~A, handler.has(~A, handlers, method), groups.eq(old_groups, groups), Handler{method, pattern, ss, action}, segments, handlers, old_groups, tail) 214 case False{}: 215 do Result<&2, &2, Error, List<&2, Path>>: 216 remaining : List<&2, Path> <- rest(Unit{}) 217 return Path{segments, handlers, old_groups, allowed} <> remaining 218 219 # ponytail: preparation merges shapes in O(routes²); replace with a shape index if construction becomes a bottleneck. 220 def insert(~A: Data, paths: List<&2, Path>, +entry: Handler, +groups: List<&2, U32>) -> Result<&2, &2, Error, List<&2, Path>>: 221 match paths: 222 case Nil{}: 223 Handler{method, pattern, +ss, action} = entry 224 Done{[Path{ss, [Handler{method, pattern, ss, action}], groups, ""}]} 225 case Path{+ss, +handlers, +old_groups, +allow} <> +t: 226 Handler{+method, +pattern, +es, +action} = entry 227 candidate = {Handler{method, pattern, es, action} : Handler} 228 insert.branch(~A, shape(ss, es), ss, handlers, old_groups, allow, t, candidate, groups, u => insert(~A, t, Handler{method, pattern, es, action}, groups)) 229 230 def method.prepend(get: Bool, method: String, tail: List<&2, String>) -> List<&2, String>: 231 match get: 232 case True{}: 233 method <> "HEAD" <> tail 234 case False{}: 235 method <> tail 236 237 def method.names(~A: Data, xs: List<&2, Handler>) -> List<&2, String>: 238 match xs: 239 case Nil{}: 240 ["OPTIONS"] 241 case Handler{+method, pattern, ss, action} <> t: 242 method.prepend(String.eq(method, "GET"), method, method.names(~A, t)) 243 244 def unique(xs: List<&2, String>, prev: String, +acc: List<&2, String>) -> List<&2, String>: 245 match xs: 246 case Nil{}: 247 List.reverse(&2, String, acc) 248 case +h <> t: 249 unique(t, h, Bool.pick(List<&2, String>, String.eq(h, prev), acc, h <> acc)) 250 251 def allow(xs: List<&2, String>) -> String: 252 String.join(unique(List.sort(~String, ~String.is_le, xs), "", Nil{}), ", ") 253 254 def finish.paths(~A: Data, paths: List<&2, Path>) -> List<&2, Path>: 255 match paths: 256 case Nil{}: 257 Nil{} 258 case Path{segments, +handlers, groups, old_allow} <> t: 259 Path{segments, handlers, groups, allow(method.names(~A, handlers))} <> finish.paths(~A, t) 260 261 def all.methods(~A: Data, paths: List<&2, Path>) -> List<&2, String>: 262 match paths: 263 case Nil{}: 264 ["OPTIONS"] 265 case Path{ss, handlers, groups, allowed} <> t: 266 List.append(&2, String, method.names(~A, handlers), all.methods(~A, t)) 267 268 def prepare.pattern(~A: Data, parsed: Maybe<&2, List<&2, Segment>>, method: String, pattern: String, action: A, groups: List<&2, U32>, paths: List<&2, Path>) -> Result<&2, &2, Error, List<&2, Path>>: 269 match parsed: 270 case None{}: 271 Fail{InvalidPattern{pattern}} 272 case Some{ss}: 273 insert(~A, paths, Handler{method, pattern, ss, action}, groups) 274 275 def prepare.entry(~A: Data, valid: Bool, entry: Entry, paths: List<&2, Path>) -> Result<&2, &2, Error, List<&2, Path>>: 276 match valid: 277 case False{}: 278 Entry{method, p, action, groups} = entry 279 Fail{InvalidMethod{method}} 280 case True{}: 281 Entry{method, +p, action, groups} = entry 282 prepare.pattern(~A, pattern(p), method, p, action, groups, paths) 283 284 def prepare.go(~A: Data, entries: List<&2, Entry>, paths: List<&2, Path>) -> Result<&2, &2, Error, Table>: 285 match entries: 286 case Nil{}: 287 +prepared = finish.paths(~A, paths) 288 Done{Table{prepared, allow(all.methods(~A, prepared))}} 289 case Entry{+method, p, action, groups} <> t: 290 do Result<&2, &2, Error, Table>: 291 next : List<&2, Path> <- prepare.entry(~A, method.valid(method), Entry{method, p, action, groups}, paths) 292 prepare.go(~A, t, next) 293 294 def prepare(~A: Data, entries: List<&2, Entry>) -> Result<&2, &2, Error, Table>: 295 prepare.go(~A, entries, Nil{}) 296 297 def best.compare(~A: Data, candidate: Path, old: Path) -> Maybe<&2, Path>: 298 Path{+cs, +ch, +cg, +ca} = candidate 299 Path{+ss, +handlers, +groups, +allowed} = old 300 Bool.pick(Maybe<&2, Path>, specific(cs, ss), Some{Path{cs, ch, cg, ca}}, Some{Path{ss, handlers, groups, allowed}}) 301 302 def best.pick(~A: Data, best: Maybe<&2, Path>, candidate: Path) -> Maybe<&2, Path>: 303 match best: 304 case None{}: 305 Some{candidate} 306 case Some{old}: 307 best.compare(~A, candidate, old) 308 309 def best.step(~A: Data, matched: Bool, candidate: Path, best: Maybe<&2, Path>) -> Maybe<&2, Path>: 310 match matched: 311 case False{}: 312 best 313 case True{}: 314 best.pick(~A, best, candidate) 315 316 # ponytail: lookup scans prepared paths in O(routes × segments); use a trie if measured dispatch cost warrants it. 317 def best(~A: Data, paths: List<&2, Path>, +segments: List<&2, String>, acc: Maybe<&2, Path>) -> Maybe<&2, Path>: 318 match paths: 319 case Nil{}: 320 acc 321 case Path{+ss, handlers, groups, allowed} <> t: 322 best(~A, t, segments, best.step(~A, matches(ss, segments), Path{ss, handlers, groups, allowed}, acc)) 323 324 def handler.next(~A: Data, same: Bool, here: Handler, later: Unit -> Maybe<&2, Handler>) -> Maybe<&2, Handler>: 325 match same: 326 case True{}: 327 Some{here} 328 case False{}: 329 later(Unit{}) 330 331 def handler(~A: Data, xs: List<&2, Handler>, +method: String) -> Maybe<&2, Handler>: 332 match xs: 333 case Nil{}: 334 None{} 335 case Handler{+want, pattern, ss, action} <> t: 336 handler.next(~A, String.eq(want, method), Handler{want, pattern, ss, action}, u => handler(~A, t, method)) 337 338 def chosen(~A: Data, hit: Maybe<&2, Handler>, segments: List<&2, String>, +groups: List<&2, U32>, +allowed: String, options: Bool) -> Choice: 339 match hit: 340 case Some{Handler{method, pattern, ss, action}}: 341 Found{action, capture(ss, segments, Map.new(&2, String)), groups, method, pattern} 342 case None{}: 343 Bool.pick(Choice, options, Options{groups, allowed}, MethodMissing{groups, allowed}) 344 345 def choose.fallback(~A: Data, head: Bool, handlers: List<&2, Handler>, method: String, segments: List<&2, String>, groups: List<&2, U32>, allowed: String) -> Choice: 346 match head: 347 case True{}: 348 chosen(~A, handler(~A, handlers, "GET"), segments, groups, allowed, False{}) 349 case False{}: 350 chosen(~A, None{}, segments, groups, allowed, String.eq(method, "OPTIONS")) 351 352 def choose.method(~A: Data, direct: Maybe<&2, Handler>, +handlers: List<&2, Handler>, +method: String, segments: List<&2, String>, groups: List<&2, U32>, allowed: String) -> Choice: 353 match direct: 354 case Some{h}: 355 chosen(~A, Some{h}, segments, groups, allowed, False{}) 356 case None{}: 357 choose.fallback(~A, String.eq(method, "HEAD"), handlers, method, segments, groups, allowed) 358 359 def choose.path(~A: Data, path: Maybe<&2, Path>, +method: String, segments: List<&2, String>) -> Choice: 360 match path: 361 case None{}: 362 NotFound{} 363 case Some{Path{ss, +handlers, groups, allowed}}: 364 choose.method(~A, handler(~A, handlers, method), handlers, method, segments, groups, allowed) 365 366 def resolve.star(~A: Data, star: Bool, paths: List<&2, Path>, allowed: String, target: Target.Target, method: String, +segments: List<&2, String>) -> Resolved: 367 match star: 368 case True{}: 369 Resolved{target, Options{Nil{}, allowed}} 370 case False{}: 371 Resolved{target, choose.path(~A, best(~A, paths, segments, None{}), method, segments)} 372 373 def resolve.target(~A: Data, parsed: Maybe<&2, Target.Target>, table: Table, method: String) -> Result<&2, &2, Error, Resolved>: 374 match parsed: 375 case None{}: 376 Fail{BadTarget{}} 377 case Some{Target.Target{+segments, +query, +authority, +star}}: 378 Table{paths, allowed} = table 379 Done{resolve.star(~A, star, paths, allowed, Target.Target{segments, query, authority, star}, method, segments)} 380 381 def resolve(~A: Data, table: Table, +method: String, target: String) -> Result<&2, &2, Error, Resolved>: 382 resolve.target(~A, Target.parse(target, method), table, method) 383 384 def route.match(ok: Bool, ss: List<&2, Segment>, got: List<&2, String>) -> Maybe<&2, Map<&2, String>>: 385 match ok: 386 case False{}: 387 None{} 388 case True{}: 389 Some{capture(ss, got, Map.new(&2, String))} 390 391 def route.path(star: Bool, +ss: List<&2, Segment>, +got: List<&2, String>) -> Maybe<&2, Map<&2, String>>: 392 match star: 393 case True{}: 394 None{} 395 case False{}: 396 route.match(matches(ss, got), ss, got) 397 398 def route.target(parsed: Maybe<&2, Target.Target>, +ss: List<&2, Segment>) -> Maybe<&2, Map<&2, String>>: 399 match parsed: 400 case None{}: 401 None{} 402 case Some{Target.Target{+got, query, authority, star}}: 403 route.path(star, ss, got) 404 405 def route.pattern(parsed: Maybe<&2, List<&2, Segment>>, path: String, method: String) -> Maybe<&2, Map<&2, String>>: 406 match parsed: 407 case None{}: 408 None{} 409 case Some{ss}: 410 route.target(Target.parse(path, method), ss) 411 412 def route.method(same: Bool, p: String, path: String, method: String) -> Maybe<&2, Map<&2, String>>: 413 match same: 414 case False{}: 415 None{} 416 case True{}: 417 route.pattern(pattern(p), path, method) 418 419 def route(want: String, +method: String, pattern: String, path: String) -> Maybe<&2, Map<&2, String>>: 420 route.method(String.eq(want, method), pattern, path, method) 421 422 def overlap(a: List<&2, Segment>, b: List<&2, Segment>) -> Bool: 423 match a b: 424 case Nil{} Nil{}: 425 True{} 426 case Literal{x} <> at Literal{y} <> bt: 427 continue(String.eq(x, y), u => overlap(at, bt)) 428 case Literal{x} <> at Parameter{y} <> bt: 429 continue(Bool.not(String.is_empty(x)), u => overlap(at, bt)) 430 case Parameter{x} <> at Literal{y} <> bt: 431 continue(Bool.not(String.is_empty(y)), u => overlap(at, bt)) 432 case Parameter{x} <> at Parameter{y} <> bt: 433 overlap(at, bt) 434 case _ _: 435 False{} 436 437 def unavailable.prepend(keep: Bool, h: String, tail: List<&2, String>) -> List<&2, String>: 438 match keep: 439 case False{}: 440 tail 441 case True{}: 442 h <> tail 443 444 def unavailable(xs: List<&2, String>, +present: List<&2, String>) -> List<&2, String>: 445 match xs: 446 case Nil{}: 447 Nil{} 448 case +h <> t: 449 unavailable.prepend(Bool.not(member(present, h)), h, unavailable(t, present)) 450 451 def hidden.add(empty: Bool, pattern: String, methods: String, tail: List<&2, Hidden>) -> List<&2, Hidden>: 452 match empty: 453 case True{}: 454 tail 455 case False{}: 456 Hidden{pattern, methods} <> tail 457 458 def hidden.handlers(~A: Data, handlers: List<&2, Handler>, +present: List<&2, String>, tail: List<&2, Hidden>) -> List<&2, Hidden>: 459 match handlers: 460 case Nil{}: 461 tail 462 case Handler{+method, +pattern, ss, action} <> rest: 463 +missing = allow(unavailable(method.prepend(String.eq(method, "GET"), method, Nil{}), present)) 464 hidden.add(String.is_empty(missing), pattern, missing, hidden.handlers(~A, rest, present, tail)) 465 466 def hidden.path(~A: Data, hides: Bool, handlers: List<&2, Handler>, present: List<&2, String>, tail: List<&2, Hidden>) -> List<&2, Hidden>: 467 match hides: 468 case False{}: 469 tail 470 case True{}: 471 hidden.handlers(~A, handlers, present, tail) 472 473 def hidden.scan(~A: Data, paths: List<&2, Path>, +segments: List<&2, Segment>, +methods: List<&2, String>) -> List<&2, Hidden>: 474 match paths: 475 case Nil{}: 476 Nil{} 477 case Path{+ss, handlers, groups, allowed} <> t: 478 hidden.path(~A, Bool.and(specific(segments, ss), overlap(segments, ss)), handlers, methods, hidden.scan(~A, t, segments, methods)) 479 480 def description.handlers(~A: Data, handlers: List<&2, Handler>, +groups: List<&2, U32>, +hidden: List<&2, Hidden>) -> List<&2, Description>: 481 match handlers: 482 case Nil{}: 483 Nil{} 484 case Handler{method, pattern, ss, action} <> t: 485 Description{method, pattern, groups, hidden} <> description.handlers(~A, t, groups, hidden) 486 487 def description.paths(~A: Data, paths: List<&2, Path>, +all: List<&2, Path>) -> List<&2, Description>: 488 match paths: 489 case Nil{}: 490 Nil{} 491 case Path{segments, +handlers, groups, allowed} <> t: 492 List.append(&2, Description, description.handlers(~A, handlers, groups, hidden.scan(~A, all, segments, method.names(~A, handlers))), description.paths(~A, t, all)) 493 494 def describe(~A: Data, table: Table) -> List<&2, Description>: 495 Table{+paths, allowed} = table 496 description.paths(~A, paths, paths)