├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── clippy.yml │ └── test-lang-rust-audit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── BUY-A-COFFEE.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── README.md ├── chat-websocket │ ├── Cargo.toml │ ├── src │ │ ├── chat │ │ │ ├── domain.rs │ │ │ ├── handlers.rs │ │ │ └── mod.rs │ │ └── main.rs │ └── templates │ │ └── index.html ├── file-multipart-form │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── hello-world │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── multiple-routers │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ ├── middlewares.rs │ │ ├── middlewares │ │ └── check_user.rs │ │ ├── routes.rs │ │ └── routes │ │ ├── about.rs │ │ ├── admin.rs │ │ └── article.rs ├── rate-limiter │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── redirect │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── shuttle-example │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── sqlx-postgres │ ├── Cargo.toml │ └── src │ │ ├── db.rs │ │ └── main.rs ├── static-react-spa-app │ ├── Cargo.toml │ ├── README.md │ ├── app │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ ├── detail.png │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ ├── index.png │ │ │ ├── logo192.png │ │ │ ├── logo512.png │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ └── src │ │ │ ├── App.test.js │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ ├── logo.svg │ │ │ ├── pages │ │ │ ├── Detail.js │ │ │ └── Home.js │ │ │ ├── reportWebVitals.js │ │ │ └── setupTests.js │ ├── run.sh │ └── src │ │ ├── data.rs │ │ └── main.rs ├── templates │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── templates │ │ └── hello.html ├── tracing-middleware │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── middlewares │ │ ├── mod.rs │ │ └── tracing.rs ├── utoipa-swagger-ui │ ├── Cargo.toml │ ├── README.md │ ├── img │ │ └── todo.png │ └── src │ │ ├── main.rs │ │ ├── routes │ │ ├── mod.rs │ │ └── todo.rs │ │ └── swagger.rs └── websocket-example │ ├── Cargo.toml │ ├── src │ └── main.rs │ └── templates │ └── index.html ├── img ├── bitcoin.png ├── compare.png ├── dogecoin.png ├── ethereum.png ├── litecoin.png ├── logo.png ├── shiba.png ├── solana.png ├── usdc.png └── usdt.png └── src ├── app.rs ├── color.rs ├── fs.rs ├── http.rs ├── http ├── methods.rs ├── request.rs ├── resource.rs └── response.rs ├── lib.rs ├── listen.rs ├── middleware └── mod.rs └── template.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [samuelbonilla] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: samuelbonilla # Replace with a single Patreon username 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | clippy: 6 | name: Clippy 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Install stable version 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | profile: minimal 15 | toolchain: stable 16 | override: true 17 | components: clippy 18 | - name: Clippy check 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: clippy 22 | args: -- -D warnings 23 | -------------------------------------------------------------------------------- /.github/workflows/test-lang-rust-audit.yml: -------------------------------------------------------------------------------- 1 | name: Rust Security Audit 2 | on: [push, pull_request] 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | jobs: 9 | audit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | # Currently does not work. See https://github.com/actions-rs/audit-check/issues/194 15 | #- name: Rust Audit 16 | # uses: actions-rs/audit-check@v1 17 | # with: 18 | # token: ${{ secrets.GITHUB_TOKEN }} 19 | # Install it manually 20 | - name: Dependency Review 21 | if: github.event_name == 'pull_request' 22 | uses: actions/dependency-review-action@v2 23 | - name: Install Cargo Audit 24 | run: cargo install cargo-audit 25 | - name: Audit 26 | run: cargo audit 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /examples/target 3 | /examples/*/target 4 | Cargo.lock 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: rust-linting 5 | name: Rust linting 6 | description: Run cargo fmt on files included in the commit. rustfmt should be installed before-hand. 7 | entry: cargo fmt --all -- 8 | pass_filenames: true 9 | types: [file, rust] 10 | language: system 11 | - id: rust-clippy 12 | name: Rust clippy 13 | description: Run cargo clippy on files included in the commit. clippy should be installed before-hand. 14 | entry: cargo clippy --all-targets --all-features -- -Dclippy::all 15 | pass_filenames: false 16 | types: [file, rust] 17 | language: system 18 | - id: cargo-check 19 | name: Rust Check 20 | description: Check the package for errors. 21 | entry: cargo check 22 | pass_filenames: false 23 | types: [file, rust] 24 | language: system 25 | - id: cargo-audit 26 | name: Rust Security Audit 27 | description: Rust Security Audit - Dependency Review 28 | entry: cargo audit 29 | pass_filenames: false 30 | types: [file, rust] 31 | language: system 32 | -------------------------------------------------------------------------------- /BUY-A-COFFEE.md: -------------------------------------------------------------------------------- 1 | ## Welcome to Graphul! 2 | 3 | Thank you for checking out Graphul, a web framework project built with Rust. I have been developing this project with passion and dedication, and I hope you find it as useful and exciting as I do. 4 | 5 | As an open-source project, Graphul relies on community support to continue to grow and improve. If you find value in this project, I would be grateful for your support. 6 | 7 | You can support Graphul in two ways: 8 | 9 | - Donate with Patreon: If you prefer to donate using a credit or debit card, you can do so by visiting our [Patreon](https://www.patreon.com/samuelbonilla). Every little bit helps, and your support will go a long way in keeping this project thriving. 10 | 11 | - Donate with cryptocurrency: If you prefer to donate with cryptocurrency, you can do so by sending your donation to the following address: 12 | 13 | - **Bitcoin**: 3C8wdfbP2iBnz5USFMsR43gTocjnbW522C 14 | - **USDT**: 0xa5f378396286e78dE116fC43dF0CFE0dd11E75C1 15 | - **Ethereum**: 0x63fBf1c51ED6B0Dd982F0e46655E497b4A8FC597 16 | - **Dogecoin**: D8rbGgiUhjtQi78ByydC1T6HoGDqpBZaEf 17 | - **USDC**: 0x4BfD991913e8A1993CBAe2C9E0b90204c12e4D7D 18 | - **Litecoin**: MRit5Gsok7cB86g2MJUyRW7tQro426rXFf 19 | - **SHIBA INU**: 0xD146cCFC9ce98e6FAc36aD9a2a0a175be2D7A729 20 | - **Solana**: EJuCB6oh9JCZAFV2K2mhGq9qjsurGx3WxxUAgzi74qTx 21 | 22 | No matter how you choose to support Graphul, your generosity will be greatly appreciated. Thank you for your support! 23 | 24 |

25 | Bitcoin 26 | Ethereum 27 | USDT 28 | DogeCoin 29 | Solana 30 | Shiba 31 | Litecoin 32 | USDC 33 |

34 | 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphul" 3 | version = "1.0.1" 4 | edition = "2021" 5 | license = "MIT" 6 | categories = ["asynchronous", "network-programming", "web-programming::http-server"] 7 | keywords = ["http", "web", "framework"] 8 | description = "Optimize, speed, scale your microservices and save money 💵" 9 | homepage = "https://graphul-rs.github.io/" 10 | documentation = "https://graphul-rs.github.io/" 11 | repository = "https://github.com/graphul-rs/graphul" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | hyper = { version = "0.14", features = ["full"] } 16 | axum = { version = "0.6", features = ["multipart", "ws", "headers"] } 17 | askama = "0.11" 18 | futures = "0.3.24" 19 | tower = { version = "0.4", features = ["limit", "util"] } 20 | num_cpus = "1.13.1" 21 | async-trait = "0.1.58" 22 | tower-layer = "0.3" 23 | serde_path_to_error = "0.1.8" 24 | serde_json = "1.0.86" 25 | serde = "1.0" 26 | serde_urlencoded = "0.7" 27 | tower-http = { version = "0.3.5", features = ["full"] } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 graphul-rs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Graphul 4 | 5 |
6 |

7 | 8 |

9 | Graphul is an Express inspired web framework using a powerful extractor system. Designed to improve, speed, and scale your microservices with a friendly syntax, Graphul is built with Rust. that means Graphul gets memory safety, reliability, concurrency, and performance for free. helping to save money on infrastructure. 10 |

