I am trying to learn how the syn and quote libraries work and basically how parsing and code gen are done. So I wrote a simple macro to convert a Rust function to a .json schema. The macro is given below. It works well except for a case where we have the Where clause.
Here is the macro below:
use proc_macro::{self, TokenStream};
use quote::{quote, format_ident};
use syn::{
parse_macro_input, ItemFn, ReturnType, Attribute, parse::Parse, parse::ParseStream, FnArg, Generics, Visibility,
LitStr, Lit,Expr, Meta, Result, WherePredicate
};
// Custom attribute parser
struct MacroAttributes {
description: Option<String>,
}
impl Parse for MacroAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let description = if input.peek(LitStr) {
let desc_lit = input.parse::<LitStr>()?;
Some(desc_lit.value())
} else {
None
};
Ok(MacroAttributes { description })
}
}
// Function to extract documentation comments from function attributes.
fn extract_doc_comments(attrs: &[Attribute]) -> Result<Option<String>> {
let mut docs = Vec::new();
for attr in attrs {
match &attr.meta {
Meta::NameValue(nv) if nv.path.is_ident("doc") => {
if let Expr::Lit(expr_lit) = &nv.value {
if let Lit::Str(lit) = &expr_lit.lit {
docs.push(lit.value());
}
}
},
_ => {}
}
}
if docs.is_empty() {
Ok(None)
} else {
Ok(Some(docs.join("\n")))
}
}
// Function to generate JSON-like schema information for generics in a function.
fn process_generics(generics: &Generics) -> Option<proc_macro2::TokenStream> {
let type_params: Vec<_> = generics.type_params().map(|param| {
let ident = ¶m.ident; // Identifier of the type parameter.
let bounds = param.bounds.iter().map(|b| quote! {#b}).collect::<Vec<_>>();
quote! {
{"name": stringify!(#ident), "bounds": [#(#bounds),*]}
}
}).collect();
// Debug print to see the processed generics
eprintln!("Processed Generics: {:?}", type_params);
if type_params.is_empty() {
None // No type parameters to process.
} else {
Some(quote! { [#(#type_params),*] })
}
}
fn process_where_clause(generics: &Generics) -> Option<proc_macro2::TokenStream> {
generics.where_clause.as_ref().map(|where_clause| {
let where_preds: Vec<_> = where_clause.predicates.iter().map(|predicate| {
if let WherePredicate::Type(predicate) = predicate {
let bounded_ty = &predicate.bounded_ty;
let bounds = predicate.bounds.iter().map(|b| quote! { #b }).collect::<Vec<_>>();
quote! { #bounded_ty: #(#bounds)+* }
} else {
quote! { compile_error!("Unsupported where predicate type"); }
}
}).collect();
// Correctly format the complete where clause
quote! { where #(#where_preds),* }
})
}
// Similar function for lifetimes.
fn process_lifetimes(generics: &Generics) -> Option<proc_macro2::TokenStream> {
let lifetimes: Vec<_> = generics.lifetimes().map(|lifetime| {
let ident = &lifetime.lifetime.ident;
quote! {stringify!(#ident)}
}).collect();
if lifetimes.is_empty() {
None
} else {
Some(quote! { [#(#lifetimes),*] })
}
}
// The `myschema` macro function that processes attributes and the function to which it is applied.
#[proc_macro_attribute]
pub fn myschema(attr: TokenStream, item: TokenStream) -> TokenStream {
eprintln!("Attributes: {:?}", attr);
eprintln!("Item: {:?}", item);
let attrs = parse_macro_input!(attr as MacroAttributes);
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let visibility_str = match &input_fn.vis {
Visibility::Public(_) => "public",
Visibility::Restricted(res) => if res.path.is_ident("crate") { "crate" } else { "restricted" },
Visibility::Inherited => "private",
};
let generics = &input_fn.sig.generics;
let generics_info = process_generics(&input_fn.sig.generics);
let lifetimes_info = process_lifetimes(&input_fn.sig.generics);
let where_info = process_where_clause(&input_fn.sig.generics);
let return_type = match &input_fn.sig.output {
ReturnType::Default => None,
ReturnType::Type(_, t) => Some(quote! {#t}),
};
let is_async = input_fn.sig.asyncness.is_some();
let doc_comments = match extract_doc_comments(&input_fn.attrs) {
Ok(Some(docs)) => docs,
Ok(None) => String::new(),
Err(_) => return TokenStream::from(quote! { compile_error!("Error parsing doc comments"); })
};
// Generate the JSON-like schema for the function parameters.
let inputs: Vec<_> = input_fn.sig.inputs.iter().map(|arg| {
if let FnArg::Typed(pat_type) = arg {
let pat = &pat_type.pat;
let ty = &pat_type.ty;
quote! {
{
"name": stringify!(#pat),
"type": stringify!(#ty)
}
}
} else {
quote! {}
}
}).collect();
let mut schema_fields = vec![
quote! { "name": stringify!(#fn_name) },
quote! { "visibility": #visibility_str },
quote! { "documentation": #doc_comments },
];
if let Some(desc) = attrs.description {
schema_fields.push(quote! { "description": #desc });
}
if !inputs.is_empty() {
schema_fields.push(quote! { "parameters": [#(#inputs),*] });
}
if let Some(rt) = return_type {
schema_fields.push(quote! { "return_type": stringify!(#rt) });
}
if is_async {
schema_fields.push(quote! { "is_async": true });
}
if let Some(generics) = generics_info {
schema_fields.push(quote! { "generics": #generics });
}
if let Some(where_clause) = where_info {
schema_fields.push(quote! { "where_clause": #where_clause });
}
if let Some(lifetimes) = lifetimes_info {
schema_fields.push(quote! { "lifetimes": #lifetimes });
}
let schema_function_name = format_ident!("schema_{}", fn_name);
let schema_fn = quote! {
pub fn #schema_function_name #generics() -> ::std::result::Result<String, serde_json::Error> {
let schema = serde_json::json!({ #(#schema_fields),* });
serde_json::to_string_pretty(&schema)
}
};
let output = quote! {
#input_fn
#schema_fn
};
output.into()
}
The macro compiles and works for all the test cases except the first one (multiply) below:
use std::ops::Mul;
use rust_macro::myschema; // Import the schema macro
// This fails
#[myschema("Generic multiplication")]
pub fn multiply<T>(x: T, y: T) -> T
where
T: Mul<Output = T>
{
x * y
}
/// My async function
#[myschema("Asynchronous fetch")]
pub async fn fetch_data(url: &str) -> Result<String, &'static str> {
Ok("data".to_string())
}
#[myschema("Multiplies two numbers")]
pub fn multiply_doc(x: i32, y: i32) -> i32 {
x * y
}
#[myschema("Determines the longest of two string slices")]
pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
#[myschema("Private function")]
fn private_function() -> bool {
true
}
fn main() {
if let Ok(schema_output) = schema_multiply::<i32>() {
println!("Generated schema: {}", schema_output);
}
if let Ok(schema_output) = schema_fetch_data() {
println!("Generated schema: {}", schema_output);
}
if let Ok(schema_output) = schema_multiply_doc() {
println!("Generated schema: {}", schema_output);
}
if let Ok(schema_output) = schema_longest() {
println!("Generated schema: {}", schema_output);
}
if let Ok(schema_output) = schema_private_function() {
println!("Generated schema: {}", schema_output);
}
}
Instead I get the error:
Compiling rust-macro v0.1.0 (/home/richard/Documents/rust_projects/practical-rust/rust-macro)
Compiling rust-macro-test v0.1.0 (/home/richard/Documents/rust_projects/practical-rust/rust-macro-test)
Attributes: TokenStream [Literal { kind: Str, symbol: "Generic multiplication", suffix: None, span: #0 bytes(99..123) }]
Item: TokenStream [Ident { ident: "pub", span: #0 bytes(126..129) }, Ident { ident: "fn", span: #0 bytes(130..132) }, Ident { ident: "multiply", span: #0 bytes(133..141) }, Punct { ch: '<', spacing: Alone, span: #0 bytes(141..142) }, Ident { ident: "T", span: #0 bytes(142..143) }, Punct { ch: '>', spacing: Alone, span: #0 bytes(143..144) }, Group { delimiter: Parenthesis, stream: TokenStream [Ident { ident: "x", span: #0 bytes(145..146) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(146..147) }, Ident { ident: "T", span: #0 bytes(148..149) }, Punct { ch: ',', spacing: Alone, span: #0 bytes(149..150) }, Ident { ident: "y", span: #0 bytes(151..152) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(152..153) }, Ident { ident: "T", span: #0 bytes(154..155) }], span: #0 bytes(144..156) }, Punct { ch: '-', spacing: Joint, span: #0 bytes(157..158) }, Punct { ch: '>', spacing: Alone, span: #0 bytes(158..159) }, Ident { ident: "T", span: #0 bytes(160..161) }, Ident { ident: "where", span: #0 bytes(162..167) }, Ident { ident: "T", span: #0 bytes(172..173) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(173..174) }, Ident { ident: "Mul", span: #0 bytes(175..178) }, Punct { ch: '<', spacing: Alone, span: #0 bytes(178..179) }, Ident { ident: "Output", span: #0 bytes(179..185) }, Punct { ch: '=', spacing: Alone, span: #0 bytes(186..187) }, Ident { ident: "T", span: #0 bytes(188..189) }, Punct { ch: '>', spacing: Alone, span: #0 bytes(189..190) }, Group { delimiter: Brace, stream: TokenStream [Ident { ident: "x", span: #0 bytes(197..198) }, Punct { ch: '*', spacing: Alone, span: #0 bytes(199..200) }, Ident { ident: "y", span: #0 bytes(201..202) }], span: #0 bytes(191..204) }]
Processed Generics: [TokenStream [Group { delimiter: Brace, stream: TokenStream [Literal { kind: Str, symbol: "name", suffix: None, span: #5 bytes(88..125) }, Punct { ch: ':', spacing: Alone, span: #5 bytes(88..125) }, Ident { ident: "stringify", span: #5 bytes(88..125) }, Punct { ch: '!', spacing: Alone, span: #5 bytes(88..125) }, Group { delimiter: Parenthesis, stream: TokenStream [Ident { ident: "T", span: #0 bytes(142..143) }], span: #5 bytes(88..125) }, Punct { ch: ',', spacing: Alone, span: #5 bytes(88..125) }, Literal { kind: Str, symbol: "bounds", suffix: None, span: #5 bytes(88..125) }, Punct { ch: ':', spacing: Alone, span: #5 bytes(88..125) }, Group { delimiter: Bracket, stream: TokenStream [], span: #5 bytes(88..125) }], span: #5 bytes(88..125) }]]
error: no rules expected the token `where`
--> src/main.rs:6:1
|
6 | #[myschema("Generic multiplication")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no rules expected this token in macro call
|
note: while trying to match meta-variable `$e:expr`
--> /home/richard/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde_json-1.0.116/src/macros.rs:303:6
|
303 | ($e:expr , $($tt:tt)*) => {};
| ^^^^^^^
= note: this error originates in the attribute macro `myschema` (in Nightly builds, run with -Z macro-backtrace for more info)
Kindly assist, as I don't know what else to do.