camber.bend
bend-kit-camber@0.4.0.0 · 800 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.4.0.0/response.bend as Response
8import bend-kit-camber@0.4.0.0/input.bend as Input
9
10type ResponseError is Data:
11 InvalidResponse{}
12 InvalidRedirect{}
13
14def validate.checked(valid: Bool, response: Http.Res) -> Result<&2, &1, ResponseError, Http.Res>:
15 match valid:
16 case True{}:
17 Done{response}
18 case False{}:
19 Fail{InvalidResponse{}}
20
21def validate(response: Http.Res) -> Result<&2, &1, ResponseError, Http.Res>:
22 Http.Res{+status, +headers, body} = response
23 validate.checked(Response.valid(status, headers), Http.Res{status, headers, body})
24
25# The encoder is an explicit public projection, never record reflection.
26def json(~T: Type, ~encode: T -> Json.Val, status: U32, value: T) -> Http.Res:
27 Http.Res{status, Http.set(Http.empty(), "content-type", "application/json"), Json.encode.bytes(encode(value))}
28
29def text(status: U32, value: String) -> Http.Res:
30 Http.Res{status, Http.set(Http.empty(), "content-type", "text/plain; charset=utf-8"), Json.utf8(value)}
31
32def raw(status: U32, body: Http.Body) -> Http.Res:
33 Http.Res{status, Http.empty(), body}
34
35def empty(status: U32) -> Http.Res:
36 raw(status, Http.from_string(""))
37
38def redirect.checked(valid: Bool, status: U32, location: String) -> Result<&2, &1, ResponseError, Http.Res>:
39 match valid:
40 case False{}:
41 Fail{InvalidRedirect{}}
42 case True{}:
43 Done{Http.Res{status, Http.set(Http.empty(), "location", location), Http.from_string("")}}
44
45def redirect(+status: U32, +location: String) -> Result<&2, &1, ResponseError, Http.Res>:
46 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)
47
48type Counts is Data:
49 Counts{capacity: Nat, in_use: Nat, completed: Nat, rejected: Nat}
50
51type Outcome<-E: Data, -O: Data> is Data:
52 Success{value: O}
53 Expected{error: E}
54 Exhausted{}
55 Stopped{}
56
57type CloseResult is Data:
58 Closed{}
59 Busy{counts: Counts}
60
61type Message<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type:
62 Submit{principal: P, input: I, reply: Chan(Outcome<E, O>)}
63 Returned{resource: R}
64 Inspect{reply: Chan(Counts)}
65 Shutdown{reply: Chan(CloseResult)}
66
67# Copy only this context, never the resources. The inbox bounds queued commands;
68# callers must count blocked submissions inside their transport admission limit.
69type Context<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Data:
70 Context{messages: Chan(Message<R, P, I, E, O>)}
71
72def outcome.value(-E: Data, -O: Data, result: Result<&2, &2, E, O>) -> Outcome<E, O>:
73 match result:
74 case Fail{error}:
75 Expected{error}
76 case Done{value}:
77 Success{value}
78
79# The resources return through the private owner protocol BEFORE a disposable
80# caller reply. A closed caller reply cannot erase a handle or strand capacity.
81def work.return(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
82 messages: Chan(Message<R, P, I, E, O>), reply: Chan(Outcome<E, O>),
83 result: R & Result<&2, &2, E, O>
84) -> IO(Unit):
85 (returned, outcome) = result
86 do IO<Unit>:
87 accepted : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Returned{returned})
88 sent : Bool <- Chan.send(Outcome<E, O>, reply, outcome.value(E, O, outcome))
89 return Unit{}
90
91def work(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
92 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>),
93 resource: R, principal: P, input: I,
94 messages: Chan(Message<R, P, I, E, O>), reply: Chan(Outcome<E, O>)
95) -> IO(Unit):
96 do IO<Unit>:
97 result : R & Result<&2, &2, E, O> <- handler(resource, principal, input)
98 work.return(R, P, I, E, O, messages, reply, result)
99
100def dispose(~R: Type, ~destroy: R -> IO(Unit), resources: List<R>) -> IO(Unit):
101 match resources:
102 case Nil{}:
103 IO.pure(Unit, Unit{})
104 case Con{resource, rest}:
105 do IO<Unit>:
106 destroy(resource)
107 dispose(~R, ~destroy, rest)
108
109# Closing Base channels retains buffered messages. Drain them and answer stopped
110# callers, rather than stranding accepted commands behind the close command.
111def drain.message(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
112 ~destroy: R -> IO(Unit), message: Message<R, P, I, E, O>
113) -> IO(Unit):
114 match message:
115 case Submit{principal, input, reply}:
116 do IO<Unit>:
117 sent : Bool <- Chan.send(Outcome<E, O>, reply, Stopped{})
118 return Unit{}
119 case Inspect{reply}:
120 Chan.close(Counts, reply)
121 case Shutdown{reply}:
122 do IO<Unit>:
123 sent : Bool <- Chan.send(CloseResult, reply, Closed{})
124 return Unit{}
125 case Returned{resource}:
126 destroy(resource)
127
128def drain.step(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
129 ~destroy: R -> IO(Unit), pending: Maybe<&1, Message<R, P, I, E, O>>,
130 next: Unit -> IO(Unit)
131) -> IO(Unit):
132 match pending:
133 case None{}:
134 IO.pure(Unit, Unit{})
135 case Some{message}:
136 do IO<Unit>:
137 drain.message(~R, ~P, ~I, ~E, ~O, ~destroy, message)
138 next(Unit{})
139
140# The closed inbox contains at most room buffered messages.
141def drain(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
142 ~destroy: R -> IO(Unit), fuel: Nat, +messages: Chan(Message<R, P, I, E, O>)
143) -> IO(Unit):
144 match fuel:
145 case 0n:
146 IO.pure(Unit, Unit{})
147 case 1n+p:
148 do IO<Unit>:
149 pending : Maybe<&1, Message<R, P, I, E, O>> <- Chan.recv(Message<R, P, I, E, O>, messages)
150 drain.step(~R, ~P, ~I, ~E, ~O, ~destroy, pending, u => drain(~R, ~P, ~I, ~E, ~O, ~destroy, p, messages))
151
152type OwnerStep<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type:
153 Waiting{resources: List<R>, counts: Counts}
154 Received{message: Message<R, P, I, E, O>, counts: Counts, resources: List<R>}
155
156# IO.join closes a one-shot channel; the owner inbox must remain reusable.
157def receive.value(-A: Type, value: Maybe<&1, A>) -> IO(A):
158 match value:
159 case None{}:
160 IO.die(A, 1, "camber internal channel closed unexpectedly")
161 case Some{message}:
162 IO.pure(A, message)
163
164def receive(-A: Type, channel: Chan(A)) -> IO(A):
165 do IO<A>:
166 value : Maybe<&1, A> <- Chan.recv(A, channel)
167 receive.value(A, value)
168
169# Only idle close ends this loop. Busy close never waits on a stalled handler.
170# @unsafe is solely the lifecycle receive loop, not a termination/proof claim.
171@unsafe def owner(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
172 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
173 +messages: Chan(Message<R, P, I, E, O>), +room: U32, step: OwnerStep<R, P, I, E, O>
174) -> IO(Unit):
175 match step:
176 case Waiting{resources, counts}:
177 do IO<Unit>:
178 message : Message<R, P, I, E, O> <- receive(Message<R, P, I, E, O>, messages)
179 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Received{message, counts, resources})
180 case Received{message, counts, resources}:
181 match message:
182 case Returned{resource}:
183 Counts{capacity, in_use, completed, rejected} = counts
184 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resource <> resources, Counts{capacity, Nat.sub(in_use, 1n), 1n+completed, rejected}})
185 case Inspect{reply}:
186 Counts{+capacity, +in_use, +completed, +rejected} = counts
187 do IO<Unit>:
188 sent : Bool <- Chan.send(Counts, reply, Counts{capacity, in_use, completed, rejected})
189 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, in_use, completed, rejected}})
190 case Shutdown{reply}:
191 Counts{+capacity, in_use, +completed, +rejected} = counts
192 match in_use:
193 case 0n:
194 do IO<Unit>:
195 dispose(~R, ~destroy, resources)
196 Chan.close(Message<R, P, I, E, O>, messages)
197 sent : Bool <- Chan.send(CloseResult, reply, Closed{})
198 drain(~R, ~P, ~I, ~E, ~O, ~destroy, U32.to_nat(room), messages)
199 case 1n+ +p:
200 do IO<Unit>:
201 sent : Bool <- Chan.send(CloseResult, reply, Busy{Counts{capacity, 1n+p, completed, rejected}})
202 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 1n+p, completed, rejected}})
203 case Submit{principal, input, reply}:
204 Counts{capacity, in_use, completed, rejected} = counts
205 match resources:
206 case Nil{}:
207 do IO<Unit>:
208 sent : Bool <- Chan.send(Outcome<E, O>, reply, Exhausted{})
209 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{Nil{}, Counts{capacity, in_use, completed, 1n+rejected}})
210 case Con{resource, rest}:
211 do IO<Unit>:
212 IO.spawn(Unit, work(~R, ~P, ~I, ~E, ~O, ~handler, resource, principal, input, messages, reply))
213 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{rest, Counts{capacity, 1n+in_use, completed, rejected}})
214
215def build.step(~R: Type, ~E: Data, ~destroy: R -> IO(Unit),
216 result: Result<&2, &1, E, R>, resources: List<R>,
217 next: List<R> -> IO(Result<&2, &1, E, List<R>>)
218) -> IO(Result<&2, &1, E, List<R>>):
219 match result:
220 case Fail{error}:
221 do IO<Result<&2, &1, E, List<R>>>:
222 dispose(~R, ~destroy, resources)
223 return Fail{error}
224 case Done{resource}:
225 next(resource <> resources)
226
227def build(~K: Data, ~R: Type, ~E: Data, ~create: K -> Nat -> IO(Result<&2, &1, E, R>), ~destroy: R -> IO(Unit),
228 remaining: Nat, +config: K, resources: List<R>
229) -> IO(Result<&2, &1, E, List<R>>):
230 match remaining:
231 case 0n:
232 IO.pure(Result<&2, &1, E, List<R>>, Done{resources})
233 case 1n+ +p:
234 do IO<Result<&2, &1, E, List<R>>>:
235 result : Result<&2, &1, E, R> <- create(config, p)
236 build.step(~R, ~E, ~destroy, result, resources, next => build(~K, ~R, ~E, ~create, ~destroy, p, config, next))
237
238def start.ready(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
239 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
240 capacity: Nat, +room: U32, result: Result<&2, &1, E, List<R>>
241) -> IO(Result<&2, &2, E, Context<R, P, I, E, O>>):
242 match result:
243 case Fail{error}:
244 IO.pure(Result<&2, &2, E, Context<R, P, I, E, O>>, Fail{error})
245 case Done{resources}:
246 do IO<Result<&2, &2, E, Context<R, P, I, E, O>>>:
247 channel : Chan(Message<R, P, I, E, O>) <- Chan.new(Message<R, P, I, E, O>, room)
248 +messages : Chan(Message<R, P, I, E, O>) = channel
249 IO.spawn(Unit, owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 0n, 0n, 0n}}))
250 return Done{Context{messages}}
251
252# The initializer closes partial handles within a failed bundle; start closes
253# all prior whole bundles. No workers exist until the complete set opens.
254def start(~K: Data, ~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
255 ~create: K -> Nat -> IO(Result<&2, &1, E, R>),
256 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
257 config: K, +capacity: Nat, room: U32
258) -> IO(Result<&2, &2, E, Context<R, P, I, E, O>>):
259 do IO<Result<&2, &2, E, Context<R, P, I, E, O>>>:
260 result : Result<&2, &1, E, List<R>> <- build(~K, ~R, ~E, ~create, ~destroy, capacity, config, Nil{})
261 start.ready(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, capacity, room, result)
262
263def submit(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
264 context: Context<R, P, I, E, O>, principal: P, input: I, reply: Chan(Outcome<E, O>)
265) -> IO(Bool):
266 Context{messages} = context
267 Chan.send(Message<R, P, I, E, O>, messages, Submit{principal, input, reply})
268
269def call.accepted(-E: Data, -O: Data, accepted: Bool, +reply: Chan(Outcome<E, O>)) -> IO(Outcome<E, O>):
270 match accepted:
271 case False{}:
272 do IO<Outcome<E, O>>:
273 Chan.close(Outcome<E, O>, reply)
274 return Stopped{}
275 case True{}:
276 do IO<Outcome<E, O>>:
277 result : Outcome<E, O> <- IO.join(Outcome<E, O>, reply)
278 Chan.close(Outcome<E, O>, reply)
279 return result
280
281def call(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
282 context: Context<R, P, I, E, O>, principal: P, input: I
283) -> IO(Outcome<E, O>):
284 do IO<Outcome<E, O>>:
285 channel : Chan(Outcome<E, O>) <- Chan.new(Outcome<E, O>, 1)
286 +reply : Chan(Outcome<E, O>) = channel
287 accepted : Bool <- submit(R, P, I, E, O, context, principal, input, reply)
288 call.accepted(E, O, accepted, reply)
289
290def inspect.value(value: Maybe<&1, Counts>) -> Maybe<&2, Counts>:
291 match value:
292 case None{}:
293 None{}
294 case Some{counts}:
295 Some{counts}
296
297def inspect.sent(sent: Bool, +reply: Chan(Counts)) -> IO(Maybe<&2, Counts>):
298 match sent:
299 case False{}:
300 do IO<Maybe<&2, Counts>>:
301 Chan.close(Counts, reply)
302 return None{}
303 case True{}:
304 do IO<Maybe<&2, Counts>>:
305 value : Maybe<&1, Counts> <- Chan.recv(Counts, reply)
306 Chan.close(Counts, reply)
307 return inspect.value(value)
308
309def inspect(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context<R, P, I, E, O>) -> IO(Maybe<&2, Counts>):
310 Context{messages} = context
311 do IO<Maybe<&2, Counts>>:
312 channel : Chan(Counts) <- Chan.new(Counts, 1)
313 +reply : Chan(Counts) = channel
314 sent : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Inspect{reply})
315 inspect.sent(sent, reply)
316
317def close.sent(sent: Bool, +reply: Chan(CloseResult)) -> IO(CloseResult):
318 match sent:
319 case False{}:
320 do IO<CloseResult>:
321 Chan.close(CloseResult, reply)
322 return Closed{}
323 case True{}:
324 do IO<CloseResult>:
325 value : CloseResult <- IO.join(CloseResult, reply)
326 Chan.close(CloseResult, reply)
327 return value
328
329# Stop external admissions before close. Busy preserves live context/capacity;
330# retry after admitted operations finish. Stalled operations remain observable.
331def close(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context<R, P, I, E, O>) -> IO(CloseResult):
332 Context{messages} = context
333 do IO<CloseResult>:
334 channel : Chan(CloseResult) <- Chan.new(CloseResult, 1)
335 +reply : Chan(CloseResult) = channel
336 sent : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Shutdown{reply})
337 close.sent(sent, reply)
338
339# Policies are prepared Data, not callbacks. Later lifecycle stages can consume
340# these root-to-route declarations without splitting patterns or finding groups.
341type Group<-P: Data> is Data:
342 Group{id: U32, ancestors: List<&2, U32>, policy: P}
343
344type GroupPlan<-P: Data> is Data:
345 GroupPlan{ids: List<&2, U32>, policies: List<&2, P>}
346
347type Route<-A: Data, -P: Data> is Data:
348 Route{method: String, pattern: String, action: A, groups: List<&2, U32>, policy: P}
349
350type Endpoint<-A: Data, -P: Data> is Data:
351 Endpoint{action: A, policies: List<&2, P>}
352
353type Application<-A: Data, -C: Data, -P: Data> is Data:
354 Application{table: Router.Table<Endpoint<A, P>>, context: C, root: P, groups: List<&2, GroupPlan<P>>}
355
356type RegistrationError is Data:
357 RouteError{error: Router.Error}
358 InvalidGroups{groups: List<&2, U32>}
359
360type Match<-P: Data> is Data:
361 Match{target: Target.Target, params: Map<&2, String>, groups: List<&2, U32>, method: String, pattern: String, policies: List<&2, P>}
362
363def groups.equal(xs: List<&2, U32>, ys: List<&2, U32>) -> Bool:
364 match xs ys:
365 case Nil{} Nil{}:
366 True{}
367 case x <> xt y <> yt:
368 U32.is_eq(x, y) && groups.equal(xt, yt)
369 case _ _:
370 False{}
371
372def group.pick(~P: Data, same: Bool, group: Group<P>, later: Unit -> Maybe<&2, Group<P>>) -> Maybe<&2, Group<P>>:
373 match same:
374 case True{}:
375 Some{group}
376 case False{}:
377 later(Unit{})
378
379def group.find(~P: Data, groups: List<&2, Group<P>>, +id: U32) -> Maybe<&2, Group<P>>:
380 match groups:
381 case Nil{}:
382 None{}
383 case Group{+here, ancestors, policy} <> rest:
384 group.pick(~P, U32.is_eq(here, id), Group{here, ancestors, policy}, u => group.find(~P, rest, id))
385
386def group.checked(~P: Data, same: Bool, policy: P) -> Maybe<&2, P>:
387 match same:
388 case False{}:
389 None{}
390 case True{}:
391 Some{policy}
392
393def group.policy(~P: Data, found: Maybe<&2, Group<P>>, ancestors: List<&2, U32>) -> Maybe<&2, P>:
394 match found:
395 case None{}:
396 None{}
397 case Some{Group{id, expected, policy}}:
398 group.checked(~P, groups.equal(expected, ancestors), policy)
399
400def groups.policies(~P: Data, ids: List<&2, U32>, +declared: List<&2, Group<P>>, +ancestors: List<&2, U32>) -> Maybe<&2, List<&2, P>>:
401 match ids:
402 case Nil{}:
403 Some{Nil{}}
404 case +id <> rest:
405 do Maybe<&2, List<&2, P>>:
406 policy : P <- group.policy(~P, group.find(~P, declared, id), ancestors)
407 policies : List<&2, P> <- groups.policies(~P, rest, declared, List.append(&2, U32, ancestors, [id]))
408 return policy <> policies
409
410def groups.unique(~P: Data, found: Maybe<&2, Group<P>>) -> Bool:
411 match found:
412 case None{}:
413 True{}
414 case Some{group}:
415 False{}
416
417def groups.valid(~P: Data, groups: List<&2, Group<P>>, +all: List<&2, Group<P>>) -> Bool:
418 match groups:
419 case Nil{}:
420 True{}
421 case Group{+id, +ancestors, policy} <> +rest:
422 chain = groups.policies(~P, List.append(&2, U32, ancestors, [id]), all, Nil{})
423 groups.unique(~P, group.find(~P, rest, id)) && Maybe.is_some(&2, List<&2, P>, chain) && groups.valid(~P, rest, all)
424
425def routes.policies(~P: Data, result: Maybe<&2, List<&2, P>>, ids: List<&2, U32>) -> Result<&2, &2, RegistrationError, List<&2, P>>:
426 match result:
427 case None{}:
428 Fail{InvalidGroups{ids}}
429 case Some{policies}:
430 Done{policies}
431
432def 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>>>>:
433 match routes:
434 case Nil{}:
435 Done{Nil{}}
436 case Route{method, pattern, action, +ids, policy} <> rest:
437 do Result<&2, &2, RegistrationError, List<&2, Router.Entry<Endpoint<A, P>>>>:
438 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, groups, Nil{}), ids)
439 entries : List<&2, Router.Entry<Endpoint<A, P>>> <- routes.prepare(~A, ~P, rest, groups, root)
440 return Router.Entry{method, pattern, Endpoint{action, root <> List.append(&2, P, policies, [policy])}, ids} <> entries
441
442def groups.prepare(~P: Data, groups: List<&2, Group<P>>, +all: List<&2, Group<P>>) -> Result<&2, &2, RegistrationError, List<&2, GroupPlan<P>>>:
443 match groups:
444 case Nil{}:
445 Done{Nil{}}
446 case Group{id, ancestors, policy} <> rest:
447 +ids = List.append(&2, U32, ancestors, [id])
448 do Result<&2, &2, RegistrationError, List<&2, GroupPlan<P>>>:
449 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, all, Nil{}), ids)
450 plans : List<&2, GroupPlan<P>> <- groups.prepare(~P, rest, all)
451 return GroupPlan{ids, policies} <> plans
452
453def groups.select(~P: Data, same: Bool, policies: List<&2, P>, later: Unit -> List<&2, P>) -> List<&2, P>:
454 match same:
455 case True{}: policies
456 case False{}: later(Unit{})
457
458def groups.prepared(~P: Data, plans: List<&2, GroupPlan<P>>, +ids: List<&2, U32>) -> List<&2, P>:
459 match plans:
460 case Nil{}:
461 Nil{}
462 case GroupPlan{here, policies} <> rest:
463 groups.select(~P, groups.equal(here, ids), policies, u => groups.prepared(~P, rest, ids))
464
465def application.table(~A: Data, ~C: Data, ~P: Data, result: Result<&2, &2, Router.Error, Router.Table<Endpoint<A, P>>>, context: C, root: P, groups: List<&2, GroupPlan<P>>) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
466 match result:
467 case Fail{error}:
468 Fail{RouteError{error}}
469 case Done{table}:
470 Done{Application{table, context, root, groups}}
471
472def 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>>:
473 match valid:
474 case False{}:
475 Fail{InvalidGroups{Nil{}}}
476 case True{}:
477 do Result<&2, &2, RegistrationError, Application<A, C, P>>:
478 entries : List<&2, Router.Entry<Endpoint<A, P>>> <- routes.prepare(~A, ~P, routes, groups, root)
479 plans : List<&2, GroupPlan<P>> <- groups.prepare(~P, groups, groups)
480 application.table(~A, ~C, ~P, Router.prepare(~Endpoint<A, P>, entries), context, root, plans)
481
482# Construction is pure and fallible; no listener or owner is started here.
483def 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>>:
484 application.valid(~A, ~C, ~P, groups.valid(~P, groups, groups), routes, groups, context, root)
485
486def describe(~A: Data, ~C: Data, ~P: Data, app: Application<A, C, P>) -> List<&2, Router.Description>:
487 Application{table, context, root, groups} = app
488 Router.describe(~Endpoint<A, P>, table)
489
490# A closed template can run repeatedly; an affine callback registry cannot.
491def plain(~handler: Http.Req -> IO(Http.Res), req: Http.Req) -> IO(Result<&2, &1, ResponseError, Http.Res>):
492 do IO<Result<&2, &1, ResponseError, Http.Res>>:
493 response : Http.Res <- handler(req)
494 return validate(response)
495
496type Policy<-H: Data> is Data:
497 Policy{id: U32, before: List<&2, H>, transforms: List<&2, H>}
498
499def policy(~H: Data, id: U32, before: List<&2, H>, transforms: List<&2, H>) -> Policy<H>:
500 Policy{id, before, List.reverse(&2, H, transforms)}
501
502type Metadata is Data:
503 Metadata{method: String, target: String, headers: Map<&2, List<&2, String>>}
504
505type Error<-E: Data> is Data:
506 ApplicationError{error: E}
507 Framework{status: U32, allow: String}
508 ResponseInvalid{}
509
510# Adapt reusable input status failures to the existing one-pass dispatch mapper.
511def input(~I: Type, ~E: Data, outcome: Result<&2, &1, U32, I>) -> Result<&2, &1, Error<E>, I>:
512 match outcome:
513 case Fail{status}: Fail{Framework{status, ""}}
514 case Done{value}: Done{value}
515
516def execute.decoded(~I: Type, ~S: Data, ~K: Data, ~E: Data,
517 ~handler: S -> K -> I -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
518 outcome: Result<&2, &1, Error<E>, I>, state: S, config: K
519) -> IO(S & Result<&2, &1, Error<E>, Http.Res>):
520 match outcome:
521 case Fail{error}: IO.pure(S & Result<&2, &1, Error<E>, Http.Res>, (state, Fail{error}))
522 case Done{value}: handler(state, config, value)
523
524# An executor, not a second dispatcher: call from dispatch's run template, after
525# all applicable before hooks. Only validated typed input reaches business IO.
526def execute(~I: Type, ~S: Data, ~K: Data, ~E: Data, ~H: Data,
527 ~decoder: K -> Match<Policy<H>> -> Http.Req -> Result<&2, &1, Error<E>, I>,
528 ~handler: S -> K -> I -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
529 state: S, +config: K, selected: Match<Policy<H>>, req: Http.Req
530) -> IO(S & Result<&2, &1, Error<E>, Http.Res>):
531 execute.decoded(~I, ~S, ~K, ~E, ~handler, decoder(config, selected, req), state, config)
532
533type Step<-S: Data, -E: Data> is Type:
534 Continue{state: S}
535 Early{state: S, response: Http.Res}
536 Reject{state: S, error: Error<E>}
537
538type Completion<-S: Data, -H: Data> is Type:
539 Completed{state: S, response: Http.Res, entered: List<&2, Policy<H>>, mapped: Bool, stopped: Bool}
540
541# Application completion only: no socket write, duration, or peer-receipt claim.
542def minimal() -> Http.Res:
543 empty(500)
544
545def problem.allow(add: Bool, headers: Map<&2, List<&2, String>>, allow: String) -> Map<&2, List<&2, String>>:
546 match add:
547 case True{}: Http.set(headers, "allow", allow)
548 case False{}: headers
549
550def problem.response(allow: String, response: Http.Res) -> Http.Res:
551 Http.Res{+status, headers, body} = response
552 Http.Res{status, problem.allow(U32.is_eq(status, 405), Http.set(headers, "content-type", "application/problem+json"), allow), body}
553
554def problem(+status: U32, title: String, allow: String) -> Http.Res:
555 problem.response(allow, json(~Json.Val, ~(x => x), status, Json.Obj{[(Json.utf8("type"), Json.Str{Json.utf8("about:blank")}), (Json.utf8("title"), Json.Str{Json.utf8(title)}), (Json.utf8("status"), Json.Num{Json.utf8(U32.show(status))})]}))
556
557def default.error(~E: Data, error: Error<E>) -> Http.Res:
558 match error:
559 case Framework{status, allow}:
560 match status:
561 case 400:
562 problem(400, "Bad Request", allow)
563 case 404:
564 problem(404, "Not Found", allow)
565 case 405:
566 problem(405, "Method Not Allowed", allow)
567 case 415:
568 problem(415, "Unsupported Media Type", allow)
569 case _:
570 minimal()
571 case _:
572 minimal()
573
574def default.mapper(~S: Data, ~K: Data, ~E: Data, state: S, config: K, error: Error<E>) -> IO(S & Result<&2, &1, Error<E>, Http.Res>):
575 IO.pure(S & Result<&2, &1, Error<E>, Http.Res>, (state, Done{default.error(~E, error)}))
576
577def mapped.checked(~S: Data, ~H: Data, state: S, entered: List<&2, Policy<H>>, stopped: Bool, result: Result<&2, &1, ResponseError, Http.Res>) -> Completion<S, H>:
578 match result:
579 case Fail{error}:
580 Completed{state, minimal(), entered, True{}, True{}}
581 case Done{response}:
582 Completed{state, response, entered, True{}, stopped}
583
584def mapped.result(~S: Data, ~E: Data, ~H: Data, entered: List<&2, Policy<H>>, stopped: Bool, result: S & Result<&2, &1, Error<E>, Http.Res>) -> Completion<S, H>:
585 (state, outcome) = result
586 match outcome:
587 case Fail{error}:
588 Completed{state, minimal(), entered, True{}, True{}}
589 case Done{response}:
590 mapped.checked(~S, ~H, state, entered, stopped, validate(response))
591
592def failed(~S: Data, ~K: Data, ~E: Data, ~H: Data,
593 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
594 mapped: Bool, state: S, config: K, error: Error<E>, entered: List<&2, Policy<H>>, stopped: Bool
595) -> IO(Completion<S, H>):
596 match mapped:
597 case True{}:
598 IO.pure(Completion<S, H>, Completed{state, minimal(), entered, True{}, True{}})
599 case False{}:
600 do IO<Completion<S, H>>:
601 result : S & Result<&2, &1, Error<E>, Http.Res> <- mapper(state, config, error)
602 return mapped.result(~S, ~E, ~H, entered, stopped, result)
603
604def result(~S: Data, ~K: Data, ~E: Data, ~H: Data,
605 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
606 config: K, entered: List<&2, Policy<H>>, outcome: S & Result<&2, &1, Error<E>, Http.Res>
607) -> IO(Completion<S, H>):
608 (state, response) = outcome
609 match response:
610 case Fail{error}:
611 failed(~S, ~K, ~E, ~H, ~mapper, False{}, state, config, error, entered, False{})
612 case Done{res}:
613 IO.pure(Completion<S, H>, Completed{state, res, entered, False{}, False{}})
614
615def before.next(~S: Data, ~E: Data, step: Step<S, E>, next: S -> IO(Step<S, E>)) -> IO(Step<S, E>):
616 match step:
617 case Continue{state}:
618 next(state)
619 case Early{state, response}:
620 IO.pure(Step<S, E>, Early{state, response})
621 case Reject{state, error}:
622 IO.pure(Step<S, E>, Reject{state, error})
623
624def before(~S: Data, ~K: Data, ~E: Data, ~H: Data,
625 ~hook: H -> S -> K -> Metadata -> IO(Step<S, E>),
626 hooks: List<&2, H>, state: S, +config: K, +meta: Metadata
627) -> IO(Step<S, E>):
628 match hooks:
629 case Nil{}:
630 IO.pure(Step<S, E>, Continue{state})
631 case h <> rest:
632 do IO<Step<S, E>>:
633 step : Step<S, E> <- hook(h, state, config, meta)
634 before.next(~S, ~E, step, s => before(~S, ~K, ~E, ~H, ~hook, rest, s, config, meta))
635
636def transformed(~S: Data, ~K: Data, ~E: Data, ~H: Data,
637 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
638 config: K, entered: List<&2, Policy<H>>, mapped: Bool, outcome: S & Result<&2, &1, Error<E>, Http.Res>
639) -> IO(Completion<S, H>):
640 (state, response) = outcome
641 match response:
642 case Fail{error}:
643 failed(~S, ~K, ~E, ~H, ~mapper, mapped, state, config, error, entered, True{})
644 case Done{res}:
645 IO.pure(Completion<S, H>, Completed{state, res, entered, mapped, False{}})
646
647def transforms(~S: Data, ~K: Data, ~E: Data, ~H: Data,
648 ~hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
649 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
650 hooks: List<&2, H>, +config: K, completion: Completion<S, H>
651) -> IO(Completion<S, H>):
652 match hooks:
653 case Nil{}:
654 IO.pure(Completion<S, H>, completion)
655 case h <> rest:
656 match completion:
657 case Completed{state, response, entered, mapped, True{}}:
658 IO.pure(Completion<S, H>, Completed{state, response, entered, mapped, True{}})
659 case Completed{state, response, +entered, +mapped, False{}}:
660 do IO<Completion<S, H>>:
661 changed : S & Result<&2, &1, Error<E>, Http.Res> <- hook(h, state, config, response)
662 next : Completion<S, H> <- transformed(~S, ~K, ~E, ~H, ~mapper, config, entered, mapped, changed)
663 transforms(~S, ~K, ~E, ~H, ~hook, ~mapper, rest, config, next)
664
665def entered.step(~S: Data, ~K: Data, ~E: Data, ~H: Data,
666 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
667 config: K, entered: List<&2, Policy<H>>, body: Http.Body, step: Step<S, E>, next: S -> List<&2, Policy<H>> -> Http.Body -> IO(Completion<S, H>)
668) -> IO(Completion<S, H>):
669 match step:
670 case Continue{state}:
671 next(state, entered, body)
672 case Early{state, response}:
673 IO.pure(Completion<S, H>, Completed{state, response, entered, False{}, False{}})
674 case Reject{state, error}:
675 failed(~S, ~K, ~E, ~H, ~mapper, False{}, state, config, error, entered, False{})
676
677def scopes(~S: Data, ~K: Data, ~E: Data, ~H: Data,
678 ~before_hook: H -> S -> K -> Metadata -> IO(Step<S, E>),
679 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
680 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
681 policies: List<&2, Policy<H>>, state: S, +config: K, +meta: Metadata, entered: List<&2, Policy<H>>, body: Http.Body,
682 next: S -> List<&2, Policy<H>> -> Http.Body -> IO(Completion<S, H>)
683) -> IO(Completion<S, H>):
684 match policies:
685 case Nil{}:
686 next(state, entered, body)
687 case Policy{+id, +hooks, +unwind} <> rest:
688 scope = {Policy{id, hooks, unwind} : Policy<H>}
689 do IO<Completion<S, H>>:
690 step : Step<S, E> <- before(~S, ~K, ~E, ~H, ~before_hook, hooks, state, config, meta)
691 inner : Completion<S, H> <- entered.step(~S, ~K, ~E, ~H, ~mapper, config, scope <> entered, body, step, s => ids => b => scopes(~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~mapper, rest, s, config, meta, ids, b, next))
692 transforms(~S, ~K, ~E, ~H, ~after_hook, ~mapper, unwind, config, inner)
693
694def finish.checked(~S: Data, ~K: Data, ~E: Data, ~H: Data,
695 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
696 state: S, config: K, entered: List<&2, Policy<H>>, mapped: Bool, stopped: Bool, checked: Result<&2, &1, ResponseError, Http.Res>
697) -> IO(Completion<S, H>):
698 match checked:
699 case Done{response}:
700 IO.pure(Completion<S, H>, Completed{state, response, entered, mapped, stopped})
701 case Fail{error}:
702 failed(~S, ~K, ~E, ~H, ~mapper, mapped, state, config, ResponseInvalid{}, entered, True{})
703
704def finish(~S: Data, ~K: Data, ~E: Data, ~H: Data,
705 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
706 config: K, completion: Completion<S, H>
707) -> IO(Completion<S, H>):
708 Completed{state, response, entered, mapped, stopped} = completion
709 finish.checked(~S, ~K, ~E, ~H, ~mapper, state, config, entered, mapped, stopped, validate(response))
710
711def policies.tail(~H: Data, policies: List<&2, Policy<H>>) -> List<&2, Policy<H>>:
712 match policies:
713 case Nil{}:
714 Nil{}
715 case root <> rest:
716 rest
717
718
719def endpoint(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data,
720 ~run: A -> S -> K -> Match<Policy<H>> -> Http.Req -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
721 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
722 action: A, state: S, +config: K, selected: Match<Policy<H>>, meta: Metadata, entered: List<&2, Policy<H>>, body: Http.Body
723) -> IO(Completion<S, H>):
724 Metadata{method, target, headers} = meta
725 do IO<Completion<S, H>>:
726 outcome : S & Result<&2, &1, Error<E>, Http.Res> <- run(action, state, config, selected, Http.Req{method, target, headers, body})
727 result(~S, ~K, ~E, ~H, ~mapper, config, entered, outcome)
728
729def framework(~S: Data, ~K: Data, ~E: Data, ~H: Data,
730 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
731 +status: U32, allow: String, state: S, config: K, entered: List<&2, Policy<H>>, body: Http.Body
732) -> IO(Completion<S, H>):
733 match status:
734 case 204:
735 IO.pure(Completion<S, H>, Completed{state, Http.Res{204, Http.set(Http.empty(), "allow", allow), Http.from_string("")}, entered, False{}, False{}})
736 case _:
737 failed(~S, ~K, ~E, ~H, ~mapper, False{}, state, config, Framework{status, allow}, entered, False{})
738
739def choice(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data,
740 ~before_hook: H -> S -> K -> Metadata -> IO(Step<S, E>),
741 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
742 ~run: A -> S -> K -> Match<Policy<H>> -> Http.Req -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
743 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
744 selected: Router.Choice<Endpoint<A, Policy<H>>>, target: Target.Target,
745 groups: List<&2, GroupPlan<Policy<H>>>, state: S, +config: K, +meta: Metadata, entered: List<&2, Policy<H>>, body: Http.Body
746) -> IO(Completion<S, H>):
747 match selected:
748 case Router.NotFound{}:
749 framework(~S, ~K, ~E, ~H, ~mapper, 404, "", state, config, entered, body)
750 case Router.MethodMissing{ids, allow}:
751 policies = groups.prepared(~Policy<H>, groups, ids)
752 scopes(~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~mapper, policies, state, config, meta, entered, body,
753 s => entered => b => framework(~S, ~K, ~E, ~H, ~mapper, 405, allow, s, config, entered, b))
754 case Router.Options{ids, allow}:
755 policies = groups.prepared(~Policy<H>, groups, ids)
756 scopes(~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~mapper, policies, state, config, meta, entered, body,
757 s => entered => b => framework(~S, ~K, ~E, ~H, ~mapper, 204, allow, s, config, entered, b))
758 case Router.Found{Endpoint{action, +policies}, params, ids, method, pattern}:
759 selected = {Match{target, params, ids, method, pattern, policies} : Match<Policy<H>>}
760 scopes(~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~mapper, policies.tail(~H, policies), state, config, meta, entered, body,
761 s => entered => b => endpoint(~A, ~S, ~K, ~E, ~H, ~run, ~mapper, action, s, config, selected, meta, entered, b))
762
763def metadata.authority(authority: Maybe<&2, String>, meta: Metadata) -> Metadata:
764 match authority:
765 case None{}:
766 meta
767 case Some{host}:
768 Metadata{method, target, headers} = meta
769 Metadata{method, target, Http.set(headers, "host", host)}
770
771def resolved(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data,
772 ~before_hook: H -> S -> K -> Metadata -> IO(Step<S, E>),
773 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
774 ~run: A -> S -> K -> Match<Policy<H>> -> Http.Req -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
775 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
776 outcome: Result<&2, &2, Router.Error, Router.Resolved<Endpoint<A, Policy<H>>>>,
777 groups: List<&2, GroupPlan<Policy<H>>>, state: S, config: K, meta: Metadata, entered: List<&2, Policy<H>>, body: Http.Body
778) -> IO(Completion<S, H>):
779 match outcome:
780 case Fail{error}:
781 framework(~S, ~K, ~E, ~H, ~mapper, 400, "", state, config, entered, body)
782 case Done{Router.Resolved{+target, selected}}:
783 Target.Target{segments, query, authority, star} = target
784 choice(~A, ~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~run, ~mapper, selected, target, groups, state, config, metadata.authority(authority, meta), entered, body)
785
786def dispatch(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data,
787 ~before_hook: H -> S -> K -> Metadata -> IO(Step<S, E>),
788 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
789 ~run: A -> S -> K -> Match<Policy<H>> -> Http.Req -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
790 ~mapper: S -> K -> Error<E> -> IO(S & Result<&2, &1, Error<E>, Http.Res>),
791 app: Application<A, K, Policy<H>>, state: S, req: Http.Req
792) -> IO(Completion<S, H>):
793 Application{table, +config, root, groups} = app
794 Http.Req{+method, +path, +headers, body} = req
795 +meta = {Metadata{method, path, headers} : Metadata}
796 do IO<Completion<S, H>>:
797 completion : Completion<S, H> <- scopes(~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~mapper, [root], state, config, meta, Nil{}, body,
798 s => entered => b => resolved(~A, ~S, ~K, ~E, ~H, ~before_hook, ~after_hook, ~run, ~mapper,
799 Router.resolve(~Endpoint<A, Policy<H>>, table, method, path), groups, s, config, meta, entered, b))
800 finish(~S, ~K, ~E, ~H, ~mapper, config, completion)