
Server.pudu
Pudu164 lines7.9 KB
1/** @Test.Support.Placeholder.Server.Module — the placeholder REST service on the loopback interface */2module Support.Placeholder.Server34import Std.Concurrent as Concurrent5import Std.Http as Http6import Std.Http.Server.Reply as Reply7import Std.Http.Server.Route as Route8import Std.Http.Server as Server9import Std.Json as Json10import Std.Option as Option11import Std.Result as Result12import Std.Sync as Sync13import Support.Placeholder.Data as Data14import Support.Placeholder.Store as Store1516/** @Test.Support.Placeholder.Server.Placeholder — the running service, its records, and its faults */17export type Placeholder = { running: Server.Running, base: Str, store: Store.Store, faults: Sync.Cell[Int], lock: Sync.Mutex, handled: Sync.Counter }181920const CONTROLS: Set[Str] = #{"_start", "_limit", "_sort", "_order", "_delay"}212223export fn start() -> Placeholder {24 let running = match Server.start("127.0.0.1", 0) {25 case Ok(found) => found26 case Err(problem) => panic("the placeholder service did not start: " + show(problem))27 }28 let made = Placeholder{running: running, base: "http://127.0.0.1:" + show(Server.portOf(&running)), store: Store.seeded(), faults: Sync.cell(0), lock: Sync.mutex(), handled: Sync.counter(0)}29 let server = Server.Server{..Server.server(&Route.routing(&routes(&made))), workers: 32, queueCapacity: 256}30 let _serving = Concurrent.start(fn() -> () { let _ran = Server.run(&server, &running, 0) })31 made32}333435export fn stop(service: &Placeholder) -> () { let _stopped = Server.stop(&service.running) }363738export fn failNext(service: &Placeholder, count: Int) -> () { let _set = Sync.set(&service.faults, count) }394041export fn handled(service: &Placeholder) -> Int { Result.unwrapOr(Sync.count(&service.handled), 0) }424344fn routes(service: &Placeholder) -> Array[Route.Route] {45 var made: Array[Route.Route] = []46 for resource in Data.RESOURCES {47 let collection = "/" + resource48 let member = collection + "/:id"49 made = made50 .push(Route.get(collection, guarded(service, |request: Route.Request| listing(service, resource, &request, []))))51 .push(Route.get(member, guarded(service, |request: Route.Request| reading(service, resource, &request))))52 .push(Route.post(collection, guarded(service, |request: Route.Request| creating(service, resource, &request))))53 .push(Route.put(member, guarded(service, |request: Route.Request| writing(service, resource, &request, false))))54 .push(Route.patch(member, guarded(service, |request: Route.Request| writing(service, resource, &request, true))))55 .push(Route.delete(member, guarded(service, |request: Route.Request| deleting(service, resource, &request))))56 }57 for relation in Data.RELATIONS {58 made = made.push(Route.get("/" + relation.parent + "/:id/" + relation.child, guarded(service, fn(request: Route.Request) -> Http.Response {59 match idParam(&request) {60 case Some(id) => if Store.find(&service.store, relation.parent, id) == None { notFound() } else { listing(service, relation.child, &request, [(relation.key, show(id))]) }61 case None => notFound()62 }63 })))64 }65 made66}67686970fn guarded(service: &Placeholder, handle: Route.Handler) -> Route.Handler {71 let held = *service72 fn(request: Route.Request) -> Http.Response {73 let _counted = Sync.increment(&held.handled, 1)74 let faulted = Result.unwrapOr(Sync.withLock(&held.lock, fn() -> Bool {75 let left = Result.unwrapOr(Sync.get(&held.faults), 0)76 if left > 0 { let _taken = Sync.set(&held.faults, left - 1) }77 left > 078 }), false)79 if faulted { return Http.Response{..json(503, &Json.object(&[("error", Json.Text("unavailable"))])), headers: [("content-type", "application/json; charset=utf-8"), ("retry-after", "0")]} }80 if let Some(millis) = Option.andThen(request.query.get("_delay"), |text: Str| text.toInt()) { let _waited = Concurrent.sleep(millis) }81 handle(request)82 }83}848586fn listing(service: &Placeholder, resource: Str, request: &Route.Request, extra: Array[(Str, Str)]) -> Http.Response {87 let filters = request.query.entries().filter(|pair: (Str, Str)| !(pair[0] in CONTROLS)).concat(extra)88 let query = Store.Query {89 filters: filters,90 start: Option.unwrapOr(Option.andThen(request.query.get("_start"), |text: Str| text.toInt()), 0),91 limit: Option.andThen(request.query.get("_limit"), |text: Str| text.toInt()),92 sort: request.query.get("_sort"),93 descending: request.query.get("_order") == Some("desc")94 }95 let (page, total) = Store.list(&service.store, resource, &query)96 Http.Response{..json(200, &Json.list(&page)), headers: [("content-type", "application/json; charset=utf-8"), ("x-total-count", show(total))]}97}9899100fn reading(service: &Placeholder, resource: Str, request: &Route.Request) -> Http.Response {101 match Option.andThen(idParam(request), |id: Int| Store.find(&service.store, resource, id)) {102 case Some(record) => json(200, &record)103 case None => notFound()104 }105}106107108109fn creating(service: &Placeholder, resource: Str, request: &Route.Request) -> Http.Response {110 match objectBody(request) {111 case Ok(fields) => {112 let made = Store.create(&service.store, resource, &fields)113 Http.Response{..json(201, &made), headers: [("content-type", "application/json; charset=utf-8"), ("location", service.base + "/" + resource + "/" + show(Store.idOf(&made)))]}114 }115 case Err(response) => response116 }117}118119120fn writing(service: &Placeholder, resource: Str, request: &Route.Request, merging: Bool) -> Http.Response {121 let id = match idParam(request) {122 case Some(found) => found123 case None => { return notFound() }124 }125 match objectBody(request) {126 case Ok(fields) => {127 let written = if merging { Store.patch(&service.store, resource, id, &fields) } else { Store.replace(&service.store, resource, id, &fields) }128 match written {129 case Some(record) => json(200, &record)130 case None => notFound()131 }132 }133 case Err(response) => response134 }135}136137138fn deleting(service: &Placeholder, resource: Str, request: &Route.Request) -> Http.Response {139 match idParam(request) {140 case Some(id) => if Store.remove(&service.store, resource, id) { json(200, &Json.object(&[])) } else { notFound() }141 case None => notFound()142 }143}144145146fn objectBody(request: &Route.Request) -> Result[Json.Json, Http.Response] {147 match Json.decode(Route.body(request)) {148 case Ok(value) => if Json.asObject(&value) == None { Err(badRequest("the body must be a JSON object")) } else { Ok(value) }149 case Err(problem) => Err(badRequest(Json.explain(&problem)))150 }151}152153154fn idParam(request: &Route.Request) -> Option[Int] { Option.andThen(Route.param(request, "id"), |text: Str| text.toInt()) }155156157fn json(code: Int, value: &Json.Json) -> Http.Response { Reply.json(code, Json.encode(value)) }158159160fn notFound() -> Http.Response { json(404, &Json.object(&[])) }161162163fn badRequest(reason: Str) -> Http.Response { json(400, &Json.object(&[("error", Json.Text(reason))])) }164