mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
Merge pull request #2 from Valerioageno/add-status-code-support
Add `StatusCode` support
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
|
||||
@@ -65,7 +65,7 @@ async fn catch_all(Path(params): Path<HashMap<String, String>>, request: Request
|
||||
|
||||
|
||||
// TODO: remove unwrap
|
||||
let payload = tuono_lib::Payload::new(&req, Box::new(""))
|
||||
let payload = tuono_lib::Payload::new(&req, &"")
|
||||
.client_payload()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
@@ -24,4 +24,5 @@ serde = { version = "1.0.202", features = ["derive"] }
|
||||
erased-serde = "0.4.5"
|
||||
serde_json = "1.0"
|
||||
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.1.2"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.1.3"}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@ pub mod ssr;
|
||||
|
||||
pub use payload::Payload;
|
||||
pub use request::Request;
|
||||
pub use response::Response;
|
||||
pub use response::{Props, Response};
|
||||
pub use ssr_rs::Ssr;
|
||||
pub use tuono_lib_macros::handler;
|
||||
|
||||
@@ -8,11 +8,11 @@ use crate::Request;
|
||||
/// This is the data shared to the client
|
||||
pub struct Payload<'a> {
|
||||
router: Location<'a>,
|
||||
props: Box<dyn Serialize>,
|
||||
props: &'a dyn Serialize,
|
||||
}
|
||||
|
||||
impl<'a> Payload<'a> {
|
||||
pub fn new(req: &Request<'a>, props: Box<dyn Serialize>) -> Payload<'a> {
|
||||
pub fn new(req: &Request<'a>, props: &'a dyn Serialize) -> Payload<'a> {
|
||||
Payload {
|
||||
router: req.location(),
|
||||
props,
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
use crate::Request;
|
||||
use crate::{ssr::Js, Payload};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::Json;
|
||||
use erased_serde::Serialize;
|
||||
|
||||
pub struct Props {
|
||||
data: Box<dyn Serialize>,
|
||||
http_code: StatusCode,
|
||||
}
|
||||
|
||||
pub enum Response {
|
||||
Redirect(String),
|
||||
Props(Box<dyn Serialize>),
|
||||
Props(Props),
|
||||
}
|
||||
|
||||
impl Props {
|
||||
pub fn new(data: impl Serialize + 'static) -> Self {
|
||||
Props {
|
||||
data: Box::new(data),
|
||||
http_code: StatusCode::OK,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_status(data: impl Serialize + 'static, http_code: StatusCode) -> Self {
|
||||
Props {
|
||||
data: Box::new(data),
|
||||
http_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props { data, http_code }) => {
|
||||
let payload = Payload::new(&req, data).client_payload().unwrap();
|
||||
|
||||
match Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload))) {
|
||||
Ok(html) => (*http_code, Html(html)),
|
||||
Err(_) => (*http_code, Html("500 Internal server error".to_string())),
|
||||
}
|
||||
}
|
||||
// TODO: Handle here other enum arms
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json(&self) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props { data, http_code }) => (*http_code, Json(data)).into_response(),
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, axum::Json("{}")).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
homepage = "https://github.com/Valerioageno/tuono"
|
||||
|
||||
@@ -8,7 +8,7 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let fn_name = item.clone().sig.ident;
|
||||
|
||||
quote! {
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::response::IntoResponse;
|
||||
use std::collections::HashMap;
|
||||
use axum::extract::{State, Path};
|
||||
use reqwest::Client;
|
||||
@@ -16,51 +16,29 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
#item
|
||||
|
||||
pub async fn route(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
State(client): State<Client>,
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
State(client): State<Client>,
|
||||
request: axum::extract::Request
|
||||
) -> Html<String> {
|
||||
) -> impl IntoResponse {
|
||||
let pathname = &request.uri();
|
||||
let headers = &request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname, headers, params);
|
||||
|
||||
let local_response = #fn_name(req.clone(), client).await;
|
||||
|
||||
let res = match local_response {
|
||||
tuono_lib::Response::Props(val) => {
|
||||
|
||||
// TODO: remove unwrap
|
||||
let payload = tuono_lib::Payload::new(&req, val).client_payload().unwrap();
|
||||
|
||||
tuono_lib::ssr::Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload)))
|
||||
},
|
||||
/// TODO: handle here redirection and rewrite
|
||||
_ => Ok("500 Internal server error".to_string())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(html) => Html(html),
|
||||
_ => Html("500 internal server error".to_string())
|
||||
}
|
||||
#fn_name(req.clone(), client).await.render_to_string(req)
|
||||
}
|
||||
|
||||
pub async fn api(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
State(client): State<Client>,
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
State(client): State<Client>,
|
||||
request: axum::extract::Request
|
||||
) -> axum::response::Response {
|
||||
let pathname = &request.uri();
|
||||
) -> impl IntoResponse{
|
||||
let pathname = &request.uri();
|
||||
let headers = &request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname, headers, params);
|
||||
|
||||
let local_response = #fn_name(req.clone(), client).await;
|
||||
|
||||
let res = match local_response{
|
||||
tuono_lib::Response::Props(val) => return axum::Json(val).into_response(),
|
||||
_ => return axum::Json("").into_response()
|
||||
};
|
||||
#fn_name(req.clone(), client).await.json()
|
||||
}
|
||||
}
|
||||
.into()
|
||||
|
||||
+109
-16
@@ -6,6 +6,20 @@ The first part is about the project setup and the base knowledge needed to work
|
||||
|
||||
> If you prefer to see directly the final outcome check out the [tutorial source](https://github.com/Valerioageno/tuono/tree/main/examples/tutorial).
|
||||
|
||||
## Table of Content
|
||||
|
||||
* [Tuono tutorial](#tuono-tutorial)
|
||||
* [Installation](#installation)
|
||||
* [Project scaffold](#project-scaffold)
|
||||
* [Start the dev environment](#start-the-dev-environment)
|
||||
* [The “/” route](#the--route)
|
||||
* [Tutorial introduction](#tutorial-introduction)
|
||||
* [Fetch all the pokemons](#fetch-all-the-pokemons)
|
||||
* [Create a stand-alone component](#create-a-stand-alone-component)
|
||||
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
|
||||
* [Error handling](#error-handling)
|
||||
* [Conclusion](#conclusion)
|
||||
|
||||
## Installation
|
||||
|
||||
The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
|
||||
@@ -93,7 +107,7 @@ Clear the `index.rs` file and paste:
|
||||
```rust
|
||||
// src/routes/index.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
|
||||
@@ -110,15 +124,13 @@ struct Pokemon {
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
|
||||
|
||||
return match fetch.get(ALL_POKEMON).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemons>().await.unwrap();
|
||||
Response::Props(Box::new(data))
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
Err(_err) => Response::Props(Box::new(Pokemons { results: vec![] })),
|
||||
Err(_err) => Response::Props(Props::new("{}")),
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -302,8 +314,9 @@ These two will handle every requests that points to `http://localhost:3000/pokem
|
||||
Let’s first work on the server side file. Paste into the new `[pokemon].rs` file the following code:
|
||||
|
||||
```rust
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
|
||||
@@ -317,22 +330,16 @@ struct Pokemon {
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
|
||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||
let pokemon = req.params.get("pokemon").unwrap();
|
||||
|
||||
|
||||
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemon>().await.unwrap();
|
||||
Response::Props(Box::new(data))
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
Err(_err) => Response::Props(Box::new(Pokemon {
|
||||
name: "Nope".to_string(),
|
||||
id: 0,
|
||||
weight: 0,
|
||||
height: 0,
|
||||
})),
|
||||
Err(_err) => Response::Props(Props::new("{}"))
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -438,6 +445,92 @@ export default function PokemonView({
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
With the current setup all the routes always return a `200 Success` http status no matter the response type.
|
||||
|
||||
In order to return a more meaningful status code to the browser the `Props` struct can be initialized with also the
|
||||
`Props::new_with_status()` method.
|
||||
|
||||
Let's see how it works!
|
||||
|
||||
```diff
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
++ use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemon {
|
||||
name: String,
|
||||
id: u16,
|
||||
weight: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
|
||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||
let pokemon = req.params.get("pokemon").unwrap();
|
||||
|
||||
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
|
||||
Ok(res) => {
|
||||
++ if res.status() == StatusCode::NOT_FOUND {
|
||||
++ return Response::Props(Props::new_with_status("{}", StatusCode::NOT_FOUND));
|
||||
++ }
|
||||
|
||||
let data = res.json::<Pokemon>().await.unwrap();
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
-- Err(_err) => Response::Props(Props::new(
|
||||
++ Err(_err) => Response::Props(Props::new_with_status(
|
||||
++ "{}",
|
||||
++ StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```diff
|
||||
// src/routes/index.rs
|
||||
++ use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemons {
|
||||
results: Vec<Pokemon>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemon {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
|
||||
return match fetch.get(ALL_POKEMON).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemons>().await.unwrap();
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
-- Err(_err) => Response::Props(Props::new(
|
||||
++ Err(_err) => Response::Props(Props::new_with_status(
|
||||
++ "{}", // Return empty JSON
|
||||
++ StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
|
||||
correctly receive a 404 status code in the console.
|
||||
|
||||
## Conclusion
|
||||
|
||||
That’s it! You just created a multi thread full stack application with rust and react.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::Serialize;
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MyResponse<'a> {
|
||||
@@ -8,7 +8,7 @@ struct MyResponse<'a> {
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_server_side_props(_req: Request<'_>, _fetch: reqwest::Client) -> Response {
|
||||
Response::Props(Box::new(MyResponse {
|
||||
Response::Props(Props::new(MyResponse {
|
||||
subtitle: "The react / rust fullstack framework",
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// src/routes/index.rs
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
|
||||
@@ -20,8 +21,11 @@ async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response
|
||||
return match fetch.get(ALL_POKEMON).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemons>().await.unwrap();
|
||||
Response::Props(Box::new(data))
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
Err(_err) => Response::Props(Box::new(Pokemons { results: vec![] })),
|
||||
Err(_err) => Response::Props(Props::new_with_status(
|
||||
"{}", // Return empty JSON
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
|
||||
@@ -18,14 +20,15 @@ async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
|
||||
|
||||
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
|
||||
Ok(res) => {
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
return Response::Props(Props::new_with_status("{}", StatusCode::NOT_FOUND));
|
||||
}
|
||||
let data = res.json::<Pokemon>().await.unwrap();
|
||||
Response::Props(Box::new(data))
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
Err(_err) => Response::Props(Box::new(Pokemon {
|
||||
name: "Nope".to_string(),
|
||||
id: 0,
|
||||
weight: 0,
|
||||
height: 0,
|
||||
})),
|
||||
Err(_err) => Response::Props(Props::new_with_status(
|
||||
"{}",
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { TuonoProps } from 'tuono'
|
||||
import type { TuonoProps } from 'tuono'
|
||||
import PokemonView from '../../components/PokemonView'
|
||||
|
||||
export default function Pokemon({ data }: TuonoProps): JSX.Element {
|
||||
interface Pokemon {
|
||||
name: string
|
||||
id: string
|
||||
weight: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export default function PokemonPage({
|
||||
data,
|
||||
}: TuonoProps<Pokemon>): JSX.Element {
|
||||
return <PokemonView pokemon={data} />
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
|
||||
Reference in New Issue
Block a user