ocaml http/1, http/2 and websocket client and server library
at main 5.8 kB view raw
1(** Type-safe router with radix trie, scopes, and per-route plugs. *) 2 3type segment = Literal of string | Param of string | Wildcard 4type params = (string * string) list 5type 'a handler_entry = { handler : 'a; plugs : Pipeline.t } 6 7type 'a trie_node = { 8 mutable handlers : (H1.Method.t option * 'a handler_entry) list; 9 literal_children : (string, 'a trie_node) Hashtbl.t; 10 mutable param_child : (string * 'a trie_node) option; 11 mutable wildcard_child : 'a trie_node option; 12} 13 14type 'a t = { root : 'a trie_node } 15type 'a match_result = { handler : 'a; params : params; plugs : Pipeline.t } 16 17let empty_node () = 18 { 19 handlers = []; 20 literal_children = Hashtbl.create 8; 21 param_child = None; 22 wildcard_child = None; 23 } 24 25let empty () = { root = empty_node () } 26 27let parse_path path = 28 let path = 29 if String.length path > 0 && path.[0] = '/' then 30 String.sub path 1 (String.length path - 1) 31 else path 32 in 33 if path = "" then [] 34 else 35 String.split_on_char '/' path 36 |> List.filter (fun s -> s <> "") 37 |> List.map (fun s -> 38 if String.length s > 0 && s.[0] = ':' then 39 Param (String.sub s 1 (String.length s - 1)) 40 else if s = "*" then Wildcard 41 else Literal s) 42 43let find_or_create_child node seg = 44 match seg with 45 | Literal s -> ( 46 match Hashtbl.find_opt node.literal_children s with 47 | Some child -> child 48 | None -> 49 let child = empty_node () in 50 Hashtbl.add node.literal_children s child; 51 child) 52 | Param name -> ( 53 match node.param_child with 54 | Some (_, child) -> child 55 | None -> 56 let child = empty_node () in 57 node.param_child <- Some (name, child); 58 child) 59 | Wildcard -> ( 60 match node.wildcard_child with 61 | Some child -> child 62 | None -> 63 let child = empty_node () in 64 node.wildcard_child <- Some child; 65 child) 66 67let add_route router ~method_ ~path ~handler ~plugs = 68 let segments = parse_path path in 69 let entry = { handler; plugs } in 70 let rec insert node = function 71 | [] -> node.handlers <- (method_, entry) :: node.handlers 72 | seg :: rest -> 73 let child = find_or_create_child node seg in 74 insert child rest 75 in 76 insert router.root segments 77 78let lookup router ~method_ ~path = 79 let len = String.length path in 80 let start = if len > 0 && path.[0] = '/' then 1 else 0 in 81 82 let rec search node pos params = 83 if pos >= len then 84 match 85 List.find_opt 86 (fun (m, _) -> match m with None -> true | Some m' -> m' = method_) 87 node.handlers 88 with 89 | Some (_, entry) -> 90 Some { handler = entry.handler; params; plugs = entry.plugs } 91 | None -> None 92 else 93 let seg_end = 94 try String.index_from path pos '/' with Not_found -> len 95 in 96 if seg_end = pos then search node (pos + 1) params 97 else 98 let seg = String.sub path pos (seg_end - pos) in 99 let next_pos = if seg_end < len then seg_end + 1 else len in 100 match Hashtbl.find_opt node.literal_children seg with 101 | Some child -> search child next_pos params 102 | None -> ( 103 match node.param_child with 104 | Some (name, child) -> search child next_pos ((name, seg) :: params) 105 | None -> ( 106 match node.wildcard_child with 107 | Some child -> 108 let rest_path = String.sub path pos (len - pos) in 109 search child len (("*", rest_path) :: params) 110 | None -> None)) 111 in 112 search router.root start [] 113 114module Route = struct 115 type 'a t = { 116 method_ : H1.Method.t option; 117 path : string; 118 handler : 'a; 119 plugs : Pipeline.t; 120 } 121 122 let make ?method_ path handler = { method_; path; handler; plugs = [] } 123 let get path handler = { method_ = Some `GET; path; handler; plugs = [] } 124 let post path handler = { method_ = Some `POST; path; handler; plugs = [] } 125 let put path handler = { method_ = Some `PUT; path; handler; plugs = [] } 126 127 let delete path handler = 128 { method_ = Some `DELETE; path; handler; plugs = [] } 129 130 let patch path handler = 131 { method_ = Some (`Other "PATCH"); path; handler; plugs = [] } 132 133 let head path handler = { method_ = Some `HEAD; path; handler; plugs = [] } 134 135 let options path handler = 136 { method_ = Some `OPTIONS; path; handler; plugs = [] } 137 138 let any path handler = { method_ = None; path; handler; plugs = [] } 139 let plug p route = { route with plugs = Pipeline.plug route.plugs p } 140end 141 142let normalize_path path = 143 if String.length path = 0 then "/" 144 else if path.[0] <> '/' then "/" ^ path 145 else path 146 147let join_paths prefix path = 148 let prefix = normalize_path prefix in 149 let path = normalize_path path in 150 if prefix = "/" then path else if path = "/" then prefix else prefix ^ path 151 152let scope ?(through = Pipeline.empty) prefix routes = 153 List.map 154 (fun (r : 'a Route.t) -> 155 { 156 r with 157 path = join_paths prefix r.path; 158 plugs = Pipeline.compose through r.plugs; 159 }) 160 routes 161 162let compile routes = 163 let router = empty () in 164 List.iter 165 (fun r -> 166 add_route router ~method_:r.Route.method_ ~path:r.Route.path 167 ~handler:r.Route.handler ~plugs:r.Route.plugs) 168 routes; 169 router 170 171let compile_scopes scopes = 172 let all_routes = List.concat scopes in 173 compile all_routes 174 175let param name params = List.assoc_opt name params 176 177let param_or name ~default params = 178 match List.assoc_opt name params with Some v -> v | None -> default 179 180let param_int name params = 181 match List.assoc_opt name params with 182 | Some v -> int_of_string_opt v 183 | None -> None 184 185let param_int_or name ~default params = 186 match param_int name params with Some v -> v | None -> default