src/wirespec.bend

emerging-ezhttp@0.17.0.0 · 354 lines · 0 definitions

1# ezhttp/src/wirespec: the text the wire effect reads, and what its answer
2# means, pure. Kept apart from wire.bend (the effect) so the proof gate can
3# import it. What the client sends for a URL (wire.exchange) and when it has
4# heard a whole answer (wire.read.next) are decided here; wire.bend and
5# client.bend act on them.
6#
7# Spec: scheme, host, port each on their own line; everything after the third
8# newline is the request verbatim. The scheme line may carry a timeout after a
9# space: 32 binary digits, least significant first, in milliseconds. A scheme
10# line with no timeout means 60000 ms. SPEC.md HTTP-CLI lists what is
11# guaranteed.
12import Base
13import emerging-ezhttp@0.17.0.0/src/http.bend as Http
14import emerging-ezhttp@0.17.0.0/src/url.bend as Url
15import emerging-ezhttp@0.17.0.0/src/body.bend as Body
16
17# the word the effect reads for a scheme that wants TLS
18def wire.scheme(secure: Bool) -> String:
19  match secure:
20    case True{}:
21      "tls"
22    case False{}:
23      "tcp"
24
25# the spec the effect reads
26def wire.spec(secure: Bool, host: String, port: U32, req: String) -> String:
27  wire.scheme(secure) ++ "\n" ++ host ++ "\n" ++ U32.show(port) ++ "\n" ++ req
28
29# --- the timeout ---
30
31# the timeout a spec with none carries: 60 s, in milliseconds
32def wire.ms.dflt() -> U32:
33  60000
34
35# one bit as a binary digit
36def wire.bit(b: Bool) -> Char:
37  match b:
38    case True{}:
39      '1'
40    case False{}:
41      '0'
42
43# a word as binary digits, least significant first
44def wire.bits(n: Nat, w: Word(n)) -> String:
45  match n:
46    case 0n:
47      SNil{}
48    case 1n+p:
49      match w:
50        case WCon{b, t}:
51          SCon{wire.bit(b), wire.bits(p, t)}
52
53# binary digits, least significant first, as a word; a missing digit is 0
54def wire.bits.read(n: Nat, s: String) -> Word(n):
55  match n:
56    case 0n:
57      WNil{}
58    case 1n+p:
59      match s:
60        case SNil{}:
61          WCon{False{}, wire.bits.read(p, SNil{})}
62        case SCon{c, t}:
63          WCon{Char.is_eq(c, '1'), wire.bits.read(p, t)}
64
65# a timeout as the scheme line carries it
66def wire.ms.text(ms: U32) -> String:
67  match ms:
68    case U32{w}:
69      wire.bits(32n, w)
70
71# the first line of a timed spec: the scheme, a space, the timeout
72def wire.head(secure: Bool, ms: U32) -> String:
73  wire.scheme(secure) ++ " " ++ wire.ms.text(ms)
74
75# the lines after the first: host, port, then the request verbatim
76def wire.rest(host: String, port: U32, req: String) -> String:
77  host ++ "\n" ++ U32.show(port) ++ "\n" ++ req
78
79# the spec the effect reads, with a timeout in milliseconds
80def wire.spec.timed(secure: Bool, ms: U32, host: String, port: U32,
81  req: String) -> String:
82  wire.head(secure, ms) ++ "\n" ++ wire.rest(host, port, req)
83
84# the timeout after the scheme word, or the default when there is none
85def wire.ms.words(ws: List<&2, String>) -> U32:
86  match ws:
87    case []:
88      wire.ms.dflt()
89    case _ <> []:
90      wire.ms.dflt()
91    case _ <> (bits <> _):
92      U32{wire.bits.read(32n, bits)}
93
94# the first line of a spec
95def wire.line(ls: List<&2, String>) -> String:
96  match ls:
97    case []:
98      ""
99    case l <> _:
100      l
101
102# the timeout the effect reads from a spec, in milliseconds
103def wire.ms(spec: String) -> U32:
104  wire.ms.words(String.split(wire.line(String.split(spec, '\n')), ' '))
105
106# --- the answer ---
107
108# the wire status for an exchange that ran out of time (curl's
109# CURLE_OPERATION_TIMEDOUT)
110def wire.late() -> U32:
111  28
112
113# what a failed exchange is reported as: a timeout is marked, any other
114# failure's reason is kept as it is
115def wire.why.of(why: String, late: Bool) -> String:
116  match late:
117    case True{}:
118      "timed out: " ++ why
119    case False{}:
120      why
121
122# a failed exchange's status and reason as the reason the client reports
123def wire.why(code: Maybe<&2, U32>, why: String) -> String:
124  match code:
125    case None{}:
126      why
127    case Some{n}:
128      wire.why.of(why, U32.is_eq(n, wire.late()))
129
130# whether a reported reason is a timeout
131def wire.timed_out(why: String) -> Bool:
132  String.starts_with(why, "timed out: ")
133
134# --- what is sent ---
135
136# what a request to a URL becomes: nothing when the URL did not parse, else
137# the scheme (secure or not), host, port, method and request text the effect
138# is given; the answer is read by that method (wire.read.next)
139type Exchange is Data:
140  NoUrl{why: String}
141  Exchange{secure: Bool, host: String, port: U32, method: String, req: String}
142
143# the exchange once it is known whether the scheme wants TLS
144def wire.exchange.at(+method: String, +secure: Bool, +host: String,
145  +port: U32, path: String, headers: List<&2, Http.Header>, body: String)
146  -> Exchange:
147  Exchange{secure, host, port, method,
148    Http.request.at(method, secure, host, port, path, headers, body)}
149
150# the method an exchange is sent and read by, none when nothing is sent
151def wire.exchange.method(ex: Exchange) -> Maybe<&2, String>:
152  match ex:
153    case NoUrl{why}:
154      None{}
155    case Exchange{secure, host, port, method, req}:
156      Some{method}
157
158# a request with this method, headers and body to a URL that came apart
159def wire.exchange(method: String, l: Url.Loc, headers: List<&2, Http.Header>,
160  body: String) -> Exchange:
161  match l:
162    case Url.Bad{why}:
163      NoUrl{why}
164    case Url.Loc{scheme, host, port, path}:
165      +secure = String.eq(scheme, "https")
166      wire.exchange.at(method, secure, host, port, path, headers, body)
167
168# a request with n octets of body to a URL that came apart: the request text
169# ends at the blank line, and the octets go after it as the tail (wire.tail)
170def wire.exchange.octets(+method: String, l: Url.Loc,
171  headers: List<&2, Http.Header>, n: Nat) -> Exchange:
172  match l:
173    case Url.Bad{why}:
174      NoUrl{why}
175    case Url.Loc{scheme, +host, +port, path}:
176      +secure = String.eq(scheme, "https")
177      Exchange{secure, host, port, method,
178        Http.request.head.at(method, secure, host, port, path, headers, n)}
179
180# a request with this body to a URL that came apart: text inside the request
181# text as UTF-8, octets after it (SPEC.md HTTP-CLI-10)
182def wire.exchange.body(method: String, l: Url.Loc,
183  headers: List<&2, Http.Header>, b: Body.Body) -> Exchange:
184  match b:
185    case Body.Empty{}:
186      wire.exchange(method, l, headers, "")
187    case Body.Plain{text}:
188      wire.exchange(method, l, headers, text)
189    case Body.Octets{bytes}:
190      wire.exchange.octets(method, l, headers, List.length(&2, U32, bytes))
191
192# the octets sent after the request text: an octet body's, else none
193def wire.tail(b: Body.Body) -> List<&2, U32>:
194  match b:
195    case Body.Empty{}:
196      []
197    case Body.Plain{text}:
198      []
199    case Body.Octets{bytes}:
200      bytes
201
202# --- what is heard ---
203
204# an answer's status: the number on its first line, if it is one
205def wire.code.of(c: Http.Cut, raw: String) -> Maybe<&2, U32>:
206  match c:
207    case Http.NoCut{}:
208      U32.read(raw)
209    case Http.Cut{before, after}:
210      U32.read(before)
211
212# an answer's status line as a number
213def wire.code(+raw: String) -> Maybe<&2, U32>:
214  wire.code.of(Http.divide(raw, "\n"), raw)
215
216# the text after an answer's first newline
217def wire.text.of(c: Http.Cut) -> String:
218  match c:
219    case Http.NoCut{}:
220      ""
221    case Http.Cut{before, after}:
222      after
223
224# the text after an answer's first newline
225def wire.text(raw: String) -> String:
226  wire.text.of(Http.divide(raw, "\n"))
227
228# whether a status is success (0)
229def wire.ok.of(m: Maybe<&2, U32>) -> Bool:
230  match m:
231    case None{}:
232      False{}
233    case Some{n}:
234      U32.is_eq(n, 0)
235
236# whether an answer has status 0
237def wire.ok(raw: String) -> Bool:
238  wire.ok.of(wire.code(raw))
239
240# what an open answered: a connection to read from, or the answer itself
241type Opened is Data:
242  Refused{answer: String}
243  Open{conn: U32}
244
245# a connection number that read, or a malformed answer when it did not
246def wire.opened.conn(m: Maybe<&2, U32>) -> Opened:
247  match m:
248    case None{}:
249      Refused{"22\na malformed connection number"}
250    case Some{n}:
251      Open{n}
252
253# an open's answer once it is known whether it succeeded
254def wire.opened.of(+got: String, ok: Bool) -> Opened:
255  match ok:
256    case False{}:
257      Refused{got}
258    case True{}:
259      wire.opened.conn(U32.read(wire.text(got)))
260
261# what an open's answer means
262def wire.opened(+got: String) -> Opened:
263  wire.opened.of(got, wire.ok(got))
264
265# what has been received so far: its pieces, the newest first, and where
266# the response stands (Http.hear)
267type Got is Data:
268  Got{pieces: List<&2, String>, hear: Http.Hear}
269
270# the text a list of pieces, newest first, stands for
271def wire.joined(ps: List<&2, String>) -> String:
272  match ps:
273    case []:
274      ""
275    case p <> rest:
276      wire.joined(rest) ++ p
277
278# the pieces put together onto what follows them, each copied once
279def wire.join.go(ps: List<&2, String>, acc: String) -> String:
280  match ps:
281    case []:
282      acc
283    case p <> rest:
284      wire.join.go(rest, p ++ acc)
285
286# the text a list of pieces, newest first, stands for, built in one pass
287def wire.join(ps: List<&2, String>) -> String:
288  wire.join.go(ps, "")
289
290# what has been received as these pieces, for a request with this method
291def wire.got(+method: String, +ps: List<&2, String>) -> Got:
292  Got{ps, Http.hear.of(method, wire.joined(ps))}
293
294# after a read: read again with what has arrived, or answer
295type Read is Data:
296  Again{acc: Got}
297  Heard{answer: String}
298
299# everything received, once it is known whether it is a whole response
300def wire.read.whole(+ps: List<&2, String>, h: Http.Hear, done: Bool) -> Read:
301  match done:
302    case True{}:
303      Heard{"0\n" ++ wire.join(ps)}
304    case False{}:
305      Again{Got{ps, h}}
306
307# a read that brought text, judged with where the response now stands
308def wire.read.add(text: String, ps: List<&2, String>, +h: Http.Hear) -> Read:
309  wire.read.whole(text <> ps, h, Http.hear.done(h))
310
311# a read that brought text, or found the connection closed
312def wire.read.got(st: Got, +text: String, closed: Bool) -> Read:
313  match st closed:
314    case Got{ps, h} True{}:
315      Heard{"0\n" ++ wire.join(ps)}
316    case Got{ps, h} False{}:
317      wire.read.add(text, ps, Http.hear.more(h, text))
318
319# a read's answer once it is known whether it succeeded
320def wire.read.of(st: Got, +got: String, ok: Bool) -> Read:
321  match ok:
322    case False{}:
323      Heard{got}
324    case True{}:
325      +text = wire.text(got)
326      wire.read.got(st, text, String.is_empty(text))
327
328# what to do after a read answered `got`, with what was received before it
329# (SPEC.md HTTP-CLI-6)
330def wire.read.next(st: Got, +got: String) -> Read:
331  wire.read.of(st, got, wire.ok(got))
332
333# the answer when the reads run out before the response ends
334def wire.spent() -> String:
335  "56\nthe answer did not end within 65536 reads"
336
337# the response an exchange's answer holds, read by the request's method
338# (SPEC.md HTTP-CLI-9); a failed exchange is torn with the reason it is
339# reported with (wire.why)
340def wire.heard.of(+method: String, +raw: String, ok: Bool) -> Http.Reply:
341  match ok:
342    case False{}:
343      Http.Torn{wire.why(wire.code(raw), wire.text(raw))}
344    case True{}:
345      Http.parse.to(method, wire.text(raw))
346
347# the reason phrase of the response an exchange's answer holds (SPEC.md
348# HTTP-CLI-11); a failed exchange's is read from its reason and not used
349def wire.phrase(raw: String) -> String:
350  Http.phrase.to(wire.text(raw))
351
352# the response an exchange's answer holds
353def wire.heard(+method: String, +raw: String) -> Http.Reply:
354  wire.heard.of(method, raw, wire.ok(raw))