src/wire.bend

emerging-ezhttp@0.12.0.0 · 88 lines · 0 definitions

1# ezhttp/src/wire: one request out and one response back. TLS and DNS stay in
2# the effect (OpenSSL via dlopen); this module does not re-specify TLS.
3#
4# Spec: scheme, host, port each on their own line; everything after the third
5# newline is the request verbatim; the scheme line may carry a timeout
6# (wirespec.bend). Answer: status on the first line, then text
7# ("0" and the raw response, or a non-zero code and the reason).
8#
9# The effect opens a connection and sends the request, then answers one read
10# at a time; when the response is whole is wirespec.bend's decision
11# (wire.read.next), so a server that keeps the connection open after a
12# complete response is not waited on (SPEC.md HTTP-CLI-5, HTTP-CLI-6).
13import Base
14import emerging-ezhttp@0.12.0.0/src/wirespec.bend as Spec
15
16# connect, send the request a spec carries, and answer a connection number
17def ezwire.open(spec: String) -> IO(String):
18  import "./effs/wire.c"
19  import "./effs/wire.js"
20
21# the next octets a connection has received, "" once the server has closed
22def ezwire.read(conn: U32) -> IO(String):
23  import "./effs/wire.c"
24  import "./effs/wire.js"
25
26# close a connection
27def ezwire.close(conn: U32) -> IO(Unit):
28  import "./effs/wire.c"
29  import "./effs/wire.js"
30
31# the spec the effect reads (wirespec.bend)
32def wire.spec(secure: Bool, host: String, port: U32, req: String) -> String:
33  Spec.wire.spec(secure, host, port, req)
34
35# the most reads one exchange makes
36def wire.reads() -> Nat:
37  65536n
38
39# a read's outcome: read again, or the answer
40def wire.loop.on(r: Spec.Read, more: String -> IO(String)) -> IO(String):
41  match r:
42    case Spec.Again{acc}:
43      more(acc)
44    case Spec.Heard{answer}:
45      IO.pure(String, answer)
46
47# read until wirespec.bend says the answer is whole
48def wire.loop(fuel: Nat, +conn: U32, +method: String, acc: String)
49  -> IO(String):
50  match fuel:
51    case 0n:
52      IO.pure(String, Spec.wire.spent())
53    case 1n+p:
54      do IO<String>:
55        +got : String <- ezwire.read(conn)
56        wire.loop.on(Spec.wire.read.next(method, acc, got),
57          next => wire.loop(p, conn, method, next))
58
59# read the answer on an open connection, then close it
60def wire.hear(+conn: U32, method: String) -> IO(String):
61  do IO<String>:
62    +answer : String <- wire.loop(wire.reads(), conn, method, "")
63    _done : Unit <- ezwire.close(conn)
64    IO.pure(String, answer)
65
66# an open's outcome: read from the connection, or the answer
67def wire.after(o: Spec.Opened, method: String) -> IO(String):
68  match o:
69    case Spec.Refused{answer}:
70      IO.pure(String, answer)
71    case Spec.Open{conn}:
72      wire.hear(conn, method)
73
74# a request with this method, sent as a spec says, and its whole answer
75def wire.run(method: String, spec: String) -> IO(String):
76  do IO<String>:
77    +got : String <- ezwire.open(spec)
78    wire.after(Spec.wire.opened(got), method)
79
80# a request sent to a host and its whole answer read back
81def wire.talk(secure: Bool, host: String, port: U32, method: String,
82  req: String) -> IO(String):
83  wire.run(method, wire.spec(secure, host, port, req))
84
85# the same exchange, bounded by a timeout in milliseconds
86def wire.talk.timed(secure: Bool, ms: U32, host: String, port: U32,
87  method: String, req: String) -> IO(String):
88  wire.run(method, Spec.wire.spec.timed(secure, ms, host, port, req))