Skip to content

Commit

Permalink
chore: Convert to Hyper and Http 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Jun 7, 2024
1 parent d1c0015 commit d90b39c
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 76 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ license = "MIT"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
bytes = "1.6.0"
chrono = "0.4.26"
futures = "0.3.28"
hex = "0.4.3"
http = "1.1.0"
hyper = { version = "1.3.1", features = ["full"] }
hyper-util = { version = "0.1.5", features = ["full"] }
http-body-util = "0.1.1"
itertools = "0.13.0"
lazy_static = "1.4.0"
maplit = "1.0.2"
serde = "1.0.163"
serde_json = "1.0.96"
tracing = "0.1.37"
tokio = { version = "1.38.0", features = ["full"] }

[dev-dependencies]
expectest = "0.12.0"
5 changes: 3 additions & 2 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
use std::collections::{BTreeMap, HashMap};

use bytes::Bytes;
use chrono::{DateTime, FixedOffset};
use maplit::hashmap;
use itertools::Itertools;
use maplit::hashmap;

use crate::headers::HeaderValue;

Expand All @@ -21,7 +22,7 @@ pub struct WebmachineRequest {
/// Request headers
pub headers: HashMap<String, Vec<HeaderValue>>,
/// Request body
pub body: Option<Vec<u8>>,
pub body: Option<Bytes>,
/// Query parameters
pub query: HashMap<String, Vec<String>>
}
Expand Down
10 changes: 5 additions & 5 deletions src/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use std::str::Chars;

use itertools::Itertools;

const SEPERATORS: [char; 10] = ['(', ')', '<', '>', '@', ',', ';', '=', '{', '}'];
const VALUE_SEPERATORS: [char; 9] = ['(', ')', '<', '>', '@', ',', ';', '{', '}'];
const SEPARATORS: [char; 10] = ['(', ')', '<', '>', '@', ',', ';', '=', '{', '}'];
const VALUE_SEPARATORS: [char; 9] = ['(', ')', '<', '>', '@', ',', ';', '{', '}'];

