Skip to content

Commit

Permalink
✨ Added api generation manually
Browse files Browse the repository at this point in the history
  • Loading branch information
nwrenger committed Jul 28, 2024
1 parent 29303e5 commit ec1b5d5
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 263 deletions.
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "gluer"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
authors = ["Nils Wrenger <[email protected]>"]
description = "A wrapper for rust frameworks which addresses the persistent issue of redundant type definitions between the frontend and backend"
description = "A wrapper for rust frameworks which addresses the persistent issue of redundant type and function definitions between the frontend and backend"
keywords = ["parser", "api", "macro"]
categories = ["accessibility", "web-programming", "api-bindings"]
rust-version = "1.64.0"
Expand All @@ -26,3 +26,4 @@ serde_yaml = "0.9.34"
[dev-dependencies]
axum = "0.7.5"
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }
46 changes: 26 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/d/gluer.svg)](https://crates.io/crates/gluer)
[![docs.rs](https://docs.rs/gluer/badge.svg)](https://docs.rs/gluer)

A wrapper for rust frameworks which addresses the persistent issue of redundant type definitions between the frontend and backend. At present, it exclusively supports the `axum` framework.
A wrapper for rust frameworks which addresses the persistent issue of redundant type and function definitions between the frontend and backend. At present, it exclusively supports the `axum` framework.

## Installation

Expand All @@ -17,7 +17,12 @@ light_magic = "0.1.0"

## Disclaimer

Please be informed that this crate is in a very early state and is expected to work in not every case. Open a Issue if you encounter one!
Please be informed that this crate is in a very early state and is expected to work in not every case. Open a Issue if you encounter one! What works is:

- Defining the routing and api generation as outlined in [How to use](#how-to-use)
- 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

## How to use

Expand All @@ -27,53 +32,54 @@ Firstly you have to use the `add_route!` macro when adding api important routes
use axum::{
routing::{get, post},
Router,
Json,
};
use gluer::add_route;

async fn root() -> &'static str {
"Hello, World!"
async fn root() -> Json<&'static str> {
"Hello, World!".into()
}

let mut app = Router::new();
let mut app: Router<()> = Router::new();

// Not api important, so adding without macro
app = app.router(get(root));
app = app.route("/", get(root));

// You currently cannot use inline functions, just path to the functions inside the methods (meaning `path(|| async &'static "Hello World!")` won't work!)
// 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));
```

Then you only have to use the `gen_spec!` which generates after specifying title, version and path the openapi doc on comptime:
Then you only have to use the `gen_spec!` macro which generates after specifying the path the api on comptime:

```rust
use gluer::gen_spec;

gen_spec!("test", "0.1.0", "tests/test.json");
gen_spec!("tests/api.ts");
```

Then use a library like `openapi-typescript` to generate your fitting `TS Client` code!

### Complete Example

```rust
use axum::{
routing::{get, post},
Router,
};
```rust,no_run
use axum::{routing::post, Json, Router};
use gluer::{add_route, gen_spec};
async fn root() -> &'static str {
"Hello, World!"
#[derive(serde::Deserialize)]
struct Hello {
_name: String,
}
async fn root(Json(_hello): Json<Hello>) -> Json<&'static str> {
"Hello World!".into()
}
#[tokio::main]
async fn main() {
let mut app = Router::new();
add_route!(app, "/", get(root).post(root));
add_route!(app, "/", post(root));
gen_spec!("test", "0.1.0", "tests/test.yaml");
gen_spec!("tests/api.ts");
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
Expand Down
120 changes: 120 additions & 0 deletions src/extractors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::{collections::HashMap, env::current_dir};

use quote::ToTokens;

pub(crate) fn extract_function(
fn_name: &str,
file_path: std::path::PathBuf,
) -> syn::Result<(Vec<syn::FnArg>, String)> {
let source = std::fs::read_to_string(file_path)
.map_err(|e| syn::Error::new(proc_macro2::Span::mixed_site(), e.to_string()))?;
let syntax = syn::parse_file(&source)?;

let mut params_map: HashMap<String, Vec<syn::FnArg>> = HashMap::new();
let mut responses_map: HashMap<String, String> = HashMap::new();

for item in syntax.items {
if let syn::Item::Fn(syn::ItemFn { sig, .. }) = item {
let fn_name = sig.ident.to_string();
let params: Vec<syn::FnArg> = 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);
}
}

let params = params_map.get(fn_name).cloned().ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"Function parameters not found",
)
})?;

let responses = responses_map.get(fn_name).cloned().ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"Function responses not found",
)
})?;

Ok((params, responses))
}

pub(crate) fn extract_struct(
struct_name: &str,
file_path: std::path::PathBuf,
) -> syn::Result<Vec<(String, String)>> {
let source = std::fs::read_to_string(&file_path)
.map_err(|e| syn::Error::new(proc_macro2::Span::mixed_site(), e.to_string()))?;
let syntax = syn::parse_file(&source)?;

for item in syntax.items {
if let syn::Item::Struct(syn::ItemStruct { ident, fields, .. }) = item {
let name = ident.to_string().trim().to_string();

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(field_vec);
}
}
}

Err(syn::Error::new(
proc_macro2::Span::call_site(),
"Struct definition not found in ".to_string() + file_path.to_string_lossy().as_ref(),
))
}

pub(crate) fn resolve_path(segments: Vec<String>) -> syn::Result<std::path::PathBuf> {
let current_dir = current_dir().map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
"Failed to get current directory",
)
})?;

if segments.len() == 1 {
// Function is in the same file, check if it's in main.rs or lib.rs (for tests)
let main_path = current_dir.join("src/main.rs");
let lib_path = current_dir.join("src/lib.rs");
if main_path.exists() {
Ok(main_path)
} else if lib_path.exists() {
Ok(current_dir.join("tests/main.rs"))
} else {
Err(syn::Error::new(
proc_macro2::Span::call_site(),
"Neither main.rs nor lib.rs found",
))?
}
} else {
// Function is in a different module
let module_path = &segments[0];
let file_path_mod = current_dir.join(format!("src/{}/mod.rs", module_path));
let file_path_alt = current_dir.join(format!("src/{}.rs", module_path));
if file_path_mod.exists() {
Ok(file_path_mod)
} else if file_path_alt.exists() {
Ok(file_path_alt)
} else {
Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!("Module file not found for {}", module_path),
))?
}
}
}
Loading

0 comments on commit ec1b5d5

Please sign in to comment.