From e71579e8c2703f0248f17374ba2295fda9418788 Mon Sep 17 00:00:00 2001 From: nwrenger Date: Mon, 29 Jul 2024 03:30:27 +0200 Subject: [PATCH] :sparkles: Added macros for getting the types && fixed other bugs --- Cargo.toml | 4 +- README.md | 95 ++++++++--- src/extractors.rs | 191 --------------------- src/lib.rs | 416 ++++++++++++++++++++++++++++++++-------------- tests/api.ts | 15 +- tests/main.rs | 22 ++- 6 files changed, 389 insertions(+), 354 deletions(-) delete mode 100644 src/extractors.rs diff --git a/Cargo.toml b/Cargo.toml index 99e6633..07e8395 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gluer" -version = "0.2.2" +version = "0.3.0" edition = "2021" authors = ["Nils Wrenger "] description = "A wrapper for rust frameworks which addresses the persistent issue of redundant type and function definitions between the frontend and backend" @@ -18,7 +18,7 @@ proc-macro = true quote = "1.0" syn = { version = "2.0.62", features = ["full"] } proc-macro2 = "1.0" -lazy_static = "1.5.0" +once_cell = "1.19.0" [dev-dependencies] axum = "0.7.5" diff --git a/README.md b/README.md index cfe01e1..11dcad4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -light_magic = "0.2.2" +light_magic = "0.3.0" ``` ## Disclaimer @@ -23,66 +23,111 @@ Please be informed that this crate is in a very early state and is expected to w - Inferring the input and output types of functions (but only `Json<...>` for inputs) - Converting them to ts types - Generating the ts file with the functions and data types -- Using types from other modules works now but only if there are not 2 or more times nested from the router implementation ## How to use -Firstly you have to use the `add_route!` macro when adding api important routes to your router: +`gluer` generates an api endpoint `.ts` file which expects that you build your frontend statically and host it via `axum`'s static file serving. To use it, follow these steps: + +### Step 1: Define Parameters and Functions + +Start by using the `#[param]` and `fun!` macros to define your data structures and functions. These macros give `gluer` access to the necessary code for type inference and conversion. ```rust use axum::{ - routing::{get, post}, - Router, Json, }; -use gluer::add_route; +use gluer::{fun, param}; + +// Define a parameter with the param macro +#[param] +#[derive(Default, serde::Serialize)] +struct Book { + // imagine some fields here +} + +// Define functions with the fun macro +fun! { + async fn root() -> Json { + "Hello, World!".to_string().into() + } + async fn book() -> Json { + Book::default().into() + } +} +``` + +### Step 2: Add Routes -async fn root() -> Json<&'static str> { - "Hello, World!".into() +Use the `add_route!` macro to add API-important routes to your router. Note that inline functions cannot generally be used due to Rust's limitations in inferring types in macros. + +```rust +use axum::{ + routing::get, + Router, + Json, +}; +use gluer::{add_route, fun}; + +// a part of the function above +fun! { + async fn root() -> String { + "Hello, World!".to_string() + } + async fn hello() -> Json { + "Hello, World!".to_string().into() + } } let mut app: Router<()> = Router::new(); -// Not api important, so adding without macro +// Add non-API-important route without macro app = app.route("/", get(root)); -// You cannot use inline functions because of rust limitations of inferring types in macros -add_route!(app, "/", post(root)); -add_route!(app, "/user", post(root).delete(root)); +// Add API-important routes with the add_route macro +add_route!(app, "/hello-world", get(hello)); ``` -Then you only have to use the `gen_spec!` macro which generates after specifying the path the api on comptime: +### Step 3: Generate API Specification + +Generate the API specification file using the `gen_ts!` macro. This macro generates the TypeScript file at compile time. ```rust -use gluer::gen_spec; +use gluer::gen_ts; -gen_spec!("tests/api.ts"); +// Generate the TypeScript API specification +gen_ts!("tests/api.ts"); ``` -It expects that you build your frontend statically and use it `axum`'s ability to serve static files! - ### Complete Example +Below is a complete example demonstrating the use of gluer with `axum`: + ```rust,no_run -use axum::{routing::post, Json, Router}; -use gluer::{add_route, gen_spec}; +use axum::{routing::get, Json, Router}; +use gluer::{add_route, fun, gen_ts, param}; -#[derive(serde::Deserialize)] +#[param] +#[derive(serde::Serialize, serde::Deserialize, Default)] struct Hello { name: String, } -async fn root(Json(hello): Json) -> Json { - hello.name.into() +fun! { + async fn add_root(Json(hello): Json) -> Json { + hello.into() + } + async fn fetch_root() -> Json { + Hello::default().into() + } } #[tokio::main] async fn main() { - let mut app = Router::new(); + let mut app: Router<()> = Router::new(); - add_route!(app, "/", post(root)); + add_route!(app, "/", get(fetch_root).post(add_root)); - gen_spec!("tests/api.ts"); + gen_ts!("tests/api.ts"); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await diff --git a/src/extractors.rs b/src/extractors.rs deleted file mode 100644 index 4fb284a..0000000 --- a/src/extractors.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::collections::HashMap; - -use quote::ToTokens; - -use crate::s_err; - -pub(crate) fn extract_function( - span: proc_macro2::Span, - fn_name: &str, - file_paths: Vec, -) -> syn::Result<(Vec, String)> { - let mut params_map: HashMap> = HashMap::new(); - let mut responses_map: HashMap = HashMap::new(); - - fn extract_from_syntax( - syntax: syn::File, - params_map: &mut HashMap>, - responses_map: &mut HashMap, - ) -> syn::Result<()> { - for item in syntax.items { - match item { - syn::Item::Fn(syn::ItemFn { sig, .. }) => { - let fn_name = sig.ident.to_string(); - let params: Vec = sig.inputs.iter().cloned().collect(); - params_map.insert(fn_name.clone(), params); - - let ty: String = match sig.output { - syn::ReturnType::Default => "()".to_string(), - syn::ReturnType::Type(_, ty) => ty.into_token_stream().to_string(), - }; - - responses_map.insert(fn_name, ty); - } - syn::Item::Mod(syn::ItemMod { - content: Some((_, items)), - .. - }) => { - extract_from_syntax( - syn::File { - shebang: None, - attrs: vec![], - items, - }, - params_map, - responses_map, - )?; - } - _ => {} - } - } - Ok(()) - } - - for file_path in file_paths { - let source = std::fs::read_to_string(&file_path) - .map_err(|e| s_err(span, format!("'{}' {e}", file_path.display())))?; - let syntax = syn::parse_file(&source)?; - extract_from_syntax(syntax, &mut params_map, &mut responses_map)?; - } - - let params = params_map - .get(fn_name) - .cloned() - .ok_or_else(|| s_err(span, "Function parameters not found"))?; - - let responses = responses_map - .get(fn_name) - .cloned() - .ok_or_else(|| s_err(span, "Function responses not found"))?; - - Ok((params, responses)) -} - -pub(crate) fn extract_struct( - span: proc_macro2::Span, - struct_name: &str, - file_paths: Vec, -) -> syn::Result> { - fn extract_from_syntax( - syntax: syn::File, - struct_name: &str, - ) -> syn::Result>> { - for item in syntax.items { - match item { - syn::Item::Struct(syn::ItemStruct { ident, fields, .. }) => { - let name = ident.to_string().trim().to_string(); - let name = name.split("::").last().unwrap(); - - if name == struct_name { - let mut field_vec = Vec::new(); - - if let syn::Fields::Named(fields) = fields { - for field in fields.named { - let field_name = field.ident.unwrap().to_string(); - let field_type = field.ty.into_token_stream().to_string(); - field_vec.push((field_name, field_type)); - } - } - - return Ok(Some(field_vec)); - } - } - syn::Item::Mod(syn::ItemMod { - content: Some((_, items)), - .. - }) => { - if let Some(result) = extract_from_syntax( - syn::File { - shebang: None, - attrs: vec![], - items, - }, - struct_name, - )? { - return Ok(Some(result)); - } - } - _ => {} - } - } - Ok(None) - } - - for file_path in file_paths { - let source = std::fs::read_to_string(&file_path) - .map_err(|e| s_err(span, format!("'{}' {e}", file_path.display())))?; - let syntax = syn::parse_file(&source)?; - - if let Some(result) = extract_from_syntax(syntax, struct_name)? { - return Ok(result); - } - } - - Err(s_err( - span, - format!("Struct definition not found for {}", struct_name), - )) -} - -/// Resolves the path to the file containing the module. Note: Two or more nested modules are not supported. -pub(crate) fn resolve_path( - span: proc_macro2::Span, - segments: Vec, -) -> syn::Result> { - let current_dir = - std::env::current_dir().map_err(|_| s_err(span, "Failed to get current directory"))?; - let src_dir = current_dir.join("src"); - let test_dir = current_dir.join("tests"); - let mut possible_paths = Vec::new(); - - let working_dir = [src_dir, test_dir]; - - dbg!(&segments); - - for dir in working_dir { - if segments.len() > 2 { - return Err(s_err( - span, - "Twice or more nested modules are currently not supported", - )); - } else { - scan_dir(&dir, &mut possible_paths)?; - } - } - - if possible_paths.is_empty() { - return Err(s_err(span, "No matching files found")); - } - - Ok(possible_paths) -} - -fn scan_dir( - dir: &std::path::Path, - possible_paths: &mut Vec, -) -> syn::Result<()> { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { - possible_paths.push(path); - } else if path.is_dir() { - let mod_path = path.join("mod.rs"); - if mod_path.exists() { - possible_paths.push(mod_path); - } - } - } - } - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index 48679b0..0cc7f19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,41 @@ #![doc = include_str!("../README.md")] -mod extractors; - -use crate::extractors::{extract_function, extract_struct, resolve_path}; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use proc_macro as pc; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use std::{fmt, io::Write, sync::Mutex}; +use std::{collections::HashMap, fmt, io::Write, sync::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) } -lazy_static! { - static ref ROUTES: Mutex> = Mutex::new(Vec::new()); +struct Route { + route: String, + method: String, + fn_name: String, +} + +struct Function { + params: HashMap, + response: String, +} + +#[derive(Clone)] +struct StructField { + ident: String, + ty: String, } +static ROUTES: Lazy>> = Lazy::new(|| RwLock::new(Vec::new())); +static STRUCTS: Lazy>>> = + Lazy::new(|| RwLock::new(HashMap::new())); +static FUNCTIONS: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + /// Adds a route to the router. Use for each api endpoint you want to expose to the frontend. -/// `Inline Functions` are not supported because of rust limitations of inferring types in macros. +/// `Inline Functions` are currently not supported. #[proc_macro] pub fn add_route(input: pc::TokenStream) -> pc::TokenStream { match add_route_inner(input.into()) { @@ -29,35 +45,19 @@ pub fn add_route(input: pc::TokenStream) -> pc::TokenStream { } 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, - params, - response, - } in &handler - { + for MethodCall { method, r#fn } in &handler { let fn_name = r#fn.segments.last().unwrap().ident.to_string(); - let params: Vec = params - .iter() - .map(|param| match param { - syn::FnArg::Typed(syn::PatType { ty, .. }) => Ok(ty.to_token_stream().to_string()), - syn::FnArg::Receiver(_) => Err(s_err(span, "Receiver not allowed")), - }) - .collect::>()?; - - ROUTES.lock().unwrap().push(Route { + + ROUTES.write().unwrap().push(Route { route: route.clone(), method: method.to_string(), fn_name, - params, - response: response.clone(), }); } @@ -92,8 +92,6 @@ impl Parse for RouterArgs { struct MethodCall { method: syn::Ident, r#fn: syn::Path, - params: Vec, - response: String, } impl Parse for MethodCall { @@ -103,19 +101,7 @@ impl Parse for MethodCall { parenthesized!(content in input); let r#fn: syn::Path = content.parse()?; - let fn_name = r#fn.segments.last().unwrap().ident.to_string(); - let segments = r#fn.segments.iter().map(|s| s.ident.to_string()).collect(); - - let file_paths = resolve_path(input.span(), segments)?; - - let (params, response) = extract_function(input.span(), &fn_name, file_paths)?; - - Ok(MethodCall { - method, - r#fn, - params, - response, - }) + Ok(MethodCall { method, r#fn }) } } @@ -129,92 +115,61 @@ impl ToTokens for MethodCall { }); } } -struct Route { - route: String, - method: String, - fn_name: String, - // Should be syn::Type but that's not thread safe - params: Vec, - response: String, -} /// Generates an api ts file from the routes added with `add_route!`. Specify the path to save the api to. #[proc_macro] -pub fn gen_spec(input: pc::TokenStream) -> pc::TokenStream { - match gen_spec_inner(input.into()) { +pub fn gen_ts(input: pc::TokenStream) -> pc::TokenStream { + match gen_ts_inner(input.into()) { Ok(result) => result.into(), Err(e) => e.into_compile_error().into(), } } -fn gen_spec_inner(input: TokenStream) -> syn::Result { +fn gen_ts_inner(input: TokenStream) -> syn::Result { let span = input.span(); let args = syn::parse2::(input)?; - let path = args.path.value(); - let routes = ROUTES.lock().unwrap(); + let routes = ROUTES.read().unwrap(); + let functions = FUNCTIONS.read().unwrap(); + let structs = STRUCTS.read().unwrap(); - let mut ts_functions = String::new(); - let mut ts_interfaces = String::new(); + let mut ts_functions = HashMap::new(); + let mut ts_interfaces = HashMap::new(); for route in routes.iter() { - let fn_name = route.method.clone() + "_" + &route.fn_name; + let fn_name = &route.fn_name; let method = &route.method; let url = &route.route; - let mut param_names = vec![]; - let mut param_types = vec![]; - - for param in &route.params { - if param.contains("Json") { - let struct_path = param - .split('<') - .nth(1) - .unwrap() - .split('>') - .next() - .unwrap() - .to_string(); - - let struct_name = struct_path.split("::").last().unwrap().trim().to_string(); - - param_names.push("data".to_string()); - param_types.push(struct_name.to_string()); - - let file_paths = resolve_path( - span, - struct_path.split("::").map(|f| f.to_string()).collect(), - )?; - - let interface = generate_ts_interface( - &struct_name, - extract_struct(span, &struct_name, file_paths)?, - ); - ts_interfaces.push_str(&interface); - } else { - let param_name = param.split(':').next().unwrap().trim().to_string(); - let param_type = convert_rust_type_to_ts(param.split(':').nth(1).unwrap().trim()); - param_names.push(param_name.clone()); - param_types.push(param_type); - } - } + let function = functions.get(fn_name).ok_or_else(|| { + s_err( + span, + format!( + "Function '{}' not found in the cache, mind adding it with fun! {{}}", + fn_name + ), + ) + })?; + + let ty = collect_params(function, &structs, span, &mut ts_interfaces)?; - let params_str = param_names - .iter() - .zip(param_types.iter()) - .map(|(name, ty)| format!("{}: {}", name, ty)) - .collect::>() - .join(", "); - let response_type = convert_rust_type_to_ts(&route.response.clone()); + let response_type = + collect_response_type(&function.response, &structs, span, &mut ts_interfaces)?; - let body_assignment = if param_names.contains(&"data".to_string()) { - "JSON.stringify(data)" + let params_str = if !ty.is_empty() { + format!("params: {}", ty) + } else { + String::new() + }; + + let body_assignment = if !ty.is_empty() { + "JSON.stringify(params)" } else { "undefined" }; - let function = format!( + let function_str = format!( r#"export async function {fn_name}({params_str}): Promise<{response_type} | any> {{ const response = await fetch("{url}", {{ method: "{method}", @@ -235,45 +190,140 @@ fn gen_spec_inner(input: TokenStream) -> syn::Result { body_assignment = body_assignment ); - ts_functions.push_str(&function); + ts_functions.insert(fn_name.to_owned(), function_str); + } + + write_to_file(path, ts_interfaces, ts_functions, span)?; + + Ok(quote! {}) +} + +fn collect_params( + function: &Function, + structs: &HashMap>, + span: proc_macro2::Span, + ts_interfaces: &mut HashMap, +) -> syn::Result { + for param in &function.params { + if param.1.contains("Json") { + let struct_name = extract_struct_name(param.1)?; + if let Some(fields) = structs.get(&struct_name).cloned() { + ts_interfaces + .entry(struct_name.clone()) + .or_insert_with(|| generate_ts_interface(&struct_name.clone(), fields)); + return Ok(struct_name); + } else { + 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 #[param]", + struct_name + ), + )); + } + } + } + } + Ok(String::new()) +} + +fn collect_response_type( + response: &str, + structs: &HashMap>, + span: proc_macro2::Span, + ts_interfaces: &mut HashMap, +) -> 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.get(&struct_name).cloned() { + ts_interfaces + .entry(struct_name.clone()) + .or_insert_with(|| generate_ts_interface(&struct_name, fields)); + return Ok(struct_name); + } } + Err(s_err( + span, + format!( + "Struct '{}' not found in the cache, mind adding it with #[param]", + response + ), + )) +} + +fn extract_struct_name(type_str: &str) -> syn::Result { + type_str + .split('<') + .nth(1) + .and_then(|s| s.split('>').next()) + .map(|s| s.split("::").last().unwrap().trim().to_string()) + .ok_or_else(|| { + s_err( + proc_macro2::Span::call_site(), + format!("Failed to extract struct name from '{}'", type_str), + ) + }) +} + +fn write_to_file( + path: String, + ts_interfaces: HashMap, + ts_functions: HashMap, + 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)))?; - file.write_all(ts_interfaces.as_bytes()) - .map_err(|e| s_err(span, format!("Failed to write to file: {}", e)))?; - file.write_all(ts_functions.as_bytes()) - .map_err(|e| s_err(span, format!("Failed to write to file: {}", e)))?; - Ok(quote! {}) + for interface in ts_interfaces.values() { + file.write_all(interface.as_bytes()) + .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| s_err(span, format!("Failed to write to file: {}", e)))?; + } + + Ok(()) } -fn convert_rust_type_to_ts(rust_type: &str) -> String { +fn convert_rust_type_to_ts(rust_type: &str) -> Option { let rust_type = rust_type.trim(); - match rust_type { + Some(match rust_type { "str" | "String" => "string".to_string(), "usize" | "isize" | "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f32" | "f64" => "number".to_string(), "bool" => "boolean".to_string(), "()" => "void".to_string(), - t if t.starts_with("Vec <") => format!("{}[]", convert_rust_type_to_ts(&t[5..t.len() - 1])), - t if t.starts_with("Option <") => convert_rust_type_to_ts(&t[8..t.len() - 1]), - t if t.starts_with("Result <") => convert_rust_type_to_ts(&t[8..t.len() - 1]), - t if t.starts_with("Json <") => convert_rust_type_to_ts(&t[6..t.len() - 1]), - t if t.starts_with('&') => convert_rust_type_to_ts(&t[1..]), - t if t.starts_with("'static") => convert_rust_type_to_ts(&t[8..]), - t => t.to_string(), - } + t if t.starts_with("Vec<") => format!( + "{}[]", + convert_rust_type_to_ts(&t[4..t.len() - 1]).unwrap_or_default() + ), + t if t.starts_with("Option<") => return convert_rust_type_to_ts(&t[7..t.len() - 1]), + t if t.starts_with("Result<") => return convert_rust_type_to_ts(&t[7..t.len() - 1]), + t if t.starts_with("Json<") => return convert_rust_type_to_ts(&t[5..t.len() - 1]), + t if t.starts_with('&') => return convert_rust_type_to_ts(&t[1..]), + t if t.starts_with("'static") => return convert_rust_type_to_ts(&t[8..]), + _ => return None, + }) } -fn generate_ts_interface(struct_name: &str, fields: Vec<(String, String)>) -> String { +fn generate_ts_interface(struct_name: &str, fields: Vec) -> String { let mut interface = format!("export interface {} {{\n", struct_name); - - for (field_name, field_type) in fields { - let field_type = convert_rust_type_to_ts(&field_type); - interface.push_str(&format!(" {}: {};\n", field_name, field_type)); + 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 } @@ -288,3 +338,117 @@ impl Parse for GenArgs { Ok(GenArgs { path }) } } + +/// Put here inside the functions which should be used by the api. +#[proc_macro] +pub fn fun(input: pc::TokenStream) -> pc::TokenStream { + match fun_inner(input.into()) { + Ok(result) => result.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn fun_inner(input: TokenStream) -> syn::Result { + let span = input.span(); + let items = syn::parse2::(input.clone())?; + + for item in items.items { + if let syn::Item::Fn(item_fn) = item { + 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(), + }; + + FUNCTIONS.write().unwrap().insert( + fn_name.clone(), + Function { + params: { + let mut map = HashMap::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().unwrap().trim(); + let fields = STRUCTS + .read().unwrap() + .get(&struct_name.to_string()) + .ok_or_else(|| { + s_err( + span, + format!( + "Struct '{}' not found in the cache, mind adding it with #[param]", + struct_name + ), + ) + })? + .clone(); + + 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, + }, + ); + } else { + return Err(s_err(span, "Expected function item")); + } + } + + Ok(quote! {#input}) +} + +/// Put before the structs which should be used by the api. +#[proc_macro_attribute] +pub fn param(args: pc::TokenStream, input: pc::TokenStream) -> pc::TokenStream { + match param_inner(args.into(), input.into()) { + Ok(result) => result.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn param_inner(args: TokenStream, input: TokenStream) -> syn::Result { + let input = syn::parse2::(input)?; + let _args = syn::parse2::(args)?; + + STRUCTS.write().unwrap().insert(input.ident.to_string(), { + let mut field_vec = Vec::new(); + + if let syn::Fields::Named(fields) = input.fields.clone() { + for field in fields.named { + let ident = field.ident.unwrap().to_string(); + let ty = field.ty.into_token_stream().to_string(); + field_vec.push(StructField { ident, ty }); + } + } + + field_vec + }); + + 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/api.ts b/tests/api.ts index 3190987..445b82b 100644 --- a/tests/api.ts +++ b/tests/api.ts @@ -2,13 +2,24 @@ export interface Hello { name: string; } -export async function post_root(data: Hello): Promise { +export async function add_root(params: Hello): Promise { const response = await fetch("/", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data) + body: JSON.stringify(params) + }); + return response.json(); +} + +export async function fetch_root(): Promise { + const response = await fetch("/", { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + body: undefined }); return response.json(); } diff --git a/tests/main.rs b/tests/main.rs index 0fa5a20..5f7c5d0 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,23 +1,29 @@ -use axum::{routing::post, Json, Router}; -use gluer::{add_route, gen_spec}; +use axum::{routing::get, Json, Router}; +use gluer::{add_route, fun, gen_ts, param}; -#[derive(serde::Deserialize)] +#[param] +#[derive(serde::Serialize, serde::Deserialize, Default)] struct Hello { name: String, } -async fn root(Json(hello): Json) -> Json { - hello.name.into() +fun! { + async fn add_root(Json(hello): Json) -> Json { + hello.into() + } + async fn fetch_root() -> Json { + Hello::default().into() + } } #[tokio::test] #[ignore = "everlasting server"] async fn main_test() { - let mut app = Router::new(); + let mut app: Router<()> = Router::new(); - add_route!(app, "/", post(root)); + add_route!(app, "/", get(fetch_root).post(add_root)); - gen_spec!("tests/api.ts"); + gen_ts!("tests/api.ts"); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await