fn batch(values: &[String]) -> Vec<(String, String)> {
values.into_iter().batching(|it| {
Expand Down Expand Up @@ -55,7 +55,7 @@ fn header_value(chars: &mut Peekable<Chars>, seperators: &[char]) -> String {
// header -> value [; parameters]
fn parse_header(s: &str) -> Vec<String> {
let mut chars = s.chars().peekable();
let header_value = header_value(&mut chars, &VALUE_SEPERATORS);
let header_value = header_value(&mut chars, &VALUE_SEPARATORS);
let mut values = vec![header_value];
if chars.peek().is_some() && chars.peek().unwrap() == &';' {
chars.next();
Expand All @@ -75,7 +75,7 @@ fn parse_header_parameters(chars: &mut Peekable<Chars>, values: &mut Vec<String>

// parameter -> attribute [= [value]]
fn parse_header_parameter(chars: &mut Peekable<Chars>, values: &mut Vec<String>) {
values.push(header_value(chars, &SEPERATORS));
values.push(header_value(chars, &SEPARATORS));
if chars.peek().is_some() && chars.peek().unwrap() == &'=' {
chars.next();
parse_header_parameter_value(chars, values);
Expand Down Expand Up @@ -325,7 +325,7 @@ mod tests {
}));
expect!(header.weak_etag()).to(be_none());

let weak_etag_value = HeaderValue::parse_string(weak_etag.clone());
let weak_etag_value = HeaderValue::parse_string(weak_etag);
expect!(weak_etag_value.clone()).to(be_equal_to(HeaderValue {
value: weak_etag.to_string(),
params: hashmap!{},
Expand Down
124 changes: 55 additions & 69 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,32 @@ This implementation has the following deficiencies:
Follow the getting started documentation from the Hyper crate to setup a Hyper service for your server.
You need to define a WebmachineDispatcher that maps resource paths to your webmachine resources (WebmachineResource).
Each WebmachineResource defines all the callbacks (via Closures) and values required to implement a resource.
The WebmachineDispatcher implementes the Hyper Service trait, so you can pass it to the `make_service_fn`.
Note: This example uses the maplit crate to provide the `btreemap` macro and the log crate for the logging macros.
```no_run
use hyper::server::Server;
use webmachine_rust::*;
use webmachine_rust::context::*;
use webmachine_rust::headers::*;
use serde_json::{Value, json};
use std::io::Read;
use std::net::SocketAddr;
use hyper::service::make_service_fn;
use std::convert::Infallible;
use std::sync::Arc;
use maplit::btreemap;
use tracing::error;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{body, Request};
# fn main() {}
// setup the dispatcher, which maps paths to resources. The requirement of make_service_fn is
// that it has a static lifetime
fn dispatcher() -> WebmachineDispatcher<'static> {
WebmachineDispatcher {
async fn start_server() -> anyhow::Result<()> {
// setup the dispatcher, which maps paths to resources. We wrap it in an Arc so we can
// use it in the loop below.
let dispatcher = Arc::new(WebmachineDispatcher {
routes: btreemap!{
"/myresource" => WebmachineResource {
// Methods allowed on this resource
Expand All @@ -79,47 +83,45 @@ Note: This example uses the maplit crate to provide the `btreemap` macro and the
.. WebmachineResource::default()
}
}
});
// Create a Hyper server that delegates to the dispatcher. See https://hyper.rs/guides/1/server/hello-world/
let addr: SocketAddr = "0.0.0.0:8080".parse()?;
let listener = TcpListener::bind(addr).await?;
loop {
let dispatcher = dispatcher.clone();
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(|req: Request<body::Incoming>| dispatcher.dispatch(req)))
.await
{
error!("Error serving connection: {:?}", err);
}
});
}
}
async fn start_server() -> Result<(), String> {
// Create a Hyper server that delegates to the dispatcher
let addr = "0.0.0.0:8080".parse().unwrap();
let make_svc = make_service_fn(|_| async { Ok::<_, Infallible>(dispatcher()) });
match Server::try_bind(&addr) {
Ok(server) => {
// start the actual server
server.serve(make_svc).await;
},
Err(err) => {
error!("could not start server: {}", err);
}
};
Ok(())
}
```
## Example implementations
For an example of a project using this crate, have a look at the [Pact Mock Server](https://github.com/pact-foundation/pact-reference/tree/master/rust/v1/pact_mock_server_cli) from the Pact reference implementation.
For an example of a project using this crate, have a look at the [Pact Mock Server](https://github.com/pact-foundation/pact-core-mock-server/tree/main/pact_mock_server_cli) from the Pact reference implementation.
*/

#![warn(missing_docs)]

use std::collections::{BTreeMap, HashMap};
use std::future::Future;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
use std::task::{Context, Poll};

use bytes::Bytes;
use chrono::{DateTime, FixedOffset, Utc};
use futures::TryStreamExt;
use http::{Request, Response};
use http::request::Parts;
use hyper::Body;
use hyper::service::Service;
use http::{HeaderMap, Request, Response};
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use itertools::Itertools;
use lazy_static::lazy_static;
use maplit::hashmap;
Expand Down Expand Up @@ -882,8 +884,8 @@ fn parse_header_values(value: &str) -> Vec<HeaderValue> {
}
}

fn headers_from_http_request(req: &Parts) -> HashMap<String, Vec<HeaderValue>> {
req.headers.iter()
fn headers_from_http_request(headers: &HeaderMap<http::HeaderValue>) -> HashMap<String, Vec<HeaderValue>> {
headers.iter()
.map(|(name, value)| (name.to_string(), parse_header_values(value.to_str().unwrap_or_default())))
.collect()
}
Expand Down Expand Up @@ -958,37 +960,35 @@ fn parse_query(query: &str) -> HashMap<String, Vec<String>> {
}
}

async fn request_from_http_request(req: Request<hyper::Body>) -> WebmachineRequest {
let (parts, body) = req.into_parts();
let request_path = parts.uri.path().to_string();
async fn request_from_http_request(req: Request<Incoming>) -> WebmachineRequest {
let request_path = req.uri().path().to_string();
let method = req.method().to_string();
let query = match req.uri().query() {
Some(query) => parse_query(query),
None => HashMap::new()
};
let headers = headers_from_http_request(req.headers());

let req_body = body.try_fold(Vec::new(), |mut data, chunk| async move {
data.extend_from_slice(&chunk);
Ok(data)
}).await;
let body = match req_body {
let body = match req.collect().await {
Ok(body) => {
let body = body.to_bytes();
if body.is_empty() {
None
} else {
Some(body.clone())
}
},
}
Err(err) => {
error!("Failed to read the request body: {}", err);
None
}
};

let query = match parts.uri.query() {
Some(query) => parse_query(query),
None => HashMap::new()
};
WebmachineRequest {
request_path: request_path.clone(),
request_path,
base_path: "/".to_string(),
method: parts.method.as_str().into(),
headers: headers_from_http_request(&parts),
method,
headers,
body,
query
}
Expand All @@ -1015,7 +1015,7 @@ fn finalise_response(context: &mut WebmachineContext, resource: &WebmachineResou
let mut vary_header = if !context.response.has_header("Vary") {
resource.variances
.iter()
.map(|h| HeaderValue::parse_string(h.clone()))
.map(|h| HeaderValue::parse_string(h))
.collect()
} else {
Vec::new()
Expand Down Expand Up @@ -1081,16 +1081,16 @@ fn finalise_response(context: &mut WebmachineContext, resource: &WebmachineResou
debug!("Final response: {:?}", context.response);
}

fn generate_http_response(context: &WebmachineContext) -> http::Result<Response<hyper::Body>> {
fn generate_http_response(context: &WebmachineContext) -> http::Result<Response<Full<Bytes>>> {
let mut response = Response::builder().status(context.response.status);

for (header, values) in context.response.headers.clone() {
let header_values = values.iter().map(|h| h.to_string()).join(", ");
response = response.header(&header, &header_values);
}
match context.response.body.clone() {
Some(body) => response.body(body.into()),
None => response.body(Body::empty())
Some(body) => response.body(Full::new(body.into())),
None => response.body(Full::new(Bytes::default()))
}
}

Expand All @@ -1104,13 +1104,13 @@ pub struct WebmachineDispatcher<'a> {
impl <'a> WebmachineDispatcher<'a> {
/// Main dispatch function for the Webmachine. This will look for a matching resource
/// based on the request path. If one is not found, a 404 Not Found response is returned
pub async fn dispatch(self, req: Request<hyper::Body>) -> http::Result<Response<hyper::Body>> {
pub async fn dispatch(&self, req: Request<Incoming>) -> http::Result<Response<Full<Bytes>>> {
let mut context = self.context_from_http_request(req).await;
self.dispatch_to_resource(&mut context);
generate_http_response(&context)
}

async fn context_from_http_request(&self, req: Request<hyper::Body>) -> WebmachineContext {
async fn context_from_http_request(&self, req: Request<Incoming>) -> WebmachineContext {
let request = request_from_http_request(req).await;
WebmachineContext {
request,
Expand Down Expand Up @@ -1154,20 +1154,6 @@ impl <'a> WebmachineDispatcher<'a> {
}
}

impl Service<Request<hyper::Body>> for WebmachineDispatcher<'static> {
type Response = Response<hyper::Body>;
type Error = http::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}

fn call(&mut self, req: Request<hyper::Body>) -> Self::Future {
Box::pin(self.clone().dispatch(req))
}
}

#[cfg(test)]
mod tests;

Expand Down

0 comments on commit d90b39c

Please sign in to comment.