builder.rs (8749B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{ 18 borrow::Cow, 19 fmt::{self}, 20 }; 21 22 use http::{ 23 HeaderMap, HeaderName, HeaderValue, StatusCode, 24 header::{self}, 25 }; 26 use http_body_util::{BodyDataStream, BodyExt, Full}; 27 use hyper::{Method, body::Bytes}; 28 use serde::{Serialize, de::DeserializeOwned}; 29 use taler_common::encoding::base64; 30 use tracing::{Level, trace}; 31 use url::Url; 32 33 use crate::{Client, ClientErr, Ctx, headers::HeaderParser, sse::SseClient}; 34 35 struct Builder { 36 headers: HeaderMap, 37 body: Full<Bytes>, 38 } 39 40 pub struct Req { 41 client: Client, 42 url: Url, 43 builder: Result<Builder, ClientErr>, 44 ctx: Ctx, 45 } 46 47 impl Req { 48 pub fn new( 49 client: &Client, 50 method: Method, 51 base_url: &Url, 52 path: impl Into<Cow<'static, str>>, 53 ) -> Self { 54 let path = path.into(); 55 let url = base_url.join(&path).unwrap(); 56 Self { 57 client: client.clone(), 58 url, 59 builder: Ok(Builder { 60 headers: HeaderMap::new(), 61 body: Full::default(), 62 }), 63 ctx: Ctx { 64 path, 65 method, 66 status: None, 67 }, 68 } 69 } 70 71 pub fn method(&self) -> &Method { 72 &self.ctx.method 73 } 74 75 pub fn url(&self) -> &Url { 76 &self.url 77 } 78 79 pub fn header<K, V>(mut self, key: K, value: V) -> Self 80 where 81 K: TryInto<HeaderName>, 82 <K as TryInto<HeaderName>>::Error: Into<http::Error>, 83 V: TryInto<HeaderValue>, 84 <V as TryInto<HeaderValue>>::Error: Into<http::Error>, 85 { 86 self.builder = self.builder.and_then(move |mut builder| { 87 let name = key.try_into().map_err(Into::into)?; 88 let value = value.try_into().map_err(Into::into)?; 89 builder.headers.insert(name, value); 90 Ok(builder) 91 }); 92 self 93 } 94 95 pub fn sensitive_header<K, V>(mut self, key: K, value: V) -> Self 96 where 97 K: TryInto<HeaderName>, 98 <K as TryInto<HeaderName>>::Error: Into<http::Error>, 99 V: TryInto<HeaderValue>, 100 <V as TryInto<HeaderValue>>::Error: Into<http::Error>, 101 { 102 self.builder = self.builder.and_then(move |mut builder| { 103 let name = key.try_into().map_err(Into::into)?; 104 let mut value = value.try_into().map_err(Into::into)?; 105 value.set_sensitive(true); 106 builder.headers.insert(name, value); 107 Ok(builder) 108 }); 109 self 110 } 111 112 pub fn query<T: Serialize>(mut self, name: &str, value: T) -> Self { 113 if self.builder.is_ok() { 114 let mut pairs = self.url.query_pairs_mut(); 115 let serializer = serde_urlencoded::Serializer::new(&mut pairs); 116 if let Err(e) = [(name, value)].serialize(serializer) { 117 drop(pairs); 118 self.builder = Err(e.into()); 119 return self; 120 } 121 } 122 123 self 124 } 125 126 pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self { 127 let mut buf = Vec::new(); 128 let serializer: &mut serde_json::Serializer<&mut Vec<u8>> = 129 &mut serde_json::Serializer::new(&mut buf); 130 if let Err(e) = serde_path_to_error::serialize(json, serializer).map_err(ClientErr::ReqJson) 131 { 132 self.builder = Err(e); 133 return self; 134 }; 135 if let Ok(builder) = &mut self.builder { 136 builder.headers.insert( 137 header::CONTENT_TYPE, 138 HeaderValue::from_static("application/json"), 139 ); 140 builder.body = Full::new(buf.into()) 141 } 142 self 143 } 144 145 pub fn basic_auth<U, P>(self, username: U, password: P) -> Req 146 where 147 U: fmt::Display, 148 P: fmt::Display, 149 { 150 let header = format!("Basic {}", base64::fmt(format!("{username}:{password}")),); 151 self.sensitive_header(header::AUTHORIZATION, header) 152 } 153 154 pub fn bearer_auth<T>(self, token: T) -> Req 155 where 156 T: fmt::Display, 157 { 158 let header = format!("Bearer {token}"); 159 self.sensitive_header(header::AUTHORIZATION, header) 160 } 161 162 pub fn req_sse(mut self, client: &SseClient) -> Req { 163 self = self 164 .header( 165 header::ACCEPT, 166 HeaderValue::from_static("text/event-stream"), 167 ) 168 .header(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")); 169 if let Some(id) = &client.last_event_id { 170 self = self.header( 171 HeaderName::from_static("last-event-id"), 172 HeaderValue::from_str(id).unwrap(), 173 ); 174 } 175 self 176 } 177 178 pub async fn send(self) -> Result<(Ctx, Res), (Ctx, ClientErr)> { 179 let Self { 180 client, 181 ctx, 182 builder, 183 url, 184 } = self; 185 let req = match async { 186 let Builder { headers, body } = builder?; 187 let mut builder = http::request::Request::builder() 188 .uri(url.as_str()) 189 .method(ctx.method.clone()); 190 if let Some(headers_mut) = builder.headers_mut() { 191 *headers_mut = headers; 192 } 193 let req = builder.body(body)?; 194 Ok(req) 195 } 196 .await 197 { 198 Ok(it) => it, 199 Err(e) => return Err((ctx, e)), 200 }; 201 match client.request(req).await { 202 Ok(res) => { 203 let (head, body) = res.into_parts(); 204 Ok((ctx, Res { head, body })) 205 } 206 Err(e) => Err((ctx, ClientErr::ReqTransport(e.into()))), 207 } 208 } 209 } 210 211 pub struct Res { 212 head: http::response::Parts, 213 body: hyper::body::Incoming, 214 } 215 216 impl Res { 217 pub fn status(&self) -> StatusCode { 218 self.head.status 219 } 220 221 pub fn str_header(&self, name: &'static str) -> Result<String, ClientErr> { 222 self.head 223 .headers 224 .str_header(name) 225 .map_err(ClientErr::Headers) 226 } 227 228 pub fn int_header(&self, name: &'static str) -> Result<u64, ClientErr> { 229 self.head 230 .headers 231 .int_header(name) 232 .map_err(ClientErr::Headers) 233 } 234 235 pub fn bool_header(&self, name: &'static str) -> Result<bool, ClientErr> { 236 self.head 237 .headers 238 .bool_header(name) 239 .map_err(ClientErr::Headers) 240 } 241 242 pub fn sse(self, client: &mut SseClient) -> Result<(), ClientErr> { 243 // TODO check content type? 244 // TODO check status 245 client.connect(BodyDataStream::new(self.body)); 246 Ok(()) 247 } 248 249 async fn full_body(self) -> Result<Bytes, ClientErr> { 250 // TODO body size limit ? 251 self.body 252 .collect() 253 .await 254 .map(|it| it.to_bytes()) 255 .map_err(|e| ClientErr::ResTransport(e.into())) 256 } 257 258 /** Parse request body into a JSON type */ 259 pub async fn json<T: DeserializeOwned>(self) -> Result<T, ClientErr> { 260 // TODO check content type? 261 let body = self.full_body().await?; 262 if tracing::enabled!(Level::TRACE) { 263 let str = std::string::String::from_utf8_lossy(&body); 264 trace!(target: "http", "JSON body: {str}"); 265 } 266 let deserializer = &mut serde_json::Deserializer::from_slice(&body); 267 let parsed = serde_path_to_error::deserialize(deserializer).map_err(ClientErr::ResJson)?; 268 Ok(parsed) 269 } 270 271 /** Parse request body into a URL encoded type */ 272 pub async fn urlencoded<T: DeserializeOwned>(self) -> Result<T, ClientErr> { 273 // TODO check content type? 274 let body = self.full_body().await?; 275 let parsed = serde_urlencoded::from_bytes(&body).map_err(ClientErr::Form)?; 276 Ok(parsed) 277 } 278 279 /** Parse request body into as text */ 280 pub async fn text(self) -> Result<String, ClientErr> { 281 let body = self.full_body().await?; 282 let parsed = 283 String::from_utf8(body.to_vec()).map_err(|e| ClientErr::Text(e.utf8_error()))?; 284 Ok(parsed) 285 } 286 }