extern crate proc_macro;
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::ParseStream, parse_macro_input, parse_quote, punctuated::Punctuated, Error, Expr,
ExprArray, ExprPath, ExprStruct, ExprTuple, FieldValue, Ident, Member, Path, QSelf, Result,
Stmt, Token, Type, TypePath,
};
#[proc_macro]
pub fn proto_proc(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let result = parse_macro_input!(input with parse_and_expand_top_level_struct);
result.to_token_stream().into()
}
fn parse_and_expand_top_level_struct(input: ParseStream) -> Result<Expr> {
expand_struct(input.parse()?, EnclosingContext::TopLevel)
}
#[derive(Clone)]
enum EnclosingContext {
TopLevel,
Struct {
field: Ident,
},
Array {
repeated_field: Ident,
},
Map { map_field: Ident },
}
impl EnclosingContext {
pub fn repeated_field(&self) -> Ident {
match self {
EnclosingContext::Array { repeated_field } => repeated_field.clone(),
_ => panic!("Can only get the repeated field of an array context"),
}
}
}
fn expand_struct(
ExprStruct { attrs, qself, path, fields, rest, .. }: ExprStruct,
enclosing_context: EnclosingContext,
) -> Result<Expr> {
if let Some(attr) = attrs.first() {
return Err(Error::new_spanned(attr, "unsupported syntax"));
}
let merge = rest.map(|rest| {
quote! {
::protobuf::MergeFrom::merge_from(&mut this, #rest);
}
});
let fields = expand_struct_fields(fields)?;
let this_type = expand_struct_type(qself.clone(), path.clone(), enclosing_context.clone());
let (head, tail) =
expand_struct_head_tail(qself.clone(), path.clone(), enclosing_context.clone())?;
Ok(parse_quote! {{
let mut this: #this_type = #head;
#merge
#(#fields)*
#tail
}})
}
fn expand_struct_fields(fields: Punctuated<FieldValue, Token![,]>) -> Result<Vec<Stmt>> {
fields
.into_iter()
.map(|field| {
let Member::Named(ident) = field.member else {
return Err(Error::new_spanned(field, "field must be an identifier, not an index"));
};
let set_method = format_ident!("set_{}", ident);
let enclosing_context = EnclosingContext::Struct { field: ident };
let expr = match field.expr {
Expr::Struct(struct_) => {
let expanded = expand_struct(struct_, enclosing_context)?;
return Ok(parse_quote!(#expanded));
}
Expr::Array(array) => expand_array(array, enclosing_context)?,
expr => expr,
};
Ok(parse_quote! {
this.#set_method(#expr);
})
})
.collect()
}
fn expand_struct_type(
qself: Option<QSelf>,
path: Path,
enclosing_context: EnclosingContext,
) -> Type {
if should_infer_message_type(&qself, &path) {
return parse_quote!(_);
}
let type_path = TypePath { qself, path };
if let EnclosingContext::Struct { .. } = enclosing_context {
parse_quote!(::protobuf::Mut<'_, #type_path>)
} else {
parse_quote!(#type_path)
}
}
fn expand_struct_head_tail(
qself: Option<QSelf>,
path: Path,
enclosing_context: EnclosingContext,
) -> Result<(Expr, Option<Expr>)> {
match enclosing_context {
EnclosingContext::TopLevel => {
if should_infer_message_type(&qself, &path) {
Err(Error::new_spanned(path, "message type must be specified explicitly"))
} else {
let path = ExprPath { attrs: Vec::new(), qself, path };
Ok((parse_quote!(#path::new()), Some(parse_quote!(this))))
}
}
EnclosingContext::Struct { field } => {
let field_mut = format_ident!("{}_mut", field);
Ok((parse_quote!(this.#field_mut()), None))
}
EnclosingContext::Array { repeated_field } => {
Ok((
parse_quote!(::protobuf::__internal::get_repeated_default_value(
::protobuf::__internal::Private,
this.#repeated_field()
)),
Some(parse_quote!(this)),
))
}
EnclosingContext::Map { map_field } => {
Ok((
parse_quote!(::protobuf::__internal::get_map_default_value(
::protobuf::__internal::Private,
this.#map_field()
)),
Some(parse_quote!(this)),
))
}
}
}
fn should_infer_message_type(qself: &Option<QSelf>, path: &Path) -> bool {
qself.is_none() && path.get_ident().is_some_and(|ident| *ident == "__")
}
fn expand_array(array: ExprArray, enclosing_context: EnclosingContext) -> Result<Expr> {
if let Some(attr) = array.attrs.first() {
return Err(Error::new_spanned(attr, "unsupported syntax"));
}
let enclosing_context = EnclosingContext::Array {
repeated_field: match enclosing_context {
EnclosingContext::Struct { field } => field,
EnclosingContext::TopLevel
| EnclosingContext::Array { .. }
| EnclosingContext::Map { .. } => {
return Err(Error::new_spanned(array, "arrays must be nested inside a message"))
}
},
};
let array = array
.elems
.into_iter()
.map(|elem| match elem {
Expr::Struct(struct_) => expand_struct(struct_, enclosing_context.clone()),
Expr::Array(array) => expand_array(array, enclosing_context.clone()),
Expr::Tuple(tuple) => expand_tuple(
tuple,
EnclosingContext::Map { map_field: enclosing_context.repeated_field() },
),
expr => Ok(expr),
})
.collect::<Result<Vec<Expr>>>()?;
Ok(parse_quote!([#(#array),*].into_iter()))
}
fn expand_tuple(tuple: ExprTuple, enclosing_context: EnclosingContext) -> Result<Expr> {
if let Some(attr) = tuple.attrs.first() {
return Err(Error::new_spanned(attr, "unsupported syntax"));
}
if tuple.elems.len() != 2 {
return Err(Error::new_spanned(tuple, "Map tuple literals must have exactly two elements"));
}
let key = tuple.elems[0].clone();
let value = match &tuple.elems[1] {
Expr::Struct(struct_) => expand_struct(struct_.clone(), enclosing_context.clone()),
expr => Ok(expr.clone()),
}?;
Ok(parse_quote!((#key, #value)))
}