src/echo.bend

emerging-ezhttp@0.20.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.20.0.0/src/http.bend as Http
8import emerging-ezhttp@0.20.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"))