src/wire.bend

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