├── .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 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
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 |
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 | [](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 |
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 |
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 |
21 |
22 | ### Detail page
23 |
24 | [http://127.0.0.1:8000/6](http://127.0.0.1:8000/6)
25 |
26 |
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 |