|
7 | 7 |
|
8 | 8 | ## ✨ Highlights
|
9 | 9 |
|
10 |
| -| Feature | Description | |
11 |
| -| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
12 |
| -| **Batteries‑included Router** | Intuitive path‑based routing with path parameters and trailing‑slash redirection (TSR). | |
13 |
| -| **Extractor system** | Strongly‑typed request extractors for headers, query/body params, JSON, form data, etc. | |
14 |
| -| **Streaming & SSE** | Built‑in helpers for Server‑Sent Events *and* arbitrary `Stream` responses. | |
15 |
| -| **Middleware** | Compose synchronous or async middleware functions with minimal boilerplate. | |
16 |
| -| **Shared State** | Application‑wide state injection without `unsafe` globals. | |
17 |
| -| **Hyper‑powered** | Built on `hyper` & `tokio` for minimal overhead and async performance.<br><sub>HTTP/2 and native TLS integration are **WIP**</sub> | |
| 10 | +* **Batteries‑included Router** — Intuitive path‑based routing with path parameters and trailing‑slash redirection (TSR). |
| 11 | +* **Extractor system** — Strongly‑typed request extractors for headers, query/body params, JSON, form data, etc. |
| 12 | +* **Streaming & SSE** — Built‑in helpers for Server‑Sent Events *and* arbitrary `Stream` responses. |
| 13 | +* **Middleware** — Compose synchronous or async middleware functions with minimal boilerplate. |
| 14 | +* **Shared State** — Application‑wide state injection. |
| 15 | +* **Plugin system** — Opt‑in extensions let you add functionality without cluttering the core API. |
| 16 | +* **Hyper‑powered** — Built on `hyper` & `tokio` for minimal overhead and async performance with **native HTTP/2 & TLS** support. |
18 | 17 |
|
19 | 18 | ---
|
20 | 19 |
|
21 | 20 | ## 📦 Installation
|
22 | 21 |
|
23 |
| -Add Tako to your **Cargo.toml** (the crate isn’t on crates.io yet, so pull it from Git): |
| 22 | +Add **Tako** to your `Cargo.toml`: |
24 | 23 |
|
25 | 24 | ```toml
|
26 | 25 | [dependencies]
|
27 |
| -tako = { git = "https://github.com/rust-dd/tako", branch = "main" } |
28 |
| -# tako = { path = "../tako" } # ← for workspace development |
| 26 | +tako-rs = "*" |
29 | 27 | ```
|
30 | 28 |
|
31 | 29 | ---
|
32 | 30 |
|
33 |
| -## 🚀 Quick Start |
| 31 | +## 🚀 Quick Start |
34 | 32 |
|
35 |
| -Below is a *minimal‑but‑mighty* example that demonstrates: |
36 |
| - |
37 |
| -* Basic GET & POST routes with parameters |
38 |
| -* Route‑scoped middleware |
39 |
| -* Shared application state |
40 |
| -* Server‑Sent Events (string & raw bytes streams) |
| 33 | +Spin up a "Hello, World!" server in a handful of lines: |
41 | 34 |
|
42 | 35 | ```rust
|
43 |
| -use std::time::Duration; |
44 |
| - |
45 |
| -use bytes::Bytes; |
46 |
| -use futures_util::{SinkExt, StreamExt}; |
47 |
| -use hyper::Method; |
48 |
| -use serde::Deserialize; |
| 36 | +use anyhow::Result; |
49 | 37 | use tako::{
|
50 |
| - body::TakoBody, |
51 |
| - extractors::{bytes::Bytes as BodyBytes, header_map::HeaderMap, params::Params, FromRequest}, |
52 | 38 | responder::Responder,
|
53 |
| - sse::{SseBytes, SseString}, |
54 |
| - state::get_state, |
55 |
| - types::{Request, Response}, |
56 |
| - ws::TakoWs, |
| 39 | + router::Router, |
| 40 | + types::Request, |
| 41 | + Method, |
57 | 42 | };
|
58 |
| -use tokio_stream::{wrappers::IntervalStream, StreamExt}; |
59 |
| -use tokio_tungstenite::tungstenite::{Message, Utf8Bytes}; |
60 |
| - |
61 |
| -/// Global application state shared via an *arc‑swap* under the hood. |
62 |
| -#[derive(Clone, Default)] |
63 |
| -struct AppState { |
64 |
| - request_count: std::sync::atomic::AtomicU64, |
65 |
| -} |
66 |
| - |
67 |
| -/// `GET /` handler that echoes the body & headers back. |
68 |
| -async fn hello(mut req: Request) -> impl Responder { |
69 |
| - let HeaderMap(headers) = HeaderMap::from_request(&mut req).await.unwrap(); |
70 |
| - let BodyBytes(body) = BodyBytes::from_request(&mut req).await.unwrap(); |
71 |
| - |
72 |
| - format!( |
73 |
| - "Hello, World!\n\nHeaders: {:#?}\nBody: {:?}", |
74 |
| - headers, body |
75 |
| - ) |
76 |
| - .into_response() |
77 |
| -} |
78 |
| - |
79 |
| -/// Typed URL parameter struct for `/user/{id}`. |
80 |
| -#[derive(Deserialize)] |
81 |
| -struct UserParams { |
82 |
| - id: u32, |
83 |
| -} |
84 |
| - |
85 |
| -async fn create_user(mut req: Request) -> impl Responder { |
86 |
| - let Params(user) = Params::<UserParams>::from_request(&mut req).await.unwrap(); |
87 |
| - let state = get_state::<AppState>("app_state").unwrap(); |
88 |
| - state.request_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
89 |
| - |
90 |
| - format!("User {} created ✅", user.id).into_response() |
91 |
| -} |
92 |
| - |
93 |
| -/// String‑based SSE endpoint emitting `Hello` every second. |
94 |
| -async fn sse_string(_: Request) -> impl Responder { |
95 |
| - let stream = IntervalStream::new(tokio::time::interval(Duration::from_secs(1))) |
96 |
| - .map(|_| "Hello".to_string()); |
97 |
| - SseString { stream } |
98 |
| -} |
99 |
| - |
100 |
| -/// Raw‑bytes SSE variant (hand‑crafted frame). |
101 |
| -async fn sse_bytes(_: Request) -> impl Responder { |
102 |
| - let stream = IntervalStream::new(tokio::time::interval(Duration::from_secs(1))) |
103 |
| - .map(|_| Bytes::from("data: hello\n\n")); |
104 |
| - SseBytes { stream } |
105 |
| -} |
106 |
| - |
107 |
| -/// Example auth middleware that short‑circuits with 401 when a header is missing. |
108 |
| -async fn auth_middleware(req: Request) -> Result<Request, Response> { |
109 |
| - if req.headers().get("x-auth").is_none() { |
110 |
| - return Err( |
111 |
| - hyper::Response::builder() |
112 |
| - .status(401) |
113 |
| - .body(TakoBody::empty()) |
114 |
| - .unwrap() |
115 |
| - .into_response(), |
116 |
| - ); |
117 |
| - } |
118 |
| - Ok(req) |
119 |
| -} |
120 |
| - |
121 |
| -pub async fn ws_echo(req: Request) -> impl Responder { |
122 |
| - TakoWs::new(req, |mut ws| async move { |
123 |
| - let _ = ws.send(Message::Text("Welcome to Tako WS!".into())).await; |
124 |
| - |
125 |
| - while let Some(Ok(msg)) = ws.next().await { |
126 |
| - match msg { |
127 |
| - Message::Text(txt) => { |
128 |
| - let _ = ws |
129 |
| - .send(Message::Text(Utf8Bytes::from(format!("Echo: {txt}")))) |
130 |
| - .await; |
131 |
| - } |
132 |
| - Message::Binary(bin) => { |
133 |
| - let _ = ws.send(Message::Binary(bin)).await; |
134 |
| - } |
135 |
| - Message::Ping(p) => { |
136 |
| - let _ = ws.send(Message::Pong(p)).await; |
137 |
| - } |
138 |
| - Message::Close(_) => { |
139 |
| - let _ = ws.send(Message::Close(None)).await; |
140 |
| - break; |
141 |
| - } |
142 |
| - _ => {} |
143 |
| - } |
144 |
| - } |
145 |
| - }) |
146 |
| -} |
| 43 | +use tokio::net::TcpListener; |
147 | 44 |
|
148 |
| -pub async fn ws_tick(req: Request) -> impl Responder { |
149 |
| - TakoWs::new(req, |mut ws| async move { |
150 |
| - let mut ticker = |
151 |
| - IntervalStream::new(tokio::time::interval(Duration::from_secs(1))).enumerate(); |
152 |
| - |
153 |
| - loop { |
154 |
| - tokio::select! { |
155 |
| - msg = ws.next() => { |
156 |
| - match msg { |
157 |
| - Some(Ok(Message::Close(_))) | None => break, |
158 |
| - _ => {} |
159 |
| - } |
160 |
| - } |
161 |
| - |
162 |
| - Some((i, _)) = ticker.next() => { |
163 |
| - let _ = ws.send(Message::Text(Utf8Bytes::from(format!("tick #{i}")))).await; |
164 |
| - } |
165 |
| - } |
166 |
| - } |
167 |
| - }) |
| 45 | +async fn hello_world(_: Request) -> impl Responder { |
| 46 | + "Hello, World!".into_response() |
168 | 47 | }
|
169 | 48 |
|
170 | 49 | #[tokio::main]
|
171 |
| -async fn main() -> anyhow::Result<()> { |
172 |
| - let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?; |
| 50 | +async fn main() -> Result<()> { |
| 51 | + // Bind a local TCP listener |
| 52 | + let listener = TcpListener::bind("127.0.0.1:8080").await?; |
173 | 53 |
|
174 |
| - let mut router = tako::router::Router::new(); |
175 |
| - router.state("app_state", AppState::default()); |
| 54 | + // Declare routes |
| 55 | + let mut router = Router::new(); |
| 56 | + router.route(Method::GET, "/", hello_world); |
176 | 57 |
|
177 |
| - // Routes -------------------------------------------------------------- |
178 |
| - router |
179 |
| - .route(Method::GET, "/", hello) |
180 |
| - .middleware(auth_middleware); |
| 58 | + println!("Server running at http://127.0.0.1:8080"); |
181 | 59 |
|
182 |
| - router.route_with_tsr(Method::POST, "/user/{id}", create_user); |
183 |
| - router.route_with_tsr(Method::GET, "/sse/string", sse_string); |
184 |
| - router.route_with_tsr(Method::GET, "/sse/bytes", sse_bytes); |
185 |
| - router.route_with_tsr(Method::GET, "/ws/echo", ws_echo); |
186 |
| - router.route_with_tsr(Method::GET, "/ws/tick", ws_tick); |
187 |
| - |
188 |
| - // Start the server (HTTP/1.1 — HTTP/2 coming soon!) |
189 |
| - #[cfg(not(feature = "tls"))] |
190 |
| - tako::serve(listener, r).await; |
191 |
| - |
192 |
| - #[cfg(feature = "tls")] |
193 |
| - tako::serve_tls(listener, r).await; |
| 60 | + // Launch the server |
| 61 | + tako::serve(listener, router).await; |
194 | 62 |
|
195 | 63 | Ok(())
|
196 | 64 | }
|
197 | 65 | ```
|
198 | 66 |
|
199 |
| -> **Tip:** Tako returns a **308 Permanent Redirect** automatically when the trailing slash in the request does not match your route declaration. Use `route_with_tsr` when you *want* that redirect. |
200 |
| -
|
201 |
| ---- |
202 |
| - |
203 |
| -## 🧑💻 Development & Contributing |
204 |
| - |
205 |
| -1. **Clone** the repo and run the examples: |
206 |
| - |
207 |
| - ```bash |
208 |
| - git clone https://github.com/rust-dd/tako |
209 |
| - cd tako && cargo run --example hello_world |
210 |
| - ``` |
211 |
| -2. Format & lint: |
212 |
| - |
213 |
| - ```bash |
214 |
| - cargo fmt && cargo clippy --all-targets --all-features |
215 |
| - ``` |
216 |
| -3. Open a PR – all contributions, big or small, are welcome! |
217 |
| - |
218 |
| ---- |
219 |
| - |
220 |
| -## 🧪 Running the Example Above |
221 |
| - |
222 |
| -```bash |
223 |
| -cargo run # in the folder with `main.rs` |
224 |
| -``` |
225 |
| - |
226 |
| -Navigate to [http://localhost:8080/](http://localhost:8080/) and watch requests stream in your terminal. |
227 |
| - |
228 |
| -For the SSE endpoints: |
229 |
| - |
230 |
| -```bash |
231 |
| -curl -N http://localhost:8080/sse/string # string frames |
232 |
| -curl -N http://localhost:8080/sse/bytes # raw bytes |
233 |
| -``` |
234 |
| - |
235 |
| ---- |
236 |
| - |
237 | 67 | ## 📜 License
|
238 | 68 |
|
239 |
| -MIT |
| 69 | +`MIT` — see [LICENSE](./LICENSE) for details. |
240 | 70 |
|
241 | 71 | ---
|
242 | 72 |
|
|
0 commit comments