Compare commits

..

16 Commits

Author SHA1 Message Date
Valerio Ageno 3de9d1be66 feat: update version to v0.1.4 2024-06-15 21:04:37 +02:00
Valerio Ageno 00916f188d Merge pull request #3 from Valerioageno/enable-template-in-tuono-new
[tuono new] Enable --template flag
2024-06-15 21:01:08 +02:00
Valerio Ageno fff5f92893 doc: update examples READMEs 2024-06-15 21:02:10 +02:00
Valerio Ageno 8b796fc1f4 doc: update tutorial doc 2024-06-15 20:59:07 +02:00
Valerio Ageno 5ac679785b feat(tuono new): support --template arg 2024-06-15 20:55:19 +02:00
Valerio Ageno 38b6cc8c65 fix: broken icons in readme tree 2024-06-15 14:36:09 +02:00
Valerio Ageno abb2d658c6 Merge pull request #2 from Valerioageno/add-status-code-support
Add `StatusCode` support
2024-06-15 14:26:30 +02:00
Valerio Ageno 3587c085b9 feat: update version to v0.1.3 2024-06-15 14:28:21 +02:00
Valerio Ageno 581a7d79b3 feat: update example/tuono source code 2024-06-15 14:26:01 +02:00
Valerio Ageno e21b80a5d8 docs: add TOC to tutorial 2024-06-15 14:24:41 +02:00
Valerio Ageno e0fc19ecaf doc: update tutorial 2024-06-15 14:17:12 +02:00
Valerio Ageno 0be0ce4c15 feat: handle 404 on tutorial 2024-06-15 14:04:08 +02:00
Valerio Ageno 792358ea9c fix: format macros 2024-06-15 12:31:39 +02:00
Valerio Ageno 26c0c2488d feat: handle status codes 2024-06-15 12:30:20 +02:00
Valerio Ageno 70b4b9c28f refactor: handle macros code in tuono_lib crate 2024-06-15 12:15:51 +02:00
Valerio Ageno 8b4db423b4 chore: improve folder structure tree 2024-06-12 13:42:02 +02:00
21 changed files with 287 additions and 114 deletions
+8 -8
View File
@@ -61,14 +61,14 @@ tuono dev
## Folder structure ## Folder structure
``` ```
| public/ ├── package.json
- src/ ├── public
| routes/ ├── src
| styles/ ├── routes
| package.json │ └── styles
| Cargo.toml ├── Cargo.toml
| .gitignore ├── README.md
| tsconfig.json └── tsconfig.json
``` ```
## License ## License
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tuono" name = "tuono"
version = "0.1.2" version = "0.1.4"
edition = "2021" edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"] authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework" description = "The react/rust fullstack framework"
+13 -3
View File
@@ -15,7 +15,14 @@ enum Actions {
/// Build the production assets /// Build the production assets
Build, Build,
/// Scaffold a new project /// Scaffold a new project
New { folder_name: Option<String> }, New {
/// The folder in which load the project. Default is the current directory.
folder_name: Option<String>,
/// The template to use to scaffold the project. The template should match one of the tuono
/// examples
#[arg(short, long)]
template: Option<String>,
},
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -46,8 +53,11 @@ pub fn cli() -> std::io::Result<()> {
let mut vite_build = Command::new("./node_modules/.bin/tuono-build-prod"); let mut vite_build = Command::new("./node_modules/.bin/tuono-build-prod");
let _ = &vite_build.output()?; let _ = &vite_build.output()?;
} }
Actions::New { folder_name } => { Actions::New {
scaffold_project::create_new_project(folder_name); folder_name,
template,
} => {
scaffold_project::create_new_project(folder_name, template);
} }
} }
+22 -15
View File
@@ -38,12 +38,11 @@ fn create_file(path: PathBuf, content: String) -> std::io::Result<()> {
Ok(()) Ok(())
} }
pub fn create_new_project(folder_name: Option<String>) { pub fn create_new_project(folder_name: Option<String>, template: Option<String>) {
let folder = folder_name.unwrap_or(".".to_string()); let folder = folder_name.unwrap_or(".".to_string());
if folder != "." { // In case of missing select the tuono example
create_dir(&folder).unwrap(); let template = template.unwrap_or("tuono".to_string());
}
let client = blocking::Client::builder() let client = blocking::Client::builder()
.user_agent("") .user_agent("")
@@ -60,21 +59,25 @@ pub fn create_new_project(folder_name: Option<String>) {
let new_project_files = res let new_project_files = res
.tree .tree
.iter() .iter()
.filter(|GithubFile { path, .. }| { .filter(|GithubFile { path, .. }| path.starts_with(&format!("examples/{template}/")))
// TODO: Handle custom example download by CLI argument --template
if path.starts_with("examples/tuono/") {
return true;
}
false
})
.collect::<Vec<&GithubFile>>(); .collect::<Vec<&GithubFile>>();
if new_project_files.is_empty() {
println!("Template not found: {template}");
return;
}
if folder != "." {
create_dir(&folder).unwrap();
}
let folder_name = PathBuf::from(&folder); let folder_name = PathBuf::from(&folder);
let current_dir = env::current_dir().expect("Failed to get current working directory"); let current_dir = env::current_dir().expect("Failed to get current working directory");
let folder_path = current_dir.join(folder_name); let folder_path = current_dir.join(folder_name);
create_directories(&new_project_files, &folder_path).expect("Failed to create directories"); create_directories(&new_project_files, &folder_path, &template)
.expect("Failed to create directories");
for GithubFile { for GithubFile {
element_type, path, .. element_type, path, ..
@@ -88,7 +91,7 @@ pub fn create_new_project(folder_name: Option<String>) {
.text() .text()
.expect("Failed to parse the repo structure"); .expect("Failed to parse the repo structure");
let path = PathBuf::from(&path.replace("examples/tuono/", "")); let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
let file_path = folder_path.join(&path); let file_path = folder_path.join(&path);
@@ -101,13 +104,17 @@ pub fn create_new_project(folder_name: Option<String>) {
outro(folder); outro(folder);
} }
fn create_directories(new_project_files: &Vec<&GithubFile>, folder_path: &Path) -> io::Result<()> { fn create_directories(
new_project_files: &[&GithubFile],
folder_path: &Path,
template: &String,
) -> io::Result<()> {
for GithubFile { for GithubFile {
element_type, path, .. element_type, path, ..
} in new_project_files.iter() } in new_project_files.iter()
{ {
if let GithubFileType::Tree = element_type { if let GithubFileType::Tree = element_type {
let path = PathBuf::from(&path.replace("examples/tuono/", "")); let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
let dir_path = folder_path.join(&path); let dir_path = folder_path.join(&path);
create_dir(&dir_path).unwrap(); create_dir(&dir_path).unwrap();
+3 -2
View File
@@ -54,6 +54,8 @@ async fn main() {
.with_state(fetch); .with_state(fetch);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("\nDevelopment app ready at http://localhost:3000/");
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
@@ -65,7 +67,7 @@ async fn catch_all(Path(params): Path<HashMap<String, String>>, request: Request
// TODO: remove unwrap // TODO: remove unwrap
let payload = tuono_lib::Payload::new(&req, Box::new("")) let payload = tuono_lib::Payload::new(&req, &"")
.client_payload() .client_payload()
.unwrap(); .unwrap();
@@ -240,7 +242,6 @@ pub fn bundle_axum_source() -> io::Result<()> {
pub fn check_tuono_folder() -> io::Result<()> { pub fn check_tuono_folder() -> io::Result<()> {
let dev_folder = Path::new(DEV_FOLDER); let dev_folder = Path::new(DEV_FOLDER);
if !&dev_folder.is_dir() { if !&dev_folder.is_dir() {
println!("exists");
fs::create_dir(dev_folder)?; fs::create_dir(dev_folder)?;
} }
+1 -1
View File
@@ -54,7 +54,7 @@ pub async fn watch() -> Result<()> {
run_server.start().await; run_server.start().await;
println!("\nDevelopment app ready at http://localhost:3000/"); build_ssr_bundle.to_wait().await;
let wx = Watchexec::new(move |mut action| { let wx = Watchexec::new(move |mut action| {
let mut should_reload = false; let mut should_reload = false;
+3 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tuono_lib" name = "tuono_lib"
version = "0.1.2" version = "0.1.4"
edition = "2021" edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"] authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework" description = "The react/rust fullstack framework"
@@ -24,4 +24,5 @@ serde = { version = "1.0.202", features = ["derive"] }
erased-serde = "0.4.5" erased-serde = "0.4.5"
serde_json = "1.0" 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.4"}
+1 -1
View File
@@ -6,6 +6,6 @@ pub mod ssr;
pub use payload::Payload; pub use payload::Payload;
pub use request::Request; pub use request::Request;
pub use response::Response; pub use response::{Props, Response};
pub use ssr_rs::Ssr; pub use ssr_rs::Ssr;
pub use tuono_lib_macros::handler; pub use tuono_lib_macros::handler;
+2 -2
View File
@@ -8,11 +8,11 @@ use crate::Request;
/// This is the data shared to the client /// This is the data shared to the client
pub struct Payload<'a> { pub struct Payload<'a> {
router: Location<'a>, router: Location<'a>,
props: Box<dyn Serialize>, props: &'a dyn Serialize,
} }
impl<'a> Payload<'a> { 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 { Payload {
router: req.location(), router: req.location(),
props, props,
+51 -1
View File
@@ -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; use erased_serde::Serialize;
pub struct Props {
data: Box<dyn Serialize>,
http_code: StatusCode,
}
pub enum Response { pub enum Response {
Redirect(String), 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 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tuono_lib_macros" name = "tuono_lib_macros"
version = "0.1.2" version = "0.1.4"
edition = "2021" edition = "2021"
description = "The react/rust fullstack framework" description = "The react/rust fullstack framework"
homepage = "https://github.com/Valerioageno/tuono" homepage = "https://github.com/Valerioageno/tuono"
+10 -32
View File
@@ -8,7 +8,7 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
let fn_name = item.clone().sig.ident; let fn_name = item.clone().sig.ident;
quote! { quote! {
use axum::response::{Html, IntoResponse}; use axum::response::IntoResponse;
use std::collections::HashMap; use std::collections::HashMap;
use axum::extract::{State, Path}; use axum::extract::{State, Path};
use reqwest::Client; use reqwest::Client;
@@ -16,51 +16,29 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
#item #item
pub async fn route( pub async fn route(
Path(params): Path<HashMap<String, String>>, Path(params): Path<HashMap<String, String>>,
State(client): State<Client>, State(client): State<Client>,
request: axum::extract::Request request: axum::extract::Request
) -> Html<String> { ) -> impl IntoResponse {
let pathname = &request.uri(); let pathname = &request.uri();
let headers = &request.headers(); let headers = &request.headers();
let req = tuono_lib::Request::new(pathname, headers, params); let req = tuono_lib::Request::new(pathname, headers, params);
let local_response = #fn_name(req.clone(), client).await; #fn_name(req.clone(), client).await.render_to_string(req)
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())
}
} }
pub async fn api( pub async fn api(
Path(params): Path<HashMap<String, String>>, Path(params): Path<HashMap<String, String>>,
State(client): State<Client>, State(client): State<Client>,
request: axum::extract::Request request: axum::extract::Request
) -> axum::response::Response { ) -> impl IntoResponse{
let pathname = &request.uri(); let pathname = &request.uri();
let headers = &request.headers(); let headers = &request.headers();
let req = tuono_lib::Request::new(pathname, headers, params); let req = tuono_lib::Request::new(pathname, headers, params);
let local_response = #fn_name(req.clone(), client).await; #fn_name(req.clone(), client).await.json()
let res = match local_response{
tuono_lib::Response::Props(val) => return axum::Json(val).into_response(),
_ => return axum::Json("").into_response()
};
} }
} }
.into() .into()
+122 -28
View File
@@ -4,9 +4,22 @@ This tutorial is meant for giving you a sneak peek of the framework and is inten
The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction). The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction).
> If you prefer to see directly the final outcome check out the [tutorial source](https://github.com/Valerioageno/tuono/tree/main/examples/tutorial). > If you have already installed the tuono CLI you can download the tutorial source with `tuono new tuono-tutorial --template tutorial`
## Installation ## Table of Content
* [CLI Installation](#cli-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)
## CLI Installation
The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal: The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
@@ -20,9 +33,11 @@ To check that is correctly installed run:
$ tuono --version $ tuono --version
``` ```
Run `tuono -h` to see all the available commands.
## Project scaffold ## Project scaffold
To setup a new project you just need to run the following command: To setup a new fresh project you just need to run the following command:
```bash ```bash
$ tuono new tuono-tutorial $ tuono new tuono-tutorial
@@ -38,15 +53,15 @@ Open it with your favourite code editor.
The project will have the following structure: The project will have the following structure:
```bash ```
| public/ ├── package.json
- src/ ├── public
| routes/ ├── src
| styles/ ├── routes
| package.json │ └── styles
| Cargo.toml ├── Cargo.toml
| .gitignore ├── README.md
| tsconfig.json └── tsconfig.json
``` ```
**public/**: put here all the files you want to be public **public/**: put here all the files you want to be public
@@ -93,7 +108,7 @@ Clear the `index.rs` file and paste:
```rust ```rust
// src/routes/index.rs // src/routes/index.rs
use serde::{Deserialize, Serialize}; 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"; const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
@@ -110,15 +125,13 @@ struct Pokemon {
#[tuono_lib::handler] #[tuono_lib::handler]
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response { async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
return match fetch.get(ALL_POKEMON).send().await { return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => { Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap(); 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 +315,9 @@ These two will handle every requests that points to `http://localhost:3000/pokem
Lets first work on the server side file. Paste into the new `[pokemon].rs` file the following code: Lets first work on the server side file. Paste into the new `[pokemon].rs` file the following code:
```rust ```rust
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize}; 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"; const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
@@ -317,22 +331,16 @@ struct Pokemon {
#[tuono_lib::handler] #[tuono_lib::handler]
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response { 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(); let pokemon = req.params.get("pokemon").unwrap();
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await { return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
Ok(res) => { Ok(res) => {
let data = res.json::<Pokemon>().await.unwrap(); let data = res.json::<Pokemon>().await.unwrap();
Response::Props(Box::new(data)) Response::Props(Props::new(data))
} }
Err(_err) => Response::Props(Box::new(Pokemon { Err(_err) => Response::Props(Props::new("{}"))
name: "Nope".to_string(),
id: 0,
weight: 0,
height: 0,
})),
}; };
} }
``` ```
@@ -438,6 +446,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 ## Conclusion
Thats it! You just created a multi thread full stack application with rust and react. Thats it! You just created a multi thread full stack application with rust and react.
+10
View File
@@ -7,3 +7,13 @@ To simply scaffold the base project run in your terminal:
```shell ```shell
$ tuono new [NAME] $ tuono new [NAME]
``` ```
You can install any example included in this folder by just using the `--template` flag:
```shell
$ tuono new [NAME] --template [TEMPLATE]
```
`[TEMPLATE]` is the folder name.
+2 -2
View File
@@ -1,5 +1,5 @@
use serde::Serialize; use serde::Serialize;
use tuono_lib::{Request, Response}; use tuono_lib::{Props, Request, Response};
#[derive(Serialize)] #[derive(Serialize)]
struct MyResponse<'a> { struct MyResponse<'a> {
@@ -8,7 +8,7 @@ struct MyResponse<'a> {
#[tuono_lib::handler] #[tuono_lib::handler]
async fn get_server_side_props(_req: Request<'_>, _fetch: reqwest::Client) -> Response { 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", subtitle: "The react / rust fullstack framework",
})) }))
} }
+6
View File
@@ -1,3 +1,9 @@
# Tuono tutorial # Tuono tutorial
This project is the outcome of the [tuono tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) This project is the outcome of the [tuono tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md)
If you want to directly install it you can run:
```bash
$ tuono new my-project -t tutorial
```
+7 -3
View File
@@ -1,6 +1,7 @@
// src/routes/index.rs // src/routes/index.rs
use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; 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"; 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 { return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => { Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap(); 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 serde::{Deserialize, Serialize};
use tuono_lib::{Request, Response}; use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon"; 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 { return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
Ok(res) => { 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(); let data = res.json::<Pokemon>().await.unwrap();
Response::Props(Box::new(data)) Response::Props(Props::new(data))
} }
Err(_err) => Response::Props(Box::new(Pokemon { Err(_err) => Response::Props(Props::new_with_status(
name: "Nope".to_string(), "{}",
id: 0, StatusCode::INTERNAL_SERVER_ERROR,
weight: 0, )),
height: 0,
})),
}; };
} }
@@ -1,6 +1,15 @@
import { TuonoProps } from 'tuono' import type { TuonoProps } from 'tuono'
import PokemonView from '../../components/PokemonView' 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} /> return <PokemonView pokemon={data} />
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "tuono", "name": "tuono",
"version": "0.1.2", "version": "0.1.4",
"description": "", "description": "",
"main": "src/index.js", "main": "src/index.js",
"packageManager": "pnpm@9.1.1", "packageManager": "pnpm@9.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "tuono", "name": "tuono",
"version": "0.1.2", "version": "0.1.4",
"description": "The react/rust fullstack framework", "description": "The react/rust fullstack framework",
"scripts": { "scripts": {
"dev": "vite build --watch", "dev": "vite build --watch",