我正在尝试了解 syn 和 quote 库的工作原理以及解析和代码生成的基本原理。因此,我编写了一个简单的宏来将 Rust 函数转换为 .json 模式。下面给出了宏。除了有Where 子句的情况之外,它工作得很好。
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
};
// 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();
if type_params.is_empty() {
None
} else {
Some(quote! { [#(#type_params),*] })
}
}
// Similar function for where clauses.
fn process_where_clause(generics: &Generics) -> Option<proc_macro2::TokenStream> {
let where_clauses: Vec<_> = generics.where_clause.as_ref().map_or(Vec::new(), |where_clause| {
where_clause.predicates.iter().map(|predicate| {
quote! { stringify!(#predicate) }
}).collect()
});
if where_clauses.is_empty() {
None
} else {
Some(quote! {
"where_clauses": [#(#where_clauses),*]
})
}
}
// 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 {
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_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() -> ::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()
}
我的测试如下所示。只有第一种情况失败了。我能做什么?
use rust_macro::myschema; // Import the schema macro
// This fails
#[myschema("Generic multiplication")]
pub fn multiply<T>(x: T, y: T) -> T
where
T: std::ops::Mul<Output = T> + Copy,
{
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);
}
}
错误是:
error[E0107]: function takes 0 generic arguments but 1 generic argument was supplied
--> src/main.rs:34:32
|
34 | if let Ok(schema_output) = schema_multiply::<i32>() {
| ^^^^^^^^^^^^^^^------- help: remove these generics
| |
| expected 0 generic arguments
|
note: function defined here, with 0 generic parameters
--> src/main.rs:4:8
|
4 | pub fn multiply<T>(x: T, y: T) -> T
| ^^^^^^^^
要解决此问题,您需要调整 schema_multiply 函数,以便在调用时不需要任何通用参数。您已经在宏输出中正确完成了此操作,因此所需的主要更改是如何在主函数中调用 schema_multiply。
if let Ok(schema_output) = schema_multiply() {
println!("Generated schema: {}", schema_output);
}