Skip to content

Commit

Permalink
✨ Replaced param and fns macros with the cached macro && Improved errors
Browse files Browse the repository at this point in the history
  • Loading branch information
nwrenger committed Jul 29, 2024
1 parent 335cf91 commit 0ac5744
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 106 deletions.
67 changes: 35 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,32 @@ Please be informed that this crate is in a very early state and is expected to w

`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
### Step 1: Define Structs and Functions

Start by using the `#[param]` and `fns!` macros to define your data structures and functions. These macros give `gluer` access to the necessary code for type inference and conversion.
Start by using the `#[cached]` macro to define your data structures and functions. This macro gives `gluer` access to the necessary code for type inference and conversion.

```rust
use axum::{
Json,
};
use gluer::{fns, param};
use gluer::cached;

// Define a parameter with the param macro used by the functions down below
#[param]
// Define a struct with the cached macro
#[cached]
#[derive(Default, serde::Serialize)]
struct Book {
// imagine some fields here
}

// Wrap the functions with the fun macro
fns! {
async fn root() -> Json<String> {
"Hello, World!".to_string().into()
}
async fn book() -> Json<Book> {
Book::default().into()
}
// Define the functions with the cached macro
#[cached]
async fn root() -> Json<String> {
"Hello, World!".to_string().into()
}

#[cached]
async fn book() -> Json<Book> {
Book::default().into()
}
```

Expand All @@ -66,16 +67,17 @@ use axum::{
Router,
Json,
};
use gluer::{add_route, fns};

