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 }