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

Loopback.pudu

Pudu105 lines4.0 KB

GitHub ↗
1/** @Test.Support.Loopback.Module — servers on the loopback interface that suites talk to */2module Support.Loopback34import Std.Bytes as ByteSeq5import Std.Concurrent as Concurrent6import Std.Http.Server.Route as Route7import Std.Http.Server as Server8import Std.Net as Net9import Std.Result as Result10import Std.Sync as Sync11import Std.Text as Text1213/** @Test.Support.Loopback.Routed — a running server and the address it answers on */14export type Routed = { running: Server.Running, base: Str }1516/** @Test.Support.Loopback.Scripted — a raw server answering connections from a script */17export type Scripted = { listener: Net.Listener, base: Str, accepted: Sync.Counter }1819/// A server routing requests, running on its own thread until `stop`.20export fn serve(routes: &Array[Route.Route]) -> Routed {21  let server = Server.server(&Route.routing(routes))22  let running = match Server.start("127.0.0.1", 0) {23    case Ok(found) => found24    case Err(problem) => panic("loopback server did not start: " + show(problem))25  }26  let _served = Concurrent.start(fn() -> () { let _ran = Server.run(&server, &running, 0) })27  Routed{running: running, base: "http://127.0.0.1:" + show(Server.portOf(&running))}28}2930/// Stops a routed server.31export fn stop(routed: &Routed) -> () { let _stopped = Server.stop(&routed.running) }3233/// A raw server that answers each accepted connection with `script`, given the connection and its34/// number from 1; it accepts until `close`.35export fn scripted(script: fn(Net.Connection, Int) -> ()) -> Scripted {36  let listener = match Net.listenLocal(0) {37    case Ok(found) => found38    case Err(problem) => panic("loopback listener did not start: " + show(problem))39  }40  let port = Result.unwrapOr(Net.portOf(&listener), 0)41  let accepted = Sync.counter(0)42  let made = Scripted{listener: listener, base: "http://127.0.0.1:" + show(port), accepted: accepted}43  let _accepting = Concurrent.start(fn() -> () {44      var going = true45      while going {46        match Net.accept(&listener) {47          case Ok(connection) => {48            let number = Result.unwrapOr(Sync.increment(&accepted, 1), 0)49            let _handled = Concurrent.start(fn() -> () { script(connection, number) })50          }51          case Err(_) => { going = false }52        }53      }54    })55  made56}5758/// How many connections a raw server accepted.59export fn connections(server: &Scripted) -> Int { Result.unwrapOr(Sync.count(&server.accepted), 0) }6061/// Stops a raw server accepting.62export fn close(server: &Scripted) -> () { let _closed = Net.closeListener(&server.listener) }6364/// Reads one request head and its body, as text; empty text once the client closed.65export fn readRequest(connection: &Net.Connection) -> Str {66  var pending = ByteSeq.empty()67  loop {68    let end = pending.indexOf("\r\n\r\n".toBytes())69    if end >= 0 {70      let head = Result.unwrapOr(ByteSeq.toText(&pending.take(end + 4)), "")71      let length = lengthOf(head)72      while pending.length() < end + 4 + length {73        match Net.receiveWithin(connection, 65536, 5000) {74          case Ok(Some(piece)) => { pending = pending.concat(piece) }75          case _ => { return Result.unwrapOr(ByteSeq.toText(&pending), "") }76        }77      }78      return Result.unwrapOr(ByteSeq.toText(&pending.take(end + 4 + length)), "")79    }80    match Net.receiveWithin(connection, 65536, 5000) {81      case Ok(Some(piece)) => { pending = pending.concat(piece) }82      case _ => { return Result.unwrapOr(ByteSeq.toText(&pending), "") }83    }84  }85}8687/// Writes raw text to the connection.88export fn write(connection: &Net.Connection, text: Str) -> () { let _sent = Net.sendText(connection, text) }8990/// Closes a connection from the server's side.91export fn hangUp(connection: &Net.Connection) -> () { let _closed = Net.close(connection) }9293/// The body length a request head states.94fn lengthOf(head: Str) -> Int {95  for line in head.split("\r\n") {96    if line.toLower().startsWith("content-length:") {97      return match Text.stripPrefix(line.toLower(), "content-length:").trim().toInt() {98        case Some(found) => found99        case None => 0100      }101    }102  }103  0104}105