taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

builder.rs (9371B)


      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::{BodyExt, Full, Limited};
     27 use hyper::{Method, body::Bytes};
     28 use serde::{Serialize, de::DeserializeOwned};
     29 use serde_path_to_error::Track;
     30 use taler_common::encoding::base64;
     31 use tracing::{Level, trace};
     32 use url::Url;
     33 
     34 use crate::{Client, ClientErr, Ctx, headers::HeaderParser, sse::SseClient};
     35 
     36 pub struct Req {
     37     client: Client,
     38     url: Url,
     39     body: Bytes,
     40     headers: HeaderMap,
     41     err: Option<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             headers: HeaderMap::new(),
     58             body: Bytes::default(),
     59             err: None,
     60             ctx: Ctx {
     61                 path,
     62                 method,
     63                 status: None,
     64             },
     65         }
     66     }
     67 
     68     pub fn method(&self) -> &Method {
     69         &self.ctx.method
     70     }
     71 
     72     pub fn url(&self) -> &Url {
     73         &self.url
     74     }
     75 
     76     pub fn headers(&self) -> &HeaderMap {
     77         &self.headers
     78     }
     79 
     80     pub fn body(&self) -> &Bytes {
     81         &self.body
     82     }
     83 
     84     pub fn header<K, V>(mut self, key: K, value: V) -> Self
     85     where
     86         K: TryInto<HeaderName>,
     87         <K as TryInto<HeaderName>>::Error: Into<http::Error>,
     88         V: TryInto<HeaderValue>,
     89         <V as TryInto<HeaderValue>>::Error: Into<http::Error>,
     90     {
     91         let res = (|| {
     92             let name = key.try_into().map_err(Into::into)?;
     93             let value = value.try_into().map_err(Into::into)?;
     94             self.headers.insert(name, value);
     95             Ok(())
     96         })();
     97         if let Err(e) = res
     98             && self.err.is_none()
     99         {
    100             self.err = Some(e);
    101         }
    102         self
    103     }
    104 
    105     pub fn sensitive_header<K, V>(mut self, key: K, value: V) -> Self
    106     where
    107         K: TryInto<HeaderName>,
    108         <K as TryInto<HeaderName>>::Error: Into<http::Error>,
    109         V: TryInto<HeaderValue>,
    110         <V as TryInto<HeaderValue>>::Error: Into<http::Error>,
    111     {
    112         let res = (|| {
    113             let name = key.try_into().map_err(Into::into)?;
    114             let mut value = value.try_into().map_err(Into::into)?;
    115             value.set_sensitive(true);
    116             self.headers.insert(name, value);
    117             Ok(())
    118         })();
    119         if let Err(e) = res
    120             && self.err.is_none()
    121         {
    122             self.err = Some(e);
    123         }
    124         self
    125     }
    126 
    127     pub fn query<T: Serialize>(mut self, name: &str, value: T) -> Self {
    128         let mut pairs = self.url.query_pairs_mut();
    129         let serializer = serde_urlencoded::Serializer::new(&mut pairs);
    130         if let Err(e) = [(name, value)].serialize(serializer)
    131             && self.err.is_none()
    132         {
    133             self.err = Some(e.into());
    134         }
    135         drop(pairs);
    136         self
    137     }
    138 
    139     pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self {
    140         let mut buf = Vec::new();
    141         let serializer: &mut serde_json::Serializer<&mut Vec<u8>> =
    142             &mut serde_json::Serializer::new(&mut buf);
    143         if let Err(e) = serde_path_to_error::serialize(json, serializer).map_err(ClientErr::ReqJson)
    144             && self.err.is_none()
    145         {
    146             self.err = Some(e);
    147             return self;
    148         };
    149         self.content(buf.into(), HeaderValue::from_static("application/json"))
    150     }
    151 
    152     pub fn content(mut self, body: Bytes, ty: HeaderValue) -> Self {
    153         self.headers.insert(header::CONTENT_TYPE, ty);
    154         self.body = body;
    155         self
    156     }
    157 
    158     pub fn basic_auth<U, P>(self, username: U, password: P) -> Req
    159     where
    160         U: fmt::Display,
    161         P: fmt::Display,
    162     {
    163         let header = format!("Basic {}", base64::fmt(format!("{username}:{password}")),);
    164         self.sensitive_header(header::AUTHORIZATION, header)
    165     }
    166 
    167     pub fn bearer_auth<T>(self, token: T) -> Req
    168     where
    169         T: fmt::Display,
    170     {
    171         let header = format!("Bearer {token}");
    172         self.sensitive_header(header::AUTHORIZATION, header)
    173     }
    174 
    175     pub fn req_sse(mut self, client: &SseClient) -> Req {
    176         self = self
    177             .header(
    178                 header::ACCEPT,
    179                 HeaderValue::from_static("text/event-stream"),
    180             )
    181             .header(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
    182         if let Some(id) = &client.last_event_id() {
    183             self = self.header(
    184                 HeaderName::from_static("last-event-id"),
    185                 HeaderValue::from_str(id).unwrap(),
    186             );
    187         }
    188         self
    189     }
    190 
    191     pub async fn send(self) -> Result<(Ctx, Res), (Ctx, ClientErr)> {
    192         let Self {
    193             client,
    194             ctx,
    195             url,
    196             err,
    197             headers,
    198             body,
    199         } = self;
    200         if let Some(e) = err {
    201             return Err((ctx, e));
    202         }
    203         let req = match async {
    204             let mut builder = http::request::Request::builder()
    205                 .uri(url.as_str())
    206                 .method(ctx.method.clone());
    207             if let Some(headers_mut) = builder.headers_mut() {
    208                 *headers_mut = headers;
    209             }
    210             let req = builder.body(Full::new(body))?;
    211             Ok(req)
    212         }
    213         .await
    214         {
    215             Ok(it) => it,
    216             Err(e) => return Err((ctx, e)),
    217         };
    218         match client.request(req).await {
    219             Ok(res) => {
    220                 let (head, body) = res.into_parts();
    221                 Ok((ctx, Res { head, body }))
    222             }
    223             Err(e) => Err((ctx, ClientErr::ReqTransport(e.into()))),
    224         }
    225     }
    226 }
    227 
    228 pub struct Res {
    229     head: http::response::Parts,
    230     body: hyper::body::Incoming,
    231 }
    232 
    233 impl Res {
    234     pub fn status(&self) -> StatusCode {
    235         self.head.status
    236     }
    237 
    238     pub fn headers(&self) -> &HeaderMap {
    239         &self.head.headers
    240     }
    241 
    242     pub fn sse(self, client: &mut SseClient) -> Result<bool, ClientErr> {
    243         match self.status() {
    244             StatusCode::OK => {}
    245             StatusCode::NO_CONTENT => return Ok(false),
    246             status => return Err(ClientErr::Sse(format!("expected HTTP 200, got {status}"))),
    247         }
    248         let content_type = self.str_header(header::CONTENT_TYPE)?;
    249         if !content_type.eq_ignore_ascii_case("text/event-stream") {
    250             return Err(ClientErr::Sse(format!(
    251                 "expected text/event-stream, got {content_type}"
    252             )));
    253         }
    254         client.connect(self.body);
    255         Ok(true)
    256     }
    257 
    258     async fn full_body(self) -> Result<Bytes, ClientErr> {
    259         // Max 1 mb
    260         Limited::new(self.body, 1024 * 1024)
    261             .collect()
    262             .await
    263             .map(|it| it.to_bytes())
    264             .map_err(|e| ClientErr::ResTransport(e.to_string()))
    265     }
    266 
    267     /** Parse request body into a JSON type */
    268     pub async fn json<T: DeserializeOwned>(self) -> Result<T, ClientErr> {
    269         // TODO check content type?
    270         let body = self.full_body().await?;
    271         if tracing::enabled!(Level::TRACE) {
    272             let str = std::string::String::from_utf8_lossy(&body);
    273             trace!(target: "http", "JSON body: {str}");
    274         }
    275         let mut de = serde_json::Deserializer::from_slice(&body);
    276         let parsed = serde_path_to_error::deserialize(&mut de).map_err(ClientErr::ResJson)?;
    277         de.end().map_err(|e| {
    278             ClientErr::ResJson(serde_path_to_error::Error::new(Track::new().path(), e))
    279         })?;
    280         Ok(parsed)
    281     }
    282 
    283     /** Parse request body into a URL encoded type */
    284     pub async fn urlencoded<T: DeserializeOwned>(self) -> Result<T, ClientErr> {
    285         // TODO check content type?
    286         let body = self.full_body().await?;
    287         let parsed = serde_urlencoded::from_bytes(&body).map_err(ClientErr::Form)?;
    288         Ok(parsed)
    289     }
    290 
    291     /** Parse request body into as text */
    292     pub async fn text(self) -> Result<String, ClientErr> {
    293         let body = self.full_body().await?;
    294         let parsed =
    295             String::from_utf8(body.to_vec()).map_err(|e| ClientErr::Text(e.utf8_error()))?;
    296         Ok(parsed)
    297     }
    298 }
    299 
    300 impl HeaderParser<ClientErr> for Res {
    301     fn parse<T>(
    302         &self,
    303         name: impl Into<HeaderName>,
    304         kind: &'static str,
    305         transform: impl FnOnce(&str) -> Result<T, ()>,
    306     ) -> Result<T, ClientErr> {
    307         self.head
    308             .headers
    309             .parse(name, kind, transform)
    310             .map_err(ClientErr::Headers)
    311     }
    312 }