Skip to content

Commit 7d16e82

Browse files
committed
feat: add Raw framework core app, router, and middleware
1 parent aa8c0b9 commit 7d16e82

File tree

13 files changed

+1145
-377
lines changed

13 files changed

+1145
-377
lines changed

Cargo.lock

Lines changed: 502 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/raw/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ license = "CC-BY-NC-ND-4.0"
1010
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1111

1212
[dependencies]
13+
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
14+
hyper = { version = "0.14", features = ["client", "server", "http1", "tcp"] }
15+
http = "0.2"
16+
serde = { version = "1", features = ["derive"] }
17+
serde_json = "1"

crates/raw/src/app.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Credit: Ben Ajaero
2+
3+
use std::convert::Infallible;
4+
use std::net::TcpListener;
5+
use std::sync::Arc;
6+
7+
use http::Method;
8+
use hyper::service::{make_service_fn, service_fn};
9+
use hyper::{Body, Request as HyperRequest, Response as HyperResponse, Server};
10+
11+
use crate::error::RawError;
12+
use crate::middleware::{handler, middleware, Middleware, Next};
13+
use crate::request::Request;
14+
use crate::response::Response;
15+
use crate::router::Router;
16+
17+
pub struct App {
18+
router: Router,
19+
middleware: Vec<Middleware>,
20+
}
21+
22+
impl App {
23+
pub fn new() -> Self {
24+
Self {
25+
router: Router::new(),
26+
middleware: Vec::new(),
27+
}
28+
}
29+
30+
pub fn get<F, Fut>(&mut self, path: &str, handler_fn: F)
31+
where
32+
F: Fn(Request) -> Fut + Send + Sync + 'static,
33+
Fut: std::future::Future<Output = Response> + Send + 'static,
34+
{
35+
self.route(Method::GET, path, handler_fn);
36+
}
37+
38+
pub fn post<F, Fut>(&mut self, path: &str, handler_fn: F)
39+
where
40+
F: Fn(Request) -> Fut + Send + Sync + 'static,
41+
Fut: std::future::Future<Output = Response> + Send + 'static,
42+
{
43+
self.route(Method::POST, path, handler_fn);
44+
}
45+
46+
pub fn route<F, Fut>(&mut self, method: Method, path: &str, handler_fn: F)
47+
where
48+
F: Fn(Request) -> Fut + Send + Sync + 'static,
49+
Fut: std::future::Future<Output = Response> + Send + 'static,
50+
{
51+
let wrapped = handler(handler_fn);
52+
self.router.add(method, path, wrapped);
53+
}
54+
55+
pub fn add_middleware<F, Fut>(&mut self, middleware_fn: F)
56+
where
57+
F: Fn(Request, Next) -> Fut + Send + Sync + 'static,
58+
Fut: std::future::Future<Output = Response> + Send + 'static,
59+
{
60+
self.middleware.push(middleware(middleware_fn));
61+
}
62+
63+
pub async fn listen(self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
64+
let listener = TcpListener::bind(addr).map_err(|err| {
65+
eprintln!("Failed to bind {}: {}", addr, err);
66+
err
67+
})?;
68+
self.serve(listener).await
69+
}
70+
71+
pub async fn serve(
72+
self,
73+
listener: TcpListener,
74+
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
75+
listener
76+
.set_nonblocking(true)
77+
.map_err(|err| {
78+
eprintln!("Failed to set non-blocking: {}", err);
79+
err
80+
})?;
81+
82+
let state = Arc::new(self);
83+
let make_svc = make_service_fn(move |_| {
84+
let state = Arc::clone(&state);
85+
async move {
86+
Ok::<_, Infallible>(service_fn(move |req| {
87+
let state = Arc::clone(&state);
88+
async move { state.handle(req).await }
89+
}))
90+
}
91+
});
92+
93+
Ok(Server::from_tcp(listener)?.serve(make_svc).await?)
94+
}
95+
96+
async fn handle(self: Arc<Self>, req: HyperRequest<Body>) -> Result<HyperResponse<Body>, Infallible> {
97+
let method = req.method().clone();
98+
let path = req.uri().path().to_string();
99+
100+
let response = if let Some(route_match) = self.router.find(&method, &path) {
101+
let request = Request::new(req, route_match.params);
102+
let handler = route_match.handler;
103+
let middleware = Arc::new(self.middleware.clone());
104+
let next = Next::new(middleware, handler);
105+
next.run(request).await
106+
} else if self.router.allows_path(&path) {
107+
RawError::MethodNotAllowed.into_response()
108+
} else {
109+
RawError::NotFound.into_response()
110+
};
111+
112+
Ok(response.into_inner())
113+
}
114+
}
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use super::App;
119+
use crate::response::{Response, Text};
120+
121+
#[tokio::test]
122+
async fn app_registers_route() {
123+
let mut app = App::new();
124+
app.get("/", |_req| async { Response::from(Text::new("ok")) });
125+
assert!(app.router.find(&http::Method::GET, "/").is_some());
126+
}
127+
}

