src/step.bend

emerging-ezhttp@0.20.0.0 · 126 lines · 0 definitions

1# ezhttp/src/step: what the server decides for one connection, kept pure so
2# a law can state it. server.bend runs the effects and asks this module what
3# each outcome means; it decides nothing itself.
4import Base
5import emerging-ezhttp@0.20.0.0/src/http.bend as Http
6import emerging-ezhttp@0.20.0.0/src/qs.bend as Qs
7
8# what a request asks of the server: answer it now, or call the handler
9type Step is Data:
10  Answer{reply: Http.Reply}
11  Call{req: Http.Request}
12
13# an effect's outcome on one connection: the value, or the line to log for
14# a connection that is lost
15type Got<-A: Type> is Type:
16  Kept{value: A}
17  Lost{line: String}
18
19# a request received as octets, as text: its method, target, header names
20# and values and body each decoded as UTF-8 (SPEC.md HTTP-SRV-2); a bad one
21# keeps its reason
22def step.text(req: Http.Request) -> Http.Request:
23  match req:
24    case Http.Bad{why}:
25      Http.Bad{why}
26    case Http.Request{method, target, headers, body}:
27      Http.Request{Qs.octets.text(method), Qs.octets.text(target),
28        Qs.octets.headers(headers), Qs.octets.text(body)}
29
30# a bad request is answered 400 with its reason and never reaches the
31# handler; a good one goes to the handler as it was parsed
32def step.of(req: Http.Request) -> Step:
33  match req:
34    case Http.Bad{why}:
35      Answer{Http.Reply{400, [], why}}
36    case Http.Request{method, target, headers, body}:
37      Call{Http.Request{method, target, headers, body}}
38
39# the line logged for a failed effect: `ezhttp: <phase>: <why>`
40def step.line(phase: String, why: String) -> String:
41  "ezhttp: " ++ phase ++ ": " ++ why
42
43# a failed accept or recv loses the connection instead of ending the server
44def step.got(
45  -A: Type,
46  phase: String,
47  res: Result<&1, &1, U32 & String, A>
48) -> Got<A>:
49  match res:
50    case Fail{(code, why)}:
51      Lost{step.line(phase, why)}
52    case Done{value}:
53      Kept{value}
54
55# a failed send loses the connection too; the bytes it did not send are dropped
56def step.sent(res: Result<&1, &1, (U32 & String) & List<&2, U32>, Unit>)
57  -> Got<Unit>:
58  match res:
59    case Fail{((code, why), left)}:
60      Lost{step.line("write", why)}
61    case Done{value}:
62      Kept{value}
63
64# --- the replies and the handler's request (SPEC.md HTTP-SRV-8, 9) ---
65
66# whether a reply is written with its head only: a reply to HEAD, or a 204
67# or 304 (RFC 9110 §9.3.2, §15.3.5, §15.4.5)
68def step.bare(+method: String, +status: U32) -> Bool:
69  Bool.or(String.eq(method, "HEAD"), Http.status.nocontent(status))
70
71# the body octets of a request, as they arrived; none for a bad one
72def step.octets(req: Http.Request) -> String:
73  match req:
74    case Http.Bad{why}:
75      ""
76    case Http.Request{method, target, headers, body}:
77      body
78
79# what an accept with a deadline brought: a connection, nothing before the
80# deadline, or a lost accept with its line (SPEC.md HTTP-SRV-4)
81type Accept<-A: Type> is Type:
82  Took{value: A}
83  Idle{}
84  Missed{line: String}
85
86# a failed accept once it is known whether its deadline passed
87def step.missed(-A: Type, +why: String, late: Bool) -> Accept<A>:
88  match late:
89    case True{}:
90      Idle{}
91    case False{}:
92      Missed{step.line("accept", why)}
93
94# an accept's outcome: ETIMEDOUT (110) is a deadline that passed
95def step.accepted(-A: Type, res: Result<&1, &1, U32 & String, A>)
96  -> Accept<A>:
97  match res:
98    case Fail{(+code, why)}:
99      step.missed(A, why, U32.is_eq(code, 110))
100    case Done{value}:
101      Took{value}
102
103# --- when a request has all arrived (SPEC.md HTTP-TRUST-2) ---
104
105# a Content-Length that reads, or none
106def step.clen(m: Maybe<&2, Nat>) -> Nat:
107  match m:
108    case None{}:
109      0n
110    case Some{n}:
111      n
112
113# octets a request needs in all once its head has ended: the head, its blank
114# line and the octets its Content-Length names (0 when it names none)
115def step.need.of(c: Http.Cut) -> Maybe<&2, Nat>:
116  match c:
117    case Http.NoCut{}:
118      None{}
119    case Http.Cut{+before, after}:
120      Some{Nat.add(Nat.add(String.length(before), 4n),
121        step.clen(Http.digits.read(Http.first(
122          Http.head.values(String.lines(before), "content-length")))))}
123
124# octets a request needs in all, none while its head has not ended
125def step.need(+text: String) -> Maybe<&2, Nat>:
126  step.need.of(Http.divide(text, "\r\n\r\n"))