api.rs (4842B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025, 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; 18 19 use http_client::{ 20 ApiErr, Client, ClientErr, Ctx, 21 builder::{Req, Res}, 22 headers::HeaderParser as _, 23 sse::SseClient, 24 }; 25 use hyper::{ 26 Method, StatusCode, 27 header::{HeaderName, HeaderValue}, 28 }; 29 use serde::{Serialize, de::DeserializeOwned}; 30 use thiserror::Error; 31 use url::Url; 32 33 use crate::cyclos_api::types::{ 34 ForbiddenError, InputError, NotFoundError, Pagination, UnauthorizedError, UnexpectedError, 35 }; 36 37 #[derive(Debug)] 38 pub enum CyclosAuth { 39 None, 40 Basic { username: String, password: String }, 41 } 42 43 #[derive(Error, Debug)] 44 pub enum CyclosErr { 45 #[error("unauthorized: {0}")] 46 Unauthorized(#[from] UnauthorizedError), 47 #[error("forbidden: {0}")] 48 Forbidden(#[from] ForbiddenError), 49 #[error("server: {0}")] 50 Server(#[from] UnexpectedError), 51 #[error("unknown: {0}")] 52 Unknown(#[from] NotFoundError), 53 #[error("input: {0}")] 54 Input(#[from] InputError), 55 #[error("status {0}")] 56 UnexpectedStatus(StatusCode), 57 #[error(transparent)] 58 Client(#[from] ClientErr), 59 } 60 61 pub type ApiResult<R> = std::result::Result<R, ApiErr<CyclosErr>>; 62 63 pub struct CyclosRequest<'a> { 64 req: Req, 65 auth: &'a CyclosAuth, 66 } 67 68 impl<'a> CyclosRequest<'a> { 69 pub fn new( 70 client: &Client, 71 method: Method, 72 base_url: &Url, 73 path: impl Into<Cow<'static, str>>, 74 auth: &'a CyclosAuth, 75 ) -> Self { 76 Self { 77 req: Req::new(client, method, base_url, path), 78 auth, 79 } 80 } 81 82 pub fn query<T: Serialize>(mut self, name: &str, value: T) -> Self { 83 self.req = self.req.query(name, value); 84 self 85 } 86 87 pub fn header(mut self, key: impl Into<HeaderName>, value: impl Into<HeaderValue>) -> Self { 88 self.req = self.req.header(key, value); 89 self 90 } 91 92 pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self { 93 self.req = self.req.json(json); 94 self 95 } 96 97 async fn send(self) -> ApiResult<(Ctx, Res)> { 98 let Self { req, auth } = self; 99 match auth { 100 CyclosAuth::None => req, 101 CyclosAuth::Basic { username, password } => req.basic_auth(username, password), 102 } 103 .send() 104 .await 105 .map_err(|(ctx, e)| ctx.wrap(e.into())) 106 } 107 108 async fn error_handling(res: Res) -> Result<Res, CyclosErr> { 109 match res.status() { 110 StatusCode::OK | StatusCode::CREATED => Ok(res), 111 StatusCode::UNAUTHORIZED => Err(CyclosErr::Unauthorized(res.json().await?)), 112 StatusCode::FORBIDDEN => Err(CyclosErr::Forbidden(res.json().await?)), 113 StatusCode::NOT_FOUND => Err(CyclosErr::Unknown(res.json().await?)), 114 StatusCode::UNPROCESSABLE_ENTITY => Err(CyclosErr::Input(res.json().await?)), 115 StatusCode::INTERNAL_SERVER_ERROR => Err(CyclosErr::Forbidden(res.json().await?)), 116 unexpected => Err(CyclosErr::UnexpectedStatus(unexpected)), 117 } 118 } 119 120 pub async fn into_sse(mut self, client: &mut SseClient) -> ApiResult<bool> { 121 self.req = self.req.req_sse(client); 122 let (ctx, res) = self.send().await?; 123 res.sse(client).map_err(|e| ctx.wrap(e.into())) 124 } 125 126 pub async fn parse_json<T: DeserializeOwned>(self) -> ApiResult<T> { 127 let (ctx, res) = self.send().await?; 128 async { 129 let res = Self::error_handling(res).await?; 130 let json = res.json().await?; 131 Ok(json) 132 } 133 .await 134 .map_err(|e| ctx.wrap(e)) 135 } 136 137 pub async fn parse_pagination<T: DeserializeOwned>(self) -> ApiResult<Pagination<T>> { 138 let (ctx, res) = self.send().await?; 139 async { 140 let res = Self::error_handling(res).await?; 141 let current_page = res.int_header(HeaderName::from_static("x-current-page"))?; 142 let has_next_page = res.bool_header(HeaderName::from_static("x-has-next-page"))?; 143 Ok(Pagination { 144 page: res.json().await?, 145 current_page, 146 has_next_page, 147 }) 148 } 149 .await 150 .map_err(|e| ctx.wrap(e)) 151 } 152 }