Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

Stub.pudu

Pudu72 lines3.3 KB

GitHub ↗
1/** @HttpClient.Stub.Module — a primary handler answering from rules, for tests */2module PuduLangHttpClient.Stub34import Std.Bytes as ByteSeq5import Std.Concurrent.Cancel as Cancel6import Std.Http as Http7import PuduLangHttpClient.Content as Content8import PuduLangHttpClient.Domain.Uri as Uri9import PuduLangHttpClient.Handler as Handler10import PuduLangHttpClient as HttpClient11import PuduLangHttpClient.Request as Request12import PuduLangHttpClient.Response as Response13import PuduLangHttpClient.Utils.Shared as Shared1415/** @HttpClient.Stub.Rule — which requests a rule answers, and how */16export type Rule = { matches: fn(Request.Request) -> Bool, answer: fn(Request.Request) -> HttpClient.Outcome[Response.Response] }1718/** @HttpClient.Stub.Stub — the rules, tried in the order added, and every request received */19export type Stub = { rules: Shared.Shared[Array[Rule]], received: Shared.Shared[Array[Request.Request]] }2021/// A stub with no rules: every request is answered `404`.22export fn create() -> Stub { Stub{rules: Shared.shared([]), received: Shared.shared([])} }2324/// Adds a rule answering the requests `matches` holds for.25export fn when(stub: &Stub, matches: fn(Request.Request) -> Bool, answer: fn(Request.Request) -> HttpClient.Outcome[Response.Response]) -> () {26  Shared.update(&stub.rules, |rules: Array[Rule]| rules.push(Rule{matches: matches, answer: answer}))27}2829/// Adds a rule answering requests with the method whose address has the path.30export fn on(stub: &Stub, method: Http.Method, path: Str, answer: fn(Request.Request) -> HttpClient.Outcome[Response.Response]) -> () {31  when(stub, |request: Request.Request| request.method == method && Uri.split(request.uri).path == path, answer)32}3334/// Adds a rule answering requests with the method and path with a status and a plain text body.35export fn respond(stub: &Stub, method: Http.Method, path: Str, code: Int, body: Str) -> () {36  on(stub, method, path, |request: Request.Request| Ok(Response.answer(&request, code).withText(body)))37}3839/// The stub as the primary handler of a pipeline; a request's receiver gets the body it is answered.40export fn send(stub: &Stub) -> Handler.Send {41  let held = *stub42  fn(request: Request.Request, token: Cancel.Token) -> HttpClient.Outcome[Response.Response] {43    if let Err(why) = Cancel.check(&token) {44      return match why {45        case Cancel.Requested(reason) => Err(HttpClient.Cancelled(reason))46        case Cancel.DeadlineExceeded => Err(HttpClient.TimedOut(0))47      }48    }49    Shared.update(&held.received, |held: Array[Request.Request]| held.push(request))50    for rule in Shared.current(&held.rules) {51      if (rule.matches)(request) { return delivered((rule.answer)(request)) }52    }53    Ok(Response.answer(&request, 404))54  }55}5657/// Every request received so far, in order.58export fn received(stub: &Stub) -> Array[Request.Request] { Shared.current(&stub.received) }5960/// The outcome, with a successful body handed to the request's receiver, when it has one, instead61/// of being kept.62fn delivered(outcome: HttpClient.Outcome[Response.Response]) -> HttpClient.Outcome[Response.Response] {63  let response = outcome ?64  match response.request.receiver {65    case Some(take) => {66      let _taken = take(response.content.payload)67      Ok(Response.Response{..response, content: Content.Content{..response.content, payload: ByteSeq.empty()}})68    }69    case None => Ok(response)70  }71}72