Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add example & update README and some for that #1

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/target
/Cargo.lock
**/target
**/Cargo.lock
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (c) 2024 kanarus

Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
55 changes: 50 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
<br>

<div align="right">
<a href="https://github.com/ohkami-rs/whttp/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/crates/l/ohkami.svg" /></a>
<a href="https://github.com/ohkami-rs/whttp/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/crates/l/whttp.svg" /></a>
<a href="https://github.com/ohkami-rs/whttp/actions"><img alt="CI status" src="https://github.com/ohkami-rs/whttp/actions/workflows/CI.yml/badge.svg"/></a>
<a href="https://crates.io/crates/whttp"><img alt="crates.io" src="https://img.shields.io/crates/v/whttp" /></a>
</div>

<br>

## What's advantage over http crate?

### fast, efficient
Expand All @@ -21,6 +19,7 @@
* pre-matching standard headers before hashing in parsing
* `Request` construction with zero or least copy from parsing buffer and very minimum allocation
* size of `Request` is *128* and size of `Response` is *64*
* [micro benchmarks](https://github.com/ohkami-rs/whttp/blob/main/benches)

### batteries included

Expand All @@ -31,8 +30,54 @@
* HTTP/1.1 parsing & writing on `http1` & `rt_*` feature
* supported runtimes ( `rt_*` ) : `tokio`, `async-std`, `smol`, `glommio`

<br>
## [Example](https://github.com/ohkami-rs/whttp/blob/main/example)

```toml
[dependencies]
whttp = { version = "0.1", features = ["http1", "rt_tokio"] }
tokio = { version = "1", features = ["full"] }
```
```rust
use whttp::{Request, Response, http1};
use whttp::header::{ContentType, Date};
use whttp::util::IMFfixdate;

#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = tokio::net::TcpListener::bind("localhost:3000").await?;

while let Ok((mut conn, addr)) = listener.accept().await {
let mut req = http1::init();
let mut req = std::pin::Pin::new(&mut req);

while let Ok(Some(())) = http1::load(
req.as_mut(), &mut conn
).await {
let res = handle(&req).await;
http1::send(res, &mut conn).await?;
}
}

Ok(())
}

async fn handle(req: &Request) -> Response {
if !(req.header(ContentType)
.is_some_and(|ct| ct.starts_with("text/plain"))
) {
return Response::BadRequest()
.with(Date, IMFfixdate::now())
.with_text("expected text payload")
}

let name = std::str::from_utf8(req.body().unwrap()).unwrap();

## Example
Response::OK()
.with(Date, IMFfixdate::now())
.with_text(format!("Hello, {name}!"))
}
```

## LICENSE

whttp is licensed under MIT LICENSE ( [LICENSE](https://github.com/ohkami-rs/whttp/blob/main/LICENSE) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) ).
2 changes: 1 addition & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ tasks:

test:doc:
cmds:
- cargo test --doc --features sse,ws,http1,rt_tokio
- cargo test --doc --features DEBUG

test:default:
cmds:
Expand Down
8 changes: 8 additions & 0 deletions example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]
whttp = { path = "..", features = ["http1", "rt_tokio"] }
tokio = { version = "1", features = ["full"] }
52 changes: 52 additions & 0 deletions example/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use whttp::{Request, Response, http1};
use whttp::header::{ContentType, Date};
use whttp::util::IMFfixdate;

#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = tokio::net::TcpListener::bind("localhost:3000").await?;

println!("Started serving at localhost:3000");
println!("Try\n\
$ curl -v http://localhost:3000\n\
$ curl -v http://localhost:3000 http://localhost:3000\n\
$ curl -v http://localhost:3000 -H 'Content-Type: text/plain' -d '{{YOUR NAME}}'\n\
");

while let Ok((mut conn, addr)) = listener.accept().await {
println!("accepcted {addr}\n");

let mut req = http1::init();
let mut req = std::pin::Pin::new(&mut req);

while let Ok(Some(())) = http1::load(
req.as_mut(), &mut conn
).await {
println!("req = {req:?}");

let res = handle(&req).await;
println!("res = {res:?}");

http1::send(res, &mut conn).await?;
println!();
}
}

Ok(())
}

async fn handle(req: &Request) -> Response {
if !(req.header(ContentType)
.is_some_and(|ct| ct.starts_with("text/plain"))
) {
return Response::BadRequest()
.with(Date, IMFfixdate::now())
.with_text("expected text payload")
}

let name = std::str::from_utf8(req.body().unwrap()).unwrap();

Response::OK()
.with(Date, IMFfixdate::now())
.with_text(format!("Hello, {name}!"))
}
45 changes: 25 additions & 20 deletions src/http1/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ use std::{pin::Pin, io::ErrorKind, str::FromStr as _};

const PAYLOAD_LIMIT: usize = 1 << 32;

pub fn init() -> Request {
parse::new()
}

pub async fn load(
mut req: Pin<&mut Request>,
conn: &mut (impl Read + Unpin)
) -> Result<Option<()>, Status> {
parse::clear(&mut req);
let buf = parse::buf(req.as_mut());

match conn.read(&mut **buf).await {
Expand Down Expand Up @@ -108,56 +113,56 @@ async fn test_load_request() {
let mut req = Pin::new(&mut req);

{
let mut case: &[u8] = {parse::clear(&mut req); b"\
"};
let mut case: &[u8] = b"\
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(None));
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
GET /HTTP/2\r\n\
\r\n\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Err(Status::BadRequest));
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
GET / HTTP/2\r\n\
\r\n\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Err(Status::HTTPVersionNotSupported));
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
GET / HTTP/1.1\r\n\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Err(Status::BadRequest));
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
GET / HTTP/1.1\r\n\
\r\n\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(())));
assert_eq!(*req, Request::GET("/"));
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
GET / HTTP/1.1\r\n\
Host: http://127.0.0.1:3000\r\n\
\r\n\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(())));
assert_eq!(*req, Request::GET("/").with(Host, "http://127.0.0.1:3000"));
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
POST /api/users HTTP/1.1\r\n\
Host: http://127.0.0.1:3000\r\n\
Content-Type: application/json\r\n\
Content-Length: 24\r\n\
\r\n\
{\"name\":\"whttp\",\"age\":0}\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(())));
assert_eq!(*req,
Request::POST("/api/users")
Expand All @@ -166,14 +171,14 @@ async fn test_load_request() {
);
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
POST /api/users HTTP/1.1\r\n\
Host: http://127.0.0.1:3000\r\n\
Content-Type: application/json\r\n\
Content-Length: 22\r\n\
\r\n\
{\"name\":\"whttp\",\"age\":0}\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(())));
assert_eq!(*req,
Request::POST("/api/users")
Expand All @@ -182,14 +187,14 @@ async fn test_load_request() {
);
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
POST /api/users HTTP/1.1\r\n\
host: http://127.0.0.1:3000\r\n\
content-type: application/json\r\n\
content-length: 24\r\n\
\r\n\
{\"name\":\"whttp\",\"age\":0}\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(())));
assert_eq!(*req,
Request::POST("/api/users")
Expand All @@ -198,14 +203,14 @@ async fn test_load_request() {
);
}
{
let mut case: &[u8] = {parse::clear(&mut req); b"\
let mut case: &[u8] = b"\
POST /api/users HTTP/1.1\r\n\
host: http://127.0.0.1:3000\r\n\
content-type: application/json\r\n\
content-Length: 24\r\n\
\r\n\
{\"name\":\"whttp\",\"age\":0}\
"};
";
assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(())));
assert_eq!(*req,
Request::POST("/api/users")
Expand Down
2 changes: 1 addition & 1 deletion src/http1/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod load;
pub use load::load;
pub use load::{init, load};

mod send;
pub use send::{send, Upgrade};
10 changes: 8 additions & 2 deletions src/http1/send.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Response, response::Body, io::Write};
use crate::header::SetCookie;
use crate::{Response, Status, response::Body, io::Write};
use crate::header::{ContentLength, SetCookie};

pub enum Upgrade {
None,
Expand Down Expand Up @@ -38,6 +38,12 @@ pub async fn send(
mut res: Response,
conn: &mut (impl Write + Unpin)
) -> Result<Upgrade, std::io::Error> {
if res.header(ContentLength).is_none()
&& res.body().is_none()
&& res.status() != Status::NoContent {
res.set(ContentLength, "0");
}

let mut buf = [
b"HTTP/1.1 ", res.status().message().as_bytes(), b"\r\n"
].concat();
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg_attr(feature="DEBUG", doc = include_str!("../README.md"))]

#[cfg(all(
feature="ws",
not(any(
Expand Down
19 changes: 16 additions & 3 deletions src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,25 @@ pub mod parse {
/// * `bytes` must be alive as long as `path` of `this` is in use;
/// especially, reading from `this.buf`
pub unsafe fn path(this: &mut Pin<&mut Request>, bytes: &[u8]) -> Result<(), Status> {
(bytes.len() > 0 && *bytes.get_unchecked(0) == b'/' && bytes.is_ascii())
.then_some(this.path = Str::Ref(UnsafeRef::new(
if bytes.len() > 0 && bytes.is_ascii() {
#[cfg(debug_assertions)]
if *bytes.get_unchecked(0) != b'/' {
eprintln!("\
Currently whttp only supports requests starting with \
`<method> <path>` , NOT `<method> <URI>`, on expectation that \
the host (and port) is contained in `Host` header. \
");
return Err(Status::NotImplemented)
}

Ok(this.path = Str::Ref(UnsafeRef::new(
// SAFETY: already checked `bytes` is ascii
std::str::from_utf8_unchecked(bytes)
)))
.ok_or(Status::BadRequest)

} else {
Err(Status::BadRequest)
}
}

#[inline]
Expand Down
Loading
Loading