Started work on http server
This commit is contained in:
@@ -7,6 +7,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.80"
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
hyper-tls = "0.5.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.91"
|
||||
time = { version = "0.3.17", features = ["parsing", "serde"] }
|
||||
|
||||
138
kordophone/src/api/http_client.rs
Normal file
138
kordophone/src/api/http_client.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
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<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),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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<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 {
|
||||
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?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
pub use crate::model::Conversation;
|
||||
|
||||
pub mod http_client;
|
||||
pub use http_client::HTTPClient;
|
||||
|
||||
#[async_trait]
|
||||
pub trait APIInterface {
|
||||
type Error;
|
||||
|
||||
Reference in New Issue
Block a user