package vif
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
A simple web framework for OCaml 5
Install
dune-project
Dependency
Authors
Maintainers
Sources
vif-0.0.1.beta4.tbz
sha256=0327abf4e7434e5de402661cc8c29c0539501269401670fed773b0dfc14e26aa
sha512=1ff5dddecf25b41986ea629bda336a990800db8410498682bf96af2b1afab74ae57172ee8d41087f131c9ceeb3255c96351668664e36c6815a249157afd05a5d
doc/src/vif.core/vif_route.ml.html
Source file vif_route.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434(* Part of this code is based on the furl project and Copyright (c) 2015 Gabriel Radanne <drupyo@zoho.com> SPDX-License-Identifier: MIT Copyright (c) 2025 Romain Calascibetta <romain.calascibetta@gmail.com> *) let src = Logs.Src.create "vif.r" module Log = (val Logs.src_log src : Logs.LOG) module Ext = struct let slash = Re.char '/' let comma = Re.char ',' let amper = Re.char '&' let question_mark = Re.char '?' (** -?[0-9]+( .[0-9]* )? *) let float = let open Re in seq [ opt (char '-'); rep1 digit; opt (seq [ char '.'; rep digit ]) ] (** -?[0-9]+ *) let arbitrary_int = let open Re in seq [ opt (char '-'); rep1 digit ] (** true|false *) let bool = let open Re in alt [ str "true"; str "false" ] (** Non empty list of safe chars *) let string component = let open Re in match component with | `Path -> rep1 @@ compl [ slash; question_mark ] | `Query_value -> rep1 @@ compl [ set "&;+," ] (** Separated by , or by / *) let list ?m ~component n re = let open Re in match component with | `Path -> repn (seq [ slash; re ]) n m | `Query_value -> if n = 0 then alt [ epsilon; seq [ re; repn (seq [ comma; re ]) 0 m ] ] else seq [ re; repn (seq [ comma; re ]) (n - 1) m ] let query_sep ~any = if not any then amper else let open Re in seq [ amper; rep @@ seq [ rep1 @@ compl [ amper ]; amper ] ] end module Utils = struct let map_snd f (x, y) = (x, f y) let rec intersperse sep = function | [] -> [] | [ x ] -> [ x ] | h :: t -> h :: sep :: intersperse sep t (** Offset of [el] in [l], given the function count. Used to get the first regexp group at a given place. *) let find_idx count el l = let rec aux el i = function | [] -> raise Not_found | x :: l' -> if x == el then i else aux el (i + count el) l' in aux el 0 l (* Invariants: - [l_before] is included in [l_after]. - No duplicates (see note on {!find_idx}). *) (** if [l' ∈ l] then [build_permutation offset count l l'] builds a mapping: index in [l => offset in l']. Offsets are computed respecting [offset] and [count]. *) let build_permutation offset count l_before l_after = let t = Array.make (List.length l_before) 0 in l_before |> List.iteri (fun i x -> let j = find_idx count x l_after in t.(i) <- offset + j); t end open Tyre.Internal let sort_query l = List.sort (fun (x, _) (y, _) -> compare (x : string) y) l type 'a re_atom = 'a Tyre.Internal.wit (** Top level atoms are specialized for path and query, see documentation. *) let re_atom re = Tyre.Internal.build re let re_atom_path : type e a. int -> (e, a) raw -> int * a re_atom * Re.t list = let open Re in fun i -> function | Rep e -> let _, w, re = re_atom 1 e in ( i + 1 , Rep (i, w, Re.compile re) , [ group @@ Ext.list ~component:`Path 0 @@ no_group re ] ) | Opt e -> let i', w, re = re_atom i e in let id, re = mark re in (i', Opt (id, w), [ alt [ epsilon; seq [ Ext.slash; re ] ] ]) | e -> let i', w, re = re_atom i e in (i', w, [ Ext.slash; re ]) let re_atom_query : type e a. int -> (e, a) raw -> int * a re_atom * Re.t = let open Re in fun i -> function | Rep e -> let _, w, re = re_atom 1 e in ( i + 1 , Rep (i, w, Re.compile re) , group @@ Ext.list ~component:`Query_value 0 @@ no_group re ) | e -> let i', w, re = re_atom i e in (i', w, re) type ('f, 'r) re_path = | Start : ('r, 'r) re_path | PathAtom : ('f, 'a -> 'r) re_path * 'a re_atom -> ('f, 'r) re_path let rec re_path : type e r f. int -> (e, f, r) Vif_uri.path -> bool * int * (f, r) re_path * Re.t list = let open Re in fun i -> function | Host str -> let re = Re.str (Uri.pct_encode ~component:`Host str) in (true, i, Start, [ re ]) | Rel -> (* NOTE(dinosaure): a relative route must match {i regardless} of the host. As the host (when available) is prepended to the string we try to match (see {!val:prepare_target}), we let the route optionally consume such a host prefix (anything up to the first ['/']). This is what allows [rel] and [host] routes to coexist in the same dispatch table. *) let re = opt (rep1 (compl [ Ext.slash ])) in (false, i, Start, [ re ]) | Path_const (p, s) -> let with_host, i', p, re = re_path i p in (with_host, i', p, str s :: Ext.slash :: re) | Path_atom (p, a) -> let with_host, i', wp, rp = re_path i p in let i'', wa, ra = re_atom_path i' @@ from_t a in (with_host, i'', PathAtom (wp, wa), List.rev_append ra rp) type ('fu, 'ret) re_query = | Nil : ('r, 'r) re_query | Any : ('r, 'r) re_query | Cons : 'a re_atom * ('f, 'r) re_query -> ('a -> 'f, 'r) re_query let rec collect_re_query : type e r f. (e, f, r) Vif_uri.query -> int * (f, r) re_query * bool * (string * (Re.t * int)) list = function | Nil -> (0, Nil, false, []) | Any -> (0, Any, true, []) | Query_atom (s, a, q) -> let grps, wa, ra = re_atom_query 0 @@ from_t a in let total_grps, wq, b_any, rq = collect_re_query q in let total_grps = total_grps + grps in (total_grps, Cons (wa, wq), b_any, (s, (ra, grps)) :: rq) let rec shift_lits : type a. int -> a re_atom -> a re_atom = fun shift -> function | Lit i -> Lit (i + shift) | Conv (x, f) -> Conv (shift_lits shift x, f) | Opt (m, x) -> Opt (m, shift_lits shift x) | Alt (m, x1, x2) -> Alt (m, shift_lits shift x1, shift_lits shift x2) | Seq (x1, x2) -> Seq (shift_lits shift x1, shift_lits shift x2) | Rep (i, x, r) -> Rep (shift + i, x, r) | Map (x, f) -> Map (shift_lits shift x, f) | Either (m, a, b) -> Either (m, shift_lits shift a, shift_lits shift b) let rec permut_query : type r f. int -> int array -> (r, f) re_query -> (r, f) re_query = fun n permutation -> function | Nil -> Nil | Any -> Any | Cons (wa, wq) -> let shift = permutation.(n) in let wa = shift_lits shift wa in Cons (wa, permut_query (n + 1) permutation wq) let re_query current_idx q = let grps, wq, b, rql = collect_re_query q in let rel = sort_query rql in let p = Utils.build_permutation current_idx (fun (_, (_, i)) -> i) rql rel in let wq = permut_query 0 p wq in (grps, wq, b, rel) type ('f, 'r) re_url = | ReUrl : bool * ('f, 'x) re_path * ('x, 'r) re_query -> ('f, 'r) re_url let re_url : type e f r. int -> (e, f, r) Vif_uri.t -> int * (f, r) re_url * Re.t = fun i -> function | Url (slash, p, q) -> begin let end_path = match slash with | No_slash -> Re.epsilon | Slash -> Re.char '/' | Maybe_slash -> Re.(opt @@ char '/') in let with_host, idx, wp, rp = re_path i p in match q with | Nil -> let re = Re.seq (List.rev (end_path :: rp)) in (idx, ReUrl (with_host, wp, Nil), re) | Any -> let end_re = Re.(opt @@ seq [ Re.char '?'; rep any ]) in let re = Re.seq (List.rev_append rp [ end_path; end_re ]) in (idx, ReUrl (with_host, wp, Nil), re) | _ -> let grps, wq, any_query, rel = re_query idx q in let query_sep = Ext.query_sep ~any:any_query in let add_around_query = if not any_query then fun x -> x else fun l -> Re.(rep any) :: l in let fn l (s, (re, _)) = Re.seq [ Re.str (s ^ "="); re ] :: l in let re = rel |> List.fold_left fn [] |> Utils.intersperse query_sep |> add_around_query |> List.rev |> add_around_query in let re = Re.seq @@ List.rev_append rp (end_path :: Re.char '?' :: re) in (idx + grps, ReUrl (with_host, wp, wq), re) end let get_re url = let _, _, re = re_url 1 url in re (** {3 Extraction.} *) exception Tyre_exn of exn (* NOTE(dinosaure): the goal of this exception is to dispatch correctly errors from [conv] values which may fail and exception from [Tyre.Internal.extract]. *) (** Extracting atom is just a matter of following the witness. We just need to take care of counting where we are in the matching groups. *) let extract_atom ~original rea s = try extract ~original rea s with exn -> raise (Tyre_exn exn) (** Since path is in reversed order, we proceed by continuation. *) let rec extract_path : type f x r. original:string -> (f, x) re_path -> Re.Group.t -> (x -> r) -> f -> r = fun ~original wp subs k -> match wp with | Start -> k | PathAtom (rep, rea) -> let v = extract_atom ~original rea subs in let k fn = k (fn v) in extract_path ~original rep subs k (** Query are in the right order, we can proceed in direct style. *) let rec extract_query : type x r. original:string -> (x, r) re_query -> Re.Group.t -> x -> r = fun ~original wq subs f -> match wq with | Nil -> f | Any -> f | Cons (rea, req) -> let v = extract_atom ~original rea subs in extract_query ~original req subs (f v) let extract_url : type r f. original:string -> (f, r) re_url -> Re.Group.t -> f -> r = fun ~original (ReUrl (_with_host, wp, wq)) subs fn -> let k = extract_query ~original wq subs in let k = extract_path ~original wp subs k in k fn let prepare_target ?host target = let target = match String.index_opt target '?' with | None -> target | Some _ -> let uri = Uri.of_string target in uri |> Uri.query |> sort_query |> Uri.with_query uri |> Uri.path_and_query in match host with | Some host -> Uri.pct_encode ~component:`Host host ^ target | None -> target let extract url = let _idx, (ReUrl (with_host, _, _) as re_url), re = re_url 1 url in let re = Re.(compile @@ whole_string re) in fun ~fn ?host uri -> let host = if with_host then host else None in let str = prepare_target ?host (Uri.path_and_query uri) in let subs = Re.exec re str in extract_url ~original:str re_url subs fn (** {4 Multiple match} *) type ('socket, 'fu, 'return) req = | Request : Vif_method.t option * ('c, 'a) Vif_type.t -> ('socket, ('socket, 'c, 'a) Vif_request.t -> 'r, 'r) req type ('socket, 'r) t = | Route : ('socket, 'f, 'x) req * ('e, 'x, 'r) Vif_uri.t * 'f -> ('socket, 'r) t let route req t f = Route (req, t, f) type ('socket, 'r) re_ex = | ReEx : ('socket, 'f, 'x) req * 'f * Re.Mark.t * ('x, 'r) re_url -> ('socket, 'r) re_ex (* It's important to keep the order here, since Re will choose the first regexp if there is ambiguity. *) let rec build_info_list : type s r. (Vif_method.t option -> bool) -> int -> (s, r) t list -> Re.t list * (s, r) re_ex list = fun p idx -> function | [] -> ([], []) | Route ((Request (meth, _) as req), url, fn) :: l when p meth -> let idx, re_url, re = re_url idx url in let rel, wl = build_info_list p idx l in let id, re = Re.mark re in (re :: rel, ReEx (req, fn, id, re_url) :: wl) | Route (Request _, _, _) :: l -> build_info_list p idx l let build_info_list p l = let rel, wl = build_info_list p 1 l in (Re.(compile @@ whole_string @@ alt rel), wl) let build_info : type s r. (s, r) t list -> (Re.re * (s, r) re_ex list) Vif_method.Map.t * (Re.re * (s, r) re_ex list) = fun l -> (* First figure out what methods the routes match *) (* We abuse Vif_method.Map as a set *) let fn : type s r. 'acc -> (s, r) t -> 'acc = fun acc r -> match r with | Route (Request (None, _), _, _) -> acc | Route (Request (Some meth, _), _, _) -> Vif_method.Map.add meth () acc in let methods = List.fold_left fn Vif_method.Map.empty l in let methods = Vif_method.Map.mapi (fun meth () -> build_info_list (function None -> true | Some meth' -> Vif_method.equal meth meth') l) methods and jokers = build_info_list Option.is_none l in (methods, jokers) type 'socket request = { extract: 'c 'a. Vif_method.t option -> ('c, 'a) Vif_type.t -> ('socket, 'c, 'a) Vif_request.t option } let rec find_and_trigger : type socket r. original:string -> socket request -> Re.Group.t -> (socket, r) re_ex list -> r = fun ~original e subs -> function | [] -> raise Not_found | ReEx (Request (meth, c), fn, id, re_url) :: l -> if Re.Mark.test subs id then begin match e.extract meth c with | None -> find_and_trigger ~original e subs l | Some v -> begin try extract_url ~original re_url subs (fn v) with Tyre_exn exn -> Log.debug (fun m -> m "route converter raised exception: %a" Fmt.exn exn); find_and_trigger ~original e subs l end end else find_and_trigger ~original e subs l let match_ (methods, jokers) meth s = match Vif_method.Map.find_opt meth methods with | Some (re, wl) -> begin match Re.exec_opt re s with | None -> None | Some subs -> Some (subs, wl) end | None -> begin match Re.exec_opt (fst jokers) s with | None -> None | Some subs -> Some (subs, snd jokers) end let dispatch : type s r c. default:((s, c, string) Vif_request.t -> string -> r) -> (s, r) t list -> meth:Vif_method.t -> request:s request -> ?host:string -> string -> r = fun ~default l -> let info = build_info l in fun ~meth ~request:e ?host target -> let str = prepare_target ?host target in match match_ info meth str with | None -> default (Option.get (e.extract None Any)) str | Some (subs, wl) -> begin try find_and_trigger ~original:str e subs wl with Not_found -> default (Option.get (e.extract None Any)) str end
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>