2025-04-17 12:39:58 +02:00

471 lines
12 KiB
Rust

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<RequestBuilder, url::ParseError> {
let url = url.make_url(self.url.clone())?;
Ok(self.client.request(method, url))
}
pub fn get(&self, url: impl MakeUrl) -> Result<RequestBuilder, url::ParseError> {
self.make_request(Method::GET, url)
}
pub fn post(&self, url: impl MakeUrl) -> Result<RequestBuilder, url::ParseError> {
self.make_request(Method::POST, url)
}
pub fn put(&self, url: impl MakeUrl) -> Result<RequestBuilder, url::ParseError> {
self.make_request(Method::PUT, url)
}
async fn credentials_request(
&self,
path: &str,
username: &str,
password: &str,
) -> Result<TokenResponse, ClientError> {
let response = self
.client
.post(path)
.json(&json!({ "username": username, "password": password }))
.send()
.await?;
let body = handle_response::<TokenResponse>(response).await?;
Ok(body)
}
pub fn login(
&self,
username: &str,
password: &str,
) -> impl Future<Output = Result<TokenResponse, ClientError>> {
self.credentials_request("/api/login", username, password)
}
pub fn register(
&self,
username: &str,
password: &str,
) -> impl Future<Output = Result<TokenResponse, ClientError>> {
self.credentials_request("/api/register", username, password)
}
}
pub struct ApiClient {
client: HttpClient,
token: RwLock<Arc<str>>,
}
impl ApiClient {
async fn credentials_request(
&self,
fut: impl Future<Output = Result<TokenResponse, ClientError>>,
) -> 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<Output = Result<(), ClientError>> {
self.credentials_request(self.client.login(username, password))
}
pub fn register(
&self,
username: &str,
password: &str,
) -> impl Future<Output = Result<(), ClientError>> {
self.credentials_request(self.client.register(username, password))
}
async fn token(&self) -> Arc<str> {
(*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<T: DeserializeOwned>(
response: reqwest::Response,
) -> Result<T, ClientError> {
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::<T>(&bytes).map_err(ClientError::Json)
} else {
let mut error =
serde_json::from_slice::<ApiError<'static>>(&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<Motd, ClientError> {
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<Bank, ClientError> {
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<Pagination<User>, 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<Vec<UserAccountInfo>, ClientError> {
let response = request!(self.api, get, "/api/users/@me/accounts")
.send()
.await?;
let body = handle_response::<UserAccounts>(response).await?;
Ok(body.result)
}
pub async fn balance(&self) -> Result<u64, ClientError> {
let response = request!(self.api, get, "/api/users/@me/balance")
.send()
.await?;
let body = handle_response::<UserBalance>(response).await?;
Ok(body.balance)
}
pub async fn transactions(
&self,
direction: Option<Direction>,
limit: u64,
offset: u64,
) -> Result<Pagination<Transaction>, 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<Pagination<Account>, 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<Direction>,
limit: u64,
offset: u64,
) -> Result<Pagination<Transaction>, 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<Pagination<ChatInfo>, 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<NameOrUuid>) -> Result<Chat, ClientError> {
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<ChatInfo, ClientError> {
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<Pagination<ChatMessage>, 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<Pagination<ChatMessage>, 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<Pagination<ChatMessage>, 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<Url, url::ParseError>;
}
impl MakeUrl for &'_ str {
fn make_url(self, base: Url) -> Result<Url, url::ParseError> {
base.join(self)
}
}
impl<'a> MakeUrl for &'a [&'a str] {
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
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<Url, url::ParseError> {
base.path_segments_mut().unwrap().extend(self);
Ok(base)
}
}
impl<'a, P, S, Q, K, V> MakeUrl for (P, Q)
where
Q: IntoIterator<Item = (K, V)>,
P: IntoIterator<Item = S>,
S: AsRef<str>,
K: AsRef<str>,
V: AsRef<str>,
{
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
base.path_segments_mut().unwrap().extend(self.0);
base.query_pairs_mut().extend_pairs(self.1);
Ok(base)
}
}
pub struct RequestUrl<P, Q> {
path: P,
pagination: RequestPagination,
extra_query: Q,
}
impl<'a, P: IntoIterator<Item = &'a str>> RequestUrl<P, ()> {
pub fn simple(
path: P,
limit: u64,
offset: u64,
) -> RequestUrl<P, std::iter::Empty<(String, String)>> {
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<P, Q>
where
P: IntoIterator<Item = &'a str>,
Q: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
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)
}
}