crates/raw/src/bin/main.rs

Lines changed: 0 additions & 128 deletions
This file was deleted.

crates/raw/src/config.rs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,53 @@
11
// Credit: Ben Ajaero
22

33
use std::env;
4-
use std::path::PathBuf;
54

65
#[derive(Debug, Clone)]
7-
pub struct Config {
6+
pub struct RawConfig {
87
pub bind_addr: String,
9-
pub threads: usize,
10-
pub doc_root: PathBuf,
8+
pub worker_threads: usize,
119
}
1210

13-
impl Config {
14-
pub fn from_env() -> Result<Config, String> {
15-
let bind_addr = env::var("RWS_BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".to_string());
16-
let threads = match env::var("RWS_THREADS") {
17-
Ok(value) => parse_threads(&value)?,
11+
impl RawConfig {
12+
pub fn from_env() -> Result<Self, String> {
13+
let bind_addr = env::var("RAW_BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:3000".to_string());
14+
let worker_threads = match env::var("RAW_WORKERS") {
15+
Ok(value) => parse_workers(&value)?,
1816
Err(_) => 4,
1917
};
20-
let doc_root = env::var("RWS_DOC_ROOT").unwrap_or_else(|_| "public".to_string());
2118

22-
Ok(Config {
19+
Ok(Self {
2320
bind_addr,
24-
threads,
25-
doc_root: PathBuf::from(doc_root),
21+
worker_threads,
2622
})
2723
}
2824
}
2925

30-
fn parse_threads(value: &str) -> Result<usize, String> {
26+
impl Default for RawConfig {
27+
fn default() -> Self {
28+
Self {
29+
bind_addr: "127.0.0.1:3000".to_string(),
30+
worker_threads: 4,
31+
}
32+
}
33+
}
34+
35+
fn parse_workers(value: &str) -> Result<usize, String> {
3136
let parsed: usize = value
3237
.parse()
33-
.map_err(|_| "RWS_THREADS must be a positive integer".to_string())?;
38+
.map_err(|_| "RAW_WORKERS must be a positive integer".to_string())?;
3439
if parsed == 0 {
35-
return Err("RWS_THREADS must be greater than zero".to_string());
40+
return Err("RAW_WORKERS must be greater than zero".to_string());
3641
}
3742
Ok(parsed)
3843
}
3944

4045
#[cfg(test)]
4146
mod tests {
42-
use super::parse_threads;
47+
use super::parse_workers;
4348

4449
#[test]
45-
fn parse_threads_rejects_zero() {
46-
assert!(parse_threads("0").is_err());
50+
fn parse_workers_rejects_zero() {
51+
assert!(parse_workers("0").is_err());
4752
}
4853
}

crates/raw/src/error.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Credit: Ben Ajaero
2+
3+
use crate::response::{Response, StatusCode};
4+
5+
#[derive(Debug)]
6+
pub enum RawError {
7+
BadRequest,
8+
NotFound,
9+
MethodNotAllowed,
10+
Internal(String),
11+
}
12+
13+
impl RawError {
14+
pub fn into_response(self) -> Response {
15+
match self {
16+
RawError::BadRequest => Response::new(StatusCode::BAD_REQUEST, "Bad Request", "text/plain"),
17+
RawError::NotFound => Response::new(StatusCode::NOT_FOUND, "Not Found", "text/plain"),
18+
RawError::MethodNotAllowed => {
19+
Response::new(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed", "text/plain")
20+
}
21+
RawError::Internal(message) => {
22+
Response::new(StatusCode::INTERNAL_SERVER_ERROR, message, "text/plain")
23+
}
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)