package vif

  1. Overview
  2. Docs
A simple web framework for OCaml 5

Install

dune-project
 Dependency

Authors

Maintainers

Sources

vif-0.0.1.beta2.tbz
sha256=a16ff3dba7675d237d59188b032052b383ad9e367eb7c570c4e6e78b978b98e5
sha512=ad553f15f33f9f2427b691713f630476fd1f15b4cb61944a401cfb35c29dd3d1d3760b02dd211bddd39b6cf6ccc8ea5d9f88eefc3776611e2a7020242a16b9a9

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
(* 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 (_, _) 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 -> int * (f, r) re_path * Re.t list =
  let open Re in
  fun i -> function
    | Host s ->
        let re = Re.str @@ Uri.pct_encode ~component:`Host s in
        (i, Start, [ re ])
    | Rel -> (i, Start, [])
    | Path_const (p, s) ->
        let i', p, re = re_path i p in
        (i', p, str s :: Ext.slash :: re)
    | Path_atom (p, a) ->
        let i', wp, rp = re_path i p in
        let i'', wa, ra = re_atom_path i' @@ from_t a in
        (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 : ('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) -> (
      let end_path =
        match slash with
        | No_slash -> Re.epsilon
        | Slash -> Re.char '/'
        | Maybe_slash -> Re.(opt @@ char '/')
      in
      let idx, wp, rp = re_path i p in
      match q with
      | Nil -> (idx, ReUrl (wp, Nil), Re.seq @@ List.rev (end_path :: rp))
      | Any ->
          let end_re = Re.(opt @@ seq [ Re.char '?'; rep any ]) in
          ( idx
          , ReUrl (wp, Nil)
          , Re.seq @@ List.rev_append rp [ end_path; end_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 (wp, wq), re))

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 f = k (f 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 (wp, wq)) subs f ->
  let k = extract_query ~original wq subs in
  let k = extract_path ~original wp subs k in
  k f

let prepare_uri uri =
  uri |> Uri.query |> sort_query |> Uri.with_query uri |> Uri.path_and_query

let extract url =
  let _idx, re_url, re = re_url 1 url in
  let re = Re.(compile @@ whole_string re) in
  fun ~f uri ->
    let s = prepare_uri uri in
    let subs = Re.exec re s in
    extract_url ~original:s re_url subs f

(** {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, f) :: 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, f, 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 prepare_uri uri =
  uri |> Uri.query |> sort_query |> Uri.with_query uri |> Uri.path_and_query

let rec find_and_trigger : type s r.
    original:string -> s request -> Re.Group.t -> (s, r) re_ex list -> r =
 fun ~original e subs -> function
  | [] -> raise Not_found
  | ReEx (Request (meth, c), f, id, re_url) :: l ->
      if Re.Mark.test subs id then
        match e.extract meth c with
        | None -> find_and_trigger ~original e subs l
        | Some v -> (
            try extract_url ~original re_url subs (f 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)
      else find_and_trigger ~original e subs l

let match_ (methods, jokers) meth s =
  let ( let+ ) x f = Option.map f x in
  match Vif_method.Map.find_opt meth methods with
  | Some (re, wl) ->
      let+ subs = Re.exec_opt re s in
      (subs, wl)
  | None ->
      let+ subs = Re.exec_opt (fst jokers) s in
      (subs, snd jokers)

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
    -> target:string
    -> r =
 fun ~default l ->
  let info = build_info l in
  fun ~meth ~request:e ~target ->
    let s = prepare_uri (Uri.of_string target) in
    match match_ info meth s with
    | None -> default (Option.get (e.extract None Any)) s
    | Some (subs, wl) -> (
        try find_and_trigger ~original:s e subs wl
        with Not_found -> default (Option.get (e.extract None Any)) s)