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": {