// a part of the function above
fns! {
async fn root() -> String {
"Hello, World!".to_string()
}
async fn hello() -> Json<String> {
"Hello, World!".to_string().into()
}
use gluer::{add_route, cached};

// done like above
#[cached]
async fn root() -> String {
"Hello, World!".to_string()
}

#[cached]
async fn hello() -> Json<String> {
"Hello, World!".to_string().into()
}

let mut app: Router<()> = Router::new();
Expand Down Expand Up @@ -104,21 +106,22 @@ Below is a complete example demonstrating the use of gluer with `axum`:

```rust,no_run
use axum::{routing::get, Json, Router};
use gluer::{add_route, fns, api, param};
use gluer::{add_route, api, cached};
#[cached]
async fn fetch_root() -> String {
String::from("Hello, World!")
}
#[param]
#[cached]
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct Hello {
name: String,
}
fns! {
async fn add_root(Json(hello): Json<Hello>) -> Json<Hello> {
hello.into()
}
async fn fetch_root() -> Json<Hello> {
Hello::default().into()
}
#[cached]
async fn add_root(Json(hello): Json<Hello>) -> Json<Hello> {
hello.into()
}
#[tokio::main]
Expand Down
127 changes: 68 additions & 59 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ fn s_err(span: proc_macro2::Span, msg: impl fmt::Display) -> syn::Error {
syn::Error::new(span, msg)
}

fn lock_err(span: proc_macro2::Span, e: impl fmt::Display) -> syn::Error {
s_err(span, format!("Failed to acquire lock: {}", e))
}

fn logic_err(span: proc_macro2::Span) -> syn::Error {
s_err(
span,
"Fatal logic error when trying to extract data from rust types",
)
}

struct Route {
route: String,
method: String,
Expand Down Expand Up @@ -45,16 +56,22 @@ pub fn add_route(input: pc::TokenStream) -> pc::TokenStream {
}

fn add_route_inner(input: TokenStream) -> syn::Result<TokenStream> {
let span = input.span();
let args = syn::parse2::<RouterArgs>(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().unwrap().ident.to_string();

ROUTES.write().unwrap().push(Route {
let fn_name = r#fn
.segments
.last()
.ok_or_else(|| logic_err(span))?
.ident
.to_string();

ROUTES.write().map_err(|e| lock_err(span, e))?.push(Route {
route: route.clone(),
method: method.to_string(),
fn_name,
Expand Down Expand Up @@ -130,9 +147,9 @@ fn api_inner(input: TokenStream) -> syn::Result<TokenStream> {
let args = syn::parse2::<GenArgs>(input)?;
let path = args.path.value();

let routes = ROUTES.read().unwrap();
let functions = FUNCTIONS.read().unwrap();
let structs = STRUCTS.read().unwrap();
let routes = ROUTES.read().map_err(|e| lock_err(span, e))?;
let functions = FUNCTIONS.read().map_err(|e| lock_err(span, e))?;
let structs = STRUCTS.read().map_err(|e| lock_err(span, e))?;

let mut ts_functions = HashMap::new();
let mut ts_interfaces = HashMap::new();
Expand All @@ -146,7 +163,7 @@ fn api_inner(input: TokenStream) -> syn::Result<TokenStream> {
s_err(
span,
format!(
"Function '{}' not found in the cache, mind adding it with fun! {{}}",
"Function '{}' not found in the cache, mind adding it with #[cached]",
fn_name
),
)
Expand Down Expand Up @@ -206,7 +223,7 @@ fn collect_params(
) -> syn::Result<String> {
for param in &function.params {
if param.1.contains("Json") {
let struct_name = extract_struct_name(param.1)?;
let struct_name = extract_struct_name(span, param.1)?;
if let Some(fields) = structs.get(&struct_name).cloned() {
ts_interfaces
.entry(struct_name.clone())
Expand All @@ -220,7 +237,7 @@ fn collect_params(
return Err(s_err(
span,
format!(
"Struct '{}' not found in the cache, mind adding it with #[param]",
"Struct '{}' not found in the cache, mind adding it with #[cached]",
struct_name
),
));
Expand All @@ -243,7 +260,7 @@ fn collect_response_type(
}

if response.contains("Json") {
let struct_name = extract_struct_name(&response)?;
let struct_name = extract_struct_name(span, &response)?;
if let Some(fields) = structs.get(&struct_name).cloned() {
ts_interfaces
.entry(struct_name.clone())
Expand All @@ -255,24 +272,30 @@ fn collect_response_type(
Err(s_err(
span,
format!(
"Struct '{}' not found in the cache, mind adding it with #[param]",
"Struct '{}' not found in the cache, mind adding it with #[cached]",
response
),
))
}

fn extract_struct_name(type_str: &str) -> syn::Result<String> {
fn extract_struct_name(span: proc_macro2::Span, type_str: &str) -> syn::Result<String> {
type_str
.split('<')
.nth(1)
.and_then(|s| s.split('>').next())
.map(|s| s.split("::").last().unwrap().trim().to_string())
.map(|s| {
Ok(s.split("::")
.last()
.ok_or_else(|| logic_err(span))?
.trim()
.to_string())
})
.ok_or_else(|| {
s_err(
proc_macro2::Span::call_site(),
span,
format!("Failed to extract struct name from '{}'", type_str),
)
})
})?
}

fn write_to_file(
Expand Down Expand Up @@ -339,29 +362,46 @@ impl Parse for GenArgs {
}
}

/// Put here inside the functions which should be used by the api.
#[proc_macro]
pub fn fns(input: pc::TokenStream) -> pc::TokenStream {
match fns_inner(input.into()) {
/// 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 fns_inner(input: TokenStream) -> syn::Result<TokenStream> {
fn cached_inner(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let span = input.span();
let items = syn::parse2::<syn::File>(input.clone())?;
let input = syn::parse2::<syn::Item>(input)?;
let _args = syn::parse2::<NoArgs>(args)?;

for item in items.items {
if let syn::Item::Fn(item_fn) = item {
match input.clone() {
syn::Item::Struct(syn::ItemStruct { ident, fields, .. }) => {
STRUCTS
.write()
.map_err(|e| lock_err(span, e))?
.insert(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(),
};

FUNCTIONS.write().unwrap().insert(
FUNCTIONS.write().map_err(|e| lock_err(span, e))?.insert(
fn_name.clone(),
Function {
params: {
Expand All @@ -371,15 +411,15 @@ fn fns_inner(input: TokenStream) -> syn::Result<TokenStream> {
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 struct_name = struct_path.split("::").last().ok_or_else(|| logic_err(span))?.trim();
let fields = STRUCTS
.read().unwrap()
.read().map_err(|e| lock_err(span, e))?
.get(&struct_name.to_string())
.ok_or_else(|| {
s_err(
span,
format!(
"Struct '{}' not found in the cache, mind adding it with #[param]",
"Struct '{}' not found in the cache, mind adding it with #[cached]",
struct_name
),
)
Expand All @@ -404,44 +444,13 @@ fn fns_inner(input: TokenStream) -> syn::Result<TokenStream> {
response,
},
);
} else {
return Err(s_err(span, "Expected function item"));
}
_ => return Err(s_err(span, "Expected struct or function")),
}

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<TokenStream> {
let input = syn::parse2::<syn::ItemStruct>(input)?;
let _args = syn::parse2::<NoArgs>(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 {
Expand Down
12 changes: 6 additions & 6 deletions tests/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ export interface Hello {
name: string;
}

export async function fetch_root(): Promise<Hello | any> {
export async function add_root(params: Hello): Promise<Hello | any> {
const response = await fetch("/", {
method: "GET",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: undefined
body: JSON.stringify(params)
});
return response.json();
}

export async function add_root(params: Hello): Promise<Hello | any> {
export async function fetch_root(): Promise<string | any> {
const response = await fetch("/", {
method: "POST",
method: "GET",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(params)
body: undefined
});
return response.json();
}
Expand Down
Loading

0 comments on commit 0ac5744

Please sign in to comment.