src/echo.bend
emerging-ezhttp@0.19.0.0 · 141 lines · 0 definitions
1# ezhttp/src/echo: the Cookie request header a client sends back after
2# Set-Cookie fields, reduced to names and values (RFC 6265 §5.2 parsing,
3# §5.3 step 11 replacement, §5.4 serialization). No attribute is read: no
4# Domain, Path, Expires, Max-Age, Secure or HttpOnly, so nothing expires and
5# every cookie goes back. Pure. SPEC.md HTTP-CK lists what is guaranteed.
6import Base
7import emerging-ezhttp@0.19.0.0/src/http.bend as Http
8import emerging-ezhttp@0.19.0.0/src/qs.bend as Qs
9
10# whitespace as RFC 6265 §5.2 strips it: SP or HTAB (RFC 5234 WSP)
11def wsp(+c: Char) -> Bool:
12 Bool.or(Char.is_eq(c, ' '), Char.is_eq(c, '\t'))
13
14# one character of the leading strip: dropped, and the strip goes on, when
15# it is WSP
16def wsp.lead.if(t: String, +h: Char, space: Bool, rest: Unit -> String)
17 -> String:
18 match space:
19 case True{}:
20 rest(Unit{})
21 case False{}:
22 SCon{h, t}
23
24# a string without its leading WSP
25def wsp.lead(s: String) -> String:
26 match s:
27 case SNil{}:
28 SNil{}
29 case SCon{+h, +t}:
30 wsp.lead.if(t, h, wsp(h), _u => wsp.lead(t))
31
32# a string without its leading and trailing WSP
33def wsp.trim(s: String) -> String:
34 String.reverse(wsp.lead(String.reverse(wsp.lead(s))))
35
36# a pair whose name is known: ignored when the name is empty (§5.2 step 5)
37def pair.named(+name: String, value: String, empty: Bool) -> Maybe<&2, Qs.Kv()>:
38 match empty:
39 case True{}:
40 None{}
41 case False{}:
42 Some{(name, value)}
43
44# the name-value-pair cut at its first `=`: none without one (§5.2 step 2),
45# else the name before it and the value after it, each without leading and
46# trailing WSP (steps 3, 4)
47def pair.cut(m: Maybe<&2, Qs.Kv()>) -> Maybe<&2, Qs.Kv()>:
48 match m:
49 case None{}:
50 None{}
51 case Some{(before, after)}:
52 +name = wsp.trim(before)
53 pair.named(name, wsp.trim(after), String.is_empty(name))
54
55# the cookie one Set-Cookie field value sets: its name-value-pair is the text
56# before its first `;`, all of it without one (§5.2 step 1)
57def pair(line: String) -> Maybe<&2, Qs.Kv()>:
58 pair.cut(Qs.cut(Http.first(String.split(line, ';'))))
59
60# one stored cookie, once it is known whether it has the new one's name:
61# that one takes the new value in its place, any other is kept and the rest
62# of the store looked at
63def put.at(key: String, old: String, value: String, t: List<&2, Qs.Kv()>,
64 hit: Bool, rest: Unit -> List<&2, Qs.Kv()>) -> List<&2, Qs.Kv()>:
65 match hit:
66 case True{}:
67 (key, value) <> t
68 case False{}:
69 (key, old) <> rest(Unit{})
70
71# a cookie put in a store: a stored one of the same name (compared exactly)
72# keeps its place and takes the new value (§5.3 step 11.3 keeps its
73# creation-time, which orders it, §5.4 step 2), else it goes after the others
74def put(jar: List<&2, Qs.Kv()>, +name: String, +value: String)
75 -> List<&2, Qs.Kv()>:
76 match jar:
77 case []:
78 [(name, value)]
79 case (+key, old) <> +t:
80 put.at(key, old, value, t, String.eq(key, name),
81 _u => put(t, name, value))
82
83# a store after one Set-Cookie field value: unchanged when it sets nothing
84def add(jar: List<&2, Qs.Kv()>, m: Maybe<&2, Qs.Kv()>) -> List<&2, Qs.Kv()>:
85 match m:
86 case None{}:
87 jar
88 case Some{(name, value)}:
89 put(jar, name, value)
90
91# a store after Set-Cookie field values, in the order they came
92def store(lines: List<&2, String>, jar: List<&2, Qs.Kv()>)
93 -> List<&2, Qs.Kv()>:
94 match lines:
95 case []:
96 jar
97 case l <> t:
98 store(t, add(jar, pair(l)))
99
100# one cookie in the Cookie header: its name, `=`, its value (§5.4 step 4.1)
101def one(name: String, value: String) -> String:
102 name ++ "=" ++ value
103
104# the cookies after the first, each after `; ` (§5.4 step 4.2)
105def more(jar: List<&2, Qs.Kv()>) -> String:
106 match jar:
107 case []:
108 ""
109 case (name, value) <> t:
110 "; " ++ one(name, value) ++ more(t)
111
112# a store as the Cookie header's value, empty for an empty store
113def line(jar: List<&2, Qs.Kv()>) -> String:
114 match jar:
115 case []:
116 ""
117 case (name, value) <> t:
118 one(name, value) ++ more(t)
119
120# the Cookie header value to send after these Set-Cookie field values
121def echo(lines: List<&2, String>) -> String:
122 line(store(lines, []))
123
124# one header field: its value in front of the rest when it is Set-Cookie
125def values.put(value: String, rest: List<&2, String>, hit: Bool)
126 -> List<&2, String>:
127 match hit:
128 case True{}:
129 value <> rest
130 case False{}:
131 rest
132
133# the values of the Set-Cookie fields among response headers, in order (field
134# names compared case-insensitively, RFC 9110 §5.1)
135def values(hs: List<&2, Http.Header>) -> List<&2, String>:
136 match hs:
137 case []:
138 []
139 case Http.H{name, value} <> t:
140 values.put(value, values(t), String.eq(String.to_lower(name),
141 "set-cookie"))