From ac3bb940abee3f25a73cefd730ddf438ff33aa8f Mon Sep 17 00:00:00 2001 From: Liu Jiang Date: Mon, 23 Mar 2020 21:26:13 +0800 Subject: [PATCH] Import http routes from Cloud Hypervisor project Import vmm/src/api/http.rs from Cloud Hyerpvisor project, commit 345c922cb9a88183e2da9d29230ecb945b0a6452 with following changes: 1) use generic type for handler argument. 2) remove server thread relative code. 3) add unit test cases. 4) refine for better code reuse. Signed-off-by: Liu Jiang --- src/common/mod.rs | 15 +++++ src/lib.rs | 2 + src/router.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 src/router.rs diff --git a/src/common/mod.rs b/src/common/mod.rs index 546c2ea..609f801 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -67,6 +67,21 @@ impl Display for ConnectionError { } } +/// Errors pertaining to `HttpRoute`. +#[derive(Debug)] +pub enum RouteError { + /// Handler for http routing path already exists. + HandlerExist(String), +} + +impl Display for RouteError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + RouteError::HandlerExist(p) => write!(f, "handler for {} already exists", p), + } + } +} + /// Errors pertaining to `HttpServer`. #[derive(Debug)] pub enum ServerError { diff --git a/src/lib.rs b/src/lib.rs index 16b7a4b..2522421 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ mod common; mod connection; mod request; mod response; +mod router; mod server; pub use self::common::headers::{Headers, MediaType}; @@ -120,4 +121,5 @@ pub use self::common::{Body, Method, Version}; pub use self::connection::{ConnectionError, HttpConnection}; pub use self::request::{Request, RequestError}; pub use self::response::{Response, StatusCode}; +pub use self::router::{EndpointHandler, HttpRoutes, RouteError, TryClone}; pub use self::server::{HttpServer, ServerError, ServerRequest, ServerResponse}; diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..2b9559e --- /dev/null +++ b/src/router.rs @@ -0,0 +1,142 @@ +// Copyright (C) 2019 Alibaba Cloud. All rights reserved. +// Copyright © 2019 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use crate::{MediaType, Request, Response, StatusCode, Version}; + +pub use crate::common::RouteError; + +/// Trait to clone the argument for EndpointHandler::handle_request. +pub trait TryClone +where + Self: std::marker::Sized, +{ + /// Clone an object from a reference. + fn try_clone(&self) -> Option; +} + +/// An HTTP endpoint handler interface +pub trait EndpointHandler: Sync + Send { + /// Handles an HTTP request. + fn handle_request(&self, req: &Request, arg: T) -> Response; +} + +/// An HTTP routes structure. +pub struct HttpRoutes { + server_id: String, + prefix: String, + media_type: MediaType, + /// routes is a hash table mapping endpoint URIs to their endpoint handlers. + routes: HashMap + Sync + Send>>, +} + +impl HttpRoutes { + /// Create a http request router. + pub fn new(server_id: String, prefix: String) -> Self { + HttpRoutes { + server_id, + prefix, + media_type: MediaType::ApplicationJson, + routes: HashMap::new(), + } + } + + /// Register a request handler for a path. + pub fn add_route( + &mut self, + path: String, + handler: Box + Sync + Send>, + ) -> Result<(), RouteError> { + let full = format!("{}{}", self.prefix, path); + if self.routes.contains_key(&full) { + Err(RouteError::HandlerExist(full)) + } else { + self.routes.insert(full, handler); + Ok(()) + } + } + + /// Handle an incoming http request and generate corresponding response. + pub fn handle_http_request(&self, request: &Request, argument: &T) -> Response { + let path = request.uri().get_abs_path().to_string(); + let mut response = match self.routes.get(&path) { + Some(route) => match argument.try_clone() { + Some(arg) => route.handle_request(&request, arg), + None => Response::new(Version::Http11, StatusCode::InternalServerError), + }, + None => Response::new(Version::Http11, StatusCode::NotFound), + }; + + response.set_server(&self.server_id); + response.set_content_type(self.media_type); + response + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct HandlerArg(bool); + + impl TryClone for HandlerArg { + fn try_clone(&self) -> Option { + match self.0 { + true => Some(HandlerArg(true)), + false => None, + } + } + } + + struct MockHandler {} + + impl EndpointHandler for MockHandler { + fn handle_request(&self, _req: &Request, _arg: HandlerArg) -> Response { + Response::new(Version::Http11, StatusCode::OK) + } + } + + #[test] + fn test_create_router() { + let mut router = HttpRoutes::new("Mock_Server".to_string(), "/api/v1".to_string()); + let handler = MockHandler {}; + let res = router.add_route("/func1".to_string(), Box::new(handler)); + assert!(res.is_ok()); + let key = format!("{}{}", "/api/v1", "/func1"); + assert!(router.routes.contains_key(&key)); + + let handler = MockHandler {}; + match router.add_route("/func1".to_string(), Box::new(handler)) { + Err(RouteError::HandlerExist(_)) => {} + _ => panic!("add_route() should return error for path with existing handler"), + } + + let handler = MockHandler {}; + let res = router.add_route("/func2".to_string(), Box::new(handler)); + assert!(res.is_ok()); + } + + #[test] + fn test_handle_http_request() { + let mut router = HttpRoutes::new("Mock_Server".to_string(), "/api/v1".to_string()); + let handler = MockHandler {}; + router + .add_route("/func1".to_string(), Box::new(handler)) + .unwrap(); + + let request = + Request::try_from(b"GET http://localhost/api/v1/func2 HTTP/1.1\r\n\r\n").unwrap(); + let arg = HandlerArg(true); + let reply = router.handle_http_request(&request, &arg); + assert_eq!(reply.status(), StatusCode::NotFound); + + let request = + Request::try_from(b"GET http://localhost/api/v1/func1 HTTP/1.1\r\n\r\n").unwrap(); + let arg = HandlerArg(false); + let reply = router.handle_http_request(&request, &arg); + assert_eq!(reply.status(), StatusCode::InternalServerError); + } +}