Skip to content

Commit 5609677

Browse files
introduce light url
1 parent 731c8af commit 5609677

File tree

6 files changed

+494
-1
lines changed

6 files changed

+494
-1
lines changed

devkit.opam

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ depends: [
3030
"uri"
3131
"yojson" {>= "1.6.0"}
3232
"odoc" {with-doc}
33+
"ppx_expect" {with-test}
3334
]
3435
depopts: [
3536
"gperftools"
3637
"jemalloc"
3738
"opentelemetry"
3839
]
40+
pin-depends: [
41+
[ "tyre.dev" "git+https://github.com/drup/tyre.git#master" ]
42+
]
3943
conflicts: [
4044
"jemalloc" {< "0.2"}
4145
"opentelemetry" {< "0.6"}

dune

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
stdlib-shims
2222
str
2323
trace.core
24+
tyre
2425
unix
2526
uri
2627
yojson
@@ -46,7 +47,10 @@
4647
lwt_flag
4748
lwt_util
4849
parallel
49-
web))
50+
web)
51+
( (pps ppx_deriving.show ppx_deriving.ord ppx_deriving.eq)
52+
lurl)
53+
)
5054
))
5155

5256
(library

lurl.ml

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
[@@@ocaml.warning "-37"]
2+
(* values are constructed from C *)
3+
4+
open Printf
5+
open ExtLib
6+
let ( let* ) = Result.bind
7+
8+
(* sync urlparse_stubs.c *)
9+
type scheme =
10+
| Http
11+
| Https
12+
[@@deriving ord, eq, show]
13+
14+
type t = {
15+
scheme : scheme;
16+
port : int;
17+
path : string list;
18+
query : (string * string list) list;
19+
fragment : string;
20+
host : string;
21+
}
22+
[@@deriving ord, eq, show]
23+
24+
type path = string list
25+
26+
type query = (string * string list) list
27+
28+
let debug = show
29+
30+
exception Malformed of string
31+
32+
let scheme u = u.scheme
33+
let host u = u.host
34+
let port u = u.port
35+
let path u = u.path
36+
let query u = u.query
37+
let fragment u = if u.fragment <> "" then Some u.fragment else None
38+
39+
let rec args_of_query query =
40+
match query with
41+
| [] -> Ok []
42+
| (k, [ v ]) :: query ->
43+
let* args = args_of_query query in
44+
Ok ((k, v) :: args)
45+
| (k, _ :: _ :: _) :: _ -> Error (sprintf "key %S has multiple values" k)
46+
| (k, []) :: _ -> Error (sprintf "key %S has no value" k)
47+
48+
let args url = url |> query |> args_of_query
49+
50+
let query_of_args args = List.map (fun (k, v) -> k, [ v ]) args
51+
52+
let default_port = function
53+
| Http -> 80
54+
| Https -> 443
55+
56+
let default_scheme = function
57+
| 443 -> Https
58+
| _ -> Http
59+
60+
let make ?scheme ~host ?port ?(path = []) ?(query = []) ?(fragment = "") () =
61+
let scheme, port =
62+
match scheme, port with
63+
| None, None -> Http, 80
64+
| None, Some port -> default_scheme port, port
65+
| Some scheme, None -> scheme, default_port scheme
66+
| Some scheme, Some port -> scheme, port
67+
in
68+
if port <= 0 || port > 65535 then invalid_arg @@ sprintf "Url.make: %s port %d" host port;
69+
{ scheme; host; port; path; query; fragment }
70+
71+
let make_args ?scheme ~host ?port ?path ?args ?fragment () =
72+
make ?scheme ?port ?path ?query:(Option.map query_of_args args) ?fragment ~host ()
73+
74+
let without_path u = {u with path=[]}
75+
let without_query u = {u with query=[]}
76+
let without_fragment u = { u with fragment = "" }
77+
let without_parameters u = { u with query = []; fragment = "" }
78+
79+
let string_of_scheme = function
80+
| Http -> "http"
81+
| Https -> "https"
82+
83+
let decode = Web.rawurldecode
84+
85+
let decode_plus = Web.urldecode
86+
87+
module Re = struct
88+
89+
open Tyre
90+
91+
let list1 r = conv (fun (elt, li) -> elt :: li) (fun li -> List.hd li, List.tl li) (r <&> list r)
92+
93+
module Charsets = struct
94+
open Charset
95+
96+
let _gen_delims = set ":/?#[]@"
97+
98+
let sub_delims = set "!$&'()*+,;="
99+
100+
let _reserved = _gen_delims || sub_delims
101+
102+
let unreserved = ascii && (alpha || digit || set "-._~")
103+
104+
let host = unreserved || sub_delims || char ':'
105+
106+
let path = unreserved || sub_delims || char ':' || char '@'
107+
end
108+
109+
let percent_encoded = matched_string (str "%" <* (xdigit <&> xdigit))
110+
111+
let charset_or_pct_enc ~allow_empty ~decode chrs =
112+
non_greedy @@
113+
map decode @@
114+
matched_string
115+
( (if allow_empty then list else list1)
116+
(matched_string (charset chrs) <|> percent_encoded)
117+
)
118+
119+
let scheme = const Http (str "http") <|> const Https (str "https")
120+
121+
let host = charset_or_pct_enc ~allow_empty:false ~decode Charsets.host
122+
123+
let port = pos_int
124+
125+
let path = non_greedy @@ list (str "/" *> charset_or_pct_enc ~allow_empty:true ~decode Charsets.path) <* opt (str "/")
126+
127+
let query =
128+
let open Tyre in
129+
non_greedy
130+
begin
131+
separated_list ~sep:(str "&")
132+
begin
133+
let+ k = charset_or_pct_enc ~allow_empty:true ~decode Charsets.path
134+
and+ _ = str "="
135+
and+ vs = separated_list ~sep:(str ",") (charset_or_pct_enc ~allow_empty:true ~decode:decode_plus Charsets.path) in
136+
k, vs
137+
end
138+
end
139+
140+
let fragment = (charset_or_pct_enc ~allow_empty:true ~decode:decode_plus Charsets.path)
141+
142+
let url =
143+
let+ scheme = opt (scheme <* str "://")
144+
and+ host
145+
and+ port = opt (str ":" *> port)
146+
and+ path
147+
and+ query = opt (str "?" *> query)
148+
and+ fragment = opt (str "#" *> fragment)
149+
in
150+
make ?scheme ~host ?port ~path ?query ?fragment ()
151+
end
152+
153+
let map_tyre_error r =
154+
r
155+
|> Result.map_error (function
156+
| `NoMatch _ -> "Invalid query string"
157+
| `ConverterFailure exn -> raise exn)
158+
159+
let parse_re re =
160+
let re = Tyre.compile Tyre.(start *> re <* stop) in
161+
fun str ->
162+
if String.length str > 128_000
163+
then (Error "String is longer than 128 000")
164+
else Tyre.exec re str |> map_tyre_error
165+
166+
let to_exn parse txt =
167+
match parse txt with
168+
| Ok v -> v
169+
| Error msg -> raise (Malformed msg)
170+
171+
let parse_query = parse_re Re.query
172+
let parse = parse_re Re.url
173+
174+
let parse_query_exn = to_exn parse_query
175+
let parse_exn = to_exn parse
176+
177+
let rec push_concat ~push ~sep f li =
178+
match li with
179+
| [ elt ] -> f elt
180+
| [] -> ()
181+
| elt :: (_ :: _ as li) ->
182+
f elt;
183+
push sep;
184+
push_concat ~push ~sep f li
185+
186+
let encode_plus = Web.urlencode
187+
188+
let encode = Web.rawurlencode
189+
190+
let push_path ~push path =
191+
push "/";
192+
path
193+
|> List.iter begin fun segment ->
194+
push (encode segment);
195+
push "/"
196+
end
197+
198+
let push_query ~push query =
199+
query
200+
|> push_concat ~push ~sep:"&" begin fun (k, vs) ->
201+
push (encode k);
202+
push "=";
203+
vs |> push_concat ~push ~sep:"," (fun v -> push (encode_plus v))
204+
end
205+
let push_full_path ~push u =
206+
push_path ~push u.path;
207+
if u.query <> [] then begin
208+
push "?";
209+
push_query ~push u.query
210+
end;
211+
if u.fragment <> "" then begin
212+
push "#";
213+
push (encode u.fragment)
214+
end
215+
216+
let push_url ~push u =
217+
let host = u.host in
218+
push @@ string_of_scheme u.scheme;
219+
push "://";
220+
push host;
221+
if default_port u.scheme <> u.port then (
222+
push ":";
223+
push (string_of_int u.port));
224+
push_full_path ~push u
225+
226+
let string_of_push f a =
227+
let b = Buffer.create 256 in
228+
let push = Buffer.add_string b in
229+
f ~push a;
230+
Buffer.contents b
231+
232+
let to_string = string_of_push push_url
233+
234+
let full_path = string_of_push push_full_path
235+
236+
let query_to_string = string_of_push push_query
237+
238+
let is_root u = u.path = [] && u.query = [] && u.fragment = ""
239+
240+
let root url = { url with path = []; query = []; fragment = "" }
241+
242+
let hash u = Hashtbl.hash @@ to_string u
243+
244+
let with_host url host = { url with host }
245+
246+
let with_scheme url scheme =
247+
match scheme, url.scheme, url.port with
248+
| Http, Http, 80 | Https, Https, 443 -> url
249+
| Http, _, _ -> { url with scheme = Http; port = 80 }
250+
| Https, _, _ -> { url with scheme = Https; port = 443 }
251+
252+
let with_query url query = { url with query }
253+
254+
let with_path url path = { url with path }
255+
256+
let with_fragment url fragment = { url with fragment }
257+
258+
module Op = struct
259+
let ( / ) u segment = { u with path = u.path @ [ segment ] }
260+
261+
let ( /? ) u args =
262+
if u.query <> [] then failwith "Query should be empty when using (/?)" else { u with query = query_of_args args }
263+
end
264+
265+
include Op

0 commit comments

Comments
 (0)