# Prepared HTTP routes with exact paths, validated registration, and path-first method selection. Source: https://github.com/paymog/bend-kit/tree/main/router import Base import ./target.bend as Target type Segment is Data: Literal{text: String} Parameter{name: String} type Entry<-A: Data> is Data: Entry{method: String, pattern: String, action: A, groups: List<&2, U32>} type Handler<-A: Data> is Data: Handler{method: String, pattern: String, segments: List<&2, Segment>, action: A} type Path<-A: Data> is Data: Path{segments: List<&2, Segment>, handlers: List<&2, Handler>, groups: List<&2, U32>, allow: String} type Table<-A: Data> is Data: Table{paths: List<&2, Path>, allow: String} type Error is Data: InvalidPattern{pattern: String} InvalidMethod{method: String} Duplicate{method: String, pattern: String} GroupConflict{pattern: String} BadTarget{} type Choice<-A: Data> is Data: Found{action: A, params: Map<&2, String>, groups: List<&2, U32>, method: String, pattern: String} MethodMissing{groups: List<&2, U32>, allow: String} Options{groups: List<&2, U32>, allow: String} NotFound{} type Resolved<-A: Data> is Data: Resolved{target: Target.Target, choice: Choice} type Hidden is Data: Hidden{pattern: String, methods: String} type Description is Data: Description{method: String, pattern: String, groups: List<&2, U32>, hidden: List<&2, Hidden>} def scalar(+c: U32) -> Bool: Bool.and(U32.is_le(c, 1114111), Bool.not(Target.between(c, 55296, 57343))) def literal.valid(s: String) -> Bool: match s: case SNil{}: True{} case SCon{Chr{+c}, t}: Bool.and(Bool.and(scalar(c), Bool.and(U32.is_ne(c, 0), U32.is_ne(c, 47))), literal.valid(t)) def name.tail(s: String) -> Bool: match s: case SNil{}: True{} case SCon{Chr{+c}, t}: Bool.and(Bool.or(Target.alpha(c), Bool.or(Target.between(c, 48, 57), U32.is_eq(c, 95))), name.tail(t)) def name.valid(s: String) -> Bool: match s: case SNil{}: False{} case SCon{Chr{+c}, t}: Bool.and(Bool.or(Target.alpha(c), U32.is_eq(c, 95)), name.tail(t)) def member(xs: List<&2, String>, +s: String) -> Bool: match xs: case Nil{}: False{} case h <> t: Bool.or(String.eq(h, s), member(t, s)) def pattern.segment(s: String, names: List<&2, String>) -> Maybe<&2, Segment>: match s: case SCon{Chr{58}, +t}: Bool.pick(Maybe<&2, Segment>, Bool.and(name.valid(t), Bool.not(member(names, t))), Some{Parameter{t}}, None{}) case SNil{}: Some{Literal{""}} case SCon{+h, +t}: +lit = {SCon{h, t} : String} Bool.pick(Maybe<&2, Segment>, literal.valid(lit), Some{Literal{lit}}, None{}) def pattern.names(segment: Segment, names: List<&2, String>) -> List<&2, String>: match segment: case Literal{text}: names case Parameter{name}: name <> names def pattern.parts(xs: List<&2, String>, +names: List<&2, String>, acc: List<&2, Segment>) -> Maybe<&2, List<&2, Segment>>: match xs: case Nil{}: Some{List.reverse(&2, Segment, acc)} case "" <> Nil{}: Some{List.reverse(&2, Segment, Literal{""} <> acc)} case "" <> t: None{} case h <> t: do Maybe<&2, List<&2, Segment>>: seg : Segment <- pattern.segment(h, names) +part : Segment = seg pattern.parts(t, pattern.names(part, names), part <> acc) def pattern(s: String) -> Maybe<&2, List<&2, Segment>>: match s: case "/": Some{Nil{}} case SCon{Chr{47}, t}: pattern.parts(String.split(t, '/'), Nil{}, Nil{}) case _: None{} def token(+c: U32) -> Bool: 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))))))))) def method.tail(s: String) -> Bool: match s: case SNil{}: True{} case SCon{Chr{c}, t}: Bool.and(token(c), method.tail(t)) def method.valid(s: String) -> Bool: match s: case SNil{}: False{} case SCon{Chr{c}, t}: Bool.and(token(c), method.tail(t)) def continue(ok: Bool, later: Unit -> Bool) -> Bool: match ok: case False{}: False{} case True{}: later(Unit{}) def matches(pat: List<&2, Segment>, got: List<&2, String>) -> Bool: match pat got: case Nil{} Nil{}: True{} case Literal{p} <> pt g <> gt: continue(String.eq(p, g), u => matches(pt, gt)) case Parameter{name} <> pt +g <> gt: continue(Bool.not(String.is_empty(g)), u => matches(pt, gt)) case _ _: False{} def capture(pat: List<&2, Segment>, got: List<&2, String>, acc: Map<&2, String>) -> Map<&2, String>: match pat got: case Parameter{name} <> pt g <> gt: capture(pt, gt, Map.set(&2, String, acc, name, g)) case Literal{text} <> pt g <> gt: capture(pt, gt, acc) case _ _: acc def shape(a: List<&2, Segment>, b: List<&2, Segment>) -> Bool: match a b: case Nil{} Nil{}: True{} case Literal{x} <> at Literal{y} <> bt: continue(String.eq(x, y), u => shape(at, bt)) case Parameter{x} <> at Parameter{y} <> bt: shape(at, bt) case _ _: False{} def specific(a: List<&2, Segment>, b: List<&2, Segment>) -> Bool: match a b: case Literal{x} <> at Parameter{y} <> bt: True{} case Parameter{x} <> at Literal{y} <> bt: False{} case Literal{x} <> at Literal{y} <> bt: specific(at, bt) case Parameter{x} <> at Parameter{y} <> bt: specific(at, bt) case _ _: False{} def groups.eq(a: List<&2, U32>, b: List<&2, U32>) -> Bool: match a b: case Nil{} Nil{}: True{} case x <> at y <> bt: continue(U32.is_eq(x, y), u => groups.eq(at, bt)) case _ _: False{} def handler.has(~A: Data, xs: List<&2, Handler>, +method: String) -> Bool: match xs: case Nil{}: False{} case Handler{want, pattern, segments, action} <> t: Bool.or(String.eq(want, method), handler.has(~A, t, method)) 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>>: match duplicate same_groups: case True{} _: Handler{method, pattern, ss, action} = entry Fail{Duplicate{method, pattern}} case False{} False{}: Handler{method, pattern, ss, action} = entry Fail{GroupConflict{pattern}} case False{} True{}: Done{Path{segments, entry <> handlers, groups, ""} <> rest} 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>>: match same: case True{}: Handler{+method, pattern, ss, action} = entry insert.merge(~A, handler.has(~A, handlers, method), groups.eq(old_groups, groups), Handler{method, pattern, ss, action}, segments, handlers, old_groups, tail) case False{}: do Result<&2, &2, Error, List<&2, Path>>: remaining : List<&2, Path> <- rest(Unit{}) return Path{segments, handlers, old_groups, allowed} <> remaining # ponytail: preparation merges shapes in O(routes²); replace with a shape index if construction becomes a bottleneck. def insert(~A: Data, paths: List<&2, Path>, +entry: Handler, +groups: List<&2, U32>) -> Result<&2, &2, Error, List<&2, Path>>: match paths: case Nil{}: Handler{method, pattern, +ss, action} = entry Done{[Path{ss, [Handler{method, pattern, ss, action}], groups, ""}]} case Path{+ss, +handlers, +old_groups, +allow} <> +t: Handler{+method, +pattern, +es, +action} = entry candidate = {Handler{method, pattern, es, action} : Handler} insert.branch(~A, shape(ss, es), ss, handlers, old_groups, allow, t, candidate, groups, u => insert(~A, t, Handler{method, pattern, es, action}, groups)) def method.prepend(get: Bool, method: String, tail: List<&2, String>) -> List<&2, String>: match get: case True{}: method <> "HEAD" <> tail case False{}: method <> tail def method.names(~A: Data, xs: List<&2, Handler>) -> List<&2, String>: match xs: case Nil{}: ["OPTIONS"] case Handler{+method, pattern, ss, action} <> t: method.prepend(String.eq(method, "GET"), method, method.names(~A, t)) def unique(xs: List<&2, String>, prev: String, +acc: List<&2, String>) -> List<&2, String>: match xs: case Nil{}: List.reverse(&2, String, acc) case +h <> t: unique(t, h, Bool.pick(List<&2, String>, String.eq(h, prev), acc, h <> acc)) def allow(xs: List<&2, String>) -> String: String.join(unique(List.sort(~String, ~String.is_le, xs), "", Nil{}), ", ") def finish.paths(~A: Data, paths: List<&2, Path>) -> List<&2, Path>: match paths: case Nil{}: Nil{} case Path{segments, +handlers, groups, old_allow} <> t: Path{segments, handlers, groups, allow(method.names(~A, handlers))} <> finish.paths(~A, t) def all.methods(~A: Data, paths: List<&2, Path>) -> List<&2, String>: match paths: case Nil{}: ["OPTIONS"] case Path{ss, handlers, groups, allowed} <> t: List.append(&2, String, method.names(~A, handlers), all.methods(~A, t)) 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>>: match parsed: case None{}: Fail{InvalidPattern{pattern}} case Some{ss}: insert(~A, paths, Handler{method, pattern, ss, action}, groups) def prepare.entry(~A: Data, valid: Bool, entry: Entry, paths: List<&2, Path>) -> Result<&2, &2, Error, List<&2, Path>>: match valid: case False{}: Entry{method, p, action, groups} = entry Fail{InvalidMethod{method}} case True{}: Entry{method, +p, action, groups} = entry prepare.pattern(~A, pattern(p), method, p, action, groups, paths) def prepare.go(~A: Data, entries: List<&2, Entry>, paths: List<&2, Path>) -> Result<&2, &2, Error, Table>: match entries: case Nil{}: +prepared = finish.paths(~A, paths) Done{Table{prepared, allow(all.methods(~A, prepared))}} case Entry{+method, p, action, groups} <> t: do Result<&2, &2, Error, Table>: next : List<&2, Path> <- prepare.entry(~A, method.valid(method), Entry{method, p, action, groups}, paths) prepare.go(~A, t, next) def prepare(~A: Data, entries: List<&2, Entry>) -> Result<&2, &2, Error, Table>: prepare.go(~A, entries, Nil{}) def best.compare(~A: Data, candidate: Path, old: Path) -> Maybe<&2, Path>: Path{+cs, +ch, +cg, +ca} = candidate Path{+ss, +handlers, +groups, +allowed} = old Bool.pick(Maybe<&2, Path>, specific(cs, ss), Some{Path{cs, ch, cg, ca}}, Some{Path{ss, handlers, groups, allowed}}) def best.pick(~A: Data, best: Maybe<&2, Path>, candidate: Path) -> Maybe<&2, Path>: match best: case None{}: Some{candidate} case Some{old}: best.compare(~A, candidate, old) def best.step(~A: Data, matched: Bool, candidate: Path, best: Maybe<&2, Path>) -> Maybe<&2, Path>: match matched: case False{}: best case True{}: best.pick(~A, best, candidate) # ponytail: lookup scans prepared paths in O(routes × segments); use a trie if measured dispatch cost warrants it. def best(~A: Data, paths: List<&2, Path>, +segments: List<&2, String>, acc: Maybe<&2, Path>) -> Maybe<&2, Path>: match paths: case Nil{}: acc case Path{+ss, handlers, groups, allowed} <> t: best(~A, t, segments, best.step(~A, matches(ss, segments), Path{ss, handlers, groups, allowed}, acc)) def handler.next(~A: Data, same: Bool, here: Handler, later: Unit -> Maybe<&2, Handler>) -> Maybe<&2, Handler>: match same: case True{}: Some{here} case False{}: later(Unit{}) def handler(~A: Data, xs: List<&2, Handler>, +method: String) -> Maybe<&2, Handler>: match xs: case Nil{}: None{} case Handler{+want, pattern, ss, action} <> t: handler.next(~A, String.eq(want, method), Handler{want, pattern, ss, action}, u => handler(~A, t, method)) def chosen(~A: Data, hit: Maybe<&2, Handler>, segments: List<&2, String>, +groups: List<&2, U32>, +allowed: String, options: Bool) -> Choice: match hit: case Some{Handler{method, pattern, ss, action}}: Found{action, capture(ss, segments, Map.new(&2, String)), groups, method, pattern} case None{}: Bool.pick(Choice, options, Options{groups, allowed}, MethodMissing{groups, allowed}) def choose.fallback(~A: Data, head: Bool, handlers: List<&2, Handler>, method: String, segments: List<&2, String>, groups: List<&2, U32>, allowed: String) -> Choice: match head: case True{}: chosen(~A, handler(~A, handlers, "GET"), segments, groups, allowed, False{}) case False{}: chosen(~A, None{}, segments, groups, allowed, String.eq(method, "OPTIONS")) 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: match direct: case Some{h}: chosen(~A, Some{h}, segments, groups, allowed, False{}) case None{}: choose.fallback(~A, String.eq(method, "HEAD"), handlers, method, segments, groups, allowed) def choose.path(~A: Data, path: Maybe<&2, Path>, +method: String, segments: List<&2, String>) -> Choice: match path: case None{}: NotFound{} case Some{Path{ss, +handlers, groups, allowed}}: choose.method(~A, handler(~A, handlers, method), handlers, method, segments, groups, allowed) def resolve.star(~A: Data, star: Bool, paths: List<&2, Path>, allowed: String, target: Target.Target, method: String, +segments: List<&2, String>) -> Resolved: match star: case True{}: Resolved{target, Options{Nil{}, allowed}} case False{}: Resolved{target, choose.path(~A, best(~A, paths, segments, None{}), method, segments)} def resolve.target(~A: Data, parsed: Maybe<&2, Target.Target>, table: Table, method: String) -> Result<&2, &2, Error, Resolved>: match parsed: case None{}: Fail{BadTarget{}} case Some{Target.Target{+segments, +query, +authority, +star}}: Table{paths, allowed} = table Done{resolve.star(~A, star, paths, allowed, Target.Target{segments, query, authority, star}, method, segments)} def resolve(~A: Data, table: Table, +method: String, target: String) -> Result<&2, &2, Error, Resolved>: resolve.target(~A, Target.parse(target, method), table, method) def route.match(ok: Bool, ss: List<&2, Segment>, got: List<&2, String>) -> Maybe<&2, Map<&2, String>>: match ok: case False{}: None{} case True{}: Some{capture(ss, got, Map.new(&2, String))} def route.path(star: Bool, +ss: List<&2, Segment>, +got: List<&2, String>) -> Maybe<&2, Map<&2, String>>: match star: case True{}: None{} case False{}: route.match(matches(ss, got), ss, got) def route.target(parsed: Maybe<&2, Target.Target>, +ss: List<&2, Segment>) -> Maybe<&2, Map<&2, String>>: match parsed: case None{}: None{} case Some{Target.Target{+got, query, authority, star}}: route.path(star, ss, got) def route.pattern(parsed: Maybe<&2, List<&2, Segment>>, path: String, method: String) -> Maybe<&2, Map<&2, String>>: match parsed: case None{}: None{} case Some{ss}: route.target(Target.parse(path, method), ss) def route.method(same: Bool, p: String, path: String, method: String) -> Maybe<&2, Map<&2, String>>: match same: case False{}: None{} case True{}: route.pattern(pattern(p), path, method) def route(want: String, +method: String, pattern: String, path: String) -> Maybe<&2, Map<&2, String>>: route.method(String.eq(want, method), pattern, path, method) def overlap(a: List<&2, Segment>, b: List<&2, Segment>) -> Bool: match a b: case Nil{} Nil{}: True{} case Literal{x} <> at Literal{y} <> bt: continue(String.eq(x, y), u => overlap(at, bt)) case Literal{x} <> at Parameter{y} <> bt: continue(Bool.not(String.is_empty(x)), u => overlap(at, bt)) case Parameter{x} <> at Literal{y} <> bt: continue(Bool.not(String.is_empty(y)), u => overlap(at, bt)) case Parameter{x} <> at Parameter{y} <> bt: overlap(at, bt) case _ _: False{} def unavailable.prepend(keep: Bool, h: String, tail: List<&2, String>) -> List<&2, String>: match keep: case False{}: tail case True{}: h <> tail def unavailable(xs: List<&2, String>, +present: List<&2, String>) -> List<&2, String>: match xs: case Nil{}: Nil{} case +h <> t: unavailable.prepend(Bool.not(member(present, h)), h, unavailable(t, present)) def hidden.add(empty: Bool, pattern: String, methods: String, tail: List<&2, Hidden>) -> List<&2, Hidden>: match empty: case True{}: tail case False{}: Hidden{pattern, methods} <> tail def hidden.handlers(~A: Data, handlers: List<&2, Handler>, +present: List<&2, String>, tail: List<&2, Hidden>) -> List<&2, Hidden>: match handlers: case Nil{}: tail case Handler{+method, +pattern, ss, action} <> rest: +missing = allow(unavailable(method.prepend(String.eq(method, "GET"), method, Nil{}), present)) hidden.add(String.is_empty(missing), pattern, missing, hidden.handlers(~A, rest, present, tail)) def hidden.path(~A: Data, hides: Bool, handlers: List<&2, Handler>, present: List<&2, String>, tail: List<&2, Hidden>) -> List<&2, Hidden>: match hides: case False{}: tail case True{}: hidden.handlers(~A, handlers, present, tail) def hidden.scan(~A: Data, paths: List<&2, Path>, +segments: List<&2, Segment>, +methods: List<&2, String>) -> List<&2, Hidden>: match paths: case Nil{}: Nil{} case Path{+ss, handlers, groups, allowed} <> t: hidden.path(~A, Bool.and(specific(segments, ss), overlap(segments, ss)), handlers, methods, hidden.scan(~A, t, segments, methods)) def description.handlers(~A: Data, handlers: List<&2, Handler>, +groups: List<&2, U32>, +hidden: List<&2, Hidden>) -> List<&2, Description>: match handlers: case Nil{}: Nil{} case Handler{method, pattern, ss, action} <> t: Description{method, pattern, groups, hidden} <> description.handlers(~A, t, groups, hidden) def description.paths(~A: Data, paths: List<&2, Path>, +all: List<&2, Path>) -> List<&2, Description>: match paths: case Nil{}: Nil{} case Path{segments, +handlers, groups, allowed} <> t: List.append(&2, Description, description.handlers(~A, handlers, groups, hidden.scan(~A, all, segments, method.names(~A, handlers))), description.paths(~A, t, all)) def describe(~A: Data, table: Table) -> List<&2, Description>: Table{+paths, allowed} = table description.paths(~A, paths, paths)