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