1 # Fixed prepared HTTP applications and bounded affine dependency bundles. 2 import Base 3 import bend-kit-http@0.25.0.2/http.bend as Http 4 import bend-kit-router@0.2.0.0/router.bend as Router 5 import bend-kit-router@0.2.0.0/target.bend as Target 6 import bend-kit-json@0.5.1.0/json.bend as Json 7 import ./response.bend as Response 8 import ./input.bend as Input 9 10 type ResponseError is Data: 11 InvalidResponse{} 12 InvalidRedirect{} 13 14 def 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 21 def 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. 26 def 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 29 def 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 32 def raw(status: U32, body: Http.Body) -> Http.Res: 33 Http.Res{status, Http.empty(), body} 34 35 def empty(status: U32) -> Http.Res: 36 raw(status, Http.from_string("")) 37 38 def 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 45 def 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 48 type Counts is Data: 49 Counts{capacity: Nat, in_use: Nat, completed: Nat, rejected: Nat} 50 51 type Outcome<-E: Data, -O: Data> is Data: 52 Success{value: O} 53 Expected{error: E} 54 Exhausted{} 55 Stopped{} 56 57 type CloseResult is Data: 58 Closed{} 59 Busy{counts: Counts} 60 61 type Message<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type: 62 Submit{principal: P, input: I, reply: Chan(Outcome)} 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. 69 type Context<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Data: 70 Context{messages: Chan(Message)} 71 72 def outcome.value(-E: Data, -O: Data, result: Result<&2, &2, E, O>) -> Outcome: 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. 81 def work.return(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, 82 messages: Chan(Message), reply: Chan(Outcome), 83 result: R & Result<&2, &2, E, O> 84 ) -> IO(Unit): 85 (returned, outcome) = result 86 do IO: 87 accepted : Bool <- Chan.send(Message, messages, Returned{returned}) 88 sent : Bool <- Chan.send(Outcome, reply, outcome.value(E, O, outcome)) 89 return Unit{} 90 91 def 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), reply: Chan(Outcome) 95 ) -> IO(Unit): 96 do IO: 97 result : R & Result<&2, &2, E, O> <- handler(resource, principal, input) 98 work.return(R, P, I, E, O, messages, reply, result) 99 100 def dispose(~R: Type, ~destroy: R -> IO(Unit), resources: List) -> IO(Unit): 101 match resources: 102 case Nil{}: 103 IO.pure(Unit, Unit{}) 104 case Con{resource, rest}: 105 do IO: 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. 111 def drain.message(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, 112 ~destroy: R -> IO(Unit), message: Message 113 ) -> IO(Unit): 114 match message: 115 case Submit{principal, input, reply}: 116 do IO: 117 sent : Bool <- Chan.send(Outcome, reply, Stopped{}) 118 return Unit{} 119 case Inspect{reply}: 120 Chan.close(Counts, reply) 121 case Shutdown{reply}: 122 do IO: 123 sent : Bool <- Chan.send(CloseResult, reply, Closed{}) 124 return Unit{} 125 case Returned{resource}: 126 destroy(resource) 127 128 def drain.step(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, 129 ~destroy: R -> IO(Unit), pending: Maybe<&1, Message>, 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: 137 drain.message(~R, ~P, ~I, ~E, ~O, ~destroy, message) 138 next(Unit{}) 139 140 # The closed inbox contains at most room buffered messages. 141 def drain(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, 142 ~destroy: R -> IO(Unit), fuel: Nat, +messages: Chan(Message) 143 ) -> IO(Unit): 144 match fuel: 145 case 0n: 146 IO.pure(Unit, Unit{}) 147 case 1n+p: 148 do IO: 149 pending : Maybe<&1, Message> <- Chan.recv(Message, messages) 150 drain.step(~R, ~P, ~I, ~E, ~O, ~destroy, pending, u => drain(~R, ~P, ~I, ~E, ~O, ~destroy, p, messages)) 151 152 type OwnerStep<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type: 153 Waiting{resources: List, counts: Counts} 154 Received{message: Message, counts: Counts, resources: List} 155 156 # IO.join closes a one-shot channel; the owner inbox must remain reusable. 157 def 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 164 def receive(-A: Type, channel: Chan(A)) -> IO(A): 165 do IO: 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), +room: U32, step: OwnerStep 174 ) -> IO(Unit): 175 match step: 176 case Waiting{resources, counts}: 177 do IO: 178 message : Message <- receive(Message, 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: 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: 195 dispose(~R, ~destroy, resources) 196 Chan.close(Message, 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: 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: 208 sent : Bool <- Chan.send(Outcome, 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: 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 215 def build.step(~R: Type, ~E: Data, ~destroy: R -> IO(Unit), 216 result: Result<&2, &1, E, R>, resources: List, 217 next: List -> IO(Result<&2, &1, E, List>) 218 ) -> IO(Result<&2, &1, E, List>): 219 match result: 220 case Fail{error}: 221 do IO>>: 222 dispose(~R, ~destroy, resources) 223 return Fail{error} 224 case Done{resource}: 225 next(resource <> resources) 226 227 def 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 229 ) -> IO(Result<&2, &1, E, List>): 230 match remaining: 231 case 0n: 232 IO.pure(Result<&2, &1, E, List>, Done{resources}) 233 case 1n+ +p: 234 do IO>>: 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 238 def 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> 241 ) -> IO(Result<&2, &2, E, Context>): 242 match result: 243 case Fail{error}: 244 IO.pure(Result<&2, &2, E, Context>, Fail{error}) 245 case Done{resources}: 246 do IO>>: 247 channel : Chan(Message) <- Chan.new(Message, room) 248 +messages : Chan(Message) = 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. 254 def 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>): 259 do IO>>: 260 result : Result<&2, &1, E, List> <- build(~K, ~R, ~E, ~create, ~destroy, capacity, config, Nil{}) 261 start.ready(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, capacity, room, result) 262 263 def submit(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, 264 context: Context, principal: P, input: I, reply: Chan(Outcome) 265 ) -> IO(Bool): 266 Context{messages} = context 267 Chan.send(Message, messages, Submit{principal, input, reply}) 268 269 def call.accepted(-E: Data, -O: Data, accepted: Bool, +reply: Chan(Outcome)) -> IO(Outcome): 270 match accepted: 271 case False{}: 272 do IO>: 273 Chan.close(Outcome, reply) 274 return Stopped{} 275 case True{}: 276 do IO>: 277 result : Outcome <- IO.join(Outcome, reply) 278 Chan.close(Outcome, reply) 279 return result 280 281 def call(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, 282 context: Context, principal: P, input: I 283 ) -> IO(Outcome): 284 do IO>: 285 channel : Chan(Outcome) <- Chan.new(Outcome, 1) 286 +reply : Chan(Outcome) = channel 287 accepted : Bool <- submit(R, P, I, E, O, context, principal, input, reply) 288 call.accepted(E, O, accepted, reply) 289 290 def 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 297 def inspect.sent(sent: Bool, +reply: Chan(Counts)) -> IO(Maybe<&2, Counts>): 298 match sent: 299 case False{}: 300 do IO>: 301 Chan.close(Counts, reply) 302 return None{} 303 case True{}: 304 do IO>: 305 value : Maybe<&1, Counts> <- Chan.recv(Counts, reply) 306 Chan.close(Counts, reply) 307 return inspect.value(value) 308 309 def inspect(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context) -> IO(Maybe<&2, Counts>): 310 Context{messages} = context 311 do IO>: 312 channel : Chan(Counts) <- Chan.new(Counts, 1) 313 +reply : Chan(Counts) = channel 314 sent : Bool <- Chan.send(Message, messages, Inspect{reply}) 315 inspect.sent(sent, reply) 316 317 def close.sent(sent: Bool, +reply: Chan(CloseResult)) -> IO(CloseResult): 318 match sent: 319 case False{}: 320 do IO: 321 Chan.close(CloseResult, reply) 322 return Closed{} 323 case True{}: 324 do IO: 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. 331 def close(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context) -> IO(CloseResult): 332 Context{messages} = context 333 do IO: 334 channel : Chan(CloseResult) <- Chan.new(CloseResult, 1) 335 +reply : Chan(CloseResult) = channel 336 sent : Bool <- Chan.send(Message, 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. 341 type Group<-P: Data> is Data: 342 Group{id: U32, ancestors: List<&2, U32>, policy: P} 343 344 type GroupPlan<-P: Data> is Data: 345 GroupPlan{ids: List<&2, U32>, policies: List<&2, P>} 346 347 type Route<-A: Data, -P: Data> is Data: 348 Route{method: String, pattern: String, action: A, groups: List<&2, U32>, policy: P} 349 350 type Endpoint<-A: Data, -P: Data> is Data: 351 Endpoint{action: A, policies: List<&2, P>} 352 353 type Application<-A: Data, -C: Data, -P: Data> is Data: 354 Application{table: Router.Table>, context: C, root: P, groups: List<&2, GroupPlan

