From cbf17bba365401bcf44c99fc215a095e32858152 Mon Sep 17 00:00:00 2001 From: nwrenger Date: Tue, 30 Jul 2024 00:44:34 +0200 Subject: [PATCH] Revert ":sparkles: Added gluer crate and gluer-macro crate and moved functions accordingly" This reverts commit f905269d692d2e618b7161060458ed4579b9a57f. --- Cargo.toml | 6 +- macros/Cargo.toml | 25 --- macros/src/lib.rs | 232 ----------------------- src/lib.rs | 458 ++++++++++++++++++++++++++++++++++++++-------- tests/main.rs | 29 +-- 5 files changed, 398 insertions(+), 352 deletions(-) delete mode 100644 macros/Cargo.toml delete mode 100644 macros/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5ecb51c..0fa74d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,13 @@ readme = "README.md" license = "MIT" [lib] +proc-macro = true [dependencies] -gluer-macros = { path = "macros", version = "*"} +quote = "1.0" +syn = { version = "2.0.62", features = ["full"] } +proc-macro2 = "1.0" +once_cell = "1.19.0" [dev-dependencies] axum = "0.7.5" diff --git a/macros/Cargo.toml b/macros/Cargo.toml deleted file mode 100644 index 4fa19dd..0000000 --- a/macros/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "gluer-macros" -version = "0.3.1" -edition = "2021" -authors = ["Nils Wrenger "] -description = "Procedural macros for the Gluer framework" -keywords = ["parser", "api", "macro"] -categories = ["accessibility", "web-programming", "api-bindings"] -rust-version = "1.64.0" -repository = "https://github.com/nwrenger/gluer" -readme = "README.md" -license = "MIT" - -[lib] -proc-macro = true - -[dependencies] -quote = "1.0" -syn = { version = "2.0.62", features = ["full"] } -proc-macro2 = "1.0" - -[dev-dependencies] -axum = "0.7.5" -tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] } -serde = { version = "1.0", features = ["derive"] } diff --git a/macros/src/lib.rs b/macros/src/lib.rs deleted file mode 100644 index 854cd37..0000000 --- a/macros/src/lib.rs +++ /dev/null @@ -1,232 +0,0 @@ -use proc_macro as pc; -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use std::fmt; -use syn::{parenthesized, parse::Parse, spanned::Spanned}; - -fn s_err(span: proc_macro2::Span, msg: impl fmt::Display) -> syn::Error { - syn::Error::new(span, msg) -} - -fn logic_err(span: proc_macro2::Span) -> syn::Error { - s_err( - span, - "Fatal logic error when trying to extract data from rust types", - ) -} - -/// Adds a route to the router. Use for each api endpoint you want to expose to the frontend. -/// `Inline Functions` are currently not supported. -#[proc_macro] -pub fn add_route(input: pc::TokenStream) -> pc::TokenStream { - match add_route_inner(input.into()) { - Ok(result) => result.into(), - Err(e) => e.into_compile_error().into(), - } -} - -fn add_route_inner(input: TokenStream) -> syn::Result { - let span = input.span(); - let args = syn::parse2::(input)?; - - let routes_ident = args.routes_ident; - let app_ident = args.app_ident; - let route = args.route; - let handler = args.handler; - - let mut routes = Vec::new(); - - for MethodCall { method, r#fn } in &handler { - let fn_name = r#fn - .segments - .last() - .ok_or_else(|| logic_err(span))? - .ident - .to_string(); - - routes.push(Route { - route: route.clone(), - method: method.to_string(), - fn_name, - }); - } - - Ok(quote! { - #app_ident = #app_ident.route(#route, #(#handler).*); - #routes_ident.extend_from_slice(&[#(#routes),*]); - }) -} - -struct RouterArgs { - routes_ident: syn::Ident, - app_ident: syn::Ident, - route: String, - handler: Vec, -} - -impl Parse for RouterArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let routes_ident = input.parse()?; - input.parse::()?; - let app_ident = input.parse()?; - input.parse::()?; - let route = input.parse::()?.value(); - input.parse::()?; - let handler = input.parse_terminated(MethodCall::parse, syn::Token![.])?; - let handler: Vec = handler.into_iter().collect(); - - Ok(RouterArgs { - routes_ident, - app_ident, - route, - handler, - }) - } -} - -struct MethodCall { - method: syn::Ident, - r#fn: syn::Path, -} - -impl Parse for MethodCall { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let method: syn::Ident = input.parse()?; - let content; - parenthesized!(content in input); - let r#fn: syn::Path = content.parse()?; - - Ok(MethodCall { method, r#fn }) - } -} - -impl ToTokens for MethodCall { - fn to_tokens(&self, tokens: &mut TokenStream) { - let method = &self.method; - let r#fn = &self.r#fn; - - tokens.extend(quote! { - #method(#r#fn) - }); - } -} - -struct Route { - route: String, - method: String, - fn_name: String, -} - -impl ToTokens for Route { - fn to_tokens(&self, tokens: &mut TokenStream) { - let route = &self.route; - let method = &self.method; - let fn_name = &self.fn_name; - - tokens.extend(quote! { - (#route, #method, #fn_name) - }); - } -} - -/// Put before structs or functions to be usable by the `glue` crate. -#[proc_macro_attribute] -pub fn cached(args: pc::TokenStream, input: pc::TokenStream) -> pc::TokenStream { - match cached_inner(args.into(), input.into()) { - Ok(result) => result.into(), - Err(e) => e.into_compile_error().into(), - } -} - -fn cached_inner(args: TokenStream, input: TokenStream) -> syn::Result { - let span = input.span(); - let item = syn::parse2::(input.clone())?; - let _args = syn::parse2::(args)?; - - let generated_const = match item.clone() { - syn::Item::Struct(item_struct) => generate_struct_const(item_struct)?, - syn::Item::Fn(item_fn) => generate_fn_const(item_fn)?, - _ => return Err(s_err(span, "Expected struct or function")), - }; - - Ok(quote! { - #generated_const - #item - }) -} - -fn generate_struct_const(item_struct: syn::ItemStruct) -> syn::Result { - let struct_name = item_struct.ident.to_string(); - let fields = item_struct - .fields - .into_iter() - .map(|field| { - let ident = field - .ident - .clone() - .ok_or_else(|| s_err(field.span(), "Unnamed field not supported"))? - .to_string(); - let ty = field.ty.into_token_stream().to_string(); - Ok((ident, ty)) - }) - .collect::>>()?; - - let const_ident = syn::Ident::new( - &format!("CACHED_STRUCT_{}", struct_name.to_uppercase()), - proc_macro2::Span::call_site(), - ); - - let const_value = fields.iter().map(|(ident, ty)| { - quote! { (#ident, #ty) } - }); - - Ok(quote! { - pub const #const_ident: (&str, &[(&str, &str)]) = (#struct_name, &[#(#const_value),*]); - }) -} - -fn generate_fn_const(item_fn: syn::ItemFn) -> syn::Result { - let fn_name = item_fn.sig.ident.to_string(); - let params = item_fn - .sig - .inputs - .iter() - .map(|param| match param { - syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => { - let pat = pat.to_token_stream().to_string(); - let ty = ty.to_token_stream().to_string(); - Ok((pat, ty)) - } - syn::FnArg::Receiver(_) => Err(s_err(param.span(), "Receiver parameter not allowed")), - }) - .collect::>>()?; - - let response = match &item_fn.sig.output { - syn::ReturnType::Type(_, ty) => ty.into_token_stream().to_string(), - syn::ReturnType::Default => "()".to_string(), - }; - - let const_ident = syn::Ident::new( - &format!("CACHED_FN_{}", fn_name.to_uppercase()), - proc_macro2::Span::call_site(), - ); - - let const_value = params.iter().map(|(pat, ty)| { - quote! { (#pat, #ty) } - }); - - Ok(quote! { - pub const #const_ident: (&str, &[(&str, &str)], &str) = (#fn_name, &[#(#const_value),*], #response); - }) -} - -struct NoArgs {} - -impl syn::parse::Parse for NoArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - if !input.is_empty() { - return Err(input.error("No arguments expected")); - } - Ok(NoArgs {}) - } -} diff --git a/src/lib.rs b/src/lib.rs index 1c4c00f..23ecc67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,44 +1,230 @@ -pub use gluer_macros::{add_route, cached}; - -use std::collections::BTreeMap; -use std::fs::File; -use std::io::Write; -use std::path::Path; - -/// Generates an api ts file from the routes added with `add_route!`. Specify the `path`, `routes`, `fns` and `structs`. -pub fn gen_ts>( - path: P, - routes: Vec<(&str, &str, &str)>, // (url, method, fn_name) - fns: &[(&str, &[(&str, &str)], &str)], // (fn_name, params, response) - structs: &[(&str, &[(&str, &str)])], // (struct_name, fields) -) -> Result<(), String> { +#![doc = include_str!("../README.md")] + +use once_cell::sync::Lazy; +use proc_macro as pc; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use std::{ + collections::BTreeMap, + fmt, + io::Write, + sync::{Arc, RwLock}, +}; +use syn::{parenthesized, parse::Parse, spanned::Spanned}; + +fn s_err(span: proc_macro2::Span, msg: impl fmt::Display) -> syn::Error { + syn::Error::new(span, msg) +} + +fn logic_err(span: proc_macro2::Span) -> syn::Error { + s_err( + span, + "Fatal logic error when trying to extract data from rust types", + ) +} + +#[derive(Default)] +struct GlobalState { + routes: RwLock>, + structs: RwLock>>, + functions: RwLock>, +} + +impl GlobalState { + fn instance() -> &'static Arc { + static INSTANCE: Lazy> = Lazy::new(|| Arc::new(GlobalState::default())); + &INSTANCE + } + + fn add_route(route: Route) { + let state = GlobalState::instance(); + let mut routes = state.routes.write().unwrap(); + routes.push(route); + } + + fn add_struct(name: String, fields: Vec) { + let state = GlobalState::instance(); + let mut structs = state.structs.write().unwrap(); + structs.insert(name, fields); + } + + fn add_function(name: String, function: Function) { + let state = GlobalState::instance(); + let mut functions = state.functions.write().unwrap(); + functions.insert(name, function); + } + + fn get_routes() -> Vec { + let state = GlobalState::instance(); + let routes = state.routes.read().unwrap(); + routes.clone() + } + + fn get_struct(name: &str) -> Option> { + let state = GlobalState::instance(); + let structs = state.structs.read().unwrap(); + structs.get(name).cloned() + } + + fn get_function(name: &str) -> Option { + let state = GlobalState::instance(); + let functions = state.functions.read().unwrap(); + functions.get(name).cloned() + } +} + +#[derive(Clone)] +struct Route { + route: String, + method: String, + fn_name: String, +} + +#[derive(Clone)] +struct Function { + params: BTreeMap, + response: String, +} + +#[derive(Clone)] +struct StructField { + ident: String, + ty: String, +} + +/// Adds a route to the router. Use for each api endpoint you want to expose to the frontend. +/// `Inline Functions` are currently not supported. +#[proc_macro] +pub fn add_route(input: pc::TokenStream) -> pc::TokenStream { + match add_route_inner(input.into()) { + Ok(result) => result.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn add_route_inner(input: TokenStream) -> syn::Result { + let span = input.span(); + let args = syn::parse2::(input)?; + + let ident = args.ident; + let route = args.route; + let handler = args.handler; + + for MethodCall { method, r#fn } in &handler { + let fn_name = r#fn + .segments + .last() + .ok_or_else(|| logic_err(span))? + .ident + .to_string(); + + GlobalState::add_route(Route { + route: route.clone(), + method: method.to_string(), + fn_name, + }); + } + + Ok(quote! { + #ident = #ident.route(#route, #(#handler).*); + }) +} + +struct RouterArgs { + ident: syn::Ident, + route: String, + handler: Vec, +} + +impl Parse for RouterArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let ident = input.parse()?; + input.parse::()?; + let route = input.parse::()?.value(); + input.parse::()?; + let handler = input.parse_terminated(MethodCall::parse, syn::Token![.])?; + let handler: Vec = handler.into_iter().collect(); + + Ok(RouterArgs { + ident, + route, + handler, + }) + } +} + +struct MethodCall { + method: syn::Ident, + r#fn: syn::Path, +} + +impl Parse for MethodCall { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let method: syn::Ident = input.parse()?; + let content; + parenthesized!(content in input); + let r#fn: syn::Path = content.parse()?; + + Ok(MethodCall { method, r#fn }) + } +} + +impl ToTokens for MethodCall { + fn to_tokens(&self, tokens: &mut TokenStream) { + let method = &self.method; + let r#fn = &self.r#fn; + + tokens.extend(quote! { + #method(#r#fn) + }); + } +} + +/// Generates an api ts file from the routes added with `add_route!`. Specify the path to save the api to. +#[proc_macro] +pub fn api(input: pc::TokenStream) -> pc::TokenStream { + match api_inner(input.into()) { + Ok(result) => result.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn api_inner(input: TokenStream) -> syn::Result { + let span = input.span(); + let args = syn::parse2::(input)?; + let path = args.path.value(); + + let routes = GlobalState::get_routes(); + let mut ts_functions = BTreeMap::new(); let mut ts_interfaces = BTreeMap::new(); - for (url, method, fn_name) in routes { - let function = fns - .iter() - .find(|&&(name, _, _)| fn_name == name) - .ok_or_else(|| { + for route in routes.iter() { + let fn_name = &route.fn_name; + let method = &route.method; + let url = &route.route; + + let function = GlobalState::get_function(fn_name).ok_or_else(|| { + s_err( + span, format!( - "Function '{}' not found in the provided functions list", + "Function '{}' not found in the cache, mind adding it with #[cached]", fn_name - ) - })?; + ), + ) + })?; - println!("function: {:?}", function); + let ty = collect_params(&function, span, &mut ts_interfaces)?; - let (fn_name, params, response) = function; - let params_type = collect_params(params, &mut ts_interfaces, structs)?; - let response_type = collect_response(response, &mut ts_interfaces, structs)?; + let response_type = collect_response_type(&function.response, span, &mut ts_interfaces)?; - let params_str = if !params_type.is_empty() { - format!("params: {}", params_type) + let params_str = if !ty.is_empty() { + format!("params: {}", ty) } else { String::new() }; - let body_assignment = if !params_type.is_empty() { + let body_assignment = if !ty.is_empty() { "JSON.stringify(params)" } else { "undefined" @@ -65,96 +251,112 @@ pub fn gen_ts>( body_assignment = body_assignment ); - ts_functions.insert(fn_name.to_string(), function_str); + ts_functions.insert(fn_name.to_owned(), function_str); } - write_to_file(path, ts_interfaces, ts_functions)?; + write_to_file(path, ts_interfaces, ts_functions, span)?; - Ok(()) + Ok(quote! {}) } fn collect_params( - params: &[(&str, &str)], + function: &Function, + span: proc_macro2::Span, ts_interfaces: &mut BTreeMap, - structs: &[(&str, &[(&str, &str)])], -) -> Result { - for &(_, param_type) in params { - if param_type.contains("Json") { - let struct_name = extract_struct_name(param_type)?; - if let Some(fields) = structs - .iter() - .find(|&&(s_name, _)| s_name == struct_name) - .map(|(_, fields)| fields) - { +) -> syn::Result { + for param in &function.params { + if param.1.contains("Json") { + let struct_name = extract_struct_name(span, param.1)?; + if let Some(fields) = GlobalState::get_struct(&struct_name) { ts_interfaces - .entry(struct_name.to_string()) - .or_insert_with(|| generate_ts_interface(&struct_name, fields)); + .entry(struct_name.clone()) + .or_insert_with(|| generate_ts_interface(&struct_name.clone(), fields)); return Ok(struct_name); } else { - return Err(format!( - "Struct '{}' not found in the provided structs list", - struct_name - )); + let interface = convert_rust_type_to_ts(&struct_name); + if let Some(_interface) = interface { + return Ok(struct_name); + } else { + return Err(s_err( + span, + format!( + "Struct '{}' not found in the cache, mind adding it with #[cached]", + struct_name + ), + )); + } } } } Ok(String::new()) } -fn collect_response( +fn collect_response_type( response: &str, + span: proc_macro2::Span, ts_interfaces: &mut BTreeMap, - structs: &[(&str, &[(&str, &str)])], -) -> Result { +) -> syn::Result { let response = response.replace(' ', ""); if let Some(response_type) = convert_rust_type_to_ts(&response) { return Ok(response_type); } if response.contains("Json") { - let struct_name = extract_struct_name(&response)?; - if let Some(fields) = structs - .iter() - .find(|&&(s_name, _)| s_name == struct_name) - .map(|(_, fields)| fields) - { + let struct_name = extract_struct_name(span, &response)?; + if let Some(fields) = GlobalState::get_struct(&struct_name) { ts_interfaces - .entry(struct_name.to_string()) + .entry(struct_name.clone()) .or_insert_with(|| generate_ts_interface(&struct_name, fields)); return Ok(struct_name); } } - Err(format!( - "Struct '{}' not found in the provided structs list", - response + Err(s_err( + span, + format!( + "Struct '{}' not found in the cache, mind adding it with #[cached]", + response + ), )) } -fn extract_struct_name(type_str: &str) -> Result { +fn extract_struct_name(span: proc_macro2::Span, type_str: &str) -> syn::Result { type_str .split('<') .nth(1) .and_then(|s| s.split('>').next()) - .map(|s| s.split("::").last().unwrap_or("").trim().to_string()) - .ok_or_else(|| format!("Failed to extract struct name from '{}'", type_str)) + .map(|s| { + Ok(s.split("::") + .last() + .ok_or_else(|| logic_err(span))? + .trim() + .to_string()) + }) + .ok_or_else(|| { + s_err( + span, + format!("Failed to extract struct name from '{}'", type_str), + ) + })? } -fn write_to_file>( - path: P, +fn write_to_file( + path: String, ts_interfaces: BTreeMap, ts_functions: BTreeMap, -) -> Result<(), String> { - let mut file = File::create(path).map_err(|e| format!("Failed to create file: {}", e))?; + span: proc_macro2::Span, +) -> syn::Result<()> { + let mut file = std::fs::File::create(path) + .map_err(|e| s_err(span, format!("Failed to create file: {}", e)))?; for interface in ts_interfaces.values() { file.write_all(interface.as_bytes()) - .map_err(|e| format!("Failed to write to file: {}", e))?; + .map_err(|e| s_err(span, format!("Failed to write to file: {}", e)))?; } for function in ts_functions.values() { file.write_all(function.as_bytes()) - .map_err(|e| format!("Failed to write to file: {}", e))?; + .map_err(|e| s_err(span, format!("Failed to write to file: {}", e)))?; } Ok(()) @@ -181,12 +383,120 @@ fn convert_rust_type_to_ts(rust_type: &str) -> Option { }) } -fn generate_ts_interface(struct_name: &str, fields: &[(&str, &str)]) -> String { +fn generate_ts_interface(struct_name: &str, fields: Vec) -> String { let mut interface = format!("export interface {} {{\n", struct_name); - for &(ident, ty) in fields { - let ty = convert_rust_type_to_ts(ty).unwrap_or_default(); + for StructField { ident, ty } in fields { + let ty = convert_rust_type_to_ts(&ty).unwrap_or_default(); interface.push_str(&format!(" {}: {};\n", ident, ty)); } interface.push_str("}\n\n"); interface } + +struct GenArgs { + path: syn::LitStr, +} + +impl Parse for GenArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let path = input.parse()?; + Ok(GenArgs { path }) + } +} + +/// Put before structs or functions to be used and cached by the `glue` crate. +#[proc_macro_attribute] +pub fn cached(args: pc::TokenStream, input: pc::TokenStream) -> pc::TokenStream { + match cached_inner(args.into(), input.into()) { + Ok(result) => result.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn cached_inner(args: TokenStream, input: TokenStream) -> syn::Result { + let span = input.span(); + let input = syn::parse2::(input)?; + let _args = syn::parse2::(args)?; + + match input.clone() { + syn::Item::Struct(syn::ItemStruct { ident, fields, .. }) => { + GlobalState::add_struct(ident.to_string(), { + let mut field_vec = Vec::new(); + + for field in fields { + let ident = field.ident.ok_or_else(|| logic_err(span))?.to_string(); + let ty = field.ty.into_token_stream().to_string(); + field_vec.push(StructField { ident, ty }); + } + + field_vec + }); + } + syn::Item::Fn(item_fn) => { + let fn_name = item_fn.sig.ident.to_string(); + let params = item_fn.sig.inputs.clone(); + let response = match item_fn.sig.output.clone() { + syn::ReturnType::Type(_, ty) => ty.into_token_stream().to_string(), + _ => "()".to_string(), + }; + + GlobalState::add_function( + fn_name, + Function { + params: { + let mut map = BTreeMap::new(); + for param in params { + match param { + syn::FnArg::Typed(syn::PatType { ty, pat, .. }) => { + if pat.to_token_stream().to_string() == "Json" { + let struct_path = ty.to_token_stream().to_string(); + let struct_name = struct_path + .split("::") + .last() + .ok_or_else(|| logic_err(span))? + .trim(); + let fields = GlobalState::get_struct(struct_name).ok_or_else(|| { + s_err( + span, + format!( + "Struct '{}' not found in the cache, mind adding it with #[cached]", + struct_name + ), + ) + })?; + + for StructField { ident, ty } in fields { + map.insert(ident, ty); + } + } else { + let ty = ty.to_token_stream().to_string(); + map.insert(pat.to_token_stream().to_string(), ty); + } + } + syn::FnArg::Receiver(_) => { + return Err(s_err(span, "Receiver not allowed")); + } + } + } + map + }, + response, + }, + ); + } + _ => return Err(s_err(span, "Expected struct or function")), + } + + Ok(quote! {#input}) +} + +struct NoArgs {} + +impl Parse for NoArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if !input.is_empty() { + return Err(input.error("No arguments expected")); + } + Ok(NoArgs {}) + } +} diff --git a/tests/main.rs b/tests/main.rs index 84c646f..e8345fc 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,20 +1,15 @@ use axum::{routing::get, Json, Router}; -use gluer::{add_route, cached, gen_ts}; -use hello::{Hello, CACHED_STRUCT_HELLO}; +use gluer::{add_route, api, cached}; #[cached] async fn fetch_root() -> String { String::from("Hello, World!") } -mod hello { - use gluer::cached; - - #[cached] - #[derive(serde::Serialize, serde::Deserialize, Default)] - pub struct Hello { - name: String, - } +#[cached] +#[derive(serde::Serialize, serde::Deserialize, Default)] +struct Hello { + name: String, } #[cached] @@ -23,22 +18,16 @@ async fn add_root(Json(hello): Json) -> Json { } #[tokio::test] +#[ignore = "everlasting server"] async fn main_test() { let mut app: Router<()> = Router::new(); - let mut routes = vec![]; - add_route!(routes, app, "/", get(fetch_root).post(add_root)); + add_route!(app, "/", get(fetch_root).post(add_root)); - gen_ts( - "tests/api.ts", - routes, - &[CACHED_FN_FETCH_ROOT, CACHED_FN_ADD_ROOT], - &[CACHED_STRUCT_HELLO], - ) - .unwrap(); + api!("tests/api.ts"); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await .unwrap(); - axum::serve(listener, app).await.unwrap(); // prevent everlasting server + axum::serve(listener, app).await.unwrap(); }