Private
Public Access
1
0
Files
Kordophone/kordophone/src/api/http_client.rs

166 lines
4.5 KiB
Rust
Raw Normal View History

2024-04-24 23:41:42 -07:00
extern crate hyper;
extern crate serde;
2024-06-01 18:17:57 -07:00
use std::{path::PathBuf, str};
use log::{error};
2024-04-24 23:41:42 -07:00
2024-06-01 18:17:57 -07:00
use hyper::{Body, Client, Method, Request, Uri};
use tower::{ServiceBuilder};
2024-06-01 18:16:25 -07:00
2024-04-24 23:41:42 -07:00
use async_trait::async_trait;
use serde::de::DeserializeOwned;
2024-06-01 18:17:57 -07:00
2024-04-24 23:41:42 -07:00
use crate::{APIInterface, model::Conversation};
type HttpClient = Client<hyper::client::HttpConnector>;
pub struct HTTPClient {
pub base_url: Uri,
client: HttpClient,
}
#[derive(Debug)]
pub enum Error {
ClientError(String),
HTTPError(hyper::Error),
SerdeError(serde_json::Error),
2024-06-01 18:16:25 -07:00
DecodeError,
2024-04-24 23:41:42 -07:00
}
impl From <hyper::Error> for Error {
fn from(err: hyper::Error) -> Error {
Error::HTTPError(err)
}
}
impl From <serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::SerdeError(err)
}
}
#[async_trait]
impl APIInterface for HTTPClient {
type Error = Error;
async fn get_version(&self) -> Result<String, Self::Error> {
let version: String = self.request("/version", Method::GET).await?;
Ok(version)
}
async fn get_conversations(&self) -> Result<Vec<Conversation>, Self::Error> {
let conversations: Vec<Conversation> = self.request("/conversations", Method::GET).await?;
Ok(conversations)
}
}
impl HTTPClient {
2024-06-01 18:16:25 -07:00
pub fn new(base_url: Uri) -> HTTPClient {
2024-06-01 18:17:57 -07:00
let client = ServiceBuilder::new()
2024-06-01 18:16:25 -07:00
.service(Client::new());
HTTPClient {
2024-06-01 18:17:57 -07:00
base_url,
client,
2024-06-01 18:16:25 -07:00
}
}
2024-04-24 23:41:42 -07:00
fn uri_for_endpoint(&self, endpoint: &str) -> Uri {
let mut parts = self.base_url.clone().into_parts();
let root_path: PathBuf = parts.path_and_query.unwrap().path().into();
let path = root_path.join(endpoint);
parts.path_and_query = Some(path.to_str().unwrap().parse().unwrap());
Uri::try_from(parts).unwrap()
}
async fn request<T: DeserializeOwned>(&self, endpoint: &str, method: Method) -> Result<T, Error> {
self.request_with_body(endpoint, method, Body::empty()).await
}
async fn request_with_body<T: DeserializeOwned>(&self, endpoint: &str, method: Method, body: Body) -> Result<T, Error> {
let uri = self.uri_for_endpoint(endpoint);
let request = Request::builder()
.method(method)
.uri(uri)
.body(body)
.unwrap();
let future = self.client.request(request);
let res = future.await?;
let status = res.status();
if status != hyper::StatusCode::OK {
let message = format!("Request failed ({:})", status);
return Err(Error::ClientError(message));
}
// Read and parse response body
let body = hyper::body::to_bytes(res.into_body()).await?;
2024-06-01 18:16:25 -07:00
let parsed: T = match serde_json::from_slice(&body) {
Ok(result) => Ok(result),
Err(json_err) => {
// If JSON deserialization fails, try to interpret it as plain text
// Unfortunately the server does return things like this...
let s = str::from_utf8(&body).map_err(|_| Error::DecodeError)?;
serde_plain::from_str(s).map_err(|_| json_err)
}
}?;
2024-04-24 23:41:42 -07:00
Ok(parsed)
}
}
mod test {
use super::*;
2024-06-01 18:16:25 -07:00
use ctor::ctor;
#[ctor]
fn init() {
pretty_env_logger::init();
log::set_max_level(log::LevelFilter::Trace);
}
2024-04-24 23:41:42 -07:00
fn local_mock_client() -> HTTPClient {
let base_url = "http://localhost:5738".parse().unwrap();
HTTPClient::new(base_url)
}
async fn mock_client_is_reachable() -> bool {
let client = local_mock_client();
let version = client.get_version().await;
2024-06-01 18:16:25 -07:00
match version {
Ok(_) => true,
Err(e) => {
error!("Mock client error: {:?}", e);
false
}
}
2024-04-24 23:41:42 -07:00
}
#[tokio::test]
async fn test_version() {
if !mock_client_is_reachable().await {
2024-06-01 18:16:25 -07:00
log::warn!("Skipping http_client tests (mock server not reachable)");
2024-04-24 23:41:42 -07:00
return;
}
let client = local_mock_client();
let version = client.get_version().await.unwrap();
assert!(version.starts_with("KordophoneMock-"));
}
#[tokio::test]
async fn test_conversations() {
if !mock_client_is_reachable().await {
2024-06-01 18:16:25 -07:00
log::warn!("Skipping http_client tests (mock server not reachable)");
2024-04-24 23:41:42 -07:00
return;
}
let client = local_mock_client();
let conversations = client.get_conversations().await.unwrap();
2024-06-01 18:17:57 -07:00
assert!(!conversations.is_empty());
2024-04-24 23:41:42 -07:00
}
}