api.rs (4757B)
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 pub struct CyclosRequest<'a> { 62 req: Req, 63 auth: &'a CyclosAuth, 64 } 65 66 impl<'a> CyclosRequest<'a> { 67 pub fn new( 68 client: &Client, 69 method: Method, 70 base_url: &Url, 71 path: impl Into<Cow<'static, str>>, 72 auth: &'a CyclosAuth, 73 ) -> Self { 74 Self { 75 req: Req::new(client, method, base_url, path), 76 auth, 77 } 78 } 79 80 pub fn query<T: Serialize>(mut self, name: &str, value: T) -> Self { 81 self.req = self.req.query(name, value); 82 self 83 } 84 85 pub fn header(mut self, key: impl Into<HeaderName>, value: impl Into<HeaderValue>) -> Self { 86 self.req = self.req.header(key, value); 87 self 88 } 89 90 pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self { 91 self.req = self.req.json(json); 92 self 93 } 94 95 async fn send(self) -> ApiResult<(Ctx, Res)> { 96 let Self { req, auth } = self; 97 match auth { 98 CyclosAuth::None => req, 99 CyclosAuth::Basic { username, password } => req.basic_auth(username, password), 100 } 101 .send() 102 .await 103 .map_err(|(ctx, e)| ctx.wrap(e.into())) 104 } 105 106 async fn error_handling(res: Res) -> Result<Res, CyclosErr> { 107 match res.status() { 108 StatusCode::OK | StatusCode::CREATED => Ok(res), 109 StatusCode::UNAUTHORIZED => Err(CyclosErr::Unauthorized(res.json().await?)), 110 StatusCode::FORBIDDEN => Err(CyclosErr::Forbidden(res.json().await?)), 111 StatusCode::NOT_FOUND => Err(CyclosErr::Unknown(res.json().await?)), 112 StatusCode::UNPROCESSABLE_ENTITY => Err(CyclosErr::Input(res.json().await?)), 113 StatusCode::INTERNAL_SERVER_ERROR => Err(CyclosErr::Forbidden(res.json().await?)), 114 unexpected => Err(CyclosErr::UnexpectedStatus(unexpected)), 115 } 116 } 117 118 pub async fn into_sse(mut self, client: &mut SseClient) -> ApiResult<()> { 119 self.req = self.req.req_sse(client); 120 let (ctx, res) = self.send().await?; 121 res.sse(client).map_err(|e| ctx.wrap(e.into())) 122 } 123 124 pub async fn parse_json<T: DeserializeOwned>(self) -> ApiResult<T> { 125 let (ctx, res) = self.send().await?; 126 async { 127 let res = Self::error_handling(res).await?; 128 let json = res.json().await?; 129 Ok(json) 130 } 131 .await 132 .map_err(|e| ctx.wrap(e)) 133 } 134 135 pub async fn parse_pagination<T: DeserializeOwned>(self) -> ApiResult<Pagination<T>> { 136 let (ctx, res) = self.send().await?; 137 async { 138 let res = Self::error_handling(res).await?; 139 let current_page = res.int_header("x-current-page")?; 140 let has_next_page = res.bool_header("x-has-next-page")?; 141 Ok(Pagination { 142 page: res.json().await?, 143 current_page, 144 has_next_page, 145 }) 146 } 147 .await 148 .map_err(|e| ctx.wrap(e)) 149 } 150 }