camber.bend
bend-kit-camber@0.2.0.0 · 511 lines · 0 definitions
1# Fixed prepared HTTP applications and bounded affine dependency bundles.
2import Base
3import bend-kit-http@0.25.0.2/http.bend as Http
4import bend-kit-router@0.2.0.0/router.bend as Router
5import bend-kit-router@0.2.0.0/target.bend as Target
6import bend-kit-json@0.5.1.0/json.bend as Json
7import bend-kit-camber@0.2.0.0/response.bend as Response
8
9type ResponseError is Data:
10 InvalidResponse{}
11 InvalidRedirect{}
12
13def validate.checked(valid: Bool, response: Http.Res) -> Result<&2, &1, ResponseError, Http.Res>:
14 match valid:
15 case True{}:
16 Done{response}
17 case False{}:
18 Fail{InvalidResponse{}}
19
20def validate(response: Http.Res) -> Result<&2, &1, ResponseError, Http.Res>:
21 Http.Res{+status, +headers, body} = response
22 validate.checked(Response.valid(status, headers), Http.Res{status, headers, body})
23
24# The encoder is an explicit public projection, never record reflection.
25def json(~T: Type, ~encode: T -> Json.Val, status: U32, value: T) -> Http.Res:
26 Http.Res{status, Http.set(Http.empty(), "content-type", "application/json"), Json.encode.bytes(encode(value))}
27
28def text(status: U32, value: String) -> Http.Res:
29 Http.Res{status, Http.set(Http.empty(), "content-type", "text/plain; charset=utf-8"), Json.utf8(value)}
30
31def raw(status: U32, body: Http.Body) -> Http.Res:
32 Http.Res{status, Http.empty(), body}
33
34def empty(status: U32) -> Http.Res:
35 raw(status, Http.from_string(""))
36
37def redirect.checked(valid: Bool, status: U32, location: String) -> Result<&2, &1, ResponseError, Http.Res>:
38 match valid:
39 case False{}:
40 Fail{InvalidRedirect{}}
41 case True{}:
42 Done{Http.Res{status, Http.set(Http.empty(), "location", location), Http.from_string("")}}
43
44def redirect(+status: U32, +location: String) -> Result<&2, &1, ResponseError, Http.Res>:
45 redirect.checked((U32.is_eq(status, 301) || U32.is_eq(status, 302) || U32.is_eq(status, 303) || U32.is_eq(status, 307) || U32.is_eq(status, 308)) && Bool.not(String.eq(location, "")) && Response.value(location), status, location)
46
47type Counts is Data:
48 Counts{capacity: Nat, in_use: Nat, completed: Nat, rejected: Nat}
49
50type Outcome<-E: Data, -O: Data> is Data:
51 Success{value: O}
52 Expected{error: E}
53 Exhausted{}
54 Stopped{}
55
56type CloseResult is Data:
57 Closed{}
58 Busy{counts: Counts}
59
60type Message<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type:
61 Submit{principal: P, input: I, reply: Chan(Outcome<E, O>)}
62 Returned{resource: R}
63 Inspect{reply: Chan(Counts)}
64 Shutdown{reply: Chan(CloseResult)}
65
66# Copy only this context, never the resources. The inbox bounds queued commands;
67# callers must count blocked submissions inside their transport admission limit.
68type Context<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Data:
69 Context{messages: Chan(Message<R, P, I, E, O>)}
70
71def outcome.value(-E: Data, -O: Data, result: Result<&2, &2, E, O>) -> Outcome<E, O>:
72 match result:
73 case Fail{error}:
74 Expected{error}
75 case Done{value}:
76 Success{value}
77
78# The resources return through the private owner protocol BEFORE a disposable
79# caller reply. A closed caller reply cannot erase a handle or strand capacity.
80def work.return(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
81 messages: Chan(Message<R, P, I, E, O>), reply: Chan(Outcome<E, O>),
82 result: R & Result<&2, &2, E, O>
83) -> IO(Unit):
84 (returned, outcome) = result
85 do IO<Unit>:
86 accepted : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Returned{returned})
87 sent : Bool <- Chan.send(Outcome<E, O>, reply, outcome.value(E, O, outcome))
88 return Unit{}
89
90def work(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
91 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>),
92 resource: R, principal: P, input: I,
93 messages: Chan(Message<R, P, I, E, O>), reply: Chan(Outcome<E, O>)
94) -> IO(Unit):
95 do IO<Unit>:
96 result : R & Result<&2, &2, E, O> <- handler(resource, principal, input)
97 work.return(R, P, I, E, O, messages, reply, result)
98
99def dispose(~R: Type, ~destroy: R -> IO(Unit), resources: List<R>) -> IO(Unit):
100 match resources:
101 case Nil{}:
102 IO.pure(Unit, Unit{})
103 case Con{resource, rest}:
104 do IO<Unit>:
105 destroy(resource)
106 dispose(~R, ~destroy, rest)
107
108# Closing Base channels retains buffered messages. Drain them and answer stopped
109# callers, rather than stranding accepted commands behind the close command.
110def drain.message(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
111 ~destroy: R -> IO(Unit), message: Message<R, P, I, E, O>
112) -> IO(Unit):
113 match message:
114 case Submit{principal, input, reply}:
115 do IO<Unit>:
116 sent : Bool <- Chan.send(Outcome<E, O>, reply, Stopped{})
117 return Unit{}
118 case Inspect{reply}:
119 Chan.close(Counts, reply)
120 case Shutdown{reply}:
121 do IO<Unit>:
122 sent : Bool <- Chan.send(CloseResult, reply, Closed{})
123 return Unit{}
124 case Returned{resource}:
125 destroy(resource)
126
127def drain.step(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
128 ~destroy: R -> IO(Unit), pending: Maybe<&1, Message<R, P, I, E, O>>,
129 next: Unit -> IO(Unit)
130) -> IO(Unit):
131 match pending:
132 case None{}:
133 IO.pure(Unit, Unit{})
134 case Some{message}:
135 do IO<Unit>:
136 drain.message(~R, ~P, ~I, ~E, ~O, ~destroy, message)
137 next(Unit{})
138
139# The closed inbox contains at most room buffered messages.
140def drain(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
141 ~destroy: R -> IO(Unit), fuel: Nat, +messages: Chan(Message<R, P, I, E, O>)
142) -> IO(Unit):
143 match fuel:
144 case 0n:
145 IO.pure(Unit, Unit{})
146 case 1n+p:
147 do IO<Unit>:
148 pending : Maybe<&1, Message<R, P, I, E, O>> <- Chan.recv(Message<R, P, I, E, O>, messages)
149 drain.step(~R, ~P, ~I, ~E, ~O, ~destroy, pending, u => drain(~R, ~P, ~I, ~E, ~O, ~destroy, p, messages))
150
151type OwnerStep<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type:
152 Waiting{resources: List<R>, counts: Counts}
153 Received{message: Message<R, P, I, E, O>, counts: Counts, resources: List<R>}
154
155# IO.join closes a one-shot channel; the owner inbox must remain reusable.
156def receive.value(-A: Type, value: Maybe<&1, A>) -> IO(A):
157 match value:
158 case None{}:
159 IO.die(A, 1, "camber internal channel closed unexpectedly")
160 case Some{message}:
161 IO.pure(A, message)
162
163def receive(-A: Type, channel: Chan(A)) -> IO(A):
164 do IO<A>:
165 value : Maybe<&1, A> <- Chan.recv(A, channel)
166 receive.value(A, value)
167
168# Only idle close ends this loop. Busy close never waits on a stalled handler.
169# @unsafe is solely the lifecycle receive loop, not a termination/proof claim.
170@unsafe def owner(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
171 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
172 +messages: Chan(Message<R, P, I, E, O>), +room: U32, step: OwnerStep<R, P, I, E, O>
173) -> IO(Unit):
174 match step:
175 case Waiting{resources, counts}:
176 do IO<Unit>:
177 message : Message<R, P, I, E, O> <- receive(Message<R, P, I, E, O>, messages)
178 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Received{message, counts, resources})
179 case Received{message, counts, resources}:
180 match message:
181 case Returned{resource}:
182 Counts{capacity, in_use, completed, rejected} = counts
183 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resource <> resources, Counts{capacity, Nat.sub(in_use, 1n), 1n+completed, rejected}})
184 case Inspect{reply}:
185 Counts{+capacity, +in_use, +completed, +rejected} = counts
186 do IO<Unit>:
187 sent : Bool <- Chan.send(Counts, reply, Counts{capacity, in_use, completed, rejected})
188 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, in_use, completed, rejected}})
189 case Shutdown{reply}:
190 Counts{+capacity, in_use, +completed, +rejected} = counts
191 match in_use:
192 case 0n:
193 do IO<Unit>:
194 dispose(~R, ~destroy, resources)
195 Chan.close(Message<R, P, I, E, O>, messages)
196 sent : Bool <- Chan.send(CloseResult, reply, Closed{})
197 drain(~R, ~P, ~I, ~E, ~O, ~destroy, U32.to_nat(room), messages)
198 case 1n+ +p:
199 do IO<Unit>:
200 sent : Bool <- Chan.send(CloseResult, reply, Busy{Counts{capacity, 1n+p, completed, rejected}})
201 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 1n+p, completed, rejected}})
202 case Submit{principal, input, reply}:
203 Counts{capacity, in_use, completed, rejected} = counts
204 match resources:
205 case Nil{}:
206 do IO<Unit>:
207 sent : Bool <- Chan.send(Outcome<E, O>, reply, Exhausted{})
208 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{Nil{}, Counts{capacity, in_use, completed, 1n+rejected}})
209 case Con{resource, rest}:
210 do IO<Unit>:
211 IO.spawn(Unit, work(~R, ~P, ~I, ~E, ~O, ~handler, resource, principal, input, messages, reply))
212 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{rest, Counts{capacity, 1n+in_use, completed, rejected}})
213
214def build.step(~R: Type, ~E: Data, ~destroy: R -> IO(Unit),
215 result: Result<&2, &1, E, R>, resources: List<R>,
216 next: List<R> -> IO(Result<&2, &1, E, List<R>>)
217) -> IO(Result<&2, &1, E, List<R>>):
218 match result:
219 case Fail{error}:
220 do IO<Result<&2, &1, E, List<R>>>:
221 dispose(~R, ~destroy, resources)
222 return Fail{error}
223 case Done{resource}:
224 next(resource <> resources)
225
226def build(~K: Data, ~R: Type, ~E: Data, ~create: K -> Nat -> IO(Result<&2, &1, E, R>), ~destroy: R -> IO(Unit),
227 remaining: Nat, +config: K, resources: List<R>
228) -> IO(Result<&2, &1, E, List<R>>):
229 match remaining:
230 case 0n:
231 IO.pure(Result<&2, &1, E, List<R>>, Done{resources})
232 case 1n+ +p:
233 do IO<Result<&2, &1, E, List<R>>>:
234 result : Result<&2, &1, E, R> <- create(config, p)
235 build.step(~R, ~E, ~destroy, result, resources, next => build(~K, ~R, ~E, ~create, ~destroy, p, config, next))
236
237def start.ready(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
238 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
239 capacity: Nat, +room: U32, result: Result<&2, &1, E, List<R>>
240) -> IO(Result<&2, &2, E, Context<R, P, I, E, O>>):
241 match result:
242 case Fail{error}:
243 IO.pure(Result<&2, &2, E, Context<R, P, I, E, O>>, Fail{error})
244 case Done{resources}:
245 do IO<Result<&2, &2, E, Context<R, P, I, E, O>>>:
246 channel : Chan(Message<R, P, I, E, O>) <- Chan.new(Message<R, P, I, E, O>, room)
247 +messages : Chan(Message<R, P, I, E, O>) = channel
248 IO.spawn(Unit, owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 0n, 0n, 0n}}))
249 return Done{Context{messages}}
250
251# The initializer closes partial handles within a failed bundle; start closes
252# all prior whole bundles. No workers exist until the complete set opens.
253def start(~K: Data, ~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
254 ~create: K -> Nat -> IO(Result<&2, &1, E, R>),
255 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
256 config: K, +capacity: Nat, room: U32
257) -> IO(Result<&2, &2, E, Context<R, P, I, E, O>>):
258 do IO<Result<&2, &2, E, Context<R, P, I, E, O>>>:
259 result : Result<&2, &1, E, List<R>> <- build(~K, ~R, ~E, ~create, ~destroy, capacity, config, Nil{})
260 start.ready(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, capacity, room, result)
261
262def submit(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
263 context: Context<R, P, I, E, O>, principal: P, input: I, reply: Chan(Outcome<E, O>)
264) -> IO(Bool):
265 Context{messages} = context
266 Chan.send(Message<R, P, I, E, O>, messages, Submit{principal, input, reply})
267
268def call.accepted(-E: Data, -O: Data, accepted: Bool, +reply: Chan(Outcome<E, O>)) -> IO(Outcome<E, O>):
269 match accepted:
270 case False{}:
271 do IO<Outcome<E, O>>:
272 Chan.close(Outcome<E, O>, reply)
273 return Stopped{}
274 case True{}:
275 do IO<Outcome<E, O>>:
276 result : Outcome<E, O> <- IO.join(Outcome<E, O>, reply)
277 Chan.close(Outcome<E, O>, reply)
278 return result
279
280def call(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
281 context: Context<R, P, I, E, O>, principal: P, input: I
282) -> IO(Outcome<E, O>):
283 do IO<Outcome<E, O>>:
284 channel : Chan(Outcome<E, O>) <- Chan.new(Outcome<E, O>, 1)
285 +reply : Chan(Outcome<E, O>) = channel
286 accepted : Bool <- submit(R, P, I, E, O, context, principal, input, reply)
287 call.accepted(E, O, accepted, reply)
288
289def inspect.value(value: Maybe<&1, Counts>) -> Maybe<&2, Counts>:
290 match value:
291 case None{}:
292 None{}
293 case Some{counts}:
294 Some{counts}
295
296def inspect.sent(sent: Bool, +reply: Chan(Counts)) -> IO(Maybe<&2, Counts>):
297 match sent:
298 case False{}:
299 do IO<Maybe<&2, Counts>>:
300 Chan.close(Counts, reply)
301 return None{}
302 case True{}:
303 do IO<Maybe<&2, Counts>>:
304 value : Maybe<&1, Counts> <- Chan.recv(Counts, reply)
305 Chan.close(Counts, reply)
306 return inspect.value(value)
307
308def inspect(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context<R, P, I, E, O>) -> IO(Maybe<&2, Counts>):
309 Context{messages} = context
310 do IO<Maybe<&2, Counts>>:
311 channel : Chan(Counts) <- Chan.new(Counts, 1)
312 +reply : Chan(Counts) = channel
313 sent : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Inspect{reply})
314 inspect.sent(sent, reply)
315
316def close.sent(sent: Bool, +reply: Chan(CloseResult)) -> IO(CloseResult):
317 match sent:
318 case False{}:
319 do IO<CloseResult>:
320 Chan.close(CloseResult, reply)
321 return Closed{}
322 case True{}:
323 do IO<CloseResult>:
324 value : CloseResult <- IO.join(CloseResult, reply)
325 Chan.close(CloseResult, reply)
326 return value
327
328# Stop external admissions before close. Busy preserves live context/capacity;
329# retry after admitted operations finish. Stalled operations remain observable.
330def close(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context<R, P, I, E, O>) -> IO(CloseResult):
331 Context{messages} = context
332 do IO<CloseResult>:
333 channel : Chan(CloseResult) <- Chan.new(CloseResult, 1)
334 +reply : Chan(CloseResult) = channel
335 sent : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Shutdown{reply})
336 close.sent(sent, reply)
337
338# Policies are prepared Data, not callbacks. Later lifecycle stages can consume
339# these root-to-route declarations without splitting patterns or finding groups.
340type Group<-P: Data> is Data:
341 Group{id: U32, ancestors: List<&2, U32>, policy: P}
342
343type Route<-A: Data, -P: Data> is Data:
344 Route{method: String, pattern: String, action: A, groups: List<&2, U32>, policy: P}
345
346type Endpoint<-A: Data, -P: Data> is Data:
347 Endpoint{action: A, policies: List<&2, P>}
348
349type Application<-A: Data, -C: Data, -P: Data> is Data:
350 Application{table: Router.Table<Endpoint<A, P>>, context: C, root: P}
351
352type RegistrationError is Data:
353 RouteError{error: Router.Error}
354 InvalidGroups{groups: List<&2, U32>}
355
356type Match<-P: Data> is Data:
357 Match{target: Target.Target, params: Map<&2, String>, groups: List<&2, U32>, method: String, pattern: String, policies: List<&2, P>}
358
359def groups.equal(xs: List<&2, U32>, ys: List<&2, U32>) -> Bool:
360 match xs ys:
361 case Nil{} Nil{}:
362 True{}
363 case x <> xt y <> yt:
364 U32.is_eq(x, y) && groups.equal(xt, yt)
365 case _ _:
366 False{}
367
368def group.pick(~P: Data, same: Bool, group: Group<P>, later: Unit -> Maybe<&2, Group<P>>) -> Maybe<&2, Group<P>>:
369 match same:
370 case True{}:
371 Some{group}
372 case False{}:
373 later(Unit{})
374
375def group.find(~P: Data, groups: List<&2, Group<P>>, +id: U32) -> Maybe<&2, Group<P>>:
376 match groups:
377 case Nil{}:
378 None{}
379 case Group{+here, ancestors, policy} <> rest:
380 group.pick(~P, U32.is_eq(here, id), Group{here, ancestors, policy}, u => group.find(~P, rest, id))
381
382def group.checked(~P: Data, same: Bool, policy: P) -> Maybe<&2, P>:
383 match same:
384 case False{}:
385 None{}
386 case True{}:
387 Some{policy}
388
389def group.policy(~P: Data, found: Maybe<&2, Group<P>>, ancestors: List<&2, U32>) -> Maybe<&2, P>:
390 match found:
391 case None{}:
392 None{}
393 case Some{Group{id, expected, policy}}:
394 group.checked(~P, groups.equal(expected, ancestors), policy)
395
396def groups.policies(~P: Data, ids: List<&2, U32>, +declared: List<&2, Group<P>>, +ancestors: List<&2, U32>) -> Maybe<&2, List<&2, P>>:
397 match ids:
398 case Nil{}:
399 Some{Nil{}}
400 case +id <> rest:
401 do Maybe<&2, List<&2, P>>:
402 policy : P <- group.policy(~P, group.find(~P, declared, id), ancestors)
403 policies : List<&2, P> <- groups.policies(~P, rest, declared, List.append(&2, U32, ancestors, [id]))
404 return policy <> policies
405
406def groups.unique(~P: Data, found: Maybe<&2, Group<P>>) -> Bool:
407 match found:
408 case None{}:
409 True{}
410 case Some{group}:
411 False{}
412
413def groups.valid(~P: Data, groups: List<&2, Group<P>>, +all: List<&2, Group<P>>) -> Bool:
414 match groups:
415 case Nil{}:
416 True{}
417 case Group{+id, +ancestors, policy} <> +rest:
418 chain = groups.policies(~P, List.append(&2, U32, ancestors, [id]), all, Nil{})
419 groups.unique(~P, group.find(~P, rest, id)) && Maybe.is_some(&2, List<&2, P>, chain) && groups.valid(~P, rest, all)
420
421def routes.policies(~P: Data, result: Maybe<&2, List<&2, P>>, ids: List<&2, U32>) -> Result<&2, &2, RegistrationError, List<&2, P>>:
422 match result:
423 case None{}:
424 Fail{InvalidGroups{ids}}
425 case Some{policies}:
426 Done{policies}
427
428def routes.prepare(~A: Data, ~P: Data, routes: List<&2, Route<A, P>>, +groups: List<&2, Group<P>>, +root: P) -> Result<&2, &2, RegistrationError, List<&2, Router.Entry<Endpoint<A, P>>>>:
429 match routes:
430 case Nil{}:
431 Done{Nil{}}
432 case Route{method, pattern, action, +ids, policy} <> rest:
433 do Result<&2, &2, RegistrationError, List<&2, Router.Entry<Endpoint<A, P>>>>:
434 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, groups, Nil{}), ids)
435 entries : List<&2, Router.Entry<Endpoint<A, P>>> <- routes.prepare(~A, ~P, rest, groups, root)
436 return Router.Entry{method, pattern, Endpoint{action, root <> List.append(&2, P, policies, [policy])}, ids} <> entries
437
438def application.table(~A: Data, ~C: Data, ~P: Data, result: Result<&2, &2, Router.Error, Router.Table<Endpoint<A, P>>>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
439 match result:
440 case Fail{error}:
441 Fail{RouteError{error}}
442 case Done{table}:
443 Done{Application{table, context, root}}
444
445def application.valid(~A: Data, ~C: Data, ~P: Data, valid: Bool, routes: List<&2, Route<A, P>>, groups: List<&2, Group<P>>, context: C, +root: P) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
446 match valid:
447 case False{}:
448 Fail{InvalidGroups{Nil{}}}
449 case True{}:
450 do Result<&2, &2, RegistrationError, Application<A, C, P>>:
451 entries : List<&2, Router.Entry<Endpoint<A, P>>> <- routes.prepare(~A, ~P, routes, groups, root)
452 application.table(~A, ~C, ~P, Router.prepare(~Endpoint<A, P>, entries), context, root)
453
454# Construction is pure and fallible; no listener or owner is started here.
455def application(~A: Data, ~C: Data, ~P: Data, routes: List<&2, Route<A, P>>, +groups: List<&2, Group<P>>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
456 application.valid(~A, ~C, ~P, groups.valid(~P, groups, groups), routes, groups, context, root)
457
458def describe(~A: Data, ~C: Data, ~P: Data, app: Application<A, C, P>) -> List<&2, Router.Description>:
459 Application{table, context, root} = app
460 Router.describe(~Endpoint<A, P>, table)
461
462def request.authority(authority: Maybe<&2, String>, req: Http.Req) -> Http.Req:
463 match authority:
464 case None{}:
465 req
466 case Some{host}:
467 Http.Req{method, path, headers, body} = req
468 Http.Req{method, path, Http.set(headers, "host", host), body}
469
470# A closed template can run repeatedly; an affine callback registry cannot.
471def plain(~handler: Http.Req -> IO(Http.Res), req: Http.Req) -> IO(Result<&2, &1, ResponseError, Http.Res>):
472 do IO<Result<&2, &1, ResponseError, Http.Res>>:
473 response : Http.Res <- handler(req)
474 return validate(response)
475
476def dispatch.choice(~A: Data, ~C: Data, ~P: Data,
477 ~run: A -> C -> Match<P> -> Http.Req -> IO(Http.Res),
478 choice: Router.Choice<Endpoint<A, P>>, context: C, target: Target.Target, req: Http.Req
479) -> IO(Http.Res):
480 match choice:
481 case Router.NotFound{}:
482 IO.pure(Http.Res, Http.Res{404, Http.empty(), Http.from_string("")})
483 case Router.MethodMissing{groups, allow}:
484 IO.pure(Http.Res, Http.Res{405, Http.set(Http.empty(), "allow", allow), Http.from_string("")})
485 case Router.Options{groups, allow}:
486 IO.pure(Http.Res, Http.Res{204, Http.set(Http.empty(), "allow", allow), Http.from_string("")})
487 case Router.Found{Endpoint{action, policies}, params, groups, method, pattern}:
488 run(action, context, Match{target, params, groups, method, pattern, policies}, req)
489
490def dispatch.resolved(~A: Data, ~C: Data, ~P: Data,
491 ~run: A -> C -> Match<P> -> Http.Req -> IO(Http.Res),
492 result: Result<&2, &2, Router.Error, Router.Resolved<Endpoint<A, P>>>, context: C, req: Http.Req
493) -> IO(Http.Res):
494 match result:
495 case Fail{error}:
496 IO.pure(Http.Res, Http.Res{400, Http.empty(), Http.from_string("")})
497 case Done{Router.Resolved{+target, choice}}:
498 Target.Target{segments, query, authority, star} = target
499 dispatch.choice(~A, ~C, ~P, ~run, choice, context, target, request.authority(authority, req))
500
501# Dispatch moves the original packed body once and returns application bytes.
502# In particular it never performs transport's HEAD/204/304 body suppression.
503def dispatch(~A: Data, ~C: Data, ~P: Data,
504 ~run: A -> C -> Match<P> -> Http.Req -> IO(Http.Res),
505 app: Application<A, C, P>, req: Http.Req
506) -> IO(Result<&2, &1, ResponseError, Http.Res>):
507 Application{table, context, root} = app
508 Http.Req{+method, +path, headers, body} = req
509 do IO<Result<&2, &1, ResponseError, Http.Res>>:
510 response : Http.Res <- dispatch.resolved(~A, ~C, ~P, ~run, Router.resolve(~Endpoint<A, P>, table, method, path), context, Http.Req{method, path, headers, body})
511 return validate(response)