Skip to content

Commit

Permalink
✨ Added namespace && meta attr with skip and into
Browse files Browse the repository at this point in the history
  • Loading branch information
nwrenger committed Jul 31, 2024
1 parent e2cff95 commit 2b938c1
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 109 deletions.
126 changes: 74 additions & 52 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,49 +125,62 @@ fn generate_struct_const(

let mut dependencies = HashMap::new();

let fields = item_struct
.fields
.iter_mut()
.map(|field| {
let ident = field
.ident
.clone()
.ok_or_else(|| syn::Error::new(field.span(), "Unnamed field not supported"))?
.to_string();

let conversion_fn = parse_field_attr(&field.attrs)?;

// clean off all "into" attributes
field.attrs = field
.attrs
.iter()
.filter(|attr| !attr.path().is_ident("into"))
.cloned()
.collect();

let field_ty = if let Some(conv_fn) = conversion_fn.clone() {
let item_struct_fields = item_struct.fields.clone();

let fields = item_struct_fields
.iter()
.enumerate()
.filter_map(|(i, field)| {
let ident = match field.ident.clone() {
Some(ident) => ident.to_string(),
None => {
return Some(Err(syn::Error::new(
field.span(),
"Unnamed field not supported",
)))
}
};

let meta_attr = match parse_field_attr(&field.attrs) {
Ok(meta_attr) => meta_attr,
Err(e) => return Some(Err(e)),
};

let MetaAttr { into, skip } = meta_attr;

// Clean off all "meta" attributes
if let Some(field) = item_struct.fields.iter_mut().nth(i) {
field.attrs.retain(|attr| !attr.path().is_ident("meta"));
}

let field_ty = if let Some(conv_fn) = into.clone() {
conv_fn.to_token_stream().to_string()
} else {
field.ty.to_token_stream().to_string()
};

if conversion_fn.is_none() {
if let Some(RustType {
is_basic, inner_ty, ..
}) = basic_rust_type(&field.ty, &generics)?
{
if !is_basic {
dependencies.insert(
inner_ty.clone(),
format!("STRUCT_{}", inner_ty.to_uppercase()),
);
if skip {
return None;
}

if into.is_none() {
match basic_rust_type(&field.ty, &generics) {
Ok(Some(RustType {
is_basic, inner_ty, ..
})) => {
if !is_basic {
dependencies.insert(
inner_ty.clone(),
format!("STRUCT_{}", inner_ty.to_uppercase()),
);
}
}
} else {
return Err(s_err(field.span(), "Unsupported field type"));
Ok(None) => return Some(Err(s_err(field.span(), "Unsupported field type"))),
Err(e) => return Some(Err(e)),
}
}

Ok((ident, field_ty))
Some(Ok((ident, field_ty)))
})
.collect::<syn::Result<Vec<_>>>()?;

Expand Down Expand Up @@ -204,30 +217,39 @@ fn generate_struct_const(
))
}

fn parse_field_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<syn::Type>> {
if let Some(attr) = attrs.iter().next() {
if !attr.path().is_ident("into") {
return Ok(None);
}
struct MetaAttr {
into: Option<syn::Type>,
skip: bool,
}

if let syn::Meta::List(meta) = &attr.meta {
let path: syn::Expr = meta.parse_args()?;
let path = syn::parse2::<syn::Type>(quote!(#path))?;
return Ok(Some(path.clone()));
fn parse_field_attr(attrs: &[syn::Attribute]) -> syn::Result<MetaAttr> {
let mut meta_attr = MetaAttr {
into: None,
skip: false,
};

for attr in attrs {
if !attr.path().is_ident("meta") {
continue;
}

if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr) = &meta.value {
if let syn::Lit::Str(lit_str) = &expr.lit {
return lit_str.parse().map(Some);
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("into") {
meta.input.parse::<syn::Token![=]>()?;
let ty: syn::Type = meta.input.parse()?;
meta_attr.into = Some(ty);
return Ok(());
}
}

let message = "expected #[into(...)]";
return Err(syn::Error::new_spanned(attr, message));
if meta.path.is_ident("skip") {
meta_attr.skip = true;
return Ok(());
}
Err(meta.error("expected #[meta(into = Type)] or #[meta(skip)]"))
})?;
}
Ok(None)

Ok(meta_attr)
}

fn generate_fn_const(
Expand Down
78 changes: 48 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,20 @@ where

/// Generate frontend TypeScript API client from the API routes.
pub fn generate_client<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), String> {
let fetch_api_function = r#"async function fetchApi(endpoint: string, options: RequestInit): Promise<any> {
const response = await fetch(endpoint, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
return response.json();
}
let fetch_api_function = r#" async function fetchApi(endpoint: string, options: RequestInit): Promise<any> {
const response = await fetch(endpoint, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
return response.json();
}
"#;
let namespace_start = "namespace api {\n";
let namespace_end = "}\n\nexport default api;";

let mut ts_functions = BTreeMap::new();
let mut ts_interfaces: BTreeMap<String, String> = BTreeMap::new();

Expand Down Expand Up @@ -115,7 +117,15 @@ where
}
}

write_to_file(path, fetch_api_function, ts_interfaces, ts_functions)?;
write_to_file(
path,
fetch_api_function,
namespace_start,
namespace_end,
ts_interfaces,
ts_functions,
)
.map_err(|e| format!("Failed to write to file: {}", e))?;

Ok(())
}
Expand Down Expand Up @@ -198,12 +208,12 @@ fn generate_ts_interface(
} else {
format!("<{}>", generics.join(", "))
};
let mut interface = format!("export interface {}{} {{\n", struct_name, generics_str);
let mut interface = format!(" export interface {}{} {{\n", struct_name, generics_str);
for Field { name, ty } in fields {
let ty = ty_to_ts(ty, generics, ts_interfaces).unwrap();
interface.push_str(&format!(" {}: {};\n", name, ty.unwrap()));
interface.push_str(&format!(" {}: {};\n", name, ty.unwrap()));
}
interface.push_str("}\n\n");
interface.push_str(" }\n");
interface
}

Expand Down Expand Up @@ -248,12 +258,11 @@ fn generate_ts_function(
}

format!(
r#"export async function {fn_name}({params_str}): Promise<{response_type}> {{
return fetchApi(`{url}`, {{
method: "{method}", {body_assignment}
}});
}}
r#" export async function {fn_name}({params_str}): Promise<{response_type}> {{
return fetchApi(`{url}`, {{
method: "{method}", {body_assignment}
}});
}}
"#,
fn_name = fn_name,
params_str = params_str,
Expand Down Expand Up @@ -372,23 +381,32 @@ fn ty_to_ts<'a>(
fn write_to_file<P: AsRef<std::path::Path>>(
path: P,
fetch_api_function: &str,
namespace_start: &str,
namespace_end: &str,
ts_interfaces: BTreeMap<String, String>,
ts_functions: BTreeMap<String, String>,
) -> Result<(), String> {
let mut file = File::create(path).map_err(|e| format!("Failed to create file: {}", e))?;
) -> std::io::Result<()> {
let mut file = File::create(path)?;

file.write_all(namespace_start.as_bytes())?;

for interface in ts_interfaces.values() {
file.write_all(interface.as_bytes())
.map_err(|e| format!("Failed to write to file: {}", e))?;
file.write_all(interface.as_bytes())?;
file.write_all(b"\n").unwrap();
}

file.write_all(fetch_api_function.as_bytes())
.map_err(|e| format!("Failed to write to file: {}", e))?;
file.write_all(fetch_api_function.as_bytes())?;
file.write_all(b"\n").unwrap();

for function in ts_functions.values() {
file.write_all(function.as_bytes())
.map_err(|e| format!("Failed to write to file: {}", e))?;
for (i, function) in ts_functions.values().enumerate() {
file.write_all(function.as_bytes())?;
if ts_functions.len() - 1 > i {
file.write_all(b"\n").unwrap();
}
}

file.write_all(namespace_end.as_bytes())?;
file.write_all(b"\n").unwrap();

Ok(())
}
55 changes: 29 additions & 26 deletions tests/api.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
export interface Age {
age: string;
}
namespace api {
export interface Age {
age: string;
}

export interface Hello<T> {
name: string;
vec: T[];
}
export interface Hello<T> {
name: string;
vec: T[];
}

async function fetchApi(endpoint: string, options: RequestInit): Promise<any> {
const response = await fetch(endpoint, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
return response.json();
}
async function fetchApi(endpoint: string, options: RequestInit): Promise<any> {
const response = await fetch(endpoint, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
return response.json();
}

export async function add_root(path: number, data: Hello<Age>): Promise<string> {
return fetchApi(`/${encodeURIComponent(path)}`, {
method: "POST",
export async function add_root(path: number, data: Hello<Age>): Promise<string> {
return fetchApi(`/${encodeURIComponent(path)}`, {
method: "POST",
body: JSON.stringify(data)
});
}
});
}

export async function fetch_root(queryMap: Record<string, string>, path: number): Promise<string> {
return fetchApi(`/${encodeURIComponent(path)}?${new URLSearchParams(queryMap).toString()}`, {
method: "GET",
});
export async function fetch_root(queryMap: Record<string, string>, path: number): Promise<string> {
return fetchApi(`/${encodeURIComponent(path)}?${new URLSearchParams(queryMap).toString()}`, {
method: "GET",
});
}
}

export default api;
2 changes: 1 addition & 1 deletion tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct Hello<T: Serialize> {
#[metadata]
#[derive(Serialize, Deserialize, Default)]
struct Age {
#[into(String)]
#[meta(into = String)]
age: AgeInner,
}

Expand Down

0 comments on commit 2b938c1

Please sign in to comment.