~/bend-docscommunity

http_router/router.bend source

http_router/router.bend on the hub · documented module

import Baseimport ../http/method.bend as Mimport ../http/request.bend as Req# grounds-http-router: match a request's method and path against routes,# and answer with data: the route value you gave, and the path's captures.# It calls no handler; your code dispatches with an exhaustive match.##   def routes() -> List<&2, Router.Route<Route>>:#     [Router.get("/todos", ListTodos{}), Router.get("/todos/:id", GetTodo{})]#   match Router.find(routes(), req): Found{route, params} | NotFound | MethodNotAllowed# a pattern's segment: a literal, or a :name capturetype Seg is Data:  Lit{text: String}  Cap{name: String}type Route<-R: Data> is Data:  Route{method: M.Method, path: List<&2, Seg>, route: R}# a captured segmenttype Param is Data:  Param{name: String, value: String}type Match<-R: Data> is Data:  Found{route: R, params: List<&2, Param>}  NotFound{}  MethodNotAllowed{allowed: List<&2, M.Method>}# A request's path, cut at /. A type of its own, not a List<String>: every# route reads it, and a generic List read and then used again makes Lists# reference counted in the whole programtype Parts is Data:  PNil{}  PCon{head: String, tail: Parts}# the router's own compare: sharing http's with the server makes one def# both a reader here and an owner theredef same(a: String, b: String, ok: Bool) -> Bool:  match a b:    case SNil{} SNil{}:      ok    case SCon{x, at} SCon{y, bt}:      same(at, bt, Bool.and(ok, Char.is_eq(x, y)))    case _ _:      False{}def parts.rev(p: Parts, acc: Parts) -> Parts:  match p:    case PNil{}:      acc    case PCon{h, t}:      parts.rev(t, PCon{h, acc})def parts.go(s: String, cur: String, acc: Parts) -> Parts:  match s:    case SNil{}:      parts.rev(PCon{String.reverse(cur), acc}, PNil{})    case SCon{Chr{47}, t}:      parts.go(t, "", PCon{String.reverse(cur), acc})    case SCon{c, t}:      parts.go(t, SCon{c, cur}, acc)# "/todos/7" is todos, 7; "/" is one empty part; a trailing / adds onedef parts(path: String) -> Parts:  match path:    case SCon{Chr{47}, t}:      parts.go(t, "", PNil{})    case other:      parts.go(other, "", PNil{})def segs.go(p: Parts, acc: List<&2, Seg>) -> List<&2, Seg>:  match p:    case PNil{}:      List.reverse(&2, Seg, acc)    case PCon{SCon{Chr{58}, name}, t}:      segs.go(t, Con{Cap{name}, acc})    case PCon{s, t}:      segs.go(t, Con{Lit{s}, acc})# a pattern like "/todos/:id"def pattern(s: String) -> List<&2, Seg>:  segs.go(parts(s), [])def get(-R: Data, p: String, r: R) -> Route<R>:  Route{M.MGet{}, pattern(p), r}def post(-R: Data, p: String, r: R) -> Route<R>:  Route{M.MPost{}, pattern(p), r}def put(-R: Data, p: String, r: R) -> Route<R>:  Route{M.MPut{}, pattern(p), r}def patch(-R: Data, p: String, r: R) -> Route<R>:  Route{M.MPatch{}, pattern(p), r}def delete(-R: Data, p: String, r: R) -> Route<R>:  Route{M.MDelete{}, pattern(p), r}def route(-R: Data, m: M.Method, p: String, r: R) -> Route<R>:  Route{m, pattern(p), r}# a copy, built by reading only: the request's parts are read by every routedef copy(s: String, acc: String) -> String:  match s:    case SNil{}:      String.reverse(acc)    case SCon{Chr{x}, t}:      copy(t, SCon{Chr{x}, acc})# a capture takes a segment with something in it: /todos/ is not /todos/:iddef filled(s: String) -> Bool:  match s:    case SNil{}:      False{}    case _:      True{}def bind.end(ok: Bool, acc: List<&2, Param>) -> Maybe<&2, List<&2, Param>>:  match ok:    case True{}:      Some{List.reverse(&2, Param, acc)}    case False{}:      None{}# the captures, if the pattern matches the parts; None if it does not. ok# says the literals so far matched: the walk goes on either way, so there# is one loopdef bind(pat: List<&2, Seg>, ps: Parts, acc: List<&2, Param>, ok: Bool) -> Maybe<&2, List<&2, Param>>:  match pat ps:    case Nil{} PNil{}:      bind.end(ok, acc)    case Con{Lit{l}, pt} PCon{p, t}:      bind(pt, t, acc, Bool.and(ok, same(l, p, True{})))    case Con{Cap{n}, pt} PCon{+p, t}:      bind(pt, t, Con{Param{n, copy(p, "")}, acc}, Bool.and(ok, filled(p)))    case _ _:      None{}def method_eq(a: M.Method, b: M.Method) -> Bool:  match a b:    case M.MGet{} M.MGet{}:      True{}    case M.MHead{} M.MHead{}:      True{}    case M.MPost{} M.MPost{}:      True{}    case M.MPut{} M.MPut{}:      True{}    case M.MDelete{} M.MDelete{}:      True{}    case M.MConnect{} M.MConnect{}:      True{}    case M.MOptions{} M.MOptions{}:      True{}    case M.MTrace{} M.MTrace{}:      True{}    case M.MPatch{} M.MPatch{}:      True{}    case M.MOther{x} M.MOther{y}:      same(x, y, True{})    case _ _:      False{}# the scan so far: a route found, or the methods of routes whose path# matched (for 405 and its Allow header)type Acc<-R: Data> is Data:  AFound{route: R, params: List<&2, Param>}  ANone{allowed: List<&2, M.Method>}def acc.step(-R: Data, st: Acc<R>, bound: Maybe<&2, List<&2, Param>>, same: Bool, m: M.Method, r: R) -> Acc<R>:  match st bound same:    case AFound{fr, fp} _ _:      AFound{fr, fp}    case ANone{_} Some{ps} True{}:      AFound{r, ps}    case ANone{al} Some{_} False{}:      ANone{Con{m, al}}    case ANone{al} None{} _:      ANone{al}def acc.end(-R: Data, st: Acc<R>) -> Match<R>:  match st:    case AFound{r, ps}:      Found{r, ps}    case ANone{Nil{}}:      NotFound{}    case ANone{al}:      MethodNotAllowed{List.reverse(&2, M.Method, al)}# every route is tried, in order; the first whose method and path match winsdef find.go(-R: Data, rs: List<&2, Route<R>>, +method: M.Method, +ps: Parts, st: Acc<R>) -> Match<R>:  match rs:    case Nil{}:      acc.end(R, st)    case Con{Route{+m, pat, r}, t}:      find.go(R, t, method, ps, acc.step(R, st, bind(pat, ps, [], True{}), method_eq(m, method), m, r))# a copy of the target's path, up to any ?, built by reading it onlydef path.of(s: String, acc: String) -> String:  match s:    case SNil{}:      String.reverse(acc)    case SCon{Chr{63}, _}:      String.reverse(acc)    case SCon{Chr{x}, t}:      path.of(t, SCon{Chr{x}, acc})# the match for a method and a target (its path before any ?)def find(-R: Data, rs: List<&2, Route<R>>, method: M.Method, target: String) -> Match<R>:  find.go(R, rs, method, parts(path.of(target, "")), ANone{[]})# a match, and the request it came from, untouchedtype Routed<-R: Data> is Data:  Routed{found: Match<R>, req: Req.Request}# match a request, handing it back for the handler. It reads the target# here rather than through find: a def whose last use of a String is a read# takes it, so the read must be in the def that uses the String afterdef route_req(-R: Data, rs: List<&2, Route<R>>, r: Req.Request) -> Routed<R>:  match r:    case Req.Request{+m, +t, hs, b}:      Routed{find.go(R, rs, m, parts(path.of(t, "")), ANone{[]}), Req.Request{m, t, hs, b}}def param.keep(hit: Bool, v: String, acc: Maybe<&2, String>) -> Maybe<&2, String>:  match hit acc:    case True{} None{}:      Some{v}    case _ a:      adef get.go(ps: List<&2, Param>, +name: String, acc: Maybe<&2, String>) -> Maybe<&2, String>:  match ps:    case Nil{}:      acc    case Con{Param{n, v}, t}:      get.go(t, name, param.keep(same(n, name, True{}), v, acc))# a capture's valuedef param(ps: List<&2, Param>, +name: String) -> Maybe<&2, String>:  get.go(ps, name, None{})def allow.go(ms: List<&2, M.Method>, out: String) -> String:  match ms:    case Nil{}:      out    case Con{m, Nil{}}:      out ++ M.show(m)    case Con{m, t}:      allow.go(t, out ++ M.show(m) ++ ", ")# the Allow header's value for a 405 (RFC 9110 §10.2.1): "GET, POST"def allow(ms: List<&2, M.Method>) -> String:  allow.go(ms, "")