From c63784f6d1665736ed49d8e2e6bbca9745f29adf Mon Sep 17 00:00:00 2001 From: nwrenger Date: Thu, 15 Aug 2024 23:56:13 +0200 Subject: [PATCH 1/4] :sparkles: Removed route! and put it's functionality into generate! Also updated naming and docs accordingly --- README.md | 53 +++---- src/lib.rs | 426 ++++++++++++++++++++++++++++---------------------- tests/api.ts | 8 +- tests/main.rs | 18 +-- 4 files changed, 275 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index b38b48e..6d54680 100644 --- a/README.md +++ b/README.md @@ -121,17 +121,18 @@ async fn book_state() -> Json { } ``` -### Step 2: Add Routes -Use the `route!` macro with `axum`'s Router to add routes. This enables the `generate!` macro to identify the route and generate corresponding functions, structs, types, and enums. Note that inline functions cannot be used because the function names in the generated TypeScript file are inferred from the handler function names. +### Step 2: Add Routes && Generate Api -```rust +Use the `generate!` macro to define your router and other telemetry to generate the Api. You have to define the `output` location of the TypeScript file and the `routes`. Note that inline functions cannot be used in the `router` field because the function names in the generated TypeScript file are inferred from the handler function names. + +```rust, ignore use axum::{ routing::get, Json, Router, extract::Path, }; -use gluer::{route, metadata}; +use gluer::{generate, metadata}; // without `#[metadata]`, it's non-API-important async fn root() -> String { @@ -144,24 +145,20 @@ async fn hello(Path(h): Path) -> Json { h.into() } -let mut app: Router<()> = Router::new() - // Add non-API-important directly on the router - .route("/", get(root)); -// Add API-important routes with the route macro -route!(app, "/:hello", get(hello)); - +let mut app: Router<()> = generate! { + routes = { + // Add API-important inside the routes field + "hello" = get(hello), + }, + output = "tests/api.ts", +} +// Add non-API-important outside the macro +.route("/", get(root)); ``` -### Step 3: Generate API - -Generate the API file using the `generate!` macro. This macro generates the TypeScript file during macro expansion (compile time). You need to specify the `project_paths` of your current project, which can be a root directory (represented by `"src"`), multiple directories, or specific files (e.g., `["dir0", "dir1", "dir2/some.rs"]`). The `project_paths` will be scanned to retrieve project data, meaning collecting the information marked by the `route!` and `#[metadata]` macros. Additionally, you need to provide a `path` where the generated file will be saved, including the filename, and a `base` URL for the API. The base URL should not end with a slash (`/`); use `""` for no base URL if you are utilizing `axum`'s static file serving, or provide a URL like `"http://localhost:8080"` for a local server. - -```rust -use gluer::generate; +### More Notes -// Make sure to change "tests" to "src" when copying this example into a normal project -generate!("tests", "tests/api.ts", ""); -``` +The `generate!` macro includes several optional fields, such as `prefix`, which allows you to modify the URL prefix. By default, this value is set to `""`, but you can change it to something like `"/api"`. Please note that the `prefix` should not end with a `/`. Additionally, you can customize the `files` field to specify the Rust project directories containing the source files that define the handler functions and dependencies. This can be a single string literal (e.g., `"src"`) or an array of string literals (e.g., `["src/db", "src"]`). These paths are used to extract type information for the TypeScript client. And now you can just simply use the router to start your server or do different things, the API should be already generated by your LSP! @@ -172,10 +169,9 @@ Below is a complete example demonstrating the use of `gluer` with `axum`: ```rust use axum::{ extract::{Path, Query}, - routing::get, Json, Router, }; -use gluer::{generate, metadata, route}; +use gluer::{generate, metadata}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -258,13 +254,14 @@ type S = String; #[tokio::main] async fn main() { - let mut _app: Router = Router::new(); - - route!(_app, "/:p", get(fetch_root).post(add_root)); - route!(_app, "/char/:path/metadata/:path", get(get_alphabet)); - - // Make sure to change "tests" to "src" when copying this example into a normal project - generate!("tests", "tests/api.ts", ""); + let _app: Router<()> = generate! { + routes = { // required + "/:p" = get(fetch_root).post(add_root), + "/char/:path/metadata/:path" = get(get_alphabet), + }, + files = "tests",// Make sure to change "tests" to "src" when copying this example into a normal project + output = "tests/api.ts", //required + }; let _listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await diff --git a/src/lib.rs b/src/lib.rs index 3da3939..42229f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,122 +1,28 @@ #![doc = include_str!("../README.md")] -use proc_macro as pc; +use proc_macro::{self as pc}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use std::{ collections::{BTreeMap, HashMap}, - fmt, fs, + fmt::{self, Debug}, + fs, io::Write, vec, }; use syn::{ - bracketed, parenthesized, + braced, bracketed, parenthesized, parse::Parse, punctuated::Punctuated, spanned::Spanned, token::{Brace, Comma}, - Item, LitStr, Stmt, StmtMacro, Type, + Item, LitStr, Token, Type, }; fn s_err(span: proc_macro2::Span, msg: impl fmt::Display) -> syn::Error { syn::Error::new(span, msg) } -/// Use this for defining the routes of the router, this is kind of a wrapper, needed for the `generate!` macro to find this. -/// -/// # Parameters -/// - `router_ident`: The ident of the router variable. -/// - `url`: The URL of the route. -/// - `method_router`: The `Method Router` of the route using `axum`'s syntax. -/// -/// # Note -/// When using state, make sure to return the router with the state, like this: -/// ```rust -/// use axum::{Router, routing::get, extract::State}; -/// use gluer::route; -/// -/// async fn fetch_root(State(_): State<()>) -> String { String::new() } -/// -/// let mut router = Router::new(); -/// -/// route!(router, "/api", get(fetch_root)); -/// -/// router.with_state::<()>(()); // <- here and remove the semicolon for returning it -#[proc_macro] -pub fn route(input: pc::TokenStream) -> pc::TokenStream { - match route_inner(input.into()) { - Ok(result) => result.into(), - Err(e) => e.to_compile_error().into(), - } -} - -fn route_inner(input: TokenStream) -> syn::Result { - let RouteArgs { - router_ident, - url, - routes, - } = syn::parse2::(input)?; - Ok(quote! { - #router_ident = #router_ident.route(#url, #(#routes).*); - }) -} - -struct RouteArgs { - router_ident: syn::Ident, - url: syn::LitStr, - routes: Vec, -} - -impl Parse for RouteArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut routes = vec![]; - - let router_ident = input.parse::()?; - input.parse::()?; - let url = input.parse::()?; - input.parse::()?; - - while !input.is_empty() { - let route = input.parse()?; - routes.push(route); - - if !input.is_empty() { - input.parse::()?; - } - } - - Ok(RouteArgs { - router_ident, - url, - routes, - }) - } -} - -struct MethodRouter { - method: syn::Ident, - handler: syn::Ident, -} - -impl Parse for MethodRouter { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let method = input.parse()?; - let content; - parenthesized!(content in input); - let handler = content.parse()?; - - Ok(MethodRouter { method, handler }) - } -} - -impl ToTokens for MethodRouter { - fn to_tokens(&self, tokens: &mut TokenStream) { - let method = &self.method; - let handler = &self.handler; - tokens.extend(quote! { #method(#handler) }); - } -} - /// Use before structs, functions, enums or types to be findable by the `generate!` macro. /// /// # Attributes @@ -199,13 +105,41 @@ impl syn::parse::Parse for MetadataAttr { /// Generates a TypeScript API client for the frontend from the API routes. /// /// ## Parameters -/// - `paths`: An array of directories and/or files to include for retrieving project data. -/// - Supports a root directory via `""`. -/// - Supports multiple directories or files via `["dir1", "dir2/some.rs"]`. -/// - `path`: The directory and filename where the generated TypeScript file will be saved. -/// - `base`: The base URL for the API. This URL should not end with a slash (`/`). Examples: -/// - Use `""` if you are utilizing `axum`'s static file serving and need no base URL. -/// - Use `"http://localhost:8080"` for a local server. +/// +/// - `prefix`: An optional parameter that allows you to specify a prefix for all generated routes. This can be useful if your API is hosted under a common base path (e.g., `/api`). +/// - `routes`: A required parameter that specifies the API routes for which the TypeScript client will be generated. Each route is defined by a URL path (which can include parameters) followed by one or more HTTP methods (e.g., `get`, `post`) and their corresponding handler functions. +/// - `files`: An optional parameter that specifies the directory or directories containing the Rust source files that define the handlers. This can be either a single string literal (e.g., `"src"`) or an array of string literals (e.g., `["src/db", "src"]`). These paths are used to extract type information for the TypeScript client. Ensure that these paths are correct and point to the appropriate directories. The default of `"src"` should handle most cases appropriately. +/// - `output`: A required parameter that specifies the path to the output file where the generated TypeScript client code will be written. Ensure that this path is correct and points to a writable location. +/// +/// ## Note +/// +/// - **Prefix URL:** The `prefix` parameter is used to prepend a common base path to all routes. It should not end with a `/`. If the prefix is not provided, it defaults to an empty string (`""`), meaning no prefix will be added. +/// +/// ## Example +/// +/// ```rust, ignore +/// use axum::Router; +/// use gluer::{generate, metadata}; +/// +/// // Define a handler function +/// #[metadata] +/// fn root() -> String { +/// "root".to_string() +/// } +/// +/// // Use the `generate` macro to create the API client and router +/// let _app: Router<()> = generate! { +/// routes = { // Defines the API routes +/// "/" = get(root), // Route for the root path, using the `root` handler for GET requests +/// }, +/// files = "src", // Specifies a single directory containing the handler implementations +/// // This can be omitted due to being the same as the default value +/// // You can also specify multiple directories: +/// // files = ["src/db", "src"], +/// +/// output = "tests/api.ts", // Specifies the output file for the generated TypeScript client +/// }; +/// ``` #[proc_macro] pub fn generate(input: pc::TokenStream) -> pc::TokenStream { match generate_inner(input.into()) { @@ -216,34 +150,39 @@ pub fn generate(input: pc::TokenStream) -> pc::TokenStream { fn generate_inner(input: TokenStream) -> syn::Result { let GenerateArgs { - project_paths, - path, - base, + prefix, + routes, + files, + output, } = syn::parse2::(input.clone())?; - let project_paths = project_paths + let files = files .iter() .map(|s: &String| std::path::PathBuf::from(s)) .collect::>(); - let mut routes = Vec::new(); + let parsed_routes: Vec = routes + .clone() + .unwrap() + .iter() + .flat_map(|f| f.to_routes()) + .collect(); let mut fn_infos = HashMap::new(); let mut type_infos = HashMap::new(); - let mut parsed_ts = ParsedTypeScript::filled(&base); + let mut parsed_ts = ParsedTypeScript::filled(&prefix); let mut needs_query_parser = false; fn process_paths( - project_paths: &[std::path::PathBuf], - routes: &mut Vec, + files: &[std::path::PathBuf], fn_infos: &mut HashMap, type_infos: &mut HashMap, ) -> syn::Result<()> { - for path in project_paths { + for path in files { if path.is_dir() { - process_single_dir(path, routes, fn_infos, type_infos)?; - } else if path.extension().and_then(|s| s.to_str()) == Some("rs") { - process_single_file(path, routes, fn_infos, type_infos)?; + process_single_dir(path, fn_infos, type_infos)?; + } else if path.extension().and_then(|s: &std::ffi::OsStr| s.to_str()) == Some("rs") { + process_single_file(path, fn_infos, type_infos)?; } else { return Err(s_err( proc_macro2::Span::call_site(), @@ -259,7 +198,6 @@ fn generate_inner(input: TokenStream) -> syn::Result { fn process_single_dir( dir: &std::path::Path, - routes: &mut Vec, fn_infos: &mut HashMap, type_infos: &mut HashMap, ) -> syn::Result<()> { @@ -277,9 +215,9 @@ fn generate_inner(input: TokenStream) -> syn::Result { })?; let path = entry.path(); if path.is_dir() { - process_single_dir(&path, routes, fn_infos, type_infos)?; + process_single_dir(&path, fn_infos, type_infos)?; } else if path.extension().and_then(|s| s.to_str()) == Some("rs") { - process_single_file(&path, routes, fn_infos, type_infos)?; + process_single_file(&path, fn_infos, type_infos)?; } } Ok(()) @@ -287,7 +225,6 @@ fn generate_inner(input: TokenStream) -> syn::Result { fn process_single_file( path: &std::path::Path, - routes: &mut Vec, fn_infos: &mut HashMap, type_infos: &mut HashMap, ) -> syn::Result<()> { @@ -298,13 +235,12 @@ fn generate_inner(input: TokenStream) -> syn::Result { ) })?; let syntax = syn::parse_file(&content)?; - process_syntax(&syntax.items, routes, fn_infos, type_infos)?; + process_syntax(&syntax.items, fn_infos, type_infos)?; Ok(()) } fn process_syntax( syntax: &Vec, - routes: &mut Vec, fn_infos: &mut HashMap, type_infos: &mut HashMap, ) -> syn::Result<()> { @@ -355,30 +291,6 @@ fn generate_inner(input: TokenStream) -> syn::Result { } } Item::Fn(item_fn) => { - for stmt in &item_fn.block.stmts { - if let Stmt::Macro(StmtMacro { mac, .. }) = stmt { - if mac.path.is_ident("route") { - let RouteArgs { - url, - routes: method_routes, - .. - } = syn::parse2::(mac.tokens.clone())?; - - for route in method_routes { - let method = route.method.to_string().to_uppercase(); - let handler = route.handler.to_string(); - let route = Route { - url: url.value(), - method: method.clone(), - handler, - }; - if !routes.contains(&route) { - routes.push(route); - } - } - } - } - } for attr in &item_fn.attrs { if attr.path().is_ident("metadata") { let metadata_attr = attr @@ -398,7 +310,6 @@ fn generate_inner(input: TokenStream) -> syn::Result { .as_ref() .unwrap_or(&(Brace::default(), vec![])) .1, - routes, fn_infos, type_infos, )?; @@ -409,20 +320,20 @@ fn generate_inner(input: TokenStream) -> syn::Result { Ok(()) } - process_paths(&project_paths, &mut routes, &mut fn_infos, &mut type_infos)?; + process_paths(&files, &mut fn_infos, &mut type_infos)?; - if routes.is_empty() { + if parsed_routes.is_empty() { return Err(s_err( proc_macro2::Span::call_site(), - "No routes found, please use the `route!` macro for defining them", + "The routes are empty, please add them in the `routes` field of the `generate!` macro", )); } - for route in routes { + for route in &parsed_routes { let fn_info = fn_infos.get(&route.handler).ok_or(s_err( proc_macro2::Span::call_site(), format!( - "Function '{}' not found, add the `#[metadata] attribute to the definition", + "Function '{}' not found, add the `#[metadata]` attribute to the definition and make sure it's included in the `files` of the `generate!` macro", route.handler ), ))?; @@ -430,7 +341,7 @@ fn generate_inner(input: TokenStream) -> syn::Result { let ty = type_infos.get(ty).ok_or(s_err( proc_macro2::Span::call_site(), format!( - "Dependency '{}' not found, add the `#[metadata] attribute to the definition", + "Dependency '{}' not found, add the `#[metadata]` attribute to the definition and make sure it's included in the `files` of the `generate!` macro", ty ), ))?; @@ -455,42 +366,179 @@ fn generate_inner(input: TokenStream) -> syn::Result { } parsed_ts - .write_to_file(path) + .write_to_file(output.unwrap()) .map_err(|e| s_err(proc_macro2::Span::call_site(), e))?; - Ok(quote! {}) + let routes_quote = MethodRoutes(routes.unwrap()); + + Ok(quote! { #routes_quote }) } struct GenerateArgs { - project_paths: Vec, - path: String, - base: String, + prefix: String, + routes: Option>, + files: Vec, + output: Option, +} + +impl GenerateArgs { + fn new() -> Self { + Self { + prefix: String::new(), + routes: None, + files: vec![String::from("src")], + output: None, + } + } } impl Parse for GenerateArgs { fn parse(input: syn::parse::ParseStream) -> syn::Result { - let project_paths = if input.peek(syn::token::Bracket) { - let content; - bracketed!(content in input); - let parsed_content: Punctuated = Punctuated::parse_terminated(&content)?; - parsed_content.iter().map(|lit| lit.value()).collect() - } else { - vec![input.parse::()?.value()] - }; - input.parse::()?; - let path = input.parse::()?.value(); - input.parse::()?; - let base = input.parse::()?.value(); + let mut ret = GenerateArgs::new(); - if !input.is_empty() { - input.parse::()?; + while !input.is_empty() { + let ident = syn::Ident::parse(input)?; + ::parse(input)?; + match ident.to_string().as_str() { + "prefix" => { + ret.prefix = input.parse::()?.value(); + } + "routes" => { + let content; + braced!(content in input); + let parsed_content: Punctuated = + Punctuated::parse_terminated(&content)?; + ret.routes = Some(parsed_content.iter().map(|lit| lit.to_owned()).collect()); + } + "files" => { + ret.files = if input.peek(syn::token::Bracket) { + let content; + bracketed!(content in input); + let parsed_content: Punctuated = + Punctuated::parse_terminated(&content)?; + parsed_content.iter().map(|lit| lit.value()).collect() + } else { + vec![input.parse::()?.value()] + }; + } + "output" => { + ret.output = Some(input.parse::()?.value()); + } + _ => return Err(s_err(ident.span(), "unknown argument")), + }; + if !input.is_empty() { + ::parse(input)?; + } } - Ok(GenerateArgs { - project_paths, - path, - base, - }) + if ret.routes.is_none() || ret.output.is_none() { + return Err(s_err( + proc_macro2::Span::call_site(), + "to generate the api both `routes` and `output` fields are required", + )); + } + + Ok(ret) + } +} + +#[derive(Clone)] +struct MethodRouter { + url: LitStr, + methods: Vec, +} + +impl fmt::Debug for MethodRouter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MethodRouter") + .field("url", &self.url.value()) + .field("methods", &self.methods) + .finish() + } +} + +impl MethodRouter { + fn to_routes(&self) -> Vec { + self.methods + .iter() + .map(|method| Route { + url: self.url.value(), + method: method.method.to_string(), + handler: method.handler.to_string(), + }) + .collect() + } +} + +impl Parse for MethodRouter { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let url = input.parse()?; + ::parse(input)?; + let mut methods = vec![]; + while !input.is_empty() { + methods.push(input.parse()?); + if input.peek(Token![.]) { + ::parse(input)?; + } else { + break; + } + } + Ok(MethodRouter { url, methods }) + } +} + +/// Wrapper over MethodRouter for ToTokens conversion +struct MethodRoutes(Vec); + +impl ToTokens for MethodRoutes { + fn to_tokens(&self, tokens: &mut TokenStream) { + let routes = self.0.iter().map(|route| { + let url = &route.url; + + let mut handlers_iter = route.methods.iter(); + let first_handler = handlers_iter.next().map(|method| { + let method_name = &method.method; + let handler_name = &method.handler; + quote! { + axum::routing::#method_name(#handler_name) + } + }); + + let rest_handlers = handlers_iter.map(|method| { + let method_name = &method.method; + let handler_name = &method.handler; + quote! { + .#method_name(#handler_name) + } + }); + + quote! { + .route(#url, #first_handler #(#rest_handlers)*) + } + }); + + let expanded = quote! { + axum::Router::new() + #(#routes)* + }; + + tokens.extend(expanded); + } +} + +#[derive(Debug, Clone)] +struct Method { + method: syn::Ident, + handler: syn::Ident, +} + +impl Parse for Method { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let method = input.parse()?; + let content; + parenthesized!(content in input); + let handler = content.parse()?; + Ok(Method { method, handler }) } } @@ -593,7 +641,7 @@ impl FnInfo { fn_str.push_str(&generate_docstring(&self.docs, " ")); fn_str.push_str(&format!( r#" export async function {fn_name}({params_str}): Promise<{response_type}> {{ - return fetch_api(`${{BASE}}{url}`, {{ + return fetch_api(`${{PREFIX}}{url}`, {{ method: "{method}", {body_assignment} }}); }} @@ -629,7 +677,7 @@ impl TypeCategory { Self::resolving_dependencies( dependencies.get(dependency).ok_or(s_err( proc_macro2::Span::call_site(), - format!("Dependency '{}' not found, add the `#[metadata] attribute to the definition", dependency), + format!("Dependency '{}' not found, add the `#[metadata]` attribute to the definition and make sure it's included in the `files` of the `generate!` macro", dependency), ))?, dependencies, parsed_ts, @@ -661,7 +709,7 @@ impl TypeCategory { Self::resolving_dependencies( dependencies.get(dependency).ok_or(s_err( proc_macro2::Span::call_site(), - format!("Dependency '{}' not found, add the `#[metadata] attribute to the definition", dependency), + format!("Dependency '{}' not found, add the `#[metadata]` attribute to the definition and make sure it's included in the `files` of the `generate!` macro", dependency), ))?, dependencies, parsed_ts, @@ -1104,7 +1152,7 @@ impl ApiType { } struct ParsedTypeScript<'a> { - base: String, + prefix: String, basic_functions: String, namespace_start: &'a str, namespace_end: &'a str, @@ -1116,13 +1164,13 @@ struct ParsedTypeScript<'a> { impl<'a> ParsedTypeScript<'a> { fn new( - base: String, + prefix: String, basic_functions: String, namespace_start: &'a str, namespace_end: &'a str, ) -> Self { Self { - base, + prefix, basic_functions, namespace_start, namespace_end, @@ -1133,8 +1181,8 @@ impl<'a> ParsedTypeScript<'a> { } } - fn filled(base: &'a str) -> ParsedTypeScript { - let base = format!("const BASE = '{}';\n", base); + fn filled(prefix: &'a str) -> ParsedTypeScript { + let prefix = format!("const PREFIX = '{}';\n", prefix); let basic_functions = r#" async function fetch_api(endpoint: string, options: RequestInit): Promise { const response = await fetch(endpoint, { @@ -1155,7 +1203,7 @@ impl<'a> ParsedTypeScript<'a> { let namespace_start = "namespace api {\n"; let namespace_end = "}\n\nexport default api;"; - ParsedTypeScript::new(base, basic_functions, namespace_start, namespace_end) + ParsedTypeScript::new(prefix, basic_functions, namespace_start, namespace_end) } fn fill_query_parser(&mut self) { @@ -1177,7 +1225,7 @@ impl<'a> ParsedTypeScript<'a> { // todo: errors let mut file = fs::File::create(path)?; - file.write_all(self.base.as_bytes())?; + file.write_all(self.prefix.as_bytes())?; file.write_all(b"\n")?; file.write_all(self.namespace_start.as_bytes())?; diff --git a/tests/api.ts b/tests/api.ts index 986e754..a618a0c 100644 --- a/tests/api.ts +++ b/tests/api.ts @@ -1,4 +1,4 @@ -const BASE = ''; +const PREFIX = ''; namespace api { /** @@ -64,7 +64,7 @@ namespace api { } export async function add_root(path: number, data: Result>>, string>, string>>): Promise> { - return fetch_api(`${BASE}/${encodeURIComponent(path)}`, { + return fetch_api(`${PREFIX}/${encodeURIComponent(path)}`, { method: "POST", body: JSON.stringify(data) }); @@ -74,13 +74,13 @@ namespace api { An example of a simple function with a `Path` and `Query` extractor */ export async function fetch_root(queryMap: Record, path: number): Promise { - return fetch_api(`${BASE}/${encodeURIComponent(path)}?${new URLSearchParams(queryMap).toString()}`, { + return fetch_api(`${PREFIX}/${encodeURIComponent(path)}?${new URLSearchParams(queryMap).toString()}`, { method: "GET", }); } export async function get_alphabet(pathTuple: [Alphabet, S]): Promise<[Alphabet, S]> { - return fetch_api(`${BASE}/char/${encodeURIComponent(pathTuple[0])}/metadata/${encodeURIComponent(pathTuple[1])}`, { + return fetch_api(`${PREFIX}/char/${encodeURIComponent(pathTuple[0])}/metadata/${encodeURIComponent(pathTuple[1])}`, { method: "GET", }); } diff --git a/tests/main.rs b/tests/main.rs index a60ac9b..a8c2e71 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,9 +1,8 @@ use axum::{ extract::{Path, Query}, - routing::get, Json, Router, }; -use gluer::{generate, metadata, route}; +use gluer::{generate, metadata}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -86,13 +85,14 @@ type S = String; #[tokio::test] async fn main_test() { - let mut _app: Router = Router::new(); - - route!(_app, "/:p", get(fetch_root).post(add_root)); - route!(_app, "/char/:path/metadata/:path", get(get_alphabet)); - - // Make sure to change "tests" to "src" when copying this example into a normal project - generate!("tests", "tests/api.ts", ""); + let _app: Router<()> = generate! { + routes = { // required + "/:p" = get(fetch_root).post(add_root), + "/char/:path/metadata/:path" = get(get_alphabet), + }, + files = "tests",// Make sure to change "tests" to "src" when copying this example into a normal project + output = "tests/api.ts", //required + }; let _listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await From 69b4033fb0c044af75f89360150103afcfbd7ddc Mon Sep 17 00:00:00 2001 From: nwrenger Date: Thu, 15 Aug 2024 23:58:35 +0200 Subject: [PATCH 2/4] :wrench: Fixed fmt --- README.md | 2 +- src/lib.rs | 2 +- tests/main.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6d54680..22b1192 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,7 @@ async fn main() { "/:p" = get(fetch_root).post(add_root), "/char/:path/metadata/:path" = get(get_alphabet), }, - files = "tests",// Make sure to change "tests" to "src" when copying this example into a normal project + files = "tests", // Make sure to change "tests" to "src" when copying this example into a normal project output = "tests/api.ts", //required }; diff --git a/src/lib.rs b/src/lib.rs index 42229f5..11129e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -356,7 +356,7 @@ fn generate_inner(input: TokenStream) -> syn::Result { } else { parsed_ts.functions.insert( fn_info.name.to_string(), - fn_info.generate_ts_function(&route, &parsed_ts, &mut needs_query_parser)?, + fn_info.generate_ts_function(route, &parsed_ts, &mut needs_query_parser)?, ); } } diff --git a/tests/main.rs b/tests/main.rs index a8c2e71..1fb468a 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -90,7 +90,7 @@ async fn main_test() { "/:p" = get(fetch_root).post(add_root), "/char/:path/metadata/:path" = get(get_alphabet), }, - files = "tests",// Make sure to change "tests" to "src" when copying this example into a normal project + files = "tests", // Make sure to change "tests" to "src" when copying this example into a normal project output = "tests/api.ts", //required }; From c1d511120786a37371d3d6a1252be27cb95e5c5e Mon Sep 17 00:00:00 2001 From: nwrenger Date: Fri, 16 Aug 2024 17:42:00 +0200 Subject: [PATCH 3/4] :wrench: Fixed docs --- README.md | 6 +++--- src/lib.rs | 7 ++++--- tests/main.rs | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 22b1192..a95a9b8 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,9 @@ let mut app: Router<()> = generate! { ### More Notes -The `generate!` macro includes several optional fields, such as `prefix`, which allows you to modify the URL prefix. By default, this value is set to `""`, but you can change it to something like `"/api"`. Please note that the `prefix` should not end with a `/`. Additionally, you can customize the `files` field to specify the Rust project directories containing the source files that define the handler functions and dependencies. This can be a single string literal (e.g., `"src"`) or an array of string literals (e.g., `["src/db", "src"]`). These paths are used to extract type information for the TypeScript client. +The `generate!` macro includes several optional fields, such as `prefix`, which allows you to modify the URL prefix. By default, this value is set to `""`, but you can change it to something like `"/api"`. Please note that the `prefix` should not end with a `/`. Additionally, you can customize the `files` field to specify the Rust project directories containing the source files that define the handler functions and dependencies. This can be a single string literal (e.g., `"src"`) or an array of string literals (e.g., `["src/db", "src"]`). These paths are used to extract type information for the TypeScript client. The default for it is `"src"` which should work in most cases. -And now you can just simply use the router to start your server or do different things, the API should be already generated by your LSP! +And now you can just simply use the router generated by the macro to start your server or do different things, the API should be already generated by your LSP! ## Complete Example @@ -259,7 +259,7 @@ async fn main() { "/:p" = get(fetch_root).post(add_root), "/char/:path/metadata/:path" = get(get_alphabet), }, - files = "tests", // Make sure to change "tests" to "src" when copying this example into a normal project + files = "tests", // Make sure to remove this when copying this example into a normal project output = "tests/api.ts", //required }; diff --git a/src/lib.rs b/src/lib.rs index 11129e6..f86e308 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,13 +102,13 @@ impl syn::parse::Parse for MetadataAttr { } } -/// Generates a TypeScript API client for the frontend from the API routes. +/// Generates a TypeScript API client and axum compatible router. /// /// ## Parameters /// /// - `prefix`: An optional parameter that allows you to specify a prefix for all generated routes. This can be useful if your API is hosted under a common base path (e.g., `/api`). -/// - `routes`: A required parameter that specifies the API routes for which the TypeScript client will be generated. Each route is defined by a URL path (which can include parameters) followed by one or more HTTP methods (e.g., `get`, `post`) and their corresponding handler functions. -/// - `files`: An optional parameter that specifies the directory or directories containing the Rust source files that define the handlers. This can be either a single string literal (e.g., `"src"`) or an array of string literals (e.g., `["src/db", "src"]`). These paths are used to extract type information for the TypeScript client. Ensure that these paths are correct and point to the appropriate directories. The default of `"src"` should handle most cases appropriately. +/// - `routes`: A required parameter that specifies the API routes for which the TypeScript client and resulting Router will be generated. Each route is defined by a URL path (which can include parameters) followed by one or more HTTP methods (e.g., `get`, `post`) and their corresponding handler functions. +/// - `files`: An optional parameter that specifies the directory or directories containing the Rust source files that define the handlers and dependencies. This can be either a single string literal (e.g., `"src"`) or an array of string literals (e.g., `["src/db", "src"]`). These paths are used to extract type information for the TypeScript client. Ensure that these paths are correct and point to the appropriate directories. The default of `"src"` should handle most cases appropriately. /// - `output`: A required parameter that specifies the path to the output file where the generated TypeScript client code will be written. Ensure that this path is correct and points to a writable location. /// /// ## Note @@ -129,6 +129,7 @@ impl syn::parse::Parse for MetadataAttr { /// /// // Use the `generate` macro to create the API client and router /// let _app: Router<()> = generate! { +/// // prefix = "" // Can be omitted by being the same as the default value /// routes = { // Defines the API routes /// "/" = get(root), // Route for the root path, using the `root` handler for GET requests /// }, diff --git a/tests/main.rs b/tests/main.rs index 1fb468a..b2eeaa8 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -90,7 +90,7 @@ async fn main_test() { "/:p" = get(fetch_root).post(add_root), "/char/:path/metadata/:path" = get(get_alphabet), }, - files = "tests", // Make sure to change "tests" to "src" when copying this example into a normal project + files = "tests", // Make sure to remove this when copying this example into a normal project output = "tests/api.ts", //required }; From 9acc70b7c4fe9371f46b732857e5f4f18b23e232 Mon Sep 17 00:00:00 2001 From: nwrenger Date: Fri, 16 Aug 2024 17:54:41 +0200 Subject: [PATCH 4/4] :sparkles: Updated docs --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f86e308..009a681 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,8 @@ impl syn::parse::Parse for MetadataAttr { /// /// // Use the `generate` macro to create the API client and router /// let _app: Router<()> = generate! { -/// // prefix = "" // Can be omitted by being the same as the default value +/// prefix = "" // Sets the prefix to `""` +/// // This can be omitted due to being the same as the default value /// routes = { // Defines the API routes /// "/" = get(root), // Route for the root path, using the `root` handler for GET requests /// },