extern crate hyper; extern crate serde; use std::path::PathBuf; use hyper::{Body, Client, Method, Request, Uri, body}; 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), } 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) } } impl HTTPClient { pub fn new(base_url: Uri) -> HTTPClient { HTTPClient { base_url: base_url, client: Client::new(), } } } #[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 { 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 = serde_json::from_slice(&body)?; Ok(parsed) } } mod test { use super::*; 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; version.is_ok() } #[tokio::test] async fn test_version() { if !mock_client_is_reachable().await { println!("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 { println!("Skipping http_client tests (mock server not reachable)"); return; } let client = local_mock_client(); let conversations = client.get_conversations().await.unwrap(); assert!(conversations.len() > 0); } }