extern crate hyper; extern crate serde; use std::{path::PathBuf, str}; use log::{error}; use hyper::{Body, Client, Method, Request, Uri}; use tower::{ServiceBuilder}; use async_trait::async_trait; use serde::de::DeserializeOwned; use crate::{APIInterface, model::Conversation}; type HttpClient = Client; pub struct HTTPClient { pub base_url: Uri, client: HttpClient, } #[derive(Debug)] pub enum Error { ClientError(String), HTTPError(hyper::Error), SerdeError(serde_json::Error), DecodeError, } impl From for Error { fn from(err: hyper::Error) -> Error { Error::HTTPError(err) } } impl From 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 { let version: String = self.request("/version", Method::GET).await?; Ok(version) } async fn get_conversations(&self) -> Result, Self::Error> { let conversations: Vec = self.request("/conversations", Method::GET).await?; Ok(conversations) } } impl HTTPClient { pub fn new(base_url: Uri) -> HTTPClient { let client = ServiceBuilder::new() .service(Client::new()); HTTPClient { base_url, client, } } 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(&self, endpoint: &str, method: Method) -> Result { self.request_with_body(endpoint, method, Body::empty()).await } async fn request_with_body(&self, endpoint: &str, method: Method, body: Body) -> Result { 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?; 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) } }?; Ok(parsed) } } mod test { use super::*; use ctor::ctor; #[ctor] fn init() { pretty_env_logger::init(); log::set_max_level(log::LevelFilter::Trace); } 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; match version { Ok(_) => true, Err(e) => { error!("Mock client error: {:?}", e); false } } } #[tokio::test] async fn test_version() { if !mock_client_is_reachable().await { log::warn!("Skipping http_client tests (mock server not reachable)"); 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 { log::warn!("Skipping http_client tests (mock server not reachable)"); return; } let client = local_mock_client(); let conversations = client.get_conversations().await.unwrap(); assert!(!conversations.is_empty()); } }