api.rs (6420B)
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, Response, StatusCode, Url, header}; 20 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 21 use taler_common::error::FmtSource; 22 use thiserror::Error; 23 use tracing::Level; 24 25 use crate::magnet_api::oauth::{Token, oauth}; 26 27 #[derive(Deserialize, Debug)] 28 struct Header { 29 #[serde(rename = "errorCode")] 30 pub error_code: Option<u16>, 31 } 32 33 #[derive(Deserialize, Debug)] 34 struct Empty {} 35 36 #[derive(Deserialize, Error, Debug)] 37 #[error("{error_code} {short_message} '{long_message}'")] 38 pub struct MagnetError { 39 #[serde(rename = "errorCode")] 40 pub error_code: u16, 41 #[serde(rename = "shortMessage")] 42 pub short_message: String, 43 #[serde(rename = "longMessage")] 44 pub long_message: String, 45 } 46 47 #[derive(Error, Debug)] 48 #[error("{method} {path} {kind}")] 49 pub struct ApiErr { 50 pub path: Cow<'static, str>, 51 pub method: Method, 52 pub kind: ErrKind, 53 } 54 55 #[derive(Error, Debug)] 56 pub enum ErrKind { 57 #[error("transport: {0}")] 58 Transport(FmtSource<reqwest::Error>), 59 #[error("magnet {0}")] 60 Magnet(#[from] MagnetError), 61 #[error("JSON body: {0}")] 62 Json(#[from] serde_path_to_error::Error<serde_json::Error>), 63 #[error("form body: {0}")] 64 Form(#[from] serde_urlencoded::de::Error), 65 #[error("status {0}")] 66 Status(StatusCode), 67 #[error("status {0} '{1}'")] 68 StatusCause(StatusCode, String), 69 } 70 71 impl From<reqwest::Error> for ErrKind { 72 fn from(value: reqwest::Error) -> Self { 73 Self::Transport(value.into()) 74 } 75 } 76 77 pub type ApiResult<R> = std::result::Result<R, ApiErr>; 78 79 /** Handle error from magnet API calls */ 80 async fn error_handling(res: reqwest::Result<Response>) -> Result<String, ErrKind> { 81 let res = res?; 82 let status = res.status(); 83 match status { 84 StatusCode::OK => Ok(res.text().await?), 85 StatusCode::BAD_REQUEST => Err(ErrKind::Status(status)), 86 StatusCode::FORBIDDEN => { 87 let cause = res 88 .headers() 89 .get(header::WWW_AUTHENTICATE) 90 .map(|s| s.to_str().unwrap_or_default()) 91 .unwrap_or_default(); 92 Err(ErrKind::StatusCause(status, cause.to_string())) 93 } 94 _ => { 95 if tracing::enabled!(Level::DEBUG) { 96 tracing::debug!("unexpected error: {:?}", &res); 97 let body = res.text().await; 98 tracing::debug!("unexpected error body: {:?}", body); 99 } 100 Err(ErrKind::Status(status)) 101 } 102 } 103 } 104 105 /** Parse JSON and track error path */ 106 fn parse<'de, T: Deserialize<'de>>(str: &'de str) -> Result<T, ErrKind> { 107 let deserializer = &mut serde_json::Deserializer::from_str(str); 108 serde_path_to_error::deserialize(deserializer).map_err(ErrKind::Json) 109 } 110 111 /** Parse magnet JSON response */ 112 async fn magnet_json<T: DeserializeOwned>(res: reqwest::Result<Response>) -> Result<T, ErrKind> { 113 let body = error_handling(res).await?; 114 let header: Header = parse(&body)?; 115 if header.error_code.unwrap_or(200) == 200 { 116 parse(&body) 117 } else { 118 Err(ErrKind::Magnet(parse(&body)?)) 119 } 120 } 121 122 /** Parse magnet URL encoded response into our own type */ 123 async fn magnet_url<T: DeserializeOwned>( 124 response: reqwest::Result<Response>, 125 ) -> Result<T, ErrKind> { 126 let body = error_handling(response).await?; 127 serde_urlencoded::from_str(&body).map_err(ErrKind::Form) 128 } 129 130 pub struct MagnetRequest<'a> { 131 path: Cow<'static, str>, 132 method: Method, 133 builder: RequestBuilder, 134 consumer: &'a Token, 135 access: Option<&'a Token>, 136 verifier: Option<&'a str>, 137 } 138 139 impl<'a> MagnetRequest<'a> { 140 pub fn new( 141 client: &Client, 142 method: Method, 143 base_url: &Url, 144 path: impl Into<Cow<'static, str>>, 145 consumer: &'a Token, 146 access: Option<&'a Token>, 147 verifier: Option<&'a str>, 148 ) -> Self { 149 let path = path.into(); 150 let url = base_url.join(&path).unwrap(); 151 let builder = client.request(method.clone(), url); 152 Self { 153 path, 154 method, 155 builder, 156 consumer, 157 access, 158 verifier, 159 } 160 } 161 162 pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self { 163 self.builder = self.builder.query(query); 164 self 165 } 166 167 pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self { 168 self.builder = self.builder.json(json); 169 self 170 } 171 172 pub async fn parse_url<T: DeserializeOwned>(self) -> ApiResult<T> { 173 let Self { 174 path, 175 builder, 176 method, 177 consumer, 178 access, 179 verifier, 180 } = self; 181 let (client, req) = builder.build_split(); 182 async { 183 let mut req = req?; 184 oauth(&mut req, consumer, access, verifier); 185 let res = client.execute(req).await; 186 magnet_url(res).await 187 } 188 .await 189 .map_err(|kind| ApiErr { path, method, kind }) 190 } 191 192 pub async fn parse_json<T: DeserializeOwned>(self) -> ApiResult<T> { 193 let Self { 194 path, 195 builder, 196 method, 197 consumer, 198 access, 199 verifier, 200 } = self; 201 let (client, req) = builder.build_split(); 202 async { 203 let mut req = req?; 204 oauth(&mut req, consumer, access, verifier); 205 let res = client.execute(req).await; 206 magnet_json(res).await 207 } 208 .await 209 .map_err(|kind| ApiErr { path, method, kind }) 210 } 211 212 pub async fn parse_empty(self) -> ApiResult<()> { 213 self.parse_json::<Empty>().await?; 214 Ok(()) 215 } 216 }