url.bend
bend-net-url@0.4.0.0 · 518 lines · 90 definitions
1
2import Base
3
4
5
6type Url is Data:
7 Url{path: String, query: Map<&2, String>}
8
9def unreserved(+c: U32) -> Bool:
10 Bool.or(Bool.and(U32.is_le(65, c), U32.is_le(c, 90)), Bool.or(Bool.and(U32.is_le(97, c), U32.is_le(c, 122)), Bool.or(Bool.and(U32.is_le(48, c), U32.is_le(c, 57)), Bool.or(U32.is_eq(c, 45), Bool.or(U32.is_eq(c, 46), Bool.or(U32.is_eq(c, 95), U32.is_eq(c, 126)))))))
11
12def path_ok(+c: U32) -> Bool:
13 Bool.or(unreserved(c), U32.is_eq(c, 47))
14
15def hexn(+n: U32) -> U32:
16 Bool.pick(U32, U32.is_le(n, 9), (48 + n : U32), (55 + n : U32))
17
18def hexhi(+n: U32) -> U32:
19 U32.div(n, 16)
20
21def hexlo(+n: U32) -> U32:
22 U32.mod(n, 16)
23
24def pct.enc.push(+c: U32, acc: String, keep: Bool) -> String:
25 match keep:
26 case True{}:
27 SCon{Chr{c}, acc}
28 case False{}:
29 SCon{Chr{hexn(hexlo(c))}, SCon{Chr{hexn(hexhi(c))}, SCon{Chr{37}, acc}}}
30
31def pct.enc.path(s: String, acc: String) -> String:
32 match s:
33 case SNil{}:
34 String.reverse(acc)
35 case SCon{Chr{+c}, t}:
36 pct.enc.path(t, pct.enc.push(c, acc, path_ok(c)))
37
38def pct.enc.query(s: String, acc: String) -> String:
39 match s:
40 case SNil{}:
41 String.reverse(acc)
42 case SCon{Chr{+c}, t}:
43 pct.enc.query(t, pct.enc.push(c, acc, unreserved(c)))
44
45def pct.encode_path(s: String) -> String:
46 pct.enc.path(s, SNil{})
47
48def pct.encode_q(s: String) -> String:
49 pct.enc.query(s, SNil{})
50
51def unhex.H(c: U32, hex: Bool) -> Maybe<&2, U32>:
52 match hex:
53 case True{}:
54 Some{(c - 55 : U32)}
55 case False{}:
56 None{}
57
58def unhex.h(+c: U32, lo: Bool) -> Maybe<&2, U32>:
59 match lo:
60 case True{}:
61 Some{(c - 87 : U32)}
62 case False{}:
63 unhex.H(c, Bool.and(U32.is_le(65, c), U32.is_le(c, 70)))
64
65def unhex.d(+c: U32, dec: Bool, lo: Bool) -> Maybe<&2, U32>:
66 match dec:
67 case True{}:
68 Some{(c - 48 : U32)}
69 case False{}:
70 unhex.h(c, lo)
71
72def unhex(+c: U32) -> Maybe<&2, U32>:
73 unhex.d(c, Bool.and(U32.is_le(48, c), U32.is_le(c, 57)), Bool.and(U32.is_le(97, c), U32.is_le(c, 102)))
74
75
76type Pd is Data:
77 PdLit{}
78 PdHi{}
79 PdLo{hi: U32}
80 PdBad{}
81
82def pct.lit(+c: U32, racc: String, pct: Bool) -> Pd & String:
83 match pct:
84 case True{}:
85 (PdHi{}, racc)
86 case False{}:
87 (PdLit{}, SCon{Chr{c}, racc})
88
89def pct.hi(m: Maybe<&2, U32>, racc: String) -> Pd & String:
90 match m:
91 case None{}:
92 (PdBad{}, racc)
93 case Some{x}:
94 (PdLo{x}, racc)
95
96def pct.lo(+x: U32, m: Maybe<&2, U32>, racc: String) -> Pd & String:
97 match m:
98 case None{}:
99 (PdBad{}, racc)
100 case Some{y}:
101 (PdLit{}, SCon{Chr{(x * 16 + y : U32)}, racc})
102
103def pct.byte(h: Char) -> U32:
104 Chr{c} = h
105 c
106
107def pct.end(st: Pd, racc: String) -> Maybe<&2, String>:
108 match st:
109 case PdLit{}:
110 Some{String.reverse(racc)}
111 case PdHi{}:
112 None{}
113 case PdLo{x}:
114 None{}
115 case PdBad{}:
116 None{}
117
118def pct.step(st: Pd, +c: U32, racc: String) -> Pd & String:
119 match st:
120 case PdLit{}:
121 pct.lit(c, racc, U32.is_eq(c, 37))
122 case PdHi{}:
123 pct.hi(unhex(c), racc)
124 case PdLo{+x}:
125 pct.lo(x, unhex(c), racc)
126 case PdBad{}:
127 (PdBad{}, racc)
128
129def pct.end.of(sr: Pd & String) -> Maybe<&2, String>:
130 (st, racc) = sr
131 pct.end(st, racc)
132
133def pct.step.of(sr: Pd & String, +c: U32) -> Pd & String:
134 (st, racc) = sr
135 pct.step(st, c, racc)
136
137def pct.decode.go(s: String, sr: Pd & String) -> Maybe<&2, String>:
138 match s:
139 case SNil{}:
140 pct.end.of(sr)
141 case SCon{h, t}:
142 pct.decode.go(t, pct.step.of(sr, pct.byte(h)))
143
144def pct.decode(s: String) -> Maybe<&2, String>:
145 pct.decode.go(s, (PdLit{}, ""))
146
147
148def take.cut(acc: String, rest: String) -> String & String:
149 (String.reverse(String.drop(acc, 1n)), rest)
150
151
152def take_to.go(s: String, +stop: U32, acc: String, hit: Bool) -> String & String:
153 match s:
154 case SNil{}:
155 match hit:
156 case True{}:
157 take.cut(acc, SNil{})
158 case False{}:
159 (String.reverse(acc), SNil{})
160 case SCon{Chr{+c}, t}:
161 match hit:
162 case True{}:
163 take.cut(acc, SCon{Chr{c}, t})
164 case False{}:
165 take_to.go(t, stop, SCon{Chr{c}, acc}, U32.is_eq(c, stop))
166
167
168def take_to(s: String, stop: U32) -> String & String:
169 take_to.go(s, stop, SNil{}, False{})
170
171def pair(kv: String) -> String & String:
172 take_to(kv, 61)
173
174def qput.decv(m: Map<&2, String>, key: String, v: Maybe<&2, String>) -> Map<&2, String>:
175 match v:
176 case None{}:
177 m
178 case Some{val}:
179 Map.set(&2, String, m, key, val)
180
181def qput.dec(m: Map<&2, String>, k: Maybe<&2, String>, v: Maybe<&2, String>) -> Map<&2, String>:
182 match k:
183 case None{}:
184 m
185 case Some{key}:
186 qput.decv(m, key, v)
187
188def qput.kv(m: Map<&2, String>, kv: String & String) -> Map<&2, String>:
189 (k, v) = kv
190 qput.dec(m, pct.decode(k), pct.decode(v))
191
192def qput(m: Map<&2, String>, kv: String) -> Map<&2, String>:
193 qput.kv(m, pair(kv))
194
195def qfold(xs: List<&2, String>, m: Map<&2, String>) -> Map<&2, String>:
196 match xs:
197 case Nil{}:
198 m
199 case Con{h, t}:
200 qfold(t, qput(m, h))
201
202def parse.q(path: String, rest: String, hasq: Bool) -> Maybe<&2, Url>:
203 match hasq:
204 case False{}:
205 Some{Url{path, Map.new(&2, String)}}
206 case True{}:
207 Some{Url{path, qfold(String.split(rest, '&'), Map.new(&2, String))}}
208
209def parse.path(path: Maybe<&2, String>, +rest: String) -> Maybe<&2, Url>:
210 match path:
211 case None{}:
212 None{}
213 case Some{p}:
214 parse.q(p, rest, Bool.not(String.is_empty(rest)))
215
216def parse.split(pr: String & String) -> Maybe<&2, Url>:
217 (path, +rest) = pr
218 parse.path(pct.decode(path), rest)
219
220def parse.ok(+s: String, ok: Bool) -> Maybe<&2, Url>:
221 match ok:
222 case False{}:
223 None{}
224 case True{}:
225 parse.split(take_to(s, 63))
226
227def parse(+s: String) -> Maybe<&2, Url>:
228 parse.ok(s, String.starts_with(s, "/"))
229
230def enc.pairs.go(xs: List<&2, Sigma<&2, &2, String, _ => String>>, acc: String) -> String:
231 match xs:
232 case Nil{}:
233 acc
234 case (k, v) <> t:
235 enc.pairs.go(t, acc ++ ("&" ++ pct.encode_q(k) ++ "=" ++ pct.encode_q(v)))
236
237def enc.pairs(m: Map<&2, String>) -> String:
238 enc.pairs.go(Map.to_list(&2, String, m), "")
239
240def encode.q(path: String, q: String, empty: Bool) -> String:
241 match empty:
242 case True{}:
243 path
244 case False{}:
245 path ++ "?" ++ String.drop(q, 1n)
246
247def encode.go(path: String, +q: String) -> String:
248 encode.q(path, q, String.is_empty(q))
249
250def encode(u: Url) -> String:
251 Url{path, query} = u
252 encode.go(pct.encode_path(path), enc.pairs(query))
253
254def form.drop(q: String, empty: Bool) -> String:
255 match empty:
256 case True{}:
257 ""
258 case False{}:
259 String.drop(q, 1n)
260
261def form.go(+q: String) -> String:
262 form.drop(q, String.is_empty(q))
263
264
265def form(m: Map<&2, String>) -> String:
266 form.go(enc.pairs(m))
267
268
269
270
271
272type Abs is Data:
273 Abs{scheme: String, host: String, port: U32, target: String}
274
275def has(s: String, +c: U32) -> Bool:
276 match s:
277 case SNil{}:
278 False{}
279 case SCon{Chr{d}, t}:
280 Bool.or(U32.is_eq(d, c), has(t, c))
281
282def auth_end(+c: U32) -> Bool:
283 Bool.or(U32.is_eq(c, 47), Bool.or(U32.is_eq(c, 63), U32.is_eq(c, 35)))
284
285def take.back(acc: String, rest: String) -> String & String:
286 match acc:
287 case SNil{}:
288 (SNil{}, rest)
289 case SCon{x, a2}:
290 (String.reverse(a2), SCon{x, rest})
291
292
293def take_auth.go(s: String, acc: String, hit: Bool) -> String & String:
294 match s:
295 case SNil{}:
296 match hit:
297 case True{}:
298 take.back(acc, SNil{})
299 case False{}:
300 (String.reverse(acc), SNil{})
301 case SCon{Chr{+c}, t}:
302 match hit:
303 case True{}:
304 take.back(acc, SCon{Chr{c}, t})
305 case False{}:
306 take_auth.go(t, SCon{Chr{c}, acc}, auth_end(c))
307
308def default_port(+scheme: String) -> Maybe<&2, U32>:
309 Bool.pick(Maybe<&2, U32>, String.eq(scheme, "http"), Some{80}, Bool.pick(Maybe<&2, U32>, String.eq(scheme, "https"), Some{443}, None{}))
310
311def abs.target(+pq: String) -> String:
312 Bool.pick(String, String.starts_with(pq, "/"), pq, "/" ++ pq)
313
314def abs.fst(ab: String & String) -> String:
315 (a, b) = ab
316 a
317
318
319def abs.build(scheme: String, host: String, port: Maybe<&2, U32>, rest: String) -> Maybe<&2, Abs>:
320 match port:
321 case None{}:
322 None{}
323 case Some{p}:
324 Some{Abs{scheme, host, p, abs.target(abs.fst(take_to(rest, 35)))}}
325
326def abs.port.num(n: Maybe<&2, U32>) -> Maybe<&2, U32>:
327 match n:
328 case None{}:
329 None{}
330 case Some{+p}:
331 Bool.pick(Maybe<&2, U32>, U32.is_le(p, 65535), Some{p}, None{})
332
333
334def abs.port(+ps: String, dflt: Maybe<&2, U32>) -> Maybe<&2, U32>:
335 Bool.pick(Maybe<&2, U32>, String.is_empty(ps), dflt, abs.port.num(U32.read(ps)))
336
337def abs.hostok(scheme: String, host: String, port: Maybe<&2, U32>, rest: String, ok: Bool) -> Maybe<&2, Abs>:
338 match ok:
339 case False{}:
340 None{}
341 case True{}:
342 abs.build(scheme, host, port, rest)
343
344
345def abs.hostport(scheme: String, dflt: Maybe<&2, U32>, hp: String & String, rest: String) -> Maybe<&2, Abs>:
346 (+host, ps) = hp
347 abs.hostok(scheme, String.to_lower(host), abs.port(ps, dflt), rest, Bool.not(Bool.or(String.is_empty(host), String.starts_with(host, "["))))
348
349
350def abs.auth.if(+scheme: String, auth: String, rest: String, creds: Bool) -> Maybe<&2, Abs>:
351 match creds:
352 case True{}:
353 None{}
354 case False{}:
355 abs.hostport(scheme, default_port(scheme), take_to(auth, 58), rest)
356
357def abs.auth(scheme: String, ar: String & String) -> Maybe<&2, Abs>:
358 (+auth, rest) = ar
359 abs.auth.if(scheme, auth, rest, has(auth, 64))
360
361def abs.slashes(scheme: String, +rest: String, ok: Bool) -> Maybe<&2, Abs>:
362 match ok:
363 case False{}:
364 None{}
365 case True{}:
366 abs.auth(String.to_lower(scheme), take_auth.go(String.drop(rest, 2n), SNil{}, False{}))
367
368def abs.scheme(sr: String & String) -> Maybe<&2, Abs>:
369 (scheme, +rest) = sr
370 abs.slashes(scheme, rest, String.starts_with(rest, "//"))
371
372
373def absolute(s: String) -> Maybe<&2, Abs>:
374 abs.scheme(take_to(s, 58))
375
376def abs.port.dflt(m: Maybe<&2, U32>) -> U32:
377 match m:
378 case None{}:
379 0
380 case Some{p}:
381 p
382
383
384def host_field(a: Abs) -> String:
385 Abs{+scheme, +host, +port, target} = a
386 Bool.pick(String, U32.is_eq(port, abs.port.dflt(default_port(scheme))), host, host ++ ":" ++ U32.show(port))
387
388
389
390
391def dots.pop(stack: List<&2, String>) -> List<&2, String>:
392 match stack:
393 case Nil{}:
394 Nil{}
395 case Con{h, t}:
396 t
397
398def dots.push(seg: String, stack: List<&2, String>, dot: Bool, up: Bool) -> List<&2, String>:
399 match dot:
400 case True{}:
401 stack
402 case False{}:
403 match up:
404 case True{}:
405 dots.pop(stack)
406 case False{}:
407 Con{seg, stack}
408
409def dots.join(xs: List<&2, String>, acc: String) -> String:
410 match xs:
411 case Nil{}:
412 acc
413 case Con{h, t}:
414 dots.join(t, "/" ++ h ++ acc)
415
416
417def dots.go(segs: List<&2, String>, stack: List<&2, String>, trail: Bool) -> String:
418 match segs:
419 case Nil{}:
420 +out = dots.join(stack, "")
421 Bool.pick(String, Bool.and(trail, Bool.not(String.is_empty(out))), out ++ "/", Bool.pick(String, String.is_empty(out), "/", out))
422 case Con{+seg, t}:
423 +dot = String.eq(seg, ".")
424 +up = String.eq(seg, "..")
425 dots.go(t, dots.push(seg, stack, dot, up), Bool.or(dot, up))
426
427def dots.abs(xs: List<&2, String>) -> String:
428 match xs:
429 case Nil{}:
430 "/"
431 case Con{lead, t}:
432 dots.go(t, Nil{}, False{})
433
434
435def remove_dots(path: String) -> String:
436 dots.abs(String.split(path, '/'))
437
438def dir.go(s: String, +acc: String, last: String) -> String:
439 match s:
440 case SNil{}:
441 String.reverse(last)
442 case SCon{Chr{+c}, t}:
443 +acc2 = {SCon{Chr{c}, acc} : String}
444 dir.go(t, acc2, Bool.pick(String, U32.is_eq(c, 47), acc2, last))
445
446
447def dir(path: String) -> String:
448 dir.go(path, SNil{}, SNil{})
449
450def with_q(path: String, q: String, has_q: Bool) -> String:
451 match has_q:
452 case True{}:
453 path ++ "?" ++ q
454 case False{}:
455 path
456
457def res.build(scheme: String, host: String, port: U32, path: String, q: String, has_q: Bool) -> Maybe<&2, Abs>:
458 Some{Abs{scheme, host, port, with_q(path, q, has_q)}}
459
460
461type PQ is Data:
462 PQ{path: String, q: String, has_q: Bool}
463
464def res.pq(pq: String & String, has_q: Bool) -> PQ:
465 (p, q) = pq
466 PQ{p, q, has_q}
467
468def res.ref(+r: String) -> PQ:
469 res.pq(take_to(r, 63), has(r, 63))
470
471
472
473def res.rel.go(+scheme: String, +host: String, +port: U32, +bpath: String, +bq: String, +bhas: Bool, +rp: String, +q: String, +rhas: Bool) -> Maybe<&2, Abs>:
474 Bool.pick(Maybe<&2, Abs>, String.is_empty(rp),
475 Bool.pick(Maybe<&2, Abs>, rhas, res.build(scheme, host, port, bpath, q, True{}), res.build(scheme, host, port, bpath, bq, bhas)),
476 res.build(scheme, host, port, remove_dots(Bool.pick(String, String.starts_with(rp, "/"), rp, dir(bpath) ++ rp)), q, rhas))
477
478def res.rel(scheme: String, host: String, port: U32, bpath: String, bq: String, bhas: Bool, r: PQ) -> Maybe<&2, Abs>:
479 PQ{rp, q, rhas} = r
480 res.rel.go(scheme, host, port, bpath, bq, bhas, rp, q, rhas)
481
482def res.base(+r: String, b: PQ, a: Abs) -> Maybe<&2, Abs>:
483 PQ{bpath, bq, bhas} = b
484 Abs{scheme, host, port, target} = a
485 res.rel(scheme, host, port, bpath, bq, bhas, res.ref(abs.fst(take_to(r, 35))))
486
487def res.local(+r: String, a: Abs) -> Maybe<&2, Abs>:
488 Abs{+scheme, +host, +port, +target} = a
489 res.base(r, res.ref(target), Abs{scheme, host, port, target})
490
491def abs.scheme_of(a: Abs) -> String:
492 Abs{scheme, host, port, target} = a
493 scheme
494
495def res.net(+r: String, a: Abs, net: Bool) -> Maybe<&2, Abs>:
496 match net:
497 case True{}:
498 absolute(abs.scheme_of(a) ++ ":" ++ r)
499 case False{}:
500 res.local(r, a)
501
502def scheme.ok(s: String) -> Bool:
503 match s:
504 case SNil{}:
505 True{}
506 case SCon{Chr{+c}, t}:
507 Bool.and(Bool.or(Bool.or(Bool.and(U32.is_le(97, c), U32.is_le(c, 122)), Bool.and(U32.is_le(65, c), U32.is_le(c, 90))), Bool.or(Bool.and(U32.is_le(48, c), U32.is_le(c, 57)), Bool.or(U32.is_eq(c, 43), Bool.or(U32.is_eq(c, 45), U32.is_eq(c, 46))))), scheme.ok(t))
508
509
510def abs.has_scheme(+pre: String, colon: Bool) -> Bool:
511 Bool.and(colon, Bool.and(Bool.not(String.is_empty(pre)), scheme.ok(pre)))
512
513
514def resolve(a: Abs, +ref: String) -> Maybe<&2, Abs>:
515 +low = String.to_lower(ref)
516 Bool.pick(Maybe<&2, Abs>, Bool.or(String.starts_with(low, "http://"), String.starts_with(low, "https://")), absolute(ref),
517 Bool.pick(Maybe<&2, Abs>, abs.has_scheme(abs.fst(take_to(ref, 58)), has(ref, 58)), None{}, res.net(ref, a, String.starts_with(ref, "//"))))
518