From df92ef5296056a3504fff5ed224913ae138d2fd2 Mon Sep 17 00:00:00 2001 From: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:28:56 +0100 Subject: [PATCH] Add support for custom global state (#111) * feat: add 'has_main_file' to App struct * feat: handle custom state from the main.rs file * feat: update tutorial template with new setup * fix: apply clippy hints * feat: add ApplicationState mention to documentation website * feat: update version to v0.13.0 --- .../src/components/sidebar/sidebar.tsx | 6 ++ .../documentation/application-state.mdx | 55 ++++++++++++++ .../documentation/tutorial/api-fetching.mdx | 72 +++++++++++++++++-- crates/tuono/Cargo.toml | 2 +- crates/tuono/src/app.rs | 15 +++- crates/tuono/src/source_builder.rs | 35 ++++++++- crates/tuono_lib/Cargo.toml | 4 +- crates/tuono_lib/src/server.rs | 12 ++-- crates/tuono_lib/src/vite_reverse_proxy.rs | 9 ++- crates/tuono_lib_macros/Cargo.toml | 2 +- crates/tuono_lib_macros/src/handler.rs | 43 +++++++++-- examples/tutorial/Cargo.toml | 1 + examples/tutorial/src/main.rs | 11 +++ packages/fs-router-vite-plugin/package.json | 2 +- packages/lazy-fn-vite-plugin/package.json | 2 +- packages/router/package.json | 2 +- packages/tuono/package.json | 2 +- 17 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 apps/documentation/src/routes/documentation/application-state.mdx create mode 100644 examples/tutorial/src/main.rs diff --git a/apps/documentation/src/components/sidebar/sidebar.tsx b/apps/documentation/src/components/sidebar/sidebar.tsx index 6a61b6ff..43588ae6 100644 --- a/apps/documentation/src/components/sidebar/sidebar.tsx +++ b/apps/documentation/src/components/sidebar/sidebar.tsx @@ -69,6 +69,12 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element { /> + + + +import Breadcrumbs, { Element } from '../../components/breadcrumbs' + + + +# Application state + +The main reason Tuono is fast it's because it loads just the features that are needed for the project. + +To defined them you need to fill the `ApplicationState` struct in the `./src/main.rs` file and they will be automatically available +in all the handlers you will define across the application. + +```rs +// src/main.rs + +#[derive(Clone)] +pub struct ApplicationState { + pub website_name: String, + pub base_path: String +} + +pub fn main() -> ApplicationState { + let website_name = "tuono".to_string(); + let base_path = "http://localhost:3000".to_string(); + + ApplicationState { + website_name, + base_path + } +} +``` + +> For the sake of simplicity here we just enable a `String` but you can add Database connections or +> HTTP clients (take a look [here](/documentation/tutorial/api-fetching) for an example with an HTTP client). + +Now the `ApplicationState` is available on all the handlers as following: + +```rs +// src/routes/index.rs + +#[tuono_lib::handler] +// The first argument always is the Request +// The ApplicationState arguments are optional. You can use just the ones you need +// to use in the handler (with no specific order). +async fn my_handler(req: Request, website_name: String) -> Response { + ... +} +``` diff --git a/apps/documentation/src/routes/documentation/tutorial/api-fetching.mdx b/apps/documentation/src/routes/documentation/tutorial/api-fetching.mdx index 82779c49..7c428c55 100644 --- a/apps/documentation/src/routes/documentation/tutorial/api-fetching.mdx +++ b/apps/documentation/src/routes/documentation/tutorial/api-fetching.mdx @@ -28,7 +28,7 @@ Clear the `index.rs` file and paste: ```rust // src/routes/index.rs use serde::{Deserialize, Serialize}; -use tuono_lib::reqwest::Client; +use reqwest::Client; use tuono_lib::{Props, Request, Response}; const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151"; @@ -56,9 +56,73 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response { } ``` -> The first argument is always the request `req: Request` which contains the request data like the query parameters and the headers. -> The rest of the arguments are optional and they don't need to be specified -> if they are not used. Enabled out of the box a [Reqwest](https://docs.rs/reqwest/latest/reqwest/) HTTP client. +> The first argument is always the request `req: Request` which contains all the request's data like the query parameters and the HTTP headers. +> The rest of the arguments represents the [ApplicationState](/documentation/application-state) and are optional. + +The terminal will complain now for two reasons: + +1. We don't have imported any `reqwest` crate +2. The second argument `fetch: Client` has not been defined yet as global state. + +Let's fix these in the next section. + +## Application state + +Compared to the common Javascript runtimes Tuono is fast because only the features you need for your project will be loaded. + +You can load them in the `ApplicationState` of your app inside the `./src/main.rs` file. This is the file that will be executed just once at the very beginning of your application. + +> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library. + +To install it just run in your terminal: + +```bash +$ cargo add reqwest +``` + +A new entry has just been added to your `Cargo.toml` file. + +```diff +[package] +name = "tuono" +version = "0.0.1" +edition = "2021" + +[[bin]] +name = "tuono" +path = ".tuono/main.rs" + +[dependencies] +tuono_lib = "0.14.0" # the version might be different +serde = { version = "1.0.202", features = ["derive"] } +++ reqwest = "0.12.9" # the version might be different +``` + +> The `Cargo.toml` is the manifest file of your application in which handling the Rust's dependencies (similarly as the `package.json` for Javascript). + +Now let's define the `ApplicationState` in the `./src/main.rs` file. + +```rs +// Import here the just added reqwest library +use reqwest::Client; + +#[derive(Clone)] +// Extend this struct with the feature you will need for your application +pub struct ApplicationState { + // This will be available to all your route handlers + pub fetch: Client, +} + +pub fn main() -> ApplicationState { + let fetch = Client::new(); + return ApplicationState { fetch }; +} +``` + +Now the `fetch: Client` argument is available in the above defined handler and the terminal should not complain anymore. +Let's see in the next section how to show the fetched data on the browser. + +## Handling the page UI Now the Pokémon are correctly fetched and hydrated on the client side, so we can actually use them. Clear the `index.tsx` file and paste: diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index 2fde4e5f..94089e5b 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono" -version = "0.12.4" +version = "0.13.0" edition = "2021" authors = ["V. Ageno "] description = "Superfast React fullstack framework" diff --git a/crates/tuono/src/app.rs b/crates/tuono/src/app.rs index 9b638c82..5aaec93d 100644 --- a/crates/tuono/src/app.rs +++ b/crates/tuono/src/app.rs @@ -1,6 +1,9 @@ use glob::glob; use glob::GlobError; use std::collections::{hash_map::Entry, HashMap}; +use std::fs::File; +use std::io::prelude::*; +use std::io::BufReader; use std::path::PathBuf; use std::process::Child; use std::process::Command; @@ -15,6 +18,15 @@ const IGNORE_FILES: [&str; 1] = ["__root"]; pub struct App { pub route_map: HashMap, pub base_path: PathBuf, + pub has_main_file: bool, +} + +fn has_main_file(base_path: PathBuf) -> std::io::Result { + let file = File::open(base_path.join("src/main.rs"))?; + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents)?; + Ok(contents.contains("pub fn main")) } impl App { @@ -23,7 +35,8 @@ impl App { let mut app = App { route_map: HashMap::new(), - base_path, + base_path: base_path.clone(), + has_main_file: has_main_file(base_path).unwrap_or(false), }; app.collect_routes(); diff --git a/crates/tuono/src/source_builder.rs b/crates/tuono/src/source_builder.rs index c7744cc5..43c93189 100644 --- a/crates/tuono/src/source_builder.rs +++ b/crates/tuono/src/source_builder.rs @@ -42,12 +42,17 @@ const MODE: Mode = /*MODE*/; // MODULE_IMPORTS +//MAIN_FILE_IMPORT// + #[tokio::main] async fn main() { println!("\n ⚡ Tuono v/*VERSION*/"); + + //MAIN_FILE_DEFINITION// + let router = Router::new() // ROUTE_BUILDER - ; + //MAIN_FILE_USAGE//; Server::init(router, MODE).start().await } @@ -131,7 +136,33 @@ fn generate_axum_source(app: &App, mode: Mode) -> String { &create_modules_declaration(&app.route_map), ) .replace("/*VERSION*/", crate_version!()) - .replace("/*MODE*/", mode.as_str()); + .replace("/*MODE*/", mode.as_str()) + .replace( + "//MAIN_FILE_IMPORT//", + if app.has_main_file { + r#"#[path="../src/main.rs"] + mod tuono_main_state; + "# + } else { + "" + }, + ) + .replace( + "//MAIN_FILE_DEFINITION//", + if app.has_main_file { + "let user_custom_state = tuono_main_state::main();" + } else { + "" + }, + ) + .replace( + "//MAIN_FILE_USAGE//", + if app.has_main_file { + ".with_state(user_custom_state)" + } else { + "" + }, + ); let has_server_handlers = app .route_map diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index 9bb27a7d..6dd545ce 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib" -version = "0.12.4" +version = "0.13.0" edition = "2021" authors = ["V. Ageno "] description = "Superfast React fullstack framework" @@ -31,7 +31,7 @@ either = "1.13.0" tower-http = {version = "0.6.0", features = ["fs"]} colored = "2.1.0" -tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.12.4"} +tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.13.0"} # Match the same version used by axum tokio-tungstenite = "0.24.0" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } diff --git a/crates/tuono_lib/src/server.rs b/crates/tuono_lib/src/server.rs index 6a4b19aa..e7181132 100644 --- a/crates/tuono_lib/src/server.rs +++ b/crates/tuono_lib/src/server.rs @@ -14,12 +14,12 @@ const DEV_PUBLIC_DIR: &str = "public"; const PROD_PUBLIC_DIR: &str = "out/client"; pub struct Server { - router: Router, + router: Router, mode: Mode, } impl Server { - pub fn init(router: Router, mode: Mode) -> Server { + pub fn init(router: Router, mode: Mode) -> Server { Ssr::create_platform(); GLOBAL_MODE.set(mode).unwrap(); @@ -34,8 +34,6 @@ impl Server { pub async fn start(&self) { let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - let fetch = reqwest::Client::new(); - if self.mode == Mode::Dev { println!(" Ready at: {}\n", "http://localhost:3000".blue().bold()); let router = self @@ -47,8 +45,7 @@ impl Server { .fallback_service( ServeDir::new(DEV_PUBLIC_DIR) .fallback(get(catch_all).layer(LoggerLayer::new())), - ) - .with_state(fetch); + ); axum::serve(listener, router) .await @@ -65,8 +62,7 @@ impl Server { .fallback_service( ServeDir::new(PROD_PUBLIC_DIR) .fallback(get(catch_all).layer(LoggerLayer::new())), - ) - .with_state(fetch); + ); axum::serve(listener, router) .await diff --git a/crates/tuono_lib/src/vite_reverse_proxy.rs b/crates/tuono_lib/src/vite_reverse_proxy.rs index daeb6644..ef56952c 100644 --- a/crates/tuono_lib/src/vite_reverse_proxy.rs +++ b/crates/tuono_lib/src/vite_reverse_proxy.rs @@ -1,5 +1,5 @@ use axum::body::Body; -use axum::extract::{Path, State}; +use axum::extract::Path; use axum::http::{HeaderName, HeaderValue}; use axum::response::{IntoResponse, Response}; @@ -7,10 +7,9 @@ use reqwest::Client; const VITE_URL: &str = "http://localhost:3001/vite-server"; -pub async fn vite_reverse_proxy( - State(client): State, - Path(path): Path, -) -> impl IntoResponse { +pub async fn vite_reverse_proxy(Path(path): Path) -> impl IntoResponse { + let client = Client::new(); + match client.get(format!("{VITE_URL}/{path}")).send().await { Ok(res) => { let mut response_builder = Response::builder().status(res.status().as_u16()); diff --git a/crates/tuono_lib_macros/Cargo.toml b/crates/tuono_lib_macros/Cargo.toml index 3b1126ce..13ba0027 100644 --- a/crates/tuono_lib_macros/Cargo.toml +++ b/crates/tuono_lib_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib_macros" -version = "0.12.4" +version = "0.13.0" edition = "2021" description = "Superfast React fullstack framework" homepage = "https://tuono.dev" diff --git a/crates/tuono_lib_macros/src/handler.rs b/crates/tuono_lib_macros/src/handler.rs index bb87b329..ffc94997 100644 --- a/crates/tuono_lib_macros/src/handler.rs +++ b/crates/tuono_lib_macros/src/handler.rs @@ -2,15 +2,35 @@ use proc_macro::TokenStream; use quote::quote; use syn::punctuated::Punctuated; use syn::token::Comma; -use syn::{parse2, parse_macro_input, FnArg, ItemFn, Pat, Type}; +use syn::{parse2, parse_macro_input, parse_quote, FnArg, ItemFn, ItemUse, Pat, Stmt}; -fn create_struct_fn_arg(arg_name: Pat, arg_type: Type) -> FnArg { +fn create_struct_fn_arg() -> FnArg { parse2(quote! { - tuono_lib::axum::extract::State(#arg_name): tuono_lib::axum::extract::State<#arg_type> + tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State }) .unwrap() } +fn import_main_application_state(argument_names: Punctuated) -> Option { + if !argument_names.is_empty() { + let use_item: ItemUse = parse_quote!(let ApplicationState { #argument_names } = state;); + return Some(use_item); + } + + None +} + +fn crate_application_state_extractor(argument_names: Punctuated) -> Option { + if !argument_names.is_empty() { + let local: Stmt = parse_quote!( + use crate::tuono_main_state::ApplicationState; + ); + return Some(local); + } + + None +} + fn params_argument() -> FnArg { parse2(quote! { tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path< @@ -41,23 +61,33 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream { continue; } + if i == 1 { + axum_arguments.insert(1, create_struct_fn_arg()) + } + if let FnArg::Typed(pat_type) = arg { let index = i - 1; let argument_name = *pat_type.pat.clone(); - let argument_type = *pat_type.ty.clone(); argument_names.insert(index, argument_name.clone()); - axum_arguments.insert(index, create_struct_fn_arg(argument_name, argument_type)) } } axum_arguments.insert(axum_arguments.len(), request_argument()); + let application_state_extractor = crate_application_state_extractor(argument_names.clone()); + let application_state_import = import_main_application_state(argument_names.clone()); + quote! { + #application_state_import + #item pub async fn route( #axum_arguments ) -> impl tuono_lib::axum::response::IntoResponse { + + #application_state_extractor + let pathname = request.uri(); let headers = request.headers(); @@ -69,6 +99,9 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream { pub async fn api( #axum_arguments ) -> impl tuono_lib::axum::response::IntoResponse { + + #application_state_extractor + let pathname = request.uri(); let headers = request.headers(); diff --git a/examples/tutorial/Cargo.toml b/examples/tutorial/Cargo.toml index e4487952..e36032a7 100644 --- a/examples/tutorial/Cargo.toml +++ b/examples/tutorial/Cargo.toml @@ -10,4 +10,5 @@ path = ".tuono/main.rs" [dependencies] tuono_lib = { path = "../../crates/tuono_lib/"} serde = { version = "1.0.202", features = ["derive"] } +reqwest = "0.12.9" diff --git a/examples/tutorial/src/main.rs b/examples/tutorial/src/main.rs new file mode 100644 index 00000000..e148b951 --- /dev/null +++ b/examples/tutorial/src/main.rs @@ -0,0 +1,11 @@ +use reqwest::Client; + +#[derive(Clone)] +pub struct ApplicationState { + pub fetch: Client, +} + +pub fn main() -> ApplicationState { + let fetch = Client::new(); + return ApplicationState { fetch }; +} diff --git a/packages/fs-router-vite-plugin/package.json b/packages/fs-router-vite-plugin/package.json index a0559c60..22d79387 100644 --- a/packages/fs-router-vite-plugin/package.json +++ b/packages/fs-router-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "tuono-fs-router-vite-plugin", - "version": "0.12.4", + "version": "0.13.0", "description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework", "homepage": "https://tuono.dev", "scripts": { diff --git a/packages/lazy-fn-vite-plugin/package.json b/packages/lazy-fn-vite-plugin/package.json index f43208b7..887d864c 100644 --- a/packages/lazy-fn-vite-plugin/package.json +++ b/packages/lazy-fn-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "tuono-lazy-fn-vite-plugin", - "version": "0.12.4", + "version": "0.13.0", "description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework", "homepage": "https://tuono.dev", "scripts": { diff --git a/packages/router/package.json b/packages/router/package.json index c0311f5b..c6ee2e08 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "tuono-router", - "version": "0.12.4", + "version": "0.13.0", "description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework", "homepage": "https://tuono.dev", "scripts": { diff --git a/packages/tuono/package.json b/packages/tuono/package.json index 7aac9834..d02568bc 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -1,6 +1,6 @@ { "name": "tuono", - "version": "0.12.4", + "version": "0.13.0", "description": "Superfast React fullstack framework", "homepage": "https://tuono.dev", "scripts": {