taler-rust

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

api.rs (4750B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025 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;
     18 
     19 use reqwest::{Client, Method, RequestBuilder, StatusCode, Url};
     20 use serde::{Deserialize, Serialize, de::DeserializeOwned};
     21 use taler_common::error::FmtSource;
     22 use thiserror::Error;
     23 
     24 use crate::cyclos_api::types::{
     25     ForbiddenError, InputError, NotFoundError, UnauthorizedError, UnexpectedError,
     26 };
     27 
     28 pub enum CyclosAuth {
     29     None,
     30     Basic { username: String, password: String },
     31 }
     32 
     33 #[derive(Error, Debug)]
     34 #[error("{method} {path} {kind}")]
     35 pub struct ApiErr {
     36     pub path: Cow<'static, str>,
     37     pub method: Method,
     38     pub kind: ErrKind,
     39 }
     40 
     41 #[derive(Error, Debug)]
     42 pub enum ErrKind {
     43     #[error("transport: {0}")]
     44     Transport(FmtSource<reqwest::Error>),
     45     #[error("JSON body: {0}")]
     46     Json(#[from] serde_path_to_error::Error<serde_json::Error>),
     47     #[error("unauthorized: {0}")]
     48     Unauthorized(#[from] UnauthorizedError),
     49     #[error("forbidden: {0}")]
     50     Forbidden(#[from] ForbiddenError),
     51     #[error("server: {0}")]
     52     Server(#[from] UnexpectedError),
     53     #[error("unknown: {0}")]
     54     Unknown(#[from] NotFoundError),
     55     #[error("input: {0}")]
     56     Input(#[from] InputError),
     57     #[error("status {0}")]
     58     UnexpectedStatus(StatusCode),
     59 }
     60 
     61 impl From<reqwest::Error> for ErrKind {
     62     fn from(value: reqwest::Error) -> Self {
     63         Self::Transport(value.into())
     64     }
     65 }
     66 
     67 pub type ApiResult<R> = std::result::Result<R, ApiErr>;
     68 
     69 /** Parse JSON and track error path */
     70 fn parse<'de, T: Deserialize<'de>>(str: &'de str) -> Result<T, ErrKind> {
     71     let deserializer = &mut serde_json::Deserializer::from_str(str);
     72     serde_path_to_error::deserialize(deserializer).map_err(ErrKind::Json)
     73 }
     74 
     75 async fn json_body<T: DeserializeOwned>(res: reqwest::Response) -> Result<T, ErrKind> {
     76     // TODO check content type?
     77     let body = res.text().await?;
     78     // println!("{body}");
     79     let parsed = parse(&body)?;
     80     Ok(parsed)
     81 }
     82 
     83 pub struct CyclosRequest<'a> {
     84     path: Cow<'static, str>,
     85     method: Method,
     86     builder: RequestBuilder,
     87     auth: &'a CyclosAuth,
     88 }
     89 
     90 impl<'a> CyclosRequest<'a> {
     91     pub fn new(
     92         client: &Client,
     93         method: Method,
     94         base_url: &Url,
     95         path: impl Into<Cow<'static, str>>,
     96         auth: &'a CyclosAuth,
     97     ) -> Self {
     98         let path = path.into();
     99         let url = base_url.join(&path).unwrap();
    100         let builder = client.request(method.clone(), url);
    101         Self {
    102             path,
    103             method,
    104             builder,
    105             auth,
    106         }
    107     }
    108 
    109     pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
    110         self.builder = self.builder.query(query);
    111         self
    112     }
    113 
    114     pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self {
    115         self.builder = self.builder.json(json);
    116         self
    117     }
    118 
    119     pub async fn parse_json<T: DeserializeOwned>(self) -> ApiResult<T> {
    120         let Self {
    121             path,
    122             builder,
    123             method,
    124             auth,
    125         } = self;
    126         let (client, req) = match auth {
    127             CyclosAuth::None => builder,
    128             CyclosAuth::Basic { username, password } => {
    129                 builder.basic_auth(username, Some(password))
    130             }
    131         }
    132         .build_split();
    133         async {
    134             let req = req?;
    135             let res = client.execute(req).await?;
    136             let status = res.status();
    137             match status {
    138                 StatusCode::OK | StatusCode::CREATED => json_body(res).await,
    139                 StatusCode::UNAUTHORIZED => Err(ErrKind::Unauthorized(json_body(res).await?)),
    140                 StatusCode::FORBIDDEN => Err(ErrKind::Forbidden(json_body(res).await?)),
    141                 StatusCode::NOT_FOUND => Err(ErrKind::Unknown(json_body(res).await?)),
    142                 StatusCode::UNPROCESSABLE_ENTITY => Err(ErrKind::Input(json_body(res).await?)),
    143                 StatusCode::INTERNAL_SERVER_ERROR => Err(ErrKind::Forbidden(json_body(res).await?)),
    144                 _ => Err(ErrKind::UnexpectedStatus(status)),
    145             }
    146         }
    147         .await
    148         .map_err(|kind| ApiErr { path, method, kind })
    149     }
    150 }