11 | 12 | 13 | ## [Buy a Coffee with Bitcoin ☕️](https://github.com/graphul-rs/graphul/blob/main/BUY-A-COFFEE.md) 14 | 15 | [![Discord](https://img.shields.io/discord/1096163462767444130?label=Discord)](https://discord.gg/3WCMgT3KCS) 16 | Join our Discord server to chat with others in the Graphul community! 17 | 18 | ## Install 19 | 20 | #### Create a new project 21 | ``` 22 | $ cargo init hello-app 23 | 24 | $ cd hello-app 25 | ``` 26 | 27 | 28 | #### Add graphul dependency 29 | 30 | ``` 31 | $ cargo add graphul 32 | ``` 33 | 34 | ## ⚡️ Quickstart 35 | 36 | ```rust 37 | use graphul::{Graphul, http::Methods}; 38 | 39 | 40 | #[tokio::main] 41 | async fn main() { 42 | let mut app = Graphul::new(); 43 | 44 | app.get("/", || async { 45 | "Hello, World 👋!" 46 | }); 47 | 48 | app.run("127.0.0.1:8000").await; 49 | } 50 | ``` 51 | 52 | ## 👀 Examples 53 | 54 | Listed below are some of the common examples. If you want to see more code examples , please visit our [Examples Folder](https://github.com/graphul-rs/graphul/tree/main/examples) 55 | 56 | ## common examples 57 | 58 | - [Context](#-context) 59 | - [JSON](#-json) 60 | - [Resource](#-resource) 61 | - [Static files](#-static-files) 62 | - [Groups](#-groups) 63 | - [Share state](#-share-state) 64 | - [Share state with Resource](#-share-state-with-resource) 65 | - [Middleware](#-middleware) 66 | - [Routers](#-routers) 67 | - [Templates](#-templates) 68 | - [Swagger - OpenAPI](https://github.com/graphul-rs/graphul/tree/main/examples/utoipa-swagger-ui) 69 | - ⭐️ help us by adding a star on [GitHub Star](https://github.com/graphul-rs/graphul/stargazers) to the project 70 | 71 | ## 🛫 Graphul vs most famous frameworks out there 72 | 73 | Graphul 74 | 75 | ## 📖 Context 76 | 77 | ```rust 78 | use graphul::{http::Methods, Context, Graphul}; 79 | 80 | #[tokio::main] 81 | async fn main() { 82 | let mut app = Graphul::new(); 83 | 84 | // /samuel?country=Colombia 85 | app.get("/:name", |c: Context| async move { 86 | /* 87 | statically typed query param extraction 88 | let value: Json = match c.parse_params().await 89 | let value: Json = match c.parse_query().await 90 | */ 91 | 92 | let name = c.params("name"); 93 | let country = c.query("country"); 94 | let ip = c.ip(); 95 | 96 | format!("My name is {name}, I'm from {country}, my IP is {ip}",) 97 | }); 98 | 99 | app.run("127.0.0.1:8000").await; 100 | } 101 | ``` 102 | 103 | ## 📖 JSON 104 | 105 | ```rust 106 | use graphul::{Graphul, http::Methods, extract::Json}; 107 | use serde_json::json; 108 | 109 | #[tokio::main] 110 | async fn main() { 111 | let mut app = Graphul::new(); 112 | 113 | app.get("/", || async { 114 | Json(json!({ 115 | "name": "full_name", 116 | "age": 98, 117 | "phones": [ 118 | format!("+44 {}", 8) 119 | ] 120 | })) 121 | }); 122 | 123 | app.run("127.0.0.1:8000").await; 124 | } 125 | ``` 126 | 127 | ## 📖 Resource 128 | 129 | ```rust 130 | use std::collections::HashMap; 131 | 132 | use graphul::{ 133 | async_trait, 134 | extract::Json, 135 | http::{resource::Resource, response::Response, StatusCode}, 136 | Context, Graphul, IntoResponse, 137 | }; 138 | use serde_json::json; 139 | 140 | type ResValue = HashMap; 141 | 142 | struct Article; 143 | 144 | #[async_trait] 145 | impl Resource for Article { 146 | async fn get(c: Context) -> Response { 147 | let posts = json!({ 148 | "posts": ["Article 1", "Article 2", "Article ..."] 149 | }); 150 | (StatusCode::OK, c.json(posts)).into_response() 151 | } 152 | 153 | async fn post(c: Context) -> Response { 154 | // you can use ctx.parse_params() or ctx.parse_query() 155 | let value: Json = match c.payload().await { 156 | Ok(data) => data, 157 | Err(err) => return err.into_response(), 158 | }; 159 | 160 | (StatusCode::CREATED, value).into_response() 161 | } 162 | 163 | // you can use put, delete, head, patch and trace 164 | } 165 | 166 | #[tokio::main] 167 | async fn main() { 168 | let mut app = Graphul::new(); 169 | 170 | app.resource("/article", Article); 171 | 172 | app.run("127.0.0.1:8000").await; 173 | } 174 | ``` 175 | 176 | ## 📖 Static files 177 | 178 | ```rust 179 | use graphul::{Graphul, FolderConfig, FileConfig}; 180 | 181 | #[tokio::main] 182 | async fn main() { 183 | let mut app = Graphul::new(); 184 | 185 | // path = "/static", dir = public 186 | app.static_files("/static", "public", FolderConfig::default()); 187 | 188 | // single page application 189 | app.static_files("/", "app/build", FolderConfig::spa()); 190 | 191 | app.static_file("/about", "templates/about.html", FileConfig::default()); 192 | 193 | app.run("127.0.0.1:8000").await; 194 | } 195 | ``` 196 | 197 | ### 🌟 static files with custom config 198 | 199 | ```rust 200 | use graphul::{Graphul, FolderConfig, FileConfig}; 201 | 202 | #[tokio::main] 203 | async fn main() { 204 | let mut app = Graphul::new(); 205 | 206 | app.static_files("/", "templates", FolderConfig { 207 | // single page application 208 | spa: false, 209 | // it support gzip, brotli and deflate 210 | compress: true, 211 | // Set a specific read buffer chunk size. 212 | // The default capacity is 64kb. 213 | chunk_size: None, 214 | // If the requested path is a directory append `index.html`. 215 | // This is useful for static sites. 216 | index: true, 217 | // fallback - This file will be called if there is no file at the path of the request. 218 | not_found: Some("templates/404.html"), // or None 219 | }); 220 | 221 | app.static_file("/path", "templates/about.html", FileConfig { 222 | // it support gzip, brotli and deflate 223 | compress: true, 224 | chunk_size: Some(65536) // buffer capacity 64KiB 225 | }); 226 | 227 | app.run("127.0.0.1:8000").await; 228 | } 229 | ``` 230 | 231 | ## 📖 Groups 232 | 233 | 234 | ```rust 235 | use graphul::{ 236 | extract::{Path, Json}, 237 | Graphul, 238 | http::{ Methods, StatusCode }, IntoResponse 239 | }; 240 | 241 | use serde_json::json; 242 | 243 | async fn index() -> &'static str { 244 | "index handler" 245 | } 246 | 247 | async fn name(Path(name): Path) -> impl IntoResponse { 248 | let user = json!({ 249 | "response": format!("my name is {}", name) 250 | }); 251 | (StatusCode::CREATED, Json(user)).into_response() 252 | } 253 | 254 | #[tokio::main] 255 | async fn main() { 256 | let mut app = Graphul::new(); 257 | 258 | // GROUP /api 259 | let mut api = app.group("api"); 260 | 261 | // GROUP /api/user 262 | let mut user = api.group("user"); 263 | 264 | // GET POST PUT DELETE ... /api/user 265 | user.resource("/", Article); 266 | 267 | // GET /api/user/samuel 268 | user.get("/:name", name); 269 | 270 | // GROUP /api/post 271 | let mut post = api.group("post"); 272 | 273 | // GET /api/post 274 | post.get("/", index); 275 | 276 | // GET /api/post/all 277 | post.get("/all", || async move { 278 | Json(json!({"message": "hello world!"})) 279 | }); 280 | 281 | app.run("127.0.0.1:8000").await; 282 | } 283 | ``` 284 | 285 | ## 📖 Share state 286 | 287 | ```rust 288 | use graphul::{http::Methods, extract::State, Graphul}; 289 | 290 | #[derive(Clone)] 291 | struct AppState { 292 | data: String 293 | } 294 | 295 | #[tokio::main] 296 | async fn main() { 297 | let state = AppState { data: "Hello, World 👋!".to_string() }; 298 | let mut app = Graphul::share_state(state); 299 | 300 | app.get("/", |State(state): State| async { 301 | state.data 302 | }); 303 | 304 | app.run("127.0.0.1:8000").await; 305 | } 306 | ``` 307 | 308 | ## 📖 Share state with Resource 309 | 310 | ```rust 311 | use graphul::{ 312 | async_trait, 313 | http::{resource::Resource, response::Response, StatusCode}, 314 | Context, Graphul, IntoResponse, 315 | }; 316 | use serde_json::json; 317 | 318 | struct Article; 319 | 320 | #[derive(Clone)] 321 | struct AppState { 322 | data: Vec<&'static str>, 323 | } 324 | 325 | #[async_trait] 326 | impl Resource for Article { 327 | 328 | async fn get(ctx: Context) -> Response { 329 | let article = ctx.state(); 330 | 331 | let posts = json!({ 332 | "posts": article.data, 333 | }); 334 | (StatusCode::OK, ctx.json(posts)).into_response() 335 | } 336 | 337 | // you can use post, put, delete, head, patch and trace 338 | 339 | } 340 | 341 | #[tokio::main] 342 | async fn main() { 343 | let state = AppState { 344 | data: vec!["Article 1", "Article 2", "Article 3"], 345 | }; 346 | let mut app = Graphul::share_state(state); 347 | 348 | app.resource("/article", Article); 349 | 350 | app.run("127.0.0.1:8000").await; 351 | } 352 | ``` 353 | 354 | ## 📖 Middleware 355 | 356 | - [Example using tracing](https://github.com/graphul-rs/graphul/tree/main/examples/tracing-middleware) 357 | 358 | ```rust 359 | use graphul::{ 360 | Req, 361 | middleware::{self, Next}, 362 | http::{response::Response,Methods}, 363 | Graphul 364 | }; 365 | 366 | async fn my_middleware( request: Req, next: Next ) -> Response { 367 | 368 | // your logic 369 | 370 | next.run(request).await 371 | } 372 | 373 | #[tokio::main] 374 | async fn main() { 375 | let mut app = Graphul::new(); 376 | 377 | app.get("/", || async { 378 | "hello world!" 379 | }); 380 | app.middleware(middleware::from_fn(my_middleware)); 381 | 382 | app.run("127.0.0.1:8000").await; 383 | } 384 | ``` 385 | 386 | ## 📖 Routers 387 | 388 | - [Example Multiple Routers](https://github.com/graphul-rs/graphul/tree/main/examples/multiple-routers) 389 | 390 | ```rust 391 | use graphul::{http::Methods, Graphul}; 392 | 393 | #[tokio::main] 394 | async fn main() { 395 | let mut app = Graphul::new(); 396 | 397 | app.get("/", || async { "Home" }); 398 | 399 | // you can use: Graphul::post, Graphul::put, Graphul::delete, Graphul::patch 400 | let route_get = Graphul::get("/hello", || async { "Hello, World 👋!" }); 401 | 402 | // you can also use the `route` variable to add the route to the app 403 | app.add_router(route_get); 404 | 405 | app.run("127.0.0.1:8000").await; 406 | ``` 407 | 408 | #### 💡 Graphul::router 409 | 410 | ```rust 411 | use graphul::{ 412 | Req, 413 | middleware::{self, Next}, 414 | http::{response::Response,Methods}, 415 | Graphul 416 | }; 417 | 418 | async fn my_router() -> Graphul { 419 | let mut router = Graphul::router(); 420 | 421 | router.get("/hi", || async { 422 | "Hey! :)" 423 | }); 424 | // this middleware will be available only on this router 425 | router.middleware(middleware::from_fn(my_middleware)); 426 | 427 | router 428 | } 429 | 430 | #[tokio::main] 431 | async fn main() { 432 | let mut app = Graphul::new(); 433 | 434 | app.get("/", || async { 435 | "hello world!" 436 | }); 437 | 438 | app.add_router(my_router().await); 439 | 440 | app.run("127.0.0.1:8000").await; 441 | } 442 | ``` 443 | 444 | ## 📖 Templates 445 | 446 | ```rust 447 | use graphul::{ 448 | http::Methods, 449 | Context, Graphul, template::HtmlTemplate, 450 | }; 451 | use askama::Template; 452 | 453 | #[derive(Template)] 454 | #[template(path = "hello.html")] 455 | struct HelloTemplate { 456 | name: String, 457 | } 458 | 459 | #[tokio::main] 460 | async fn main() { 461 | let mut app = Graphul::new(); 462 | 463 | app.get("/:name", |c: Context| async move { 464 | let template = HelloTemplate { name: c.params("name") }; 465 | HtmlTemplate(template) 466 | }); 467 | 468 | app.run("127.0.0.1:8000").await; 469 | } 470 | ``` 471 | 472 | ## License 473 | 474 | This project is licensed under the [MIT license](https://github.com/graphul-rs/graphul/blob/main/LICENSE). 475 | 476 | ### Contribution 477 | 478 | Unless you explicitly state otherwise, any contribution intentionally submitted 479 | for inclusion in `Graphul` by you, shall be licensed as MIT, without any 480 | additional terms or conditions. 481 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This folder contains numerous example showing how to use Graphul. Each example is 4 | setup as its own crate so its dependencies are clear. 5 | -------------------------------------------------------------------------------- /examples/chat-websocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-chat" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1", features = ["full"] } 10 | futures = "0.3" 11 | -------------------------------------------------------------------------------- /examples/chat-websocket/src/chat/domain.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use tokio::sync::broadcast; 7 | 8 | // Our shared state 9 | pub struct AppState { 10 | // We require unique usernames. This tracks which usernames have been taken. 11 | pub user_set: Mutex>, 12 | // Channel used to send messages to all connected clients. 13 | pub tx: broadcast::Sender, 14 | } 15 | 16 | pub fn app_state() -> Arc { 17 | // Set up application state for use with share_state. 18 | let user_set = Mutex::new(HashSet::new()); 19 | let (tx, _rx) = broadcast::channel(100); 20 | 21 | Arc::new(AppState { user_set, tx }) 22 | } 23 | -------------------------------------------------------------------------------- /examples/chat-websocket/src/chat/handlers.rs: -------------------------------------------------------------------------------- 1 | use futures::{sink::SinkExt, stream::StreamExt}; 2 | use graphul::{ 3 | extract::{ 4 | ws::{Message, WebSocket, WebSocketUpgrade}, 5 | State, 6 | }, 7 | IntoResponse, 8 | }; 9 | use std::sync::Arc; 10 | 11 | use crate::domain; 12 | 13 | pub async fn websocket_handler( 14 | ws: WebSocketUpgrade, 15 | State(state): State>, 16 | ) -> impl IntoResponse { 17 | ws.on_upgrade(|socket| websocket(socket, state)) 18 | } 19 | 20 | // This function deals with a single websocket connection, i.e., a single 21 | // connected client / user, for which we will spawn two independent tasks (for 22 | // receiving / sending chat messages). 23 | async fn websocket(stream: WebSocket, state: Arc) { 24 | // By splitting, we can send and receive at the same time. 25 | let (mut sender, mut receiver) = stream.split(); 26 | 27 | // Username gets set in the receive loop, if it's valid. 28 | let mut username = String::new(); 29 | // Loop until a text message is found. 30 | while let Some(Ok(message)) = receiver.next().await { 31 | if let Message::Text(name) = message { 32 | // If username that is sent by client is not taken, fill username string. 33 | check_username(&state, &mut username, &name); 34 | 35 | // If not empty we want to quit the loop else we want to quit function. 36 | if !username.is_empty() { 37 | break; 38 | } else { 39 | // Only send our client that username is taken. 40 | let _ = sender 41 | .send(Message::Text(String::from("Username already taken."))) 42 | .await; 43 | 44 | return; 45 | } 46 | } 47 | } 48 | 49 | // We subscribe *before* sending the "joined" message, so that we will also 50 | // display it to our client. 51 | let mut rx = state.tx.subscribe(); 52 | 53 | // Now send the "joined" message to all subscribers. 54 | let msg = format!("{} joined.", username); 55 | println!("{}", msg); 56 | let _ = state.tx.send(msg); 57 | 58 | // Spawn the first task that will receive broadcast messages and send text 59 | // messages over the websocket to our client. 60 | let mut send_task = tokio::spawn(async move { 61 | while let Ok(msg) = rx.recv().await { 62 | // In any websocket error, break loop. 63 | if sender.send(Message::Text(msg)).await.is_err() { 64 | break; 65 | } 66 | } 67 | }); 68 | 69 | // Clone things we want to pass (move) to the receiving task. 70 | let tx = state.tx.clone(); 71 | let name = username.clone(); 72 | 73 | // Spawn a task that takes messages from the websocket, prepends the user 74 | // name, and sends them to all broadcast subscribers. 75 | let mut recv_task = tokio::spawn(async move { 76 | while let Some(Ok(Message::Text(text))) = receiver.next().await { 77 | // Add username before message. 78 | let _ = tx.send(format!("{}: {}", name, text)); 79 | } 80 | }); 81 | 82 | // If any one of the tasks run to completion, we abort the other. 83 | tokio::select! { 84 | _ = (&mut send_task) => recv_task.abort(), 85 | _ = (&mut recv_task) => send_task.abort(), 86 | }; 87 | 88 | // Send "user left" message (similar to "joined" above). 89 | let msg = format!("{} left.", username); 90 | println!("{}", msg); 91 | let _ = state.tx.send(msg); 92 | 93 | // Remove username from map so new clients can take it again. 94 | state.user_set.lock().unwrap().remove(&username); 95 | } 96 | 97 | fn check_username(state: &domain::AppState, string: &mut String, name: &str) { 98 | let mut user_set = state.user_set.lock().unwrap(); 99 | 100 | if !user_set.contains(name) { 101 | user_set.insert(name.to_owned()); 102 | 103 | string.push_str(name); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /examples/chat-websocket/src/chat/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod domain; 2 | pub mod handlers; 3 | -------------------------------------------------------------------------------- /examples/chat-websocket/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Example chat application. 2 | //! 3 | 4 | mod chat; 5 | 6 | use chat::{domain, handlers}; 7 | 8 | use graphul::{http::Methods, FileConfig, Graphul}; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let state = domain::app_state(); 13 | 14 | let mut app = Graphul::share_state(state); 15 | 16 | app.static_file("/", "templates/index.html", FileConfig::default()); 17 | 18 | app.get("/websocket", handlers::websocket_handler); 19 | 20 | app.run("127.0.0.1:3000").await; 21 | } 22 | -------------------------------------------------------------------------------- /examples/chat-websocket/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Chat 6 | 7 | 8 |

WebSocket Chat Example With Graphul