>} 355 356 type RegistrationError is Data: 357 RouteError{error: Router.Error} 358 InvalidGroups{groups: List<&2, U32>} 359 360 type 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 363 def 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 372 def group.pick(~P: Data, same: Bool, group: Group

, later: Unit -> Maybe<&2, Group

>) -> Maybe<&2, Group

>: 373 match same: 374 case True{}: 375 Some{group} 376 case False{}: 377 later(Unit{}) 378 379 def group.find(~P: Data, groups: List<&2, Group

>, +id: U32) -> Maybe<&2, Group

>: 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 386 def 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 393 def group.policy(~P: Data, found: Maybe<&2, Group

>, 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 400 def groups.policies(~P: Data, ids: List<&2, U32>, +declared: List<&2, Group

>, +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 410 def groups.unique(~P: Data, found: Maybe<&2, Group

>) -> Bool: 411 match found: 412 case None{}: 413 True{} 414 case Some{group}: 415 False{} 416 417 def groups.valid(~P: Data, groups: List<&2, Group

>, +all: List<&2, Group

>) -> 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 425 def 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 432 def routes.prepare(~A: Data, ~P: Data, routes: List<&2, Route>, +groups: List<&2, Group

>, +root: P) -> Result<&2, &2, RegistrationError, List<&2, Router.Entry>>>: 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>>>: 438 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, groups, Nil{}), ids) 439 entries : List<&2, Router.Entry>> <- 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 442 def groups.prepare(~P: Data, groups: List<&2, Group

>, +all: List<&2, Group

>) -> Result<&2, &2, RegistrationError, List<&2, GroupPlan

>>: 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

>>: 449 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, all, Nil{}), ids) 450 plans : List<&2, GroupPlan

> <- groups.prepare(~P, rest, all) 451 return GroupPlan{ids, policies} <> plans 452 453 def 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 458 def groups.prepared(~P: Data, plans: List<&2, GroupPlan

>, +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 465 def application.table(~A: Data, ~C: Data, ~P: Data, result: Result<&2, &2, Router.Error, Router.Table>>, context: C, root: P, groups: List<&2, GroupPlan

>) -> Result<&2, &2, RegistrationError, Application>: 466 match result: 467 case Fail{error}: 468 Fail{RouteError{error}} 469 case Done{table}: 470 Done{Application{table, context, root, groups}} 471 472 def application.valid(~A: Data, ~C: Data, ~P: Data, valid: Bool, routes: List<&2, Route>, +groups: List<&2, Group

>, context: C, +root: P) -> Result<&2, &2, RegistrationError, Application>: 473 match valid: 474 case False{}: 475 Fail{InvalidGroups{Nil{}}} 476 case True{}: 477 do Result<&2, &2, RegistrationError, Application>: 478 entries : List<&2, Router.Entry>> <- routes.prepare(~A, ~P, routes, groups, root) 479 plans : List<&2, GroupPlan

> <- groups.prepare(~P, groups, groups) 480 application.table(~A, ~C, ~P, Router.prepare(~Endpoint, entries), context, root, plans) 481 482 # Construction is pure and fallible; no listener or owner is started here. 483 def application(~A: Data, ~C: Data, ~P: Data, routes: List<&2, Route>, +groups: List<&2, Group

>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application>: 484 application.valid(~A, ~C, ~P, groups.valid(~P, groups, groups), routes, groups, context, root) 485 486 def describe(~A: Data, ~C: Data, ~P: Data, app: Application) -> List<&2, Router.Description>: 487 Application{table, context, root, groups} = app 488 Router.describe(~Endpoint, table) 489 490 # A closed template can run repeatedly; an affine callback registry cannot. 491 def plain(~handler: Http.Req -> IO(Http.Res), req: Http.Req) -> IO(Result<&2, &1, ResponseError, Http.Res>): 492 do IO>: 493 response : Http.Res <- handler(req) 494 return validate(response) 495 496 type Policy<-H: Data> is Data: 497 Policy{id: U32, before: List<&2, H>, transforms: List<&2, H>} 498 499 def policy(~H: Data, id: U32, before: List<&2, H>, transforms: List<&2, H>) -> Policy: 500 Policy{id, before, List.reverse(&2, H, transforms)} 501 502 type Metadata is Data: 503 Metadata{method: String, target: String, headers: Map<&2, List<&2, String>>} 504 505 type 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. 511 def input(~I: Type, ~E: Data, outcome: Result<&2, &1, U32, I>) -> Result<&2, &1, Error, I>: 512 match outcome: 513 case Fail{status}: Fail{Framework{status, ""}} 514 case Done{value}: Done{value} 515 516 def execute.decoded(~I: Type, ~S: Data, ~K: Data, ~E: Data, 517 ~handler: S -> K -> I -> IO(S & Result<&2, &1, Error, Http.Res>), 518 outcome: Result<&2, &1, Error, I>, state: S, config: K 519 ) -> IO(S & Result<&2, &1, Error, Http.Res>): 520 match outcome: 521 case Fail{error}: IO.pure(S & Result<&2, &1, Error, 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. 526 def execute(~I: Type, ~S: Data, ~K: Data, ~E: Data, ~H: Data, 527 ~decoder: K -> Match> -> Http.Req -> Result<&2, &1, Error, I>, 528 ~handler: S -> K -> I -> IO(S & Result<&2, &1, Error, Http.Res>), 529 state: S, +config: K, selected: Match>, req: Http.Req 530 ) -> IO(S & Result<&2, &1, Error, Http.Res>): 531 execute.decoded(~I, ~S, ~K, ~E, ~handler, decoder(config, selected, req), state, config) 532 533 type Step<-S: Data, -E: Data> is Type: 534 Continue{state: S} 535 Early{state: S, response: Http.Res} 536 Reject{state: S, error: Error} 537 538 type Completion<-S: Data, -H: Data> is Type: 539 Completed{state: S, response: Http.Res, entered: List<&2, Policy>, mapped: Bool, stopped: Bool} 540 541 # Application completion only: no socket write, duration, or peer-receipt claim. 542 def minimal() -> Http.Res: 543 empty(500) 544 545 def 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 550 def 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 554 def 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 557 def default.error(~E: Data, error: Error) -> 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 574 def default.mapper(~S: Data, ~K: Data, ~E: Data, state: S, config: K, error: Error) -> IO(S & Result<&2, &1, Error, Http.Res>): 575 IO.pure(S & Result<&2, &1, Error, Http.Res>, (state, Done{default.error(~E, error)})) 576 577 def mapped.checked(~S: Data, ~H: Data, state: S, entered: List<&2, Policy>, stopped: Bool, result: Result<&2, &1, ResponseError, Http.Res>) -> Completion: 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 584 def mapped.result(~S: Data, ~E: Data, ~H: Data, entered: List<&2, Policy>, stopped: Bool, result: S & Result<&2, &1, Error, Http.Res>) -> Completion: 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 592 def failed(~S: Data, ~K: Data, ~E: Data, ~H: Data, 593 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 594 mapped: Bool, state: S, config: K, error: Error, entered: List<&2, Policy>, stopped: Bool 595 ) -> IO(Completion): 596 match mapped: 597 case True{}: 598 IO.pure(Completion, Completed{state, minimal(), entered, True{}, True{}}) 599 case False{}: 600 do IO>: 601 result : S & Result<&2, &1, Error, Http.Res> <- mapper(state, config, error) 602 return mapped.result(~S, ~E, ~H, entered, stopped, result) 603 604 def result(~S: Data, ~K: Data, ~E: Data, ~H: Data, 605 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 606 config: K, entered: List<&2, Policy>, outcome: S & Result<&2, &1, Error, Http.Res> 607 ) -> IO(Completion): 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, Completed{state, res, entered, False{}, False{}}) 614 615 def before.next(~S: Data, ~E: Data, step: Step, next: S -> IO(Step)) -> IO(Step): 616 match step: 617 case Continue{state}: 618 next(state) 619 case Early{state, response}: 620 IO.pure(Step, Early{state, response}) 621 case Reject{state, error}: 622 IO.pure(Step, Reject{state, error}) 623 624 def before(~S: Data, ~K: Data, ~E: Data, ~H: Data, 625 ~hook: H -> S -> K -> Metadata -> IO(Step), 626 hooks: List<&2, H>, state: S, +config: K, +meta: Metadata 627 ) -> IO(Step): 628 match hooks: 629 case Nil{}: 630 IO.pure(Step, Continue{state}) 631 case h <> rest: 632 do IO>: 633 step : Step <- hook(h, state, config, meta) 634 before.next(~S, ~E, step, s => before(~S, ~K, ~E, ~H, ~hook, rest, s, config, meta)) 635 636 def transformed(~S: Data, ~K: Data, ~E: Data, ~H: Data, 637 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 638 config: K, entered: List<&2, Policy>, mapped: Bool, outcome: S & Result<&2, &1, Error, Http.Res> 639 ) -> IO(Completion): 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, Completed{state, res, entered, mapped, False{}}) 646 647 def transforms(~S: Data, ~K: Data, ~E: Data, ~H: Data, 648 ~hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error, Http.Res>), 649 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 650 hooks: List<&2, H>, +config: K, completion: Completion 651 ) -> IO(Completion): 652 match hooks: 653 case Nil{}: 654 IO.pure(Completion, completion) 655 case h <> rest: 656 match completion: 657 case Completed{state, response, entered, mapped, True{}}: 658 IO.pure(Completion, Completed{state, response, entered, mapped, True{}}) 659 case Completed{state, response, +entered, +mapped, False{}}: 660 do IO>: 661 changed : S & Result<&2, &1, Error, Http.Res> <- hook(h, state, config, response) 662 next : Completion <- transformed(~S, ~K, ~E, ~H, ~mapper, config, entered, mapped, changed) 663 transforms(~S, ~K, ~E, ~H, ~hook, ~mapper, rest, config, next) 664 665 def entered.step(~S: Data, ~K: Data, ~E: Data, ~H: Data, 666 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 667 config: K, entered: List<&2, Policy>, body: Http.Body, step: Step, next: S -> List<&2, Policy> -> Http.Body -> IO(Completion) 668 ) -> IO(Completion): 669 match step: 670 case Continue{state}: 671 next(state, entered, body) 672 case Early{state, response}: 673 IO.pure(Completion, 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 677 def scopes(~S: Data, ~K: Data, ~E: Data, ~H: Data, 678 ~before_hook: H -> S -> K -> Metadata -> IO(Step), 679 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error, Http.Res>), 680 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 681 policies: List<&2, Policy>, state: S, +config: K, +meta: Metadata, entered: List<&2, Policy>, body: Http.Body, 682 next: S -> List<&2, Policy> -> Http.Body -> IO(Completion) 683 ) -> IO(Completion): 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} 689 do IO>: 690 step : Step <- before(~S, ~K, ~E, ~H, ~before_hook, hooks, state, config, meta) 691 inner : Completion <- 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 694 def finish.checked(~S: Data, ~K: Data, ~E: Data, ~H: Data, 695 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 696 state: S, config: K, entered: List<&2, Policy>, mapped: Bool, stopped: Bool, checked: Result<&2, &1, ResponseError, Http.Res> 697 ) -> IO(Completion): 698 match checked: 699 case Done{response}: 700 IO.pure(Completion, Completed{state, response, entered, mapped, stopped}) 701 case Fail{error}: 702 failed(~S, ~K, ~E, ~H, ~mapper, mapped, state, config, ResponseInvalid{}, entered, True{}) 703 704 def finish(~S: Data, ~K: Data, ~E: Data, ~H: Data, 705 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 706 config: K, completion: Completion 707 ) -> IO(Completion): 708 Completed{state, response, entered, mapped, stopped} = completion 709 finish.checked(~S, ~K, ~E, ~H, ~mapper, state, config, entered, mapped, stopped, validate(response)) 710 711 def policies.tail(~H: Data, policies: List<&2, Policy>) -> List<&2, Policy>: 712 match policies: 713 case Nil{}: 714 Nil{} 715 case root <> rest: 716 rest 717 718 719 def endpoint(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data, 720 ~run: A -> S -> K -> Match> -> Http.Req -> IO(S & Result<&2, &1, Error, Http.Res>), 721 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 722 action: A, state: S, +config: K, selected: Match>, meta: Metadata, entered: List<&2, Policy>, body: Http.Body 723 ) -> IO(Completion): 724 Metadata{method, target, headers} = meta 725 do IO>: 726 outcome : S & Result<&2, &1, Error, Http.Res> <- run(action, state, config, selected, Http.Req{method, target, headers, body}) 727 result(~S, ~K, ~E, ~H, ~mapper, config, entered, outcome) 728 729 def framework(~S: Data, ~K: Data, ~E: Data, ~H: Data, 730 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 731 +status: U32, allow: String, state: S, config: K, entered: List<&2, Policy>, body: Http.Body 732 ) -> IO(Completion): 733 match status: 734 case 204: 735 IO.pure(Completion, 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 739 def choice(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data, 740 ~before_hook: H -> S -> K -> Metadata -> IO(Step), 741 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error, Http.Res>), 742 ~run: A -> S -> K -> Match> -> Http.Req -> IO(S & Result<&2, &1, Error, Http.Res>), 743 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 744 selected: Router.Choice>>, target: Target.Target, 745 groups: List<&2, GroupPlan>>, state: S, +config: K, +meta: Metadata, entered: List<&2, Policy>, body: Http.Body 746 ) -> IO(Completion): 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, 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, 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>} 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 763 def 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 771 def resolved(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data, 772 ~before_hook: H -> S -> K -> Metadata -> IO(Step), 773 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error, Http.Res>), 774 ~run: A -> S -> K -> Match> -> Http.Req -> IO(S & Result<&2, &1, Error, Http.Res>), 775 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 776 outcome: Result<&2, &2, Router.Error, Router.Resolved>>>, 777 groups: List<&2, GroupPlan>>, state: S, config: K, meta: Metadata, entered: List<&2, Policy>, body: Http.Body 778 ) -> IO(Completion): 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 786 def dispatch(~A: Data, ~S: Data, ~K: Data, ~E: Data, ~H: Data, 787 ~before_hook: H -> S -> K -> Metadata -> IO(Step), 788 ~after_hook: H -> S -> K -> Http.Res -> IO(S & Result<&2, &1, Error, Http.Res>), 789 ~run: A -> S -> K -> Match> -> Http.Req -> IO(S & Result<&2, &1, Error, Http.Res>), 790 ~mapper: S -> K -> Error -> IO(S & Result<&2, &1, Error, Http.Res>), 791 app: Application>, state: S, req: Http.Req 792 ) -> IO(Completion): 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>: 797 completion : Completion <- 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>, table, method, path), groups, s, config, meta, entered, b)) 800 finish(~S, ~K, ~E, ~H, ~mapper, config, completion)