
Client.pudu
Pudu198 lines9.5 KB
1/** @HttpClient.Client.Module — a configured client sending requests through its pipeline */2module PuduLangHttpClient.Client34import Std.Concurrent.Cancel as Cancel5import Std.Http as Http6import Std.Option as Option7import PuduLangHttpClient.Constants.Defaults as Defaults8import PuduLangHttpClient.Constants.Messages as Messages9import PuduLangHttpClient.Content as Content10import PuduLangHttpClient.Domain.Headers as Headers11import PuduLangHttpClient.Domain.Uri as Uri12import PuduLangHttpClient.Domain.Version as Version13import PuduLangHttpClient.Handler as Handler14import PuduLangHttpClient as HttpClient15import PuduLangHttpClient.Options as RequestOptions16import PuduLangHttpClient.Request as Request17import PuduLangHttpClient.Response as Response18import PuduLangHttpClient.Transport as Transport19import PuduLangHttpClient.Utils.Shared as Shared20import PuduLangHttpClient.Utils.Template as Template21import PuduLangHttpClient.Utils.Tokens as Tokens2223/** @HttpClient.Client.Client — a pipeline and the defaults every request through it gets */24export type Client = {25 name: Str,26 send: Handler.Send,27 baseAddress: Option[Str],28 defaultHeaders: Array[(Str, Str)],29 timeout: Int,30 maxResponseContentBufferSize: Int,31 defaultVersion: Http.Version,32 defaultVersionPolicy: Version.Policy,33 pending: Shared.Shared[Cancel.Token]34}353637const PENDING_CANCELLED: Str = "the client cancelled its pending requests"38394041export fn create(sender: Handler.Send) -> Client {42 Client {43 name: Defaults.DEFAULT_NAME,44 send: sender,45 baseAddress: None,46 defaultHeaders: [],47 timeout: Defaults.CLIENT_TIMEOUT,48 maxResponseContentBufferSize: Defaults.MAX_RESPONSE_BUFFER,49 defaultVersion: Http.Http11,50 defaultVersionPolicy: Version.OrLower,51 pending: Shared.shared(Cancel.token())52 }53}545556export fn over(transport: &Transport.Transport) -> Client { create(Transport.send(transport)) }5758596061export fn pooled(options: Transport.Options) -> Client { over(&Transport.create(options)) }626364export fn validate(client: &Client) -> Array[Str] {65 var problems: Array[Str] = []66 if let Some(base) = client.baseAddress {67 if Option.isNone(&Uri.endpointOf(base)) { problems = problems.push("baseAddress must be an absolute http or https address: " + base) }68 }69 if client.timeout < 1 && client.timeout != Defaults.INFINITE { problems = problems.push("timeout must be positive or INFINITE") }70 if client.maxResponseContentBufferSize < 0 { problems = problems.push("maxResponseContentBufferSize must be at least 0") }71 if let Some(name) = Headers.unwritable(&client.defaultHeaders) { problems = problems.push(Template.fill(Messages.BAD_HEADER, &[name])) }72 problems73}7475/** @HttpClient.Client.Configuring — a client changed one decision at a time */76export trait Configuring {77 78 fn withBaseAddress(self: &Self, address: Str) -> Self79 80 fn withDefaultHeader(self: &Self, name: Str, value: Str) -> Self81 82 fn withTimeout(self: &Self, millis: Int) -> Self83 84 fn withMaxResponseContentBufferSize(self: &Self, bytes: Int) -> Self85 86 fn withDefaultVersion(self: &Self, version: Http.Version, policy: Version.Policy) -> Self87 88 fn named(self: &Self, name: Str) -> Self89}9091impl Configuring for Client {92 93 fn withBaseAddress(self: &Self, address: Str) -> Self { Client{..*self, baseAddress: Some(address)} }9495 96 fn withDefaultHeader(self: &Self, name: Str, value: Str) -> Self { Client{..*self, defaultHeaders: Headers.add(&self.defaultHeaders, name, value)} }9798 99 fn withTimeout(self: &Self, millis: Int) -> Self { Client{..*self, timeout: millis} }100101 102 fn withMaxResponseContentBufferSize(self: &Self, bytes: Int) -> Self { Client{..*self, maxResponseContentBufferSize: bytes} }103104 105 fn withDefaultVersion(self: &Self, version: Http.Version, policy: Version.Policy) -> Self { Client{..*self, defaultVersion: version, defaultVersionPolicy: policy} }106107 108 fn named(self: &Self, name: Str) -> Self { Client{..*self, name: name} }109}110111112export fn send(client: &Client, request: Request.Request) -> HttpClient.Outcome[Response.Response] {113 sendWith(client, request, Cancel.token())114}115116117118119export fn sendWith(client: &Client, request: Request.Request, token: Cancel.Token) -> HttpClient.Outcome[Response.Response] {120 let uri = resolved(client, request.uri) ?121 let prepared = Request.Request {122 ..request,123 uri: uri,124 headers: Headers.merged(&client.defaultHeaders, &request.headers),125 options: RequestOptions.set(&request.options, &Transport.bufferLimit(), client.maxResponseContentBufferSize)126 }127 let linked = Tokens.linked(&[token, Shared.current(&client.pending)], client.timeout)128 match (client.send)(prepared, linked) {129 case Err(HttpClient.TimedOut(elapsed)) => {130 if client.timeout != Defaults.INFINITE && !Cancel.stopped(&token) { Err(HttpClient.TimedOut(client.timeout)) } else { Err(HttpClient.TimedOut(elapsed)) }131 }132 case other => other133 }134}135136137export fn cancelPending(client: &Client) -> () {138 let previous = Shared.change(&client.pending, |current: Cancel.Token| (Cancel.token(), current))139 let _cancelled = Cancel.cancel(&previous, PENDING_CANCELLED)140}141142143export fn build(client: &Client, method: Http.Method, uri: Str) -> Request.Request {144 Request.Request{..Request.create(method, uri), version: client.defaultVersion, versionPolicy: client.defaultVersionPolicy}145}146147148export fn get(client: &Client, uri: Str) -> HttpClient.Outcome[Response.Response] { send(client, build(client, Http.Get, uri)) }149150151export fn getText(client: &Client, uri: Str) -> HttpClient.Outcome[Str] {152 let response = Response.ensureSuccess(get(client, uri) ?) ?153 Response.text(&response)154}155156157export fn getBytes(client: &Client, uri: Str) -> HttpClient.Outcome[Bytes] {158 let response = Response.ensureSuccess(get(client, uri) ?) ?159 Ok(Response.bytes(&response))160}161162163164export fn stream(client: &Client, uri: Str, receiver: Request.Receiver) -> HttpClient.Outcome[Response.Response] {165 Response.ensureSuccess(send(client, Request.Request{..build(client, Http.Get, uri), receiver: Some(receiver)}) ?)166}167168169export fn head(client: &Client, uri: Str) -> HttpClient.Outcome[Response.Response] { send(client, build(client, Http.Head, uri)) }170171172export fn post(client: &Client, uri: Str, content: Content.Content) -> HttpClient.Outcome[Response.Response] {173 send(client, Request.Request{..build(client, Http.Post, uri), content: Some(content)})174}175176177export fn put(client: &Client, uri: Str, content: Content.Content) -> HttpClient.Outcome[Response.Response] {178 send(client, Request.Request{..build(client, Http.Put, uri), content: Some(content)})179}180181182export fn patch(client: &Client, uri: Str, content: Content.Content) -> HttpClient.Outcome[Response.Response] {183 send(client, Request.Request{..build(client, Http.Patch, uri), content: Some(content)})184}185186187export fn delete(client: &Client, uri: Str) -> HttpClient.Outcome[Response.Response] { send(client, build(client, Http.Delete, uri)) }188189190fn resolved(client: &Client, uri: Str) -> HttpClient.Outcome[Str] {191 let escaped = Uri.escape(uri)192 if Uri.isAbsolute(escaped) { return Ok(escaped) }193 match client.baseAddress {194 case Some(base) => Ok(Uri.resolve(base, escaped))195 case None => HttpClient.invalid(Template.fill(Messages.RELATIVE_WITHOUT_BASE, &[uri]))196 }197}198