use std::sync::Arc; use async_lock::RwLock; use bank_core::{ ApiError, NameOrUuid, TokenResponse, account::{Account, UserAccountInfo}, chat::{Chat, ChatInfo, ChatMessage, SendMessage, StartChat}, meta::{Bank, Motd}, pagination::{Pagination, RequestPagination}, transaction::{Direction, Transaction}, user::{User, UserAccounts, UserBalance}, }; use reqwest::{Method, RequestBuilder, header::CONTENT_TYPE}; use serde::de::DeserializeOwned; use serde_json::json; use thiserror::Error; use url::Url; use uuid::Uuid; pub struct HttpClient { url: Url, client: reqwest::Client, } #[derive(Debug, Error)] pub enum ClientError { #[error("url: {_0}")] Url(#[from] url::ParseError), #[error("request: {_0}")] Reqwest(#[from] reqwest::Error), #[error("unexpected content type")] UnexpectedContentType, #[error("api: {_0:?}")] Api(ApiError<'static>), #[error("json: {_0:?}")] Json(serde_json::Error), } impl HttpClient { fn make_request( &self, method: reqwest::Method, url: impl MakeUrl, ) -> Result { let url = url.make_url(self.url.clone())?; Ok(self.client.request(method, url)) } pub fn get(&self, url: impl MakeUrl) -> Result { self.make_request(Method::GET, url) } pub fn post(&self, url: impl MakeUrl) -> Result { self.make_request(Method::POST, url) } pub fn put(&self, url: impl MakeUrl) -> Result { self.make_request(Method::PUT, url) } async fn credentials_request( &self, path: &str, username: &str, password: &str, ) -> Result { let response = self .client .post(path) .json(&json!({ "username": username, "password": password })) .send() .await?; let body = handle_response::(response).await?; Ok(body) } pub fn login( &self, username: &str, password: &str, ) -> impl Future> { self.credentials_request("/api/login", username, password) } pub fn register( &self, username: &str, password: &str, ) -> impl Future> { self.credentials_request("/api/register", username, password) } } pub struct ApiClient { client: HttpClient, token: RwLock>, } impl ApiClient { async fn credentials_request( &self, fut: impl Future>, ) -> Result<(), ClientError> { let mut token = self.token.write().await; let body = fut.await?; *token = body.token.into(); Ok(()) } pub fn login( &self, username: &str, password: &str, ) -> impl Future> { self.credentials_request(self.client.login(username, password)) } pub fn register( &self, username: &str, password: &str, ) -> impl Future> { self.credentials_request(self.client.register(username, password)) } async fn token(&self) -> Arc { (*self.token.read().await).clone() } #[inline] pub const fn users(&self) -> UsersApi { UsersApi { api: self } } #[inline] pub const fn accounts(&self) -> AccountsApi { AccountsApi { api: self } } #[inline] pub const fn chats(&self) -> ChatsApi { ChatsApi { api: self } } } pub async fn handle_response( response: reqwest::Response, ) -> Result { let response = response.error_for_status()?; let content_type = response.headers().get(CONTENT_TYPE).expect("content-type"); if content_type != "application/json" { return Err(ClientError::UnexpectedContentType); } let status = response.status(); let bytes = response.bytes().await?; if status.is_success() { serde_json::from_slice::(&bytes).map_err(ClientError::Json) } else { let mut error = serde_json::from_slice::>(&bytes).map_err(ClientError::Json)?; error.status = status; Err(ClientError::Api(error)) } } macro_rules! request { ($api:expr, $method:ident, $path:expr) => {{ let api = &$api; api.client.$method($path)?.auth(api).await }}; } pub struct MetaApi<'a> { api: &'a ApiClient, } impl MetaApi<'_> { pub async fn motd(&self) -> Result { let response = request!(self.api, get, "/api/meta/motd").send().await?; let body = handle_response(response).await?; Ok(body) } pub async fn bank(&self) -> Result { let response = request!(self.api, get, "/api/meta/bank").send().await?; let body = handle_response(response).await?; Ok(body) } } pub struct UsersApi<'a> { api: &'a ApiClient, } impl UsersApi<'_> { pub async fn list(&self, limit: u64, offset: u64) -> Result, ClientError> { let response = request!( self.api, get, RequestUrl::simple(["api", "users"], limit, offset) ) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn accounts(&self) -> Result, ClientError> { let response = request!(self.api, get, "/api/users/@me/accounts") .send() .await?; let body = handle_response::(response).await?; Ok(body.result) } pub async fn balance(&self) -> Result { let response = request!(self.api, get, "/api/users/@me/balance") .send() .await?; let body = handle_response::(response).await?; Ok(body.balance) } pub async fn transactions( &self, direction: Option, limit: u64, offset: u64, ) -> Result, ClientError> { let response = request!( self.api, get, RequestUrl { path: ["api", "users", "@me", "transactions"], pagination: RequestPagination { limit, offset }, extra_query: direction.map(|dir| ("direction", dir)) } ) .send() .await?; let body = handle_response(response).await?; Ok(body) } } pub struct AccountsApi<'a> { api: &'a ApiClient, } impl AccountsApi<'_> { pub async fn list(&self, limit: u64, offset: u64) -> Result, ClientError> { let response = request!( self.api, get, RequestUrl::simple(["api", "accounts"], limit, offset) ) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn transactions( &self, id: Uuid, direction: Option, limit: u64, offset: u64, ) -> Result, ClientError> { let response = request!( self.api, get, RequestUrl { path: ["api", "accounts", &id.to_string(), "transactions"], pagination: RequestPagination { limit, offset }, extra_query: direction.map(|dir| ("direction", dir)) } ) .send() .await?; let body = handle_response(response).await?; Ok(body) } } pub struct ChatsApi<'a> { api: &'a ApiClient, } impl ChatsApi<'_> { pub async fn list(&self, limit: u64, offset: u64) -> Result, ClientError> { let response = request!( self.api, get, RequestUrl::simple(["api", "chats"], limit, offset) ) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn start(&self, user: impl Into) -> Result { let response = request!(self.api, get, "/api/chats") .json(&StartChat { user: user.into() }) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn info(&self, id: Uuid) -> Result { let response = request!(self.api, get, ["api", "chats", &id.to_string()]) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn messages( &self, chat: Uuid, limit: u64, offset: u64, ) -> Result, ClientError> { let response = request!( self.api, get, RequestUrl::simple(["api", "chats", &chat.to_string()], limit, offset) ) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn send( &self, chat: Uuid, message: &SendMessage, ) -> Result, ClientError> { let response = request!( self.api, get, ["api", "chats", &chat.to_string(), "messages"] ) .json(message) .send() .await?; let body = handle_response(response).await?; Ok(body) } pub async fn mark_read( &self, chat: Uuid, message: Uuid, ) -> Result, ClientError> { let response = request!( self.api, get, [ "api", "chats", &chat.to_string(), "messages", &message.to_string(), "read" ] ) .send() .await?; let body = handle_response(response).await?; Ok(body) } } trait RequestBuilderExt { async fn auth(self, api: &ApiClient) -> Self; } impl RequestBuilderExt for RequestBuilder { async fn auth(self, api: &ApiClient) -> Self { self.bearer_auth(api.token().await) } } pub trait MakeUrl { fn make_url(self, base: Url) -> Result; } impl MakeUrl for &'_ str { fn make_url(self, base: Url) -> Result { base.join(self) } } impl<'a> MakeUrl for &'a [&'a str] { fn make_url(self, mut base: Url) -> Result { base.path_segments_mut().unwrap().extend(self); Ok(base) } } impl<'a, const N: usize> MakeUrl for [&'a str; N] { fn make_url(self, mut base: Url) -> Result { base.path_segments_mut().unwrap().extend(self); Ok(base) } } impl<'a, P, S, Q, K, V> MakeUrl for (P, Q) where Q: IntoIterator, P: IntoIterator, S: AsRef, K: AsRef, V: AsRef, { fn make_url(self, mut base: Url) -> Result { base.path_segments_mut().unwrap().extend(self.0); base.query_pairs_mut().extend_pairs(self.1); Ok(base) } } pub struct RequestUrl { path: P, pagination: RequestPagination, extra_query: Q, } impl<'a, P: IntoIterator> RequestUrl { pub fn simple( path: P, limit: u64, offset: u64, ) -> RequestUrl> { RequestUrl { path, pagination: RequestPagination { limit, offset }, extra_query: std::iter::empty(), } } } macro_rules! num { ($num:expr) => { itoa::Buffer::new().format($num) }; } impl<'a, P, Q, K, V> MakeUrl for RequestUrl where P: IntoIterator, Q: IntoIterator, K: AsRef, V: AsRef, { fn make_url(self, mut base: Url) -> Result { base.path_segments_mut().unwrap().extend(self.path); let mut query = base.query_pairs_mut(); query.extend_pairs([ ("limit", num!(self.pagination.limit)), ("offset", num!(self.pagination.offset)), ]); query.extend_pairs(self.extra_query); drop(query); Ok(base) } }