9 | 10 | 11 | 12 | 13 | 14 | 15 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/file-multipart-form/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-file-multipart-form" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1.0", features = ["full"] } 10 | -------------------------------------------------------------------------------- /examples/file-multipart-form/src/main.rs: -------------------------------------------------------------------------------- 1 | use graphul::{ 2 | extract::{DefaultBodyLimit, Multipart}, 3 | http::response::Html, 4 | http::Methods, 5 | ContextPart, Graphul, 6 | }; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | // build our application with some routes 11 | let mut app = Graphul::new(); 12 | app.get("/", show_form); 13 | app.post("/", accept_form); 14 | 15 | // limit the size of the file 16 | app.middleware(DefaultBodyLimit::max(250 * 1024 * 1024 /* 250mb */)); 17 | 18 | app.run("0.0.0.0:3000").await; 19 | } 20 | 21 | async fn show_form() -> Html<&'static str> { 22 | Html( 23 | r#" 24 | 25 | 26 | 27 | 28 |
29 | 33 | 34 | 35 |
36 | 37 | 38 | "#, 39 | ) 40 | } 41 | 42 | async fn accept_form(ctx: ContextPart, mut multipart: Multipart) { 43 | while let Some(field) = multipart.next_field().await.unwrap() { 44 | let name = field.name().unwrap().to_string(); 45 | 46 | // POST /?name=my_new_file_name 47 | let mut file_name = ctx.query("name"); 48 | if file_name.is_empty() { 49 | file_name = field.file_name().unwrap().to_string(); 50 | } 51 | 52 | let content_type = field.content_type().unwrap().to_string(); 53 | let data = field.bytes().await.unwrap(); 54 | 55 | println!( 56 | "Length of `{}` (`{}`: `{}`) is {} bytes", 57 | name, 58 | file_name, 59 | content_type, 60 | data.len() 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-hello-world" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1.0", features = ["full"] } 10 | -------------------------------------------------------------------------------- /examples/hello-world/src/main.rs: -------------------------------------------------------------------------------- 1 | use graphul::{http::Methods, Graphul, Context}; 2 | 3 | fn api_router() -> Graphul { 4 | let mut router = Graphul::router(); 5 | 6 | router.get("/users/:id", |c: Context| async move { 7 | format!("User with id: {}", c.params("id")) 8 | }); 9 | 10 | router 11 | } 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let mut app = Graphul::new(); 16 | 17 | app.get("/", || async { "Home" }); 18 | 19 | app.add_router(api_router()); 20 | 21 | app.run("127.0.0.1:8000").await; 22 | } 23 | -------------------------------------------------------------------------------- /examples/multiple-routers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-multiple-routers" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1", features = ["full"] } 10 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/main.rs: -------------------------------------------------------------------------------- 1 | mod middlewares; 2 | mod routes; 3 | 4 | use graphul::{http::Methods, Graphul}; 5 | use routes::routes; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let mut app = Graphul::new(); 10 | 11 | app.get("/login", || async { "Login!" }); 12 | 13 | app.add_router(routes().await); 14 | 15 | // print routes on the console 16 | // it will print : 17 | // ["GET /login", "GET /about", "POST /article", "GET /article/:id", "GET /admin"] 18 | println!("{:?}", app.routes()); 19 | 20 | app.run("127.0.0.1:8000").await; 21 | } 22 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/middlewares.rs: -------------------------------------------------------------------------------- 1 | pub mod check_user; 2 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/middlewares/check_user.rs: -------------------------------------------------------------------------------- 1 | use graphul::{ 2 | http::response::{Redirect, Response}, 3 | middleware::Next, 4 | ContextPart, IntoResponse, Req, 5 | }; 6 | 7 | pub async fn check_user_is_authenticated(ctx: ContextPart, request: Req, next: Next) -> Response { 8 | let login = ctx.query("login"); 9 | if login == "false" || login.is_empty() { 10 | return Redirect::to("/login").into_response(); 11 | } 12 | next.run(request).await 13 | } 14 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/routes.rs: -------------------------------------------------------------------------------- 1 | mod about; 2 | mod admin; 3 | mod article; 4 | 5 | use graphul::Graphul; 6 | 7 | pub async fn routes() -> Graphul { 8 | let mut router = Graphul::router(); 9 | 10 | router.add_routers(vec![ 11 | about::routes().await, 12 | article::routes().await, 13 | admin::routes().await, 14 | ]); 15 | 16 | router 17 | } 18 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/routes/about.rs: -------------------------------------------------------------------------------- 1 | use graphul::{http::Methods, Graphul}; 2 | 3 | async fn about() -> &'static str { 4 | "About this page ..." 5 | } 6 | 7 | pub async fn routes() -> Graphul { 8 | let mut router = Graphul::router(); 9 | 10 | router.get("/about", about); 11 | 12 | router 13 | } 14 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/routes/admin.rs: -------------------------------------------------------------------------------- 1 | use graphul::{http::Methods, middleware, Graphul}; 2 | 3 | use crate::middlewares::check_user::check_user_is_authenticated; 4 | 5 | async fn admin() -> &'static str { 6 | "Protected Admin Route" 7 | } 8 | 9 | pub async fn routes() -> Graphul { 10 | let mut router = Graphul::router(); 11 | 12 | // http://127.0.0.1:8000/admin?login=true 13 | router.get("/admin", admin); 14 | router.middleware(middleware::from_fn(check_user_is_authenticated)); 15 | 16 | router 17 | } 18 | -------------------------------------------------------------------------------- /examples/multiple-routers/src/routes/article.rs: -------------------------------------------------------------------------------- 1 | use graphul::{http::Methods, Context, Graphul}; 2 | 3 | async fn article() -> &'static str { 4 | "Article list" 5 | } 6 | 7 | async fn get_article(ctx: Context) -> String { 8 | format!("Article id: {}", ctx.params("id")) 9 | } 10 | 11 | pub async fn routes() -> Graphul { 12 | let mut router = Graphul::router(); 13 | 14 | let mut article_group = router.group("article"); 15 | 16 | // http://127.0.0.1:8000/article 17 | article_group.post("/", article); 18 | 19 | // http://127.0.0.1:8000/article/my_id 20 | article_group.get("/:id", get_article); 21 | 22 | router 23 | } 24 | -------------------------------------------------------------------------------- /examples/rate-limiter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rate-limiter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1.0", features = ["full"] } 10 | -------------------------------------------------------------------------------- /examples/rate-limiter/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use graphul::{http::Methods, Graphul, middleware::limit::RateLimitLayer}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let mut app = Graphul::new(); 8 | 9 | app.get("/", || async { 10 | "hello world!" 11 | }); 12 | // 1000 requests per 10 seconds max 13 | app.middleware(RateLimitLayer::new(1000, Duration::from_secs(100))); 14 | 15 | app.run("127.0.0.1:8000").await; 16 | } -------------------------------------------------------------------------------- /examples/redirect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-redirect" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1.0", features = ["full"] } 10 | -------------------------------------------------------------------------------- /examples/redirect/src/main.rs: -------------------------------------------------------------------------------- 1 | use graphul::{ 2 | http::{response::Redirect, Methods}, 3 | Context, Graphul, IntoResponse, 4 | }; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let mut app = Graphul::new(); 9 | 10 | // http://127.0.0.1:8000?redirect=true 11 | app.get("/", |c: Context| async move { 12 | // Redirect::temporary(uri) Create a new Redirect that uses a 307 Temporary Redirect status code. 13 | // Redirect::permanent(uri) Create a new Redirect that uses a 308 Permanent Redirect status code. 14 | if c.query("redirect") == "true" { 15 | return Redirect::to("/hi/samuel").into_response(); // Create a new Redirect that uses a 303 See Other status code. 16 | } 17 | "index".into_response() 18 | }); 19 | 20 | app.get("/hi/:name", |c: Context| async move { 21 | format!("hello, {}", c.params("name")) 22 | }); 23 | 24 | app.run("127.0.0.1:8000").await; 25 | } 26 | -------------------------------------------------------------------------------- /examples/shuttle-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shuttle-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | 9 | 10 | [dependencies] 11 | graphul = { path = "../../." } 12 | shuttle-service = { version = "0.8.0", features = ["web-axum"] } 13 | sync_wrapper = "0.1" -------------------------------------------------------------------------------- /examples/shuttle-example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use graphul::{http::Methods, Graphul}; 2 | use sync_wrapper::SyncWrapper; 3 | 4 | #[shuttle_service::main] 5 | async fn graphul() -> shuttle_service::ShuttleAxum { 6 | let mut app = Graphul::new(); 7 | 8 | app.get("/", || async { "Hello, World 👋!" }); 9 | let sync_wrapper = SyncWrapper::new(app.export_routes()); 10 | 11 | Ok(sync_wrapper) 12 | } 13 | -------------------------------------------------------------------------------- /examples/sqlx-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-sqlx-postgres" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../."} 9 | tokio = { version = "1.0", features = ["full"] } 10 | sqlx = { version = "0.5.10", features = ["runtime-tokio-rustls", "any", "postgres"] } 11 | -------------------------------------------------------------------------------- /examples/sqlx-postgres/src/db.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use graphul::http::StatusCode; 4 | use sqlx::{postgres::PgPoolOptions, Error, Pool, Postgres}; 5 | 6 | pub async fn db_con() -> Result, Error> { 7 | let db_uri = std::env::var("DATABASE_URL") 8 | .unwrap_or_else(|_| "postgres://postgres:password@localhost".to_string()); 9 | 10 | PgPoolOptions::new() 11 | .max_connections(5) 12 | .connect_timeout(Duration::from_secs(3)) 13 | .connect(&db_uri) 14 | .await 15 | } 16 | 17 | /// Utility function for mapping any error into a `500 Internal Server Error` 18 | /// response. 19 | pub fn internal_error(err: E) -> (StatusCode, String) 20 | where 21 | E: std::error::Error, 22 | { 23 | (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/sqlx-postgres/src/main.rs: -------------------------------------------------------------------------------- 1 | mod db; 2 | 3 | use graphul::{ 4 | http::{Methods, StatusCode}, 5 | Context, Graphul, 6 | }; 7 | use sqlx::PgPool; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let pool = db::db_con().await.expect("can connect to database"); 12 | 13 | // build our application 14 | let mut app = Graphul::share_state(pool); 15 | 16 | app.get("/", using_connection_pool); 17 | 18 | app.run("127.0.0.1:3000").await; 19 | } 20 | 21 | // we can extract the connection pool with `State` or `Context` 22 | async fn using_connection_pool(c: Context) -> Result { 23 | let pool = c.state(); 24 | sqlx::query_scalar("select 'hello world from pg'") 25 | .fetch_one(pool) 26 | .await 27 | .map_err(db::internal_error) 28 | } 29 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-static-react-spa-app" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1.0", features = ["full"] } 10 | serde_json = "1.0" 11 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Graphul and React 2 | 3 | Graphul supports native spa routes :) 4 | 5 | ## Available Scripts 6 | 7 | ### Requirements: 8 | 9 | * Nodejs 10 | * Npm 11 | * Rust :) 12 | 13 | In the project directory, you can run: 14 | 15 | ### `bash run.sh` 16 | 17 | Runs the graphul server.\ 18 | Open [http://127.0.0.1:8000](http://127.0.0.1:8000) to view it in your browser. 19 | 20 | Graphul 21 | 22 | ### Detail page 23 | 24 | [http://127.0.0.1:8000/6](http://127.0.0.1:8000/6) 25 | 26 | Graphul 27 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "history": "^5.3.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-router-dom": "^6.4.2", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/examples/static-react-spa-app/app/public/detail.png -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/examples/static-react-spa-app/app/public/favicon.ico -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 29 | React App 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/examples/static-react-spa-app/app/public/index.png -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/examples/static-react-spa-app/app/public/logo192.png -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/examples/static-react-spa-app/app/public/logo512.png -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/index.js: -------------------------------------------------------------------------------- 1 | import reportWebVitals from './reportWebVitals'; 2 | import React from "react"; 3 | import { createRoot } from "react-dom/client"; 4 | import { 5 | createBrowserRouter, 6 | RouterProvider, 7 | } from "react-router-dom"; 8 | import Home from './pages/Home'; 9 | import Detail from './pages/Detail'; 10 | 11 | const router = createBrowserRouter([ 12 | { 13 | path: "/", 14 | element: , 15 | }, 16 | { 17 | path: "/:id", 18 | element: , 19 | }, 20 | ]); 21 | 22 | createRoot(document.getElementById("root")).render( 23 | 24 | ); 25 | 26 | // If you want to start measuring performance in your app, pass a function 27 | // to log results (for example: reportWebVitals(console.log)) 28 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 29 | reportWebVitals(); 30 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/pages/Detail.js: -------------------------------------------------------------------------------- 1 | import {React, useEffect, useState} from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | export default function Detail() { 5 | let [data, setData] = useState(null) 6 | let { id } = useParams(); 7 | 8 | let graphul_data = async () => { 9 | const response = await fetch(`http://127.0.0.1:8000/api/article/${id}`); 10 | const data = await response.json(); 11 | setData(data) 12 | } 13 | 14 | useEffect(() => { 15 | graphul_data() 16 | }, []) 17 | 18 | return ( 19 |
20 |
21 | { !data &&

Loading..

} 22 | { data && 23 |
24 |

{data.title}

25 | 26 |
27 |
28 | 29 | 30 |
31 |

{data.body}

32 | Author: {data.user_name} 33 |
34 |
35 |
36 |
} 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import {React, useEffect, useState} from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | function Home() { 5 | 6 | let [data, setData] = useState([]) 7 | 8 | let graphul_data = async () => { 9 | const response = await fetch(`http://127.0.0.1:8000/api/articles`); 10 | const data = await response.json(); 11 | setData(data) 12 | } 13 | 14 | useEffect(() => { 15 | graphul_data() 16 | }, []) 17 | 18 | return ( 19 |
20 |
21 |
22 |

Graphul Blog

23 |
24 | {data.map((item) => 25 |
26 | 27 | 28 |
29 | 30 | {item.title} 31 | 32 | 33 | Author: {item.user_name} 34 |
35 |
36 | )} 37 |
38 |
39 |
40 |
41 | ); 42 | } 43 | 44 | export default Home; 45 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/app/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/run.sh: -------------------------------------------------------------------------------- 1 | cd app && npm run build && cd .. 2 | cargo run 3 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/src/data.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{json, Value}; 2 | 3 | pub fn articles() -> Value { 4 | json!( 5 | [ 6 | { 7 | "id": 1, 8 | "user_id": 3678, 9 | "user_name": "Samuel", 10 | "img": "https://images.unsplash.com/photo-1515378960530-7c0da6231fb1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80", 11 | "title": "Bardus truculenter suspendo patruus beatus voluptatem vito alter arcesso demens supra deprecator ut aveho somnus.", 12 | "body": "Laboriosam curriculum autem. Claudeo viduo tunc. Defendo arca conventus. Suscipio summa basium. Sed aer arguo. Acer volo quaerat. Bardus ulterius arguo. Creo suppono dolores. Depromo absorbeo et. Strenuus villa temperantia. Non xiphias iusto. Solitudo tenuis in. Cupiditas campana desipio. Aduro sollicito cervus. Molestias curia qui. Argumentum possimus unus. Inflammatio ut nemo. Stipes pecunia nihil. Autus facilis ancilla. Omnis aggero victus." 13 | }, 14 | { 15 | "id": 2, 16 | "user_id": 3676, 17 | "user_name": "Samuel", 18 | "img": "https://images.unsplash.com/photo-1497032628192-86f99bcd76bc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80", 19 | "title": "Maxime vae cogito conspergo accedo arbustum ultra sed acsi cornu decet undique quia sint aggredior viduo tantillus assumenda.", 20 | "body": "Solium valeo ante. Vicinus omnis tenetur. Utrimque tristis ex. Tui magni adnuo. Et delibero comis. Bis celebrer creo. Delectatio victoria apud. Stabilis vulgivagus triumphus. Ut cras trans. Denuo volva vivo. Nihil conventus conatus. Veritatis comedo terga. Stultus vito crux. Caute aedificium optio. Est desparatus asper. Tremo eligendi virtus. Acceptus tracto canonicus. Solium taceo molestias. Vomica deporto veritas. Patria conqueror cum." 21 | }, 22 | { 23 | "id": 3, 24 | "user_id": 3675, 25 | "user_name": "Samuel", 26 | "img": "https://images.unsplash.com/photo-1544654803-b69140b285a1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80", 27 | "title": "Ut pecunia ver accommodo triginta aliquid repellat.", 28 | "body": "Accommodo adimpleo cultellus. Magnam atrocitas et. Eligendi cur tres. Acceptus vinculum sto. Tergum appello umbra. Sumo timidus aeneus. Deprimo totidem verus. Cognomen subito concido. Ultio sit triginta. Altus amiculum cetera. Cimentarius laudantium assentator. Ullam nostrum degero. Nulla verbera amita. Ea aeger delego. Quia temporibus terreo. Vere aut cibo. Adinventitias utpote ager. Delectatio magnam candidus. Cuppedia valde anser. Pel cogito denuncio." 29 | }, 30 | { 31 | "id": 4, 32 | "user_id": 3674, 33 | "user_name": "Samuel", 34 | "img": "https://images.unsplash.com/photo-1530099486328-e021101a494a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1547&q=80", 35 | "title": "Demo validus fugiat vallum crur causa beatus dedico canis aggero videlicet vulgus suscipio ait denique nemo.", 36 | "body": "Aeger ut quidem. Iste aegre aggero. Adduco et sed. Teres id vulnus. Eius id surculus. Ut speciosus adhuc. Arcus vorax valeo. Vel demulceo eveniet. Sequi tenetur arto. Nemo xiphias eligendi. Et magni textus. Consequatur correptius tametsi. Clementia claustrum animi. Contabesco vesica aliquam. Crudelis curto volutabrum. Viscus id ter. Celo teneo cumque. Articulus pecto ter. Virtus cena tego. Trucido decens angulus. Avarus explicabo illum. Vel sortitus canonicus. Aestus delectatio theologus." 37 | }, 38 | { 39 | "id": 5, 40 | "user_id": 3674, 41 | "user_name": "Samuel", 42 | "img": "https://images.unsplash.com/photo-1521737604893-d14cc237f11d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1484&q=80", 43 | "title": "Cupiditas sponte fugit careo vilitas strenuus culpo vomica est tenus.", 44 | "body": "Capitulus possimus voluptas. Cerno magnam spiculum. Adiuvo auctus tremo. Cogo suscipio aliquam. Conculco autem cibus. Votum thesis careo. Ago benigne conspergo. Ars cras uberrime. Corona toties numquam. Aegrus alter vaco. Correptius dapifer aegrotatio. Accusantium nihil vinitor. Thymbra vinitor id. Statua adsuesco vito. Tametsi audio accipio. Video auris officia. Coruscus ulterius volo. Sodalitas cohors pariatur. Paulatim taceo nam. Et consequatur bellicus." 45 | }, 46 | { 47 | "id": 6, 48 | "user_id": 3668, 49 | "user_name": "Samuel", 50 | "img": "https://images.unsplash.com/photo-1624996379697-f01d168b1a52?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80", 51 | "title": "Vorago coerceo tergiversatio conventus volup beneficium cruciamentum strues vulpes stipes atque ventus voluptatem defleo.", 52 | "body": "Eligendi tabernus conor. Vita crepusculum votum. Voluptatibus saepe numquam. Damno consequatur facilis. Adsuesco adnuo aetas. Sublime conicio suadeo. Attonbitus nam verumtamen. Capillus vobis viridis. Delego casso delinquo. Valens solum beneficium. Texo suffragium custodia. Traho tabella color. Via clementia maxime. Peccatus causa conor. Demonstro ut cometes. Textilis voluptatem crux. Cena antepono sufficio. Aequitas trans atrocitas. Paens fuga et. Beatus accommodo solus." 53 | } 54 | ] 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /examples/static-react-spa-app/src/main.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | 3 | use data::articles; 4 | use graphul::{ 5 | http::{utils::Method, Methods, StatusCode}, 6 | middleware::tower::cors, 7 | Context, FolderConfig, Graphul, IntoResponse, 8 | }; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let mut app = Graphul::new(); 13 | 14 | app.static_files("/", "app/build", FolderConfig::spa()); 15 | 16 | app.get( 17 | "/api/articles", 18 | |c: Context| async move { c.json(articles()) }, 19 | ); 20 | 21 | app.get("/api/article/:id", |c: Context| async move { 22 | let id = match c.params("id").parse::() { 23 | Ok(id) => id - 1, 24 | Err(_) => { 25 | return (StatusCode::BAD_REQUEST, "Id is not a number").into_response(); 26 | } 27 | }; 28 | match articles().get(id) { 29 | Some(item) => c.json(item.clone()).into_response(), 30 | None => (StatusCode::NOT_FOUND, "article does not exist :(").into_response(), 31 | } 32 | }); 33 | 34 | app.middleware( 35 | cors::CorsLayer::new() 36 | // allow `GET` and `POST` when accessing the resource 37 | .allow_methods([Method::GET, Method::POST]) 38 | // allow requests from any origin 39 | .allow_origin(cors::Any), 40 | ); 41 | 42 | app.run("127.0.0.1:8000").await; 43 | } 44 | -------------------------------------------------------------------------------- /examples/templates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-templates" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | askama = "0.11" 9 | graphul = { path = "../../." } 10 | tokio = { version = "1.0", features = ["full"] } 11 | -------------------------------------------------------------------------------- /examples/templates/src/main.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use graphul::{http::Methods, template::HtmlTemplate, Context, Graphul}; 3 | 4 | #[derive(Template)] 5 | #[template(path = "hello.html")] 6 | struct HelloTemplate { 7 | name: String, 8 | } 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let mut app = Graphul::new(); 13 | 14 | app.get("/:name", |c: Context| async move { 15 | let template = HelloTemplate { 16 | name: c.params("name"), 17 | }; 18 | HtmlTemplate(template) 19 | }); 20 | 21 | app.run("127.0.0.1:8000").await; 22 | } 23 | -------------------------------------------------------------------------------- /examples/templates/templates/hello.html: -------------------------------------------------------------------------------- 1 |

Hello, {{ name }}!

2 | -------------------------------------------------------------------------------- /examples/tracing-middleware/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-middleware" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | graphul = { path = "../../." } 10 | tokio = { version = "1.0", features = ["full"] } 11 | tracing = "0.1.36" 12 | tracing-subscriber = "0.3.15" 13 | -------------------------------------------------------------------------------- /examples/tracing-middleware/src/main.rs: -------------------------------------------------------------------------------- 1 | use graphul::{http::Methods, middleware, Graphul}; 2 | 3 | pub mod middlewares; 4 | use middlewares::tracing::tracing_middleware; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let mut app = Graphul::new(); 9 | tracing_subscriber::fmt().init(); 10 | 11 | // router 12 | app.get("/", || async { "hello world!" }); 13 | app.middleware(middleware::from_fn(tracing_middleware)); 14 | app.run("127.0.0.1:8000").await; 15 | } 16 | -------------------------------------------------------------------------------- /examples/tracing-middleware/src/middlewares/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tracing; 2 | -------------------------------------------------------------------------------- /examples/tracing-middleware/src/middlewares/tracing.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use graphul::{http::response::Response, middleware::Next, Req}; 4 | use tracing::{Instrument, Level}; 5 | 6 | pub async fn tracing_middleware(request: Req, next: Next) -> Response { 7 | let span = tracing::span!( 8 | Level::INFO, 9 | "Request", 10 | path = request.uri().to_string(), 11 | version = ?request.version(), 12 | method = request.method().to_string() 13 | ); 14 | 15 | async move { 16 | let now = Instant::now(); 17 | let response = next.run(request).await; 18 | let duration = now.elapsed(); 19 | 20 | tracing::info!( 21 | status = response.status().to_string(), 22 | duration = ?duration, 23 | "Response" 24 | ); 25 | response 26 | } 27 | .instrument(span) 28 | .await 29 | } 30 | -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utoipa-swagger-ui" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | hyper = { version = "0.14", features = ["full"] } 10 | tokio = { version = "1.0", features = ["full"] } 11 | tower = "0.4" 12 | utoipa = { version="3.0.1", features = ["axum_extras"] } 13 | utoipa-swagger-ui = { version="3.0.2", features = ["axum"] } 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | env_logger = "0.10.0" 17 | log = "0.4" 18 | -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/README.md: -------------------------------------------------------------------------------- 1 | ## Swagger-ui 2 | 3 | run 4 | ``` 5 | cargo run 6 | ``` 7 | 8 | Go to 9 | 10 | ``` 11 | http://127.0.0.1:8000/swagger-ui/ 12 | ``` 13 | 14 | Graphul 15 | -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/img/todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/examples/utoipa-swagger-ui/img/todo.png -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/src/main.rs: -------------------------------------------------------------------------------- 1 | mod routes; 2 | mod swagger; 3 | 4 | use std::sync::Arc; 5 | 6 | use routes::todo::{self, Store}; 7 | 8 | use graphul::{http::Methods, Graphul}; 9 | use swagger::ApiDoc; 10 | use utoipa::OpenApi; 11 | use utoipa_swagger_ui::SwaggerUi; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let store = Arc::new(Store::default()); 16 | let mut app = Graphul::share_state(store); 17 | 18 | let swagger = SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", ApiDoc::openapi()); 19 | app.merge(swagger); 20 | 21 | app.get("/todo", todo::list_todos); 22 | app.post("/todo", todo::create_todo); 23 | 24 | app.get("/todo/search", todo::search_todos); 25 | 26 | app.put("/todo/:id", todo::mark_done); 27 | app.delete("/todo/:id", todo::delete_todo); 28 | 29 | app.run("127.0.0.1:8000").await; 30 | } 31 | -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod todo; 2 | -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/src/routes/todo.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::swagger::{Todo, TodoError}; 4 | use graphul::{ 5 | extract::{Json, Path, Query}, 6 | Context, ContextPart, IntoResponse, 7 | }; 8 | use hyper::{HeaderMap, StatusCode}; 9 | use serde::Deserialize; 10 | use tokio::sync::Mutex; 11 | use utoipa::IntoParams; 12 | 13 | /// In-memory todo store 14 | pub type Store = Mutex>; 15 | 16 | /// List all Todo items 17 | /// 18 | /// List all Todo items from in-memory storage. 19 | #[utoipa::path( 20 | get, 21 | path = "/todo", 22 | responses( 23 | (status = 200, description = "List all todos successfully", body = [Todo]) 24 | ) 25 | )] 26 | pub async fn list_todos(ctx: Context>) -> Json> { 27 | let todos = ctx.state().lock().await.clone(); 28 | 29 | Json(todos) 30 | } 31 | 32 | /// Todo search query 33 | #[derive(Deserialize, IntoParams)] 34 | pub struct TodoSearchQuery { 35 | /// Search by value. Search is incase sensitive. 36 | value: String, 37 | /// Search by `done` status. 38 | done: bool, 39 | } 40 | 41 | /// Search Todos by query params. 42 | /// 43 | /// Search `Todo`s by query params and return matching `Todo`s. 44 | #[utoipa::path( 45 | get, 46 | path = "/todo/search", 47 | params( 48 | TodoSearchQuery 49 | ), 50 | responses( 51 | (status = 200, description = "List matching todos by query", body = [Todo]) 52 | ) 53 | )] 54 | pub async fn search_todos( 55 | query: Query, 56 | ctx: Context>, 57 | ) -> Json> { 58 | Json( 59 | ctx.state() 60 | .lock() 61 | .await 62 | .iter() 63 | .filter(|todo| { 64 | todo.value.to_lowercase() == query.value.to_lowercase() && todo.done == query.done 65 | }) 66 | .cloned() 67 | .collect(), 68 | ) 69 | } 70 | 71 | /// Create new Todo 72 | /// 73 | /// Tries to create a new Todo item to in-memory storage or fails with 409 conflict if already exists. 74 | #[utoipa::path( 75 | post, 76 | path = "/todo", 77 | request_body = Todo, 78 | responses( 79 | (status = 201, description = "Todo item created successfully", body = Todo), 80 | (status = 409, description = "Todo already exists", body = TodoError) 81 | ) 82 | )] 83 | pub async fn create_todo( 84 | ctx: ContextPart>, 85 | Json(todo): Json, 86 | ) -> impl IntoResponse { 87 | let mut todos = ctx.state().lock().await; 88 | 89 | todos 90 | .iter_mut() 91 | .find(|existing_todo| existing_todo.id == todo.id) 92 | .map(|found| { 93 | ( 94 | StatusCode::CONFLICT, 95 | Json(TodoError::Conflict(format!( 96 | "todo already exists: {}", 97 | found.id 98 | ))), 99 | ) 100 | .into_response() 101 | }) 102 | .unwrap_or_else(|| { 103 | todos.push(todo.clone()); 104 | 105 | (StatusCode::CREATED, Json(todo)).into_response() 106 | }) 107 | } 108 | 109 | /// Mark Todo item done by id 110 | /// 111 | /// Mark Todo item done by given id. Return only status 200 on success or 404 if Todo is not found. 112 | #[utoipa::path( 113 | put, 114 | path = "/todo/{id}", 115 | responses( 116 | (status = 200, description = "Todo marked done successfully"), 117 | (status = 404, description = "Todo not found") 118 | ), 119 | params( 120 | ("id" = i32, Path, description = "Todo database id") 121 | ), 122 | security( 123 | (), // <-- make optional authentication 124 | ("api_key" = []) 125 | ) 126 | )] 127 | pub async fn mark_done( 128 | Path(id): Path, 129 | headers: HeaderMap, 130 | ctx: Context>, 131 | ) -> StatusCode { 132 | match check_api_key(false, headers) { 133 | Ok(_) => (), 134 | Err(_) => return StatusCode::UNAUTHORIZED, 135 | } 136 | 137 | let mut todos = ctx.state().lock().await; 138 | 139 | todos 140 | .iter_mut() 141 | .find(|todo| todo.id == id) 142 | .map(|todo| { 143 | todo.done = true; 144 | StatusCode::OK 145 | }) 146 | .unwrap_or(StatusCode::NOT_FOUND) 147 | } 148 | 149 | /// Delete Todo item by id 150 | /// 151 | /// Delete Todo item from in-memory storage by id. Returns either 200 success of 404 with TodoError if Todo is not found. 152 | #[utoipa::path( 153 | delete, 154 | path = "/todo/{id}", 155 | responses( 156 | (status = 200, description = "Todo marked done successfully"), 157 | (status = 401, description = "Unauthorized to delete Todo", body = TodoError, example = json!(TodoError::Unauthorized(String::from("missing api key")))), 158 | (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) 159 | ), 160 | params( 161 | ("id" = i32, Path, description = "Todo database id") 162 | ), 163 | security( 164 | ("api_key" = []) 165 | ) 166 | )] 167 | pub async fn delete_todo( 168 | Path(id): Path, 169 | headers: HeaderMap, 170 | ctx: Context>, 171 | ) -> impl IntoResponse { 172 | match check_api_key(true, headers) { 173 | Ok(_) => (), 174 | Err(error) => return error.into_response(), 175 | } 176 | 177 | let mut todos = ctx.state().lock().await; 178 | 179 | let len = todos.len(); 180 | 181 | todos.retain(|todo| todo.id != id); 182 | 183 | if todos.len() != len { 184 | StatusCode::OK.into_response() 185 | } else { 186 | ( 187 | StatusCode::NOT_FOUND, 188 | Json(TodoError::NotFound(format!("id = {id}"))), 189 | ) 190 | .into_response() 191 | } 192 | } 193 | 194 | // normally you should create a middleware for this but this is sufficient for sake of example. 195 | fn check_api_key(require_api_key: bool, headers: HeaderMap) -> Result<(), impl IntoResponse> { 196 | match headers.get("todo_apikey") { 197 | Some(header) if header != "utoipa-rocks" => Err(( 198 | StatusCode::UNAUTHORIZED, 199 | Json(TodoError::Unauthorized(String::from("incorrect api key"))), 200 | ) 201 | .into_response()), 202 | None if require_api_key => Err(( 203 | StatusCode::UNAUTHORIZED, 204 | Json(TodoError::Unauthorized(String::from("missing api key"))), 205 | ) 206 | .into_response()), 207 | _ => Ok(()), 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /examples/utoipa-swagger-ui/src/swagger.rs: -------------------------------------------------------------------------------- 1 | use utoipa::{ 2 | openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, 3 | Modify, OpenApi, 4 | }; 5 | 6 | use crate::routes::todo; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | use utoipa::ToSchema; 10 | 11 | struct SecurityAddon; 12 | 13 | impl Modify for SecurityAddon { 14 | fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { 15 | if let Some(components) = openapi.components.as_mut() { 16 | components.add_security_scheme( 17 | "api_key", 18 | SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))), 19 | ) 20 | } 21 | } 22 | } 23 | 24 | #[derive(OpenApi)] 25 | #[openapi( 26 | paths( 27 | todo::list_todos, 28 | todo::search_todos, 29 | todo::create_todo, 30 | todo::mark_done, 31 | todo::delete_todo, 32 | ), 33 | components( 34 | schemas(Todo, TodoError) 35 | ), 36 | modifiers(&SecurityAddon), 37 | tags( 38 | (name = "todo", description = "Todo items management API") 39 | ) 40 | )] 41 | pub struct ApiDoc; 42 | 43 | #[derive(Serialize, Deserialize, ToSchema, Clone)] 44 | pub struct Todo { 45 | pub id: i32, 46 | #[schema(example = "Buy groceries")] 47 | pub value: String, 48 | pub done: bool, 49 | } 50 | 51 | /// Todo operation errors 52 | #[derive(Serialize, Deserialize, ToSchema)] 53 | pub enum TodoError { 54 | /// Todo already exists conflict. 55 | #[schema(example = "Todo already exists")] 56 | Conflict(String), 57 | /// Todo not found by id. 58 | #[schema(example = "id = 1")] 59 | NotFound(String), 60 | /// Todo operation unauthorized 61 | #[schema(example = "missing api key")] 62 | Unauthorized(String), 63 | } 64 | -------------------------------------------------------------------------------- /examples/websocket-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-websocket" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | graphul = { path = "../../." } 9 | tokio = { version = "1", features = ["full"] } 10 | futures = "0.3" 11 | -------------------------------------------------------------------------------- /examples/websocket-example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Example websocket application. 2 | //! 3 | 4 | use graphul::{ 5 | extract::ws::{Message, WebSocket, WebSocketUpgrade}, 6 | http::Methods, 7 | FileConfig, Graphul, 8 | }; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let mut app = Graphul::new(); 13 | 14 | app.static_file("/", "templates/index.html", FileConfig::default()); 15 | 16 | app.get("/websocket", |ws: WebSocketUpgrade| async move { 17 | ws.on_upgrade(|mut socket: WebSocket| async move { 18 | let _ = socket 19 | .send(Message::Text("Hello Graphul".to_string())) 20 | .await; 21 | }) 22 | }); 23 | 24 | app.run("127.0.0.1:3000").await; 25 | } 26 | -------------------------------------------------------------------------------- /examples/websocket-example/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket 6 | 7 | 8 |

WebSocket With Graphul

9 | 10 | 11 | 12 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /img/bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/bitcoin.png -------------------------------------------------------------------------------- /img/compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/compare.png -------------------------------------------------------------------------------- /img/dogecoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/dogecoin.png -------------------------------------------------------------------------------- /img/ethereum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/ethereum.png -------------------------------------------------------------------------------- /img/litecoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/litecoin.png -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/logo.png -------------------------------------------------------------------------------- /img/shiba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/shiba.png -------------------------------------------------------------------------------- /img/solana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/solana.png -------------------------------------------------------------------------------- /img/usdc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/usdc.png -------------------------------------------------------------------------------- /img/usdt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphul-rs/graphul/7de5cf5ab44bd25f4f2aa448fa76200a8c2d4740/img/usdt.png -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | pub const VERSION: &str = "1.0.1"; 2 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | // Colors is a struct to define custom colors for Graphul app and middlewares. 2 | #[derive(Default)] 3 | pub struct Colors { 4 | // Black color. 5 | // 6 | // Optional. Default: "\u001b[90m" 7 | pub black: &'static str, 8 | 9 | // Red color. 10 | // 11 | // Optional. Default: "\u001b[91m" 12 | pub red: &'static str, 13 | 14 | // Green color. 15 | // 16 | // Optional. Default: "\u001b[92m" 17 | pub green: &'static str, 18 | 19 | // Yellow color. 20 | // 21 | // Optional. Default: "\u001b[93m" 22 | pub yellow: &'static str, 23 | 24 | // Blue color. 25 | // 26 | // Optional. Default: "\u001b[94m" 27 | pub blue: &'static str, 28 | 29 | // Magenta color. 30 | // 31 | // Optional. Default: "\u001b[95m" 32 | pub magenta: &'static str, 33 | 34 | // Cyan color. 35 | // 36 | // Optional. Default: "\u001b[96m" 37 | pub cyan: &'static str, 38 | 39 | // White color. 40 | // 41 | // Optional. Default: "\u001b[97m" 42 | pub white: &'static str, 43 | 44 | // Reset color. 45 | // 46 | // Optional. Default: "\u001b[0m" 47 | pub reset: &'static str, 48 | } 49 | 50 | // Default color codes 51 | pub const DEFAULT_COLORS: Colors = Colors { 52 | black: "\x1b[90m", 53 | red: "\x1b[91m", 54 | green: "\x1b[92m", 55 | yellow: "\x1b[93m", 56 | blue: "\x1b[94m", 57 | magenta: "\x1b[95m", 58 | cyan: "\x1b[96m", 59 | white: "\x1b[97m", 60 | reset: "\x1b[0m", 61 | }; 62 | 63 | // defaultColors is a function to override default colors to config 64 | #[allow(dead_code)] 65 | pub fn replace_default_colors(colors: &mut Colors) { 66 | if colors.black.is_empty() { 67 | colors.black = DEFAULT_COLORS.black; 68 | } 69 | 70 | if colors.red.is_empty() { 71 | colors.red = DEFAULT_COLORS.red; 72 | } 73 | 74 | if colors.green.is_empty() { 75 | colors.green = DEFAULT_COLORS.green; 76 | } 77 | 78 | if colors.yellow.is_empty() { 79 | colors.yellow = DEFAULT_COLORS.yellow; 80 | } 81 | 82 | if colors.blue.is_empty() { 83 | colors.blue = DEFAULT_COLORS.blue; 84 | } 85 | 86 | if colors.magenta.is_empty() { 87 | colors.magenta = DEFAULT_COLORS.magenta; 88 | } 89 | 90 | if colors.cyan.is_empty() { 91 | colors.cyan = DEFAULT_COLORS.cyan; 92 | } 93 | 94 | if colors.white.is_empty() { 95 | colors.white = DEFAULT_COLORS.white; 96 | } 97 | 98 | if colors.reset.is_empty() { 99 | colors.reset = DEFAULT_COLORS.reset; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct FolderConfig { 3 | pub spa: bool, 4 | pub compress: bool, 5 | pub chunk_size: Option, 6 | pub index: bool, 7 | pub not_found: Option<&'static str>, 8 | } 9 | 10 | impl FolderConfig { 11 | pub fn spa() -> Self { 12 | Self { 13 | spa: true, 14 | compress: true, 15 | chunk_size: None, 16 | index: true, 17 | not_found: None, 18 | } 19 | } 20 | } 21 | 22 | impl Default for FolderConfig { 23 | fn default() -> Self { 24 | Self { 25 | spa: false, 26 | compress: true, 27 | chunk_size: None, 28 | index: Default::default(), 29 | not_found: Default::default(), 30 | } 31 | } 32 | } 33 | 34 | #[derive(Clone, Debug)] 35 | pub struct FileConfig { 36 | pub compress: bool, 37 | pub chunk_size: Option, 38 | } 39 | 40 | impl Default for FileConfig { 41 | fn default() -> Self { 42 | Self { 43 | compress: true, 44 | chunk_size: None, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | pub mod methods; 2 | pub mod request; 3 | pub mod resource; 4 | pub mod response; 5 | 6 | pub use axum::http as utils; 7 | 8 | pub use methods::Methods; 9 | 10 | pub type StatusCode = hyper::StatusCode; 11 | -------------------------------------------------------------------------------- /src/http/methods.rs: -------------------------------------------------------------------------------- 1 | use axum::handler::Handler; 2 | 3 | pub trait Methods { 4 | fn post(&mut self, path: &str, handler: H) 5 | where 6 | H: Handler, 7 | T: 'static; 8 | 9 | fn put(&mut self, path: &str, handler: H) 10 | where 11 | H: Handler, 12 | T: 'static; 13 | 14 | fn get(&mut self, path: &str, handler: H) 15 | where 16 | H: Handler, 17 | T: 'static; 18 | 19 | fn delete(&mut self, path: &str, handler: H) 20 | where 21 | H: Handler, 22 | T: 'static; 23 | 24 | fn patch(&mut self, path: &str, handler: H) 25 | where 26 | H: Handler, 27 | T: 'static; 28 | 29 | fn options(&mut self, path: &str, handler: H) 30 | where 31 | H: Handler, 32 | T: 'static; 33 | 34 | fn trace(&mut self, path: &str, handler: H) 35 | where 36 | H: Handler, 37 | T: 'static; 38 | 39 | fn head(&mut self, path: &str, handler: H) 40 | where 41 | H: Handler, 42 | T: 'static; 43 | } 44 | -------------------------------------------------------------------------------- /src/http/request.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | net::{IpAddr, Ipv4Addr, SocketAddr}, 4 | }; 5 | 6 | use async_trait::async_trait; 7 | pub use axum::http::request::Parts; 8 | pub use axum::http::Request; 9 | use axum::{ 10 | body::Bytes, 11 | extract::{ 12 | connect_info::ConnectInfo, 13 | rejection::{ExtensionRejection, JsonRejection, PathRejection, QueryRejection}, 14 | FromRef, FromRequest, FromRequestParts, Path, Query, 15 | }, 16 | Json, 17 | }; 18 | use futures::StreamExt; 19 | use hyper::{HeaderMap, Method, Uri, Version}; 20 | use serde::de::DeserializeOwned; 21 | use serde_json::Value; 22 | 23 | use crate::Body; 24 | 25 | type HashMapRequest = HashMap; 26 | 27 | #[derive(Debug)] 28 | pub struct Context { 29 | params_map: HashMapRequest, 30 | query_map: HashMapRequest, 31 | connect_info: Result, ExtensionRejection>, 32 | bytes: Bytes, 33 | inner_state: InnerState, 34 | headers: HeaderMap, 35 | method: Method, 36 | uri: Uri, 37 | version: Version, 38 | } 39 | 40 | // update context to get params and query implementar params y query genericos que no solo soporte maps si no tambien otros structs 41 | // Json 42 | 43 | impl Context { 44 | pub fn headers(&self) -> &HeaderMap { 45 | &self.headers 46 | } 47 | pub fn method(&self) -> &Method { 48 | &self.method 49 | } 50 | pub fn version(&self) -> &Version { 51 | &self.version 52 | } 53 | pub fn uri(&self) -> &Uri { 54 | &self.uri 55 | } 56 | 57 | pub fn addr(&self) -> SocketAddr { 58 | match self.connect_info { 59 | Ok(ConnectInfo(addr)) => addr, 60 | Err(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8080), 61 | } 62 | } 63 | 64 | pub fn ip(&self) -> IpAddr { 65 | self.addr().ip() 66 | } 67 | 68 | pub fn body(&self) -> String { 69 | String::from_utf8(self.bytes.to_vec()).expect("") 70 | } 71 | pub fn bytes(&self) -> &Bytes { 72 | &self.bytes 73 | } 74 | pub fn state(&self) -> &InnerState { 75 | &self.inner_state 76 | } 77 | 78 | pub async fn parse_params(&self) -> Result, JsonRejection> { 79 | let value = match serde_json::to_string(&self.params_map) { 80 | Ok(data) => data, 81 | Err(_) => String::new(), 82 | }; 83 | let request = Request::builder() 84 | .header("Content-Type", "application/json") 85 | .body(Body::from(value)); 86 | 87 | let request = match request { 88 | Ok(value) => value, 89 | Err(_) => Request::default(), 90 | }; 91 | 92 | Json::from_request(request, &()).await 93 | } 94 | pub fn all_params(&self) -> &HashMapRequest { 95 | &self.params_map 96 | } 97 | pub fn params(&self, key: &'static str) -> String { 98 | match self.params_map.get(key) { 99 | Some(value) => value.clone(), 100 | None => String::new(), 101 | } 102 | } 103 | pub async fn parse_query(&self) -> Result, JsonRejection> { 104 | let value = match serde_json::to_string(&self.query_map) { 105 | Ok(data) => data, 106 | Err(_) => String::new(), 107 | }; 108 | let request = Request::builder() 109 | .header("Content-Type", "application/json") 110 | .body(Body::from(value)); 111 | 112 | let request = match request { 113 | Ok(value) => value, 114 | Err(_) => Request::default(), 115 | }; 116 | 117 | Json::from_request(request, &()).await 118 | } 119 | pub fn query(&self, key: &'static str) -> String { 120 | match self.query_map.get(key) { 121 | Some(value) => value.clone(), 122 | None => String::new(), 123 | } 124 | } 125 | pub fn all_query(&self) -> &HashMapRequest { 126 | &self.query_map 127 | } 128 | 129 | pub async fn payload(&self) -> Result, JsonRejection> { 130 | // forse parsing 131 | let request = Request::builder() 132 | .header("Content-Type", "application/json") 133 | .body(Body::from(self.bytes.clone())); 134 | 135 | let request = match request { 136 | Ok(value) => value, 137 | Err(_) => Request::default(), 138 | }; 139 | 140 | Json::from_request(request, &()).await 141 | } 142 | 143 | pub fn json(&self, payload: Value) -> Json { 144 | Json(payload) 145 | } 146 | 147 | pub fn send(value: &str) -> &str { 148 | value 149 | } 150 | } 151 | 152 | #[async_trait] 153 | impl FromRequest for Context 154 | where 155 | OuterState: Send + Sync + 'static, 156 | InnerState: FromRef + Send + Sync, 157 | { 158 | type Rejection = JsonRejection; 159 | 160 | async fn from_request( 161 | req: axum::http::Request, 162 | state: &OuterState, 163 | ) -> Result { 164 | let inner_state = InnerState::from_ref(state); 165 | let headers = req.headers().clone(); 166 | let method = req.method().clone(); 167 | let uri = req.uri().clone(); 168 | let version = req.version(); 169 | let (parts, body) = &mut req.into_parts(); 170 | let mut params_map = HashMap::new(); 171 | let mut query_map = HashMap::new(); 172 | let result_params: Result, PathRejection> = 173 | Path::from_request_parts(parts, &()).await; 174 | 175 | let connect_info: Result, ExtensionRejection> = 176 | ConnectInfo::from_request_parts(parts, state).await; 177 | 178 | if let Ok(params) = result_params { 179 | match params { 180 | Path(parse_params) => { 181 | params_map = parse_params; 182 | } 183 | } 184 | } 185 | 186 | let result_query: Result, QueryRejection> = 187 | Query::from_request_parts(parts, &()).await; 188 | if let Ok(params) = result_query { 189 | match params { 190 | Query(parse_params) => { 191 | query_map = parse_params; 192 | } 193 | } 194 | } 195 | 196 | let mut bytes = Bytes::new(); 197 | let n = body.map(|x| { 198 | if let Ok(value) = x { 199 | bytes = value 200 | } 201 | }); 202 | // get value from iter map 203 | n.collect::>().await; 204 | Ok(Context { 205 | version, 206 | connect_info, 207 | headers, 208 | method, 209 | uri, 210 | bytes, 211 | inner_state, 212 | params_map, 213 | query_map, 214 | }) 215 | } 216 | } 217 | 218 | #[derive(Debug)] 219 | pub struct ContextPart { 220 | params_map: HashMapRequest, 221 | connect_info: Result, ExtensionRejection>, 222 | query_map: HashMapRequest, 223 | inner_state: InnerState, 224 | headers: HeaderMap, 225 | method: Method, 226 | uri: Uri, 227 | version: Version, 228 | } 229 | 230 | // update context to get params and query implementar params y query genericos que no solo soporte maps si no tambien otros structs 231 | // Json 232 | 233 | impl ContextPart { 234 | pub fn headers(&self) -> &HeaderMap { 235 | &self.headers 236 | } 237 | pub fn method(&self) -> &Method { 238 | &self.method 239 | } 240 | pub fn version(&self) -> &Version { 241 | &self.version 242 | } 243 | pub fn uri(&self) -> &Uri { 244 | &self.uri 245 | } 246 | pub fn state(&self) -> &InnerState { 247 | &self.inner_state 248 | } 249 | 250 | pub fn addr(&self) -> SocketAddr { 251 | match self.connect_info { 252 | Ok(ConnectInfo(addr)) => addr, 253 | Err(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8080), 254 | } 255 | } 256 | 257 | pub fn ip(&self) -> IpAddr { 258 | self.addr().ip() 259 | } 260 | 261 | pub async fn parse_params(&self) -> Result, JsonRejection> { 262 | let value = match serde_json::to_string(&self.params_map) { 263 | Ok(data) => data, 264 | Err(_) => String::new(), 265 | }; 266 | let request = Request::builder() 267 | .header("Content-Type", "application/json") 268 | .body(Body::from(value)); 269 | 270 | let request = match request { 271 | Ok(value) => value, 272 | Err(_) => Request::default(), 273 | }; 274 | 275 | Json::from_request(request, &()).await 276 | } 277 | pub fn all_params(&self) -> &HashMapRequest { 278 | &self.params_map 279 | } 280 | pub fn params(&self, key: &'static str) -> String { 281 | match self.params_map.get(key) { 282 | Some(value) => value.clone(), 283 | None => String::new(), 284 | } 285 | } 286 | pub async fn parse_query(&self) -> Result, JsonRejection> { 287 | let value = match serde_json::to_string(&self.query_map) { 288 | Ok(data) => data, 289 | Err(_) => String::new(), 290 | }; 291 | let request = Request::builder() 292 | .header("Content-Type", "application/json") 293 | .body(Body::from(value)); 294 | 295 | let request = match request { 296 | Ok(value) => value, 297 | Err(_) => Request::default(), 298 | }; 299 | 300 | Json::from_request(request, &()).await 301 | } 302 | pub fn query(&self, key: &'static str) -> String { 303 | match self.query_map.get(key) { 304 | Some(value) => value.clone(), 305 | None => String::new(), 306 | } 307 | } 308 | pub fn all_query(&self) -> &HashMapRequest { 309 | &self.query_map 310 | } 311 | 312 | pub fn json(&self, payload: Value) -> Json { 313 | Json(payload) 314 | } 315 | 316 | pub fn send(value: &str) -> &str { 317 | value 318 | } 319 | } 320 | 321 | #[async_trait] 322 | impl FromRequestParts for ContextPart 323 | where 324 | OuterState: Send + Sync + 'static, 325 | InnerState: FromRef + Send + Sync, 326 | { 327 | type Rejection = JsonRejection; 328 | 329 | async fn from_request_parts( 330 | parts: &mut Parts, 331 | state: &OuterState, 332 | ) -> Result { 333 | let inner_state = InnerState::from_ref(state); 334 | let headers = parts.headers.clone(); 335 | let method = parts.method.clone(); 336 | let uri = parts.uri.clone(); 337 | let version = parts.version; 338 | let mut params_map = HashMap::new(); 339 | let mut query_map = HashMap::new(); 340 | let result_params: Result, PathRejection> = 341 | Path::from_request_parts(parts, &()).await; 342 | 343 | let connect_info: Result, ExtensionRejection> = 344 | ConnectInfo::from_request_parts(parts, state).await; 345 | 346 | if let Ok(params) = result_params { 347 | match params { 348 | Path(parse_params) => { 349 | params_map = parse_params; 350 | } 351 | } 352 | } 353 | 354 | let result_query: Result, QueryRejection> = 355 | Query::from_request_parts(parts, &()).await; 356 | if let Ok(params) = result_query { 357 | match params { 358 | Query(parse_params) => { 359 | query_map = parse_params; 360 | } 361 | } 362 | } 363 | 364 | Ok(ContextPart { 365 | version, 366 | connect_info, 367 | headers, 368 | method, 369 | uri, 370 | inner_state, 371 | params_map, 372 | query_map, 373 | }) 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/http/resource.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use axum::response::{IntoResponse, Response}; 3 | use hyper::StatusCode; 4 | 5 | use crate::Context; 6 | 7 | /* 8 | 9 | take from : https://rust-lang.github.io/async-book/07_workarounds/05_async_in_traits.html 10 | async in Traits 11 | Currently, async fn cannot be used in traits. The reasons for this are somewhat complex, but there are plans to remove this restriction in the future. 12 | 13 | In the meantime, however, this can be worked around using the async-trait crate from crates.io. 14 | 15 | Note that using these trait methods will result in a heap allocation per-function-call. 16 | This is not a significant cost for the vast majority of applications, but should be considered 17 | when deciding whether to use this functionality in the public API of a low-level function that is 18 | expected to be called millions of times a second. 19 | 20 | */ 21 | 22 | #[async_trait] 23 | pub trait Resource 24 | where 25 | S: Clone + Send + Sync + 'static, 26 | { 27 | async fn get(_ctx: Context) -> Response { 28 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 29 | } 30 | 31 | async fn post(_ctx: Context) -> Response { 32 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 33 | } 34 | 35 | async fn put(_ctx: Context) -> Response { 36 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 37 | } 38 | 39 | async fn delete(_ctx: Context) -> Response { 40 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 41 | } 42 | 43 | async fn patch(_ctx: Context) -> Response { 44 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 45 | } 46 | 47 | async fn options(_ctx: Context) -> Response { 48 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 49 | } 50 | 51 | async fn trace(_ctx: Context) -> Response { 52 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 53 | } 54 | 55 | async fn head(_ctx: Context) -> Response { 56 | (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/http/response.rs: -------------------------------------------------------------------------------- 1 | pub type Response = axum::response::Response; 2 | pub type Value = serde_json::Value; 3 | 4 | pub use axum::response::{Form, Html, Redirect}; 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //pub mod types; 2 | 3 | //use types::*; 4 | 5 | mod app; 6 | mod color; 7 | mod fs; 8 | pub mod http; 9 | mod listen; 10 | pub mod middleware; 11 | pub mod template; 12 | 13 | use std::convert::Infallible; 14 | use std::io; 15 | use std::net::SocketAddr; 16 | 17 | pub use async_trait::async_trait; 18 | 19 | pub use axum::extract; 20 | use axum::handler::Handler; 21 | pub use axum::response::IntoResponse; 22 | use axum::routing::{delete, get, get_service, head, options, patch, post, put, trace, Route}; 23 | use axum::Router; 24 | 25 | pub use http::request::Context; 26 | pub use http::request::ContextPart; 27 | use http::resource::Resource; 28 | use http::StatusCode; 29 | use hyper::service::Service; 30 | use hyper::Request; 31 | use tower_http::services::{ServeDir, ServeFile}; 32 | use tower_http::set_status::SetStatus; 33 | use tower_layer::Layer; 34 | 35 | use tower::ServiceExt; 36 | 37 | pub type FolderConfig = fs::FolderConfig; 38 | pub type FileConfig = fs::FileConfig; 39 | 40 | pub type Body = axum::body::Body; 41 | 42 | pub type Req = axum::http::Request; 43 | 44 | pub struct Group<'a, S = ()> { 45 | app: &'a mut Graphul, 46 | prefix: String, 47 | } 48 | 49 | impl<'a, S> Group<'a, S> 50 | where 51 | S: Clone + Send + Sync + 'static, 52 | { 53 | pub fn resource(&mut self, path: &str, res: impl Resource + 'static) { 54 | let route_name = self.get_route_name(path); 55 | self.app.resource(route_name.as_str(), res); 56 | } 57 | } 58 | 59 | impl<'a, S> http::Methods for Group<'a, S> 60 | where 61 | S: Clone + Send + Sync + 'static, 62 | { 63 | fn post(&mut self, path: &str, handler: H) 64 | where 65 | H: Handler, 66 | T: 'static, 67 | { 68 | let route_name = self.get_route_name(path); 69 | self.app.post(route_name.as_str(), handler); 70 | } 71 | 72 | fn get(&mut self, path: &str, handler: H) 73 | where 74 | H: Handler, 75 | T: 'static, 76 | { 77 | let route_name = self.get_route_name(path); 78 | self.app.get(route_name.as_str(), handler); 79 | } 80 | fn put(&mut self, path: &str, handler: H) 81 | where 82 | H: Handler, 83 | T: 'static, 84 | { 85 | let route_name = self.get_route_name(path); 86 | self.app.put(route_name.as_str(), handler); 87 | } 88 | 89 | fn delete(&mut self, path: &str, handler: H) 90 | where 91 | H: Handler, 92 | T: 'static, 93 | { 94 | let route_name = self.get_route_name(path); 95 | self.app.delete(route_name.as_str(), handler); 96 | } 97 | fn head(&mut self, path: &str, handler: H) 98 | where 99 | H: Handler, 100 | T: 'static, 101 | { 102 | let route_name = self.get_route_name(path); 103 | self.app.head(route_name.as_str(), handler); 104 | } 105 | 106 | fn options(&mut self, path: &str, handler: H) 107 | where 108 | H: Handler, 109 | T: 'static, 110 | { 111 | let route_name = self.get_route_name(path); 112 | self.app.options(route_name.as_str(), handler); 113 | } 114 | fn patch(&mut self, path: &str, handler: H) 115 | where 116 | H: Handler, 117 | T: 'static, 118 | { 119 | let route_name = self.get_route_name(path); 120 | self.app.patch(route_name.as_str(), handler); 121 | } 122 | 123 | fn trace(&mut self, path: &str, handler: H) 124 | where 125 | H: Handler, 126 | T: 'static, 127 | { 128 | let route_name = self.get_route_name(path); 129 | self.app.trace(route_name.as_str(), handler); 130 | } 131 | } 132 | 133 | impl<'a, S> Group<'a, S> 134 | where 135 | S: Clone + Send + Sync + 'static, 136 | { 137 | fn new(app: &'a mut Graphul, prefix: &str) -> Self { 138 | Group { 139 | app, 140 | prefix: prefix.to_string(), 141 | } 142 | } 143 | 144 | fn get_route_name(&self, name: &str) -> String { 145 | if name == "/" { 146 | return self.prefix.clone(); 147 | } 148 | format!("{}{}", self.prefix, name) 149 | } 150 | 151 | pub fn group(&mut self, name: &str) -> Group { 152 | self.app 153 | .group(format!("/{}/{}", self.prefix, name).as_str()) 154 | } 155 | } 156 | 157 | #[derive(Debug)] 158 | pub struct Graphul { 159 | routes: Router, 160 | count_routes: usize, 161 | route_list: Vec, 162 | state: S, 163 | } 164 | 165 | impl Graphul 166 | where 167 | S: Clone + Send + Sync + 'static, 168 | { 169 | pub fn resource + 'static>(&mut self, path: &str, _res: T) { 170 | // get 171 | self.increase_route_counter(path.into()); 172 | self.routes = self.routes.clone().route(path, get(T::get)); 173 | 174 | // post 175 | self.increase_route_counter(path.into()); 176 | self.routes = self.routes.clone().route(path, post(T::post)); 177 | 178 | // put 179 | self.increase_route_counter(path.into()); 180 | self.routes = self.routes.clone().route(path, put(T::put)); 181 | 182 | // delete 183 | self.increase_route_counter(path.into()); 184 | self.routes = self.routes.clone().route(path, delete(T::delete)); 185 | 186 | // patch 187 | self.increase_route_counter(path.into()); 188 | self.routes = self.routes.clone().route(path, patch(T::patch)); 189 | 190 | // options 191 | self.increase_route_counter(path.into()); 192 | self.routes = self.routes.clone().route(path, options(T::options)); 193 | 194 | // trace 195 | self.increase_route_counter(path.into()); 196 | self.routes = self.routes.clone().route(path, trace(T::trace)); 197 | 198 | // head 199 | self.increase_route_counter(path.into()); 200 | self.routes = self.routes.clone().route(path, head(T::head)); 201 | } 202 | } 203 | 204 | impl http::Methods for Graphul 205 | where 206 | S: Clone + Send + Sync + 'static, 207 | { 208 | fn get(&mut self, path: &str, handler: H) 209 | where 210 | H: Handler, 211 | T: 'static, 212 | { 213 | self.increase_route_counter(format!("GET {path}")); 214 | self.routes = self.routes.clone().route(path, get(handler)); 215 | } 216 | fn post(&mut self, path: &str, handler: H) 217 | where 218 | H: Handler, 219 | T: 'static, 220 | { 221 | self.increase_route_counter(format!("POST {path}")); 222 | self.routes = self.routes.clone().route(path, post(handler)); 223 | } 224 | fn put(&mut self, path: &str, handler: H) 225 | where 226 | H: Handler, 227 | T: 'static, 228 | { 229 | self.increase_route_counter(format!("PUT {path}")); 230 | self.routes = self.routes.clone().route(path, put(handler)); 231 | } 232 | fn delete(&mut self, path: &str, handler: H) 233 | where 234 | H: Handler, 235 | T: 'static, 236 | { 237 | self.increase_route_counter(format!("DELETE {path}")); 238 | self.routes = self.routes.clone().route(path, delete(handler)); 239 | } 240 | fn head(&mut self, path: &str, handler: H) 241 | where 242 | H: Handler, 243 | T: 'static, 244 | { 245 | self.increase_route_counter(format!("HEAD {path}")); 246 | self.routes = self.routes.clone().route(path, head(handler)); 247 | } 248 | fn options(&mut self, path: &str, handler: H) 249 | where 250 | H: Handler, 251 | T: 'static, 252 | { 253 | self.increase_route_counter(format!("OPTIONS {path}")); 254 | self.routes = self.routes.clone().route(path, options(handler)); 255 | } 256 | fn patch(&mut self, path: &str, handler: H) 257 | where 258 | H: Handler, 259 | T: 'static, 260 | { 261 | self.increase_route_counter(format!("PATCH {path}")); 262 | self.routes = self.routes.clone().route(path, patch(handler)); 263 | } 264 | fn trace(&mut self, path: &str, handler: H) 265 | where 266 | H: Handler, 267 | T: 'static, 268 | { 269 | self.increase_route_counter(format!("TRACE {path}")); 270 | self.routes = self.routes.clone().route(path, trace(handler)); 271 | } 272 | } 273 | 274 | impl Graphul<()> { 275 | pub fn new() -> Self { 276 | Self { 277 | routes: Router::new(), 278 | count_routes: 0, 279 | route_list: vec![], 280 | state: (), 281 | } 282 | } 283 | 284 | // new alias to create sub-routes 285 | pub fn router() -> Self { 286 | Self { 287 | routes: Router::new(), 288 | count_routes: 0, 289 | route_list: vec![], 290 | state: (), 291 | } 292 | } 293 | 294 | // Graphul::get("/", || async {}); 295 | // let 296 | 297 | pub fn get(path: &str, handler: H) -> Graphul 298 | where 299 | H: Handler, 300 | T: 'static, 301 | { 302 | use http::Methods; 303 | 304 | let mut app = Graphul::new(); 305 | app.get(path, handler); 306 | app 307 | } 308 | 309 | // Get with state 310 | // Graphul::Get(my_state, "/", || async {}); 311 | pub fn state_get(state: S, path: &str, handler: H) -> Graphul 312 | where 313 | H: Handler, 314 | T: 'static, 315 | S: Clone + Send + Sync + 'static, 316 | { 317 | use http::Methods; 318 | 319 | let mut app: Graphul = Graphul::share_state(state); 320 | app.get(path, handler); 321 | app 322 | } 323 | 324 | // Post without state 325 | // Graphul::post("/", || async {}); 326 | pub fn post(path: &str, handler: H) -> Graphul 327 | where 328 | H: Handler, 329 | T: 'static, 330 | { 331 | use http::Methods; 332 | 333 | let mut app = Graphul::new(); 334 | app.post(path, handler); 335 | app 336 | } 337 | 338 | // Post with state 339 | // Graphul::Post(my_state, "/", || async {}); 340 | pub fn state_post(state: S, path: &str, handler: H) -> Graphul 341 | where 342 | H: Handler, 343 | T: 'static, 344 | S: Clone + Send + Sync + 'static, 345 | { 346 | use http::Methods; 347 | 348 | let mut app: Graphul = Graphul::share_state(state); 349 | app.post(path, handler); 350 | app 351 | } 352 | 353 | // Put without state 354 | // Graphul::put("/", || async {}); 355 | pub fn put(path: &str, handler: H) -> Graphul 356 | where 357 | H: Handler, 358 | T: 'static, 359 | { 360 | use http::Methods; 361 | 362 | let mut app = Graphul::new(); 363 | app.put(path, handler); 364 | app 365 | } 366 | 367 | // Put with state 368 | // Graphul::Put(my_state, "/", || async {}); 369 | pub fn state_put(state: S, path: &str, handler: H) -> Graphul 370 | where 371 | H: Handler, 372 | T: 'static, 373 | S: Clone + Send + Sync + 'static, 374 | { 375 | use http::Methods; 376 | 377 | let mut app: Graphul = Graphul::share_state(state); 378 | app.put(path, handler); 379 | app 380 | } 381 | 382 | // Delete without state 383 | // Graphul::delete("/", || async {}); 384 | pub fn delete(path: &str, handler: H) -> Graphul 385 | where 386 | H: Handler, 387 | T: 'static, 388 | { 389 | use http::Methods; 390 | 391 | let mut app = Graphul::new(); 392 | app.delete(path, handler); 393 | app 394 | } 395 | 396 | // Delete with state 397 | // Graphul::Delete(my_state, "/", || async {}); 398 | pub fn state_delete(state: S, path: &str, handler: H) -> Graphul 399 | where 400 | H: Handler, 401 | T: 'static, 402 | S: Clone + Send + Sync + 'static, 403 | { 404 | use http::Methods; 405 | 406 | let mut app: Graphul = Graphul::share_state(state); 407 | app.delete(path, handler); 408 | app 409 | } 410 | 411 | // Patch without state 412 | // Graphul::patch("/", || async {}); 413 | pub fn patch(path: &str, handler: H) -> Graphul 414 | where 415 | H: Handler, 416 | T: 'static, 417 | { 418 | use http::Methods; 419 | 420 | let mut app = Graphul::new(); 421 | app.patch(path, handler); 422 | app 423 | } 424 | 425 | // Patch with state 426 | // Graphul::Patch(my_state, "/", || async {}); 427 | pub fn state_patch(state: S, path: &str, handler: H) -> Graphul 428 | where 429 | H: Handler, 430 | T: 'static, 431 | S: Clone + Send + Sync + 'static, 432 | { 433 | use http::Methods; 434 | 435 | let mut app: Graphul = Graphul::share_state(state); 436 | app.patch(path, handler); 437 | app 438 | } 439 | 440 | // Options without state 441 | // Graphul::options("/", || async {}); 442 | pub fn options(path: &str, handler: H) -> Graphul 443 | where 444 | H: Handler, 445 | T: 'static, 446 | { 447 | use http::Methods; 448 | 449 | let mut app = Graphul::new(); 450 | app.options(path, handler); 451 | app 452 | } 453 | 454 | // Options with state 455 | // Graphul::Options(my_state, "/", || async {}); 456 | pub fn state_options(state: S, path: &str, handler: H) -> Graphul 457 | where 458 | H: Handler, 459 | T: 'static, 460 | S: Clone + Send + Sync + 'static, 461 | { 462 | use http::Methods; 463 | 464 | let mut app: Graphul = Graphul::share_state(state); 465 | app.options(path, handler); 466 | app 467 | } 468 | 469 | // Trace without state 470 | // Graphul::trace("/", || async {}); 471 | pub fn trace(path: &str, handler: H) -> Graphul 472 | where 473 | H: Handler, 474 | T: 'static, 475 | { 476 | use http::Methods; 477 | 478 | let mut app = Graphul::new(); 479 | app.trace(path, handler); 480 | app 481 | } 482 | 483 | // Trace with state 484 | // Graphul::Trace(my_state, "/", || async {}); 485 | pub fn state_trace(state: S, path: &str, handler: H) -> Graphul 486 | where 487 | H: Handler, 488 | T: 'static, 489 | S: Clone + Send + Sync + 'static, 490 | { 491 | use http::Methods; 492 | 493 | let mut app: Graphul = Graphul::share_state(state); 494 | app.trace(path, handler); 495 | app 496 | } 497 | 498 | // Head without state 499 | // Graphul::head("/", || async {}); 500 | pub fn head(path: &str, handler: H) -> Graphul 501 | where 502 | H: Handler, 503 | T: 'static, 504 | { 505 | use http::Methods; 506 | 507 | let mut app = Graphul::new(); 508 | app.head(path, handler); 509 | app 510 | } 511 | 512 | // Head with state 513 | // Graphul::Head(my_state, "/", || async {}); 514 | pub fn state_head(state: S, path: &str, handler: H) -> Graphul 515 | where 516 | H: Handler, 517 | T: 'static, 518 | S: Clone + Send + Sync + 'static, 519 | { 520 | use http::Methods; 521 | 522 | let mut app: Graphul = Graphul::share_state(state); 523 | app.head(path, handler); 524 | app 525 | } 526 | } 527 | 528 | impl Default for Graphul<()> { 529 | fn default() -> Self { 530 | Self::new() 531 | } 532 | } 533 | 534 | impl Graphul 535 | where 536 | S: Clone + Send + Sync + 'static, 537 | { 538 | pub fn share_state(state: S) -> Self { 539 | Self { 540 | routes: Router::new(), 541 | count_routes: 0, 542 | route_list: vec![], 543 | state, 544 | } 545 | } 546 | 547 | pub fn routes(&self) -> Vec { 548 | self.route_list.clone() 549 | } 550 | 551 | fn increase_route_counter(&mut self, path: String) { 552 | self.count_routes += 1; 553 | self.route_list.push(path); 554 | } 555 | 556 | fn add_route_to_list(&mut self, paths: Vec) { 557 | for path in paths { 558 | self.increase_route_counter(path) 559 | } 560 | } 561 | 562 | pub fn merge(&mut self, route: R) 563 | where 564 | R: Into>, 565 | { 566 | self.routes = self.routes.clone().merge(route); 567 | } 568 | 569 | pub fn add_router(&mut self, route: Graphul) { 570 | self.merge(route.routes); 571 | self.add_route_to_list(route.route_list); 572 | } 573 | 574 | pub fn add_routers(&mut self, routes: Vec>) { 575 | for route in routes { 576 | self.merge(route.routes); 577 | self.add_route_to_list(route.route_list); 578 | } 579 | } 580 | 581 | pub fn set_server_file_config( 582 | &self, 583 | file_dir: String, 584 | compress: bool, 585 | chunk_size: Option, 586 | ) -> ServeFile { 587 | let mut serve_file = ServeFile::new(file_dir); 588 | if compress { 589 | serve_file = serve_file 590 | .precompressed_gzip() 591 | .precompressed_deflate() 592 | .precompressed_br() 593 | } 594 | if let Some(chunk_size) = chunk_size { 595 | serve_file = serve_file.with_buf_chunk_size(chunk_size) 596 | } 597 | serve_file 598 | } 599 | 600 | pub fn static_files(&mut self, path: &'static str, dir: &'static str, config: FolderConfig) { 601 | let mut serve_dir: ServeDir>; 602 | if config.spa { 603 | serve_dir = ServeDir::new(dir) 604 | .append_index_html_on_directories(config.index) 605 | .fallback(SetStatus::new( 606 | self.set_server_file_config( 607 | format!("{dir}/index.html"), 608 | config.compress, 609 | config.chunk_size, 610 | ), 611 | StatusCode::OK, 612 | )); 613 | } else { 614 | let mut not_found = ""; 615 | if let Some(not_f) = config.not_found { 616 | not_found = not_f; 617 | } 618 | serve_dir = ServeDir::new(dir) 619 | .append_index_html_on_directories(config.index) 620 | .fallback(SetStatus::new( 621 | self.set_server_file_config( 622 | not_found.to_string(), 623 | config.compress, 624 | config.chunk_size, 625 | ), 626 | StatusCode::NOT_FOUND, 627 | )); 628 | } 629 | if config.compress { 630 | serve_dir = serve_dir 631 | .precompressed_gzip() 632 | .precompressed_deflate() 633 | .precompressed_br() 634 | } 635 | if let Some(chunk_size) = config.chunk_size { 636 | serve_dir = serve_dir.with_buf_chunk_size(chunk_size) 637 | } 638 | let serve_dir = get_service(serve_dir).handle_error(Graphul::::handle_error); 639 | self.routes = self.routes.clone().nest_service(path, serve_dir); 640 | } 641 | 642 | pub fn static_file(&mut self, path: &'static str, file: &'static str, config: FileConfig) { 643 | let serve_dir = 644 | self.set_server_file_config(file.to_string(), config.compress, config.chunk_size); 645 | self.routes = self.routes.clone().route( 646 | path, 647 | get(move |request: Request| async move { 648 | let serve_dir = get_service(serve_dir).handle_error(Graphul::::handle_error); 649 | serve_dir.oneshot(request).await 650 | }), 651 | ); 652 | } 653 | 654 | async fn handle_error(_err: io::Error) -> impl IntoResponse { 655 | (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...") 656 | } 657 | 658 | pub fn middleware(&mut self, service: L) 659 | where 660 | L: Layer> + Clone + Send + 'static, 661 | L::Service: Service> + Clone + Send + 'static, 662 | >>::Response: IntoResponse + 'static, 663 | >>::Error: Into + 'static, 664 | >>::Future: Send + 'static, 665 | { 666 | self.routes = self.routes.clone().route_layer(service); 667 | } 668 | 669 | pub fn group(&mut self, name: &str) -> Group { 670 | Group::new(self, format!("/{name}").as_str()) 671 | } 672 | 673 | async fn fallback(req: Req) -> (StatusCode, String) { 674 | ( 675 | StatusCode::NOT_FOUND, 676 | format!("Cannot {} {}", req.method().as_str(), req.uri()), 677 | ) 678 | } 679 | 680 | pub fn export_routes(self) -> Router { 681 | self.routes 682 | .with_state(self.state) 683 | .fallback(Graphul::::fallback) 684 | } 685 | 686 | pub async fn run(self, addr: &str) { 687 | let addr: SocketAddr = addr.parse().unwrap(); 688 | 689 | listen::startup_message(&addr, false, self.count_routes); 690 | 691 | axum::Server::bind(&addr) 692 | .serve( 693 | self.routes 694 | .with_state(self.state) 695 | .fallback(Graphul::::fallback) 696 | .into_make_service_with_connect_info::(), 697 | ) 698 | .await 699 | .unwrap(); 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /src/listen.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::process; 3 | 4 | extern crate num_cpus; 5 | 6 | use crate::app; 7 | use crate::color; 8 | 9 | pub fn startup_message(addr: &SocketAddr, tls: bool, route_counter: usize) { 10 | // Alias colors 11 | let colors = color::DEFAULT_COLORS; 12 | 13 | let value = |s: String, width: usize| -> String { 14 | let pad = width - s.len(); 15 | let mut string = "".to_string(); 16 | for _i in 0..pad { 17 | string += "."; 18 | } 19 | string += format!(" {}{}{}", colors.cyan, s, colors.black).as_str(); 20 | string 21 | }; 22 | 23 | let center = |s: String| -> String { format!("{s:^49}") }; 24 | 25 | let center_value = 26 | |s: String| -> String { format!("{}{:^49}{}", colors.cyan, s, colors.black) }; 27 | 28 | let mut host = addr.ip().to_string(); 29 | let port = addr.port(); 30 | if host.is_empty() { 31 | if addr.is_ipv6() { 32 | host = "[::1]".to_string(); 33 | } else { 34 | host = "0.0.0.0".to_string(); 35 | } 36 | } 37 | 38 | let mut scheme = "http"; 39 | if tls { 40 | scheme = "https"; 41 | } 42 | 43 | let mut main_logo = format!( 44 | "{}{}", 45 | colors.black, " ┌───────────────────────────────────────────────────┐\n" 46 | ); 47 | /*if app_name != "" { 48 | main_logo = format!("{}{}{}{}", main_logo, " │ ", center_value(app_name), " │\n"); 49 | }*/ 50 | main_logo = format!( 51 | "{}{}{}{}", 52 | main_logo, 53 | " │ ", 54 | center_value(format!("Graphul v{}", app::VERSION)), 55 | " │\n" 56 | ); 57 | 58 | if host == "0.0.0.0" { 59 | main_logo = format!( 60 | "{}{}{}{}", 61 | main_logo, 62 | " │ ", 63 | center(format!("{scheme}://127.0.0.1:{port}")), 64 | " │\n" 65 | ); 66 | main_logo = format!( 67 | "{}{}{}{}", 68 | main_logo, 69 | " │ ", 70 | center(format!("(bound on host 0.0.0.0 and port {port})")), 71 | " │\n" 72 | ); 73 | } else { 74 | main_logo = format!( 75 | "{}{}{}{}", 76 | main_logo, 77 | " │ ", 78 | center(format!("{scheme}://{host}:{port}")), 79 | " │\n" 80 | ); 81 | } 82 | 83 | main_logo = format!("{}{}{}{}", main_logo, " │ ", center("".to_string()), " │\n"); 84 | main_logo = format!( 85 | "{} │ Handlers {} Processes {} │\n", 86 | main_logo, 87 | value(route_counter.to_string(), 14), 88 | value(num_cpus::get().to_string(), 12) 89 | ); 90 | main_logo = format!( 91 | "{} │ Runtime .{} PID ....{} │\n", 92 | main_logo, 93 | value("Tokio".to_string(), 14), 94 | value(process::id().to_string(), 14) 95 | ); 96 | main_logo = format!( 97 | "{}{}{}", 98 | main_logo, " └───────────────────────────────────────────────────┘", colors.reset 99 | ); 100 | 101 | println!("{main_logo}"); 102 | println!( 103 | "{}Support Graphul{}: https://github.com/graphul-rs/graphul/blob/main/BUY-A-COFFEE.md", 104 | colors.cyan, colors.black 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Body; 2 | 3 | pub use axum::middleware::{self, from_fn, from_fn_with_state}; 4 | 5 | pub type Next = middleware::Next; 6 | pub use tower_http as tower; 7 | 8 | pub mod limit { 9 | use std::time::Duration; 10 | 11 | pub use ::tower::limit::{ConcurrencyLimit, RateLimit}; 12 | use ::tower::limit::{ 13 | ConcurrencyLimitLayer as OriginalConcurrencyLimitLayer, 14 | GlobalConcurrencyLimitLayer as OriginalGlobalConcurrencyLimitLayer, 15 | RateLimitLayer as OriginalRateLimitLayer, 16 | }; 17 | use tower_http::add_extension::{AddExtension, AddExtensionLayer}; 18 | 19 | /// Wrapper of RateLimitLayer of tower 20 | /// Enforces a rate limit on the number of requests the underlying 21 | /// service can handle over a period of time. 22 | #[derive(Debug, Clone)] 23 | pub struct RateLimitLayer(AddExtensionLayer); 24 | /// Wrapper of ConcurrencyLimitLayer of tower 25 | /// Enforces a limit on the concurrent number of requests the underlying 26 | /// service can handle. 27 | #[derive(Debug, Clone)] 28 | pub struct ConcurrencyLimitLayer(AddExtensionLayer); 29 | /// Wrapper of GlobalConcurrencyLimitLayer of tower 30 | /// Enforces a limit on the concurrent number of requests the underlying 31 | /// service can handle. 32 | /// 33 | /// Unlike [`ConcurrencyLimitLayer`], which enforces a per-service concurrency 34 | /// limit, this layer accepts a owned semaphore (`Arc`) which can be 35 | /// shared across multiple services. 36 | /// 37 | /// Cloning this layer will not create a new semaphore. 38 | #[derive(Debug, Clone)] 39 | pub struct GlobalConcurrencyLimitLayer(AddExtensionLayer); 40 | 41 | impl RateLimitLayer { 42 | /// Create new rate limit layer. 43 | pub fn new(limit: u64, per: Duration) -> Self { 44 | Self(AddExtensionLayer::new(OriginalRateLimitLayer::new( 45 | limit, per, 46 | ))) 47 | } 48 | } 49 | 50 | impl tower_layer::Layer for RateLimitLayer { 51 | type Service = AddExtension; 52 | fn layer(&self, service: S) -> Self::Service { 53 | self.0.layer(service) 54 | } 55 | } 56 | 57 | impl ConcurrencyLimitLayer { 58 | /// Create a new concurrency limit layer. 59 | pub fn new(max: usize) -> Self { 60 | Self(AddExtensionLayer::new(OriginalConcurrencyLimitLayer::new( 61 | max, 62 | ))) 63 | } 64 | } 65 | 66 | impl tower_layer::Layer for ConcurrencyLimitLayer { 67 | type Service = AddExtension; 68 | fn layer(&self, service: S) -> Self::Service { 69 | self.0.layer(service) 70 | } 71 | } 72 | 73 | impl GlobalConcurrencyLimitLayer { 74 | /// Create a new `GlobalConcurrencyLimitLayer`. 75 | pub fn new(max: usize) -> Self { 76 | Self(AddExtensionLayer::new( 77 | OriginalGlobalConcurrencyLimitLayer::new(max), 78 | )) 79 | } 80 | } 81 | 82 | impl tower_layer::Layer for GlobalConcurrencyLimitLayer { 83 | type Service = AddExtension; 84 | fn layer(&self, service: S) -> Self::Service { 85 | self.0.layer(service) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use axum::response::{Html, IntoResponse, Response}; 3 | use hyper::StatusCode; 4 | 5 | pub struct HtmlTemplate(pub T); 6 | 7 | impl IntoResponse for HtmlTemplate 8 | where 9 | T: Template, 10 | { 11 | fn into_response(self) -> Response { 12 | match self.0.render() { 13 | Ok(html) => Html(html).into_response(), 14 | Err(err) => ( 15 | StatusCode::INTERNAL_SERVER_ERROR, 16 | format!("Failed to render template. Error: {err}"), 17 | ) 18 | .into_response(), 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------