mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
Add support for api/ folder (#129)
* feat: detect api/ folder from tuono CLI * feat: prevent adding the API in the data endpoints list * feat: create basic API handler proc macro * feat: parse api/ and build .tuono/main.rs file * chore: remove unused cargo dependencies * refactor: tuono base health_check * fix: remove failing test * feat: update version to v0.14.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.13.3"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
@@ -32,3 +32,4 @@ regex = "1.10.4"
|
||||
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
||||
serde_json = "1.0"
|
||||
fs_extra = "1.3.0"
|
||||
http = "1.1.0"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use glob::glob;
|
||||
use glob::GlobError;
|
||||
use http::Method;
|
||||
use std::collections::hash_set::HashSet;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
@@ -115,6 +117,24 @@ impl App {
|
||||
.spawn()
|
||||
.expect("Failed to run the rust server")
|
||||
}
|
||||
|
||||
pub fn get_used_http_methods(&self) -> HashSet<Method> {
|
||||
let mut acc = HashSet::new();
|
||||
|
||||
for (_, route) in self.route_map.clone().into_iter() {
|
||||
if route.axum_info.is_some() {
|
||||
acc.insert(Method::GET);
|
||||
}
|
||||
if !route.is_api() {
|
||||
continue;
|
||||
}
|
||||
for method in route.api_data.unwrap().methods.into_iter() {
|
||||
acc.insert(method);
|
||||
}
|
||||
}
|
||||
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use http::Method;
|
||||
use regex::Regex;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn has_dynamic_path(route: &str) -> bool {
|
||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||
regex.is_match(route)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct AxumInfo {
|
||||
// Path for importing the module
|
||||
pub module_import: String,
|
||||
@@ -65,11 +67,56 @@ impl AxumInfo {
|
||||
// TODO: to be extended with common scenarios
|
||||
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
// TODO: Refine this function to catch
|
||||
// if the methods are commented.
|
||||
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
|
||||
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
|
||||
|
||||
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
|
||||
|
||||
regex
|
||||
.find_iter(&file)
|
||||
.map(|proc_macro| {
|
||||
let http_method = proc_macro
|
||||
.as_str()
|
||||
// Extract just the element surrounded by the phrantesist.
|
||||
.replace("tuono_lib::api(", "")
|
||||
.replace(")]", "");
|
||||
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
|
||||
})
|
||||
.collect::<Vec<Method>>()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ApiData {
|
||||
pub methods: Vec<Method>,
|
||||
}
|
||||
|
||||
impl ApiData {
|
||||
pub fn new(path: &String) -> Option<Self> {
|
||||
if !path.starts_with("/api/") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_path = std::env::current_dir().expect("Failed to get the base_path");
|
||||
|
||||
let file_path = base_path
|
||||
.join(format!("src/routes{path}.rs"))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let methods = read_http_methods_from_file(&file_path);
|
||||
|
||||
Some(ApiData { methods })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Route {
|
||||
path: String,
|
||||
pub is_dynamic: bool,
|
||||
pub axum_info: Option<AxumInfo>,
|
||||
pub api_data: Option<ApiData>,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
@@ -78,9 +125,14 @@ impl Route {
|
||||
path: cleaned_path.clone(),
|
||||
axum_info: None,
|
||||
is_dynamic: has_dynamic_path(&cleaned_path),
|
||||
api_data: ApiData::new(&cleaned_path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_api(&self) -> bool {
|
||||
self.api_data.is_some()
|
||||
}
|
||||
|
||||
pub fn update_axum_info(&mut self) {
|
||||
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
||||
}
|
||||
|
||||
@@ -82,13 +82,23 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
module_import,
|
||||
} = axum_info.as_ref().unwrap();
|
||||
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::route))"#
|
||||
));
|
||||
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
||||
));
|
||||
if !route.is_api() {
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::route))"#
|
||||
));
|
||||
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
||||
));
|
||||
} else {
|
||||
for method in route.api_data.as_ref().unwrap().methods.clone() {
|
||||
let method = method.to_string().to_lowercase();
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", {method}({module_import}::{method}__tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,20 +174,16 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
|
||||
},
|
||||
);
|
||||
|
||||
let has_server_handlers = app
|
||||
.route_map
|
||||
.iter()
|
||||
.filter(|(_, route)| route.axum_info.is_some())
|
||||
.collect::<Vec<(&String, &Route)>>()
|
||||
.is_empty();
|
||||
let mut import_http_handler = String::new();
|
||||
|
||||
if !has_server_handlers {
|
||||
return src.replace(
|
||||
"// AXUM_GET_ROUTE_HANDLER",
|
||||
"use tuono_lib::axum::routing::get;",
|
||||
);
|
||||
let used_http_methods = app.get_used_http_methods();
|
||||
|
||||
for method in used_http_methods.into_iter() {
|
||||
let method = method.to_string().to_lowercase();
|
||||
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
|
||||
}
|
||||
src
|
||||
|
||||
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
|
||||
}
|
||||
|
||||
pub fn check_tuono_folder() -> io::Result<()> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.13.3"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
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.13.3"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.14.0"}
|
||||
# Match the same version used by axum
|
||||
tokio-tungstenite = "0.24.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||
|
||||
@@ -15,7 +15,7 @@ pub use payload::Payload;
|
||||
pub use request::Request;
|
||||
pub use response::{Props, Response};
|
||||
pub use server::Server;
|
||||
pub use tuono_lib_macros::handler;
|
||||
pub use tuono_lib_macros::{api, handler};
|
||||
|
||||
// Re-exports
|
||||
pub use axum;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.13.3"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::utils::{
|
||||
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
|
||||
params_argument, request_argument,
|
||||
};
|
||||
use proc_macro::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse_macro_input, FnArg, Ident, ItemFn, Pat};
|
||||
|
||||
pub fn api_core(attrs: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(item as ItemFn);
|
||||
let http_method = parse_macro_input!(attrs as Ident)
|
||||
.to_string()
|
||||
.to_lowercase();
|
||||
|
||||
let api_fn_name = Ident::new(
|
||||
&format!("{}__tuono_internal_api", http_method),
|
||||
Span::call_site().into(),
|
||||
);
|
||||
|
||||
let fn_name = &item.sig.ident;
|
||||
let return_type = &item.sig.output;
|
||||
|
||||
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
|
||||
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
|
||||
|
||||
// Fn Arguments minus the first which always is the request
|
||||
for (i, arg) in item.sig.inputs.iter().enumerate() {
|
||||
if i == 0 {
|
||||
axum_arguments.insert(i, params_argument());
|
||||
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();
|
||||
argument_names.insert(index, argument_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
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 #api_fn_name(#axum_arguments)#return_type {
|
||||
|
||||
#application_state_extractor
|
||||
|
||||
let pathname = request.uri();
|
||||
let headers = request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||
|
||||
#fn_name(req.clone(), #argument_names).await
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1,51 +1,14 @@
|
||||
use crate::utils::{
|
||||
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
|
||||
params_argument, request_argument,
|
||||
};
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse2, parse_macro_input, parse_quote, FnArg, ItemFn, Pat, Stmt};
|
||||
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
|
||||
|
||||
fn create_struct_fn_arg() -> FnArg {
|
||||
parse2(quote! {
|
||||
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
|
||||
if !argument_names.is_empty() {
|
||||
let local: Stmt = parse_quote!(
|
||||
use crate::tuono_main_state::ApplicationState;
|
||||
);
|
||||
return Some(local);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
|
||||
if !argument_names.is_empty() {
|
||||
let use_item: Stmt = parse_quote!(let ApplicationState { #argument_names } = state;);
|
||||
return Some(use_item);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn params_argument() -> FnArg {
|
||||
parse2(quote! {
|
||||
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
|
||||
std::collections::HashMap<String, String>
|
||||
>
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn request_argument() -> FnArg {
|
||||
parse2(quote! {
|
||||
request: tuono_lib::axum::extract::Request
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(item as ItemFn);
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
mod api;
|
||||
mod handler;
|
||||
mod utils;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
handler::handler_core(args, item)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn api(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
api::api_core(args, item)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use quote::quote;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse2, parse_quote, FnArg, Pat, Stmt};
|
||||
|
||||
pub fn create_struct_fn_arg() -> FnArg {
|
||||
parse2(quote! {
|
||||
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
|
||||
if !argument_names.is_empty() {
|
||||
let local: Stmt = parse_quote!(
|
||||
use crate::tuono_main_state::ApplicationState;
|
||||
);
|
||||
return Some(local);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
|
||||
if !argument_names.is_empty() {
|
||||
let use_item: Stmt = parse_quote!(let ApplicationState { #argument_names } = state;);
|
||||
return Some(use_item);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn params_argument() -> FnArg {
|
||||
parse2(quote! {
|
||||
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
|
||||
std::collections::HashMap<String, String>
|
||||
>
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn request_argument() -> FnArg {
|
||||
parse2(quote! {
|
||||
request: tuono_lib::axum::extract::Request
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use tuono_lib::axum::http::StatusCode;
|
||||
use tuono_lib::Request;
|
||||
|
||||
#[tuono_lib::api(GET)]
|
||||
pub async fn my_get_request(_req: Request) -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-fs-router-vite-plugin",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.0",
|
||||
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-lazy-fn-vite-plugin",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.0",
|
||||
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-router",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.0",
|
||||
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.0",
|
||||
"description": "Superfast React fullstack framework",
|
||||
"homepage": "https://tuono.dev",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user