builder.rs (8830B)
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::{borrow::Cow, fmt}; 18 19 use base64::Engine; 20 use http::{ 21 HeaderMap, HeaderName, HeaderValue, StatusCode, 22 header::{self}, 23 }; 24 use http_body_util::BodyExt; 25 use http_body_util::{BodyDataStream, Full}; 26 use hyper::{Method, body::Bytes}; 27 use serde::{Serialize, de::DeserializeOwned}; 28 use tracing::{Level, trace}; 29 use url::Url; 30 31 use crate::{Client, ClientErr, Ctx, headers::HeaderParser, sse::SseClient}; 32 33 struct Builder { 34 headers: HeaderMap, 35 body: Full<Bytes>, 36 } 37 38 pub struct Req { 39 client: Client, 40 url: Url, 41 builder: Result<Builder, ClientErr>, 42 ctx: Ctx, 43 } 44 45 impl Req { 46 pub fn new( 47 client: &Client, 48 method: Method, 49 base_url: &Url, 50 path: impl Into<Cow<'static, str>>, 51 ) -> Self { 52 let path = path.into(); 53 let url = base_url.join(&path).unwrap(); 54 Self { 55 client: client.clone(), 56 url, 57 builder: Ok(Builder { 58 headers: HeaderMap::new(), 59 body: Full::default(), 60 }), 61 ctx: Ctx { 62 path, 63 method, 64 status: None, 65 }, 66 } 67 } 68 69 pub fn method(&self) -> &Method { 70 &self.ctx.method 71 } 72 73 pub fn url(&self) -> &Url { 74 &self.url 75 } 76 77 pub fn header<K, V>(mut self, key: K, value: V) -> Self 78 where 79 K: TryInto<HeaderName>, 80 <K as TryInto<HeaderName>>::Error: Into<http::Error>, 81 V: TryInto<HeaderValue>, 82 <V as TryInto<HeaderValue>>::Error: Into<http::Error>, 83 { 84 self.builder = self.builder.and_then(move |mut builder| { 85 let name = key.try_into().map_err(Into::into)?; 86 let value = value.try_into().map_err(Into::into)?; 87 builder.headers.insert(name, value); 88 Ok(builder) 89 }); 90 self 91 } 92 93 pub fn sensitive_header<K, V>(mut self, key: K, value: V) -> Self 94 where 95 K: TryInto<HeaderName>, 96 <K as TryInto<HeaderName>>::Error: Into<http::Error>, 97 V: TryInto<HeaderValue>, 98 <V as TryInto<HeaderValue>>::Error: Into<http::Error>, 99 { 100 self.builder = self.builder.and_then(move |mut builder| { 101 let name = key.try_into().map_err(Into::into)?; 102 let mut value = value.try_into().map_err(Into::into)?; 103 value.set_sensitive(true); 104 builder.headers.insert(name, value); 105 Ok(builder) 106 }); 107 self 108 } 109 110 pub fn query<T: Serialize>(mut self, name: &str, value: T) -> Self { 111 if self.builder.is_ok() { 112 let mut pairs = self.url.query_pairs_mut(); 113 let serializer = serde_urlencoded::Serializer::new(&mut pairs); 114 if let Err(e) = [(name, value)].serialize(serializer) { 115 drop(pairs); 116 self.builder = Err(e.into()); 117 return self; 118 } 119 } 120 121 self 122 } 123 124 pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self { 125 let mut buf = Vec::new(); 126 let serializer: &mut serde_json::Serializer<&mut Vec<u8>> = 127 &mut serde_json::Serializer::new(&mut buf); 128 if let Err(e) = serde_path_to_error::serialize(json, serializer).map_err(ClientErr::ReqJson) 129 { 130 self.builder = Err(e); 131 return self; 132 }; 133 if let Ok(builder) = &mut self.builder { 134 builder.headers.insert( 135 header::CONTENT_TYPE, 136 HeaderValue::from_static("application/json"), 137 ); 138 builder.body = Full::new(buf.into()) 139 } 140 self 141 } 142 143 pub fn basic_auth<U, P>(self, username: U, password: P) -> Req 144 where 145 U: fmt::Display, 146 P: fmt::Display, 147 { 148 let token = format!("{username}:{password}"); 149 let mut header = "Basic ".to_string(); 150 base64::engine::general_purpose::STANDARD.encode_string(token, &mut header); 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 }