Skip to main content

mcproto_derive/
lib.rs

1//! Derive macros for `mcproto-types`.
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6    Data, DeriveInput, Error, Fields, Ident, Index, Member, Result, Type, parse_macro_input,
7    parse_quote,
8};
9
10/// Derives [`mcproto_types::TypeCodec`] for a protocol structure.
11///
12/// Fields are encoded and decoded in declaration order. A codec kind must be
13/// supplied so errors from individual fields retain the enclosing structure:
14///
15/// ```ignore
16/// #[derive(TypeStructCodec)]
17/// #[type_struct_codec(kind = Slot)]
18/// struct Item {
19///     id: VarInt,
20///     count: VarInt,
21/// }
22/// ```
23#[proc_macro_derive(TypeStructCodec, attributes(type_struct_codec))]
24pub fn derive_type_struct_codec(input: TokenStream) -> TokenStream {
25    let input = parse_macro_input!(input as DeriveInput);
26    match expand_type_struct_codec(&input) {
27        Ok(tokens) => tokens.into(),
28        Err(error) => error.into_compile_error().into(),
29    }
30}
31
32fn expand_type_struct_codec(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
33    let Data::Struct(data) = &input.data else {
34        return Err(Error::new_spanned(
35            input,
36            "TypeStructCodec can only be derived for structs",
37        ));
38    };
39    let kind = type_struct_codec_kind(input)?;
40    let name = &input.ident;
41
42    let fields: Vec<(Member, &Type)> = match &data.fields {
43        Fields::Named(fields) => fields
44            .named
45            .iter()
46            .map(|field| {
47                (
48                    Member::Named(field.ident.clone().expect("named field")),
49                    &field.ty,
50                )
51            })
52            .collect(),
53        Fields::Unnamed(fields) => fields
54            .unnamed
55            .iter()
56            .enumerate()
57            .map(|(index, field)| (Member::Unnamed(Index::from(index)), &field.ty))
58            .collect(),
59        Fields::Unit => Vec::new(),
60    };
61
62    let mut bounded_generics = input.generics.clone();
63    for (_, field_type) in &fields {
64        bounded_generics
65            .make_where_clause()
66            .predicates
67            .push(parse_quote!(#field_type: ::mcproto_types::TypeCodec));
68    }
69    let (impl_generics, _, where_clause) = bounded_generics.split_for_impl();
70    let (_, type_generics, _) = input.generics.split_for_impl();
71
72    let encode_fields = fields.iter().map(|(member, _)| {
73        quote! {
74            ::mcproto_types::TypeCodec::encode(&self.#member, writer)
75                .map_err(|error| error.with_context(
76                    ::mcproto_types::__private::CodecKind::#kind,
77                ))?;
78        }
79    });
80    let decode_fields: Vec<_> = fields
81        .iter()
82        .map(|(_, field_type)| {
83            quote! {
84                <#field_type as ::mcproto_types::TypeCodec>::decode(reader)
85                    .map_err(|error| error.with_context(
86                        ::mcproto_types::__private::CodecKind::#kind,
87                    ))?
88            }
89        })
90        .collect();
91    let construct = match &data.fields {
92        Fields::Named(fields) => {
93            let names = fields
94                .named
95                .iter()
96                .map(|field| field.ident.as_ref().unwrap());
97            quote! { Self { #(#names: #decode_fields,)* } }
98        }
99        Fields::Unnamed(_) => quote! { Self(#(#decode_fields,)*) },
100        Fields::Unit => quote! { Self },
101    };
102
103    Ok(quote! {
104        impl #impl_generics ::mcproto_types::TypeCodec for #name #type_generics #where_clause {
105            fn encode(
106                &self,
107                writer: &mut impl ::std::io::Write,
108            ) -> ::std::result::Result<(), ::mcproto_types::__private::CodecError> {
109                #(#encode_fields)*
110                ::std::result::Result::Ok(())
111            }
112
113            fn decode(
114                reader: &mut impl ::std::io::Read,
115            ) -> ::std::result::Result<Self, ::mcproto_types::__private::CodecError> {
116                ::std::result::Result::Ok(#construct)
117            }
118        }
119    })
120}
121
122fn type_struct_codec_kind(input: &DeriveInput) -> Result<Ident> {
123    let mut kind = None;
124    for attribute in &input.attrs {
125        if !attribute.path().is_ident("type_struct_codec") {
126            continue;
127        }
128        attribute.parse_nested_meta(|meta| {
129            if !meta.path.is_ident("kind") {
130                return Err(meta.error("expected `kind = CodecKindVariant`"));
131            }
132            if kind.is_some() {
133                return Err(meta.error("duplicate `kind` argument"));
134            }
135            kind = Some(meta.value()?.parse()?);
136            Ok(())
137        })?;
138    }
139    kind.ok_or_else(|| {
140        Error::new_spanned(
141            input,
142            "TypeStructCodec requires `#[type_struct_codec(kind = CodecKindVariant)]`",
143        )
144    })
145}
146
147/// Derives [`mcproto_types::ProtocolEnum`] and [`mcproto_types::TypeCodec`] for
148/// a fieldless enum with a numeric protocol representation.
149///
150/// # Example
151///
152/// ```ignore
153/// #[derive(ProtocolEnum)]
154/// #[protocol_enum(repr = VarInt)]
155/// enum GameMode {
156///     Survival = 0,
157///     Creative = 1,
158/// }
159/// ```
160///
161/// The `repr` value must implement [`mcproto_types::EnumRepr`]. Built-in
162/// numeric protocol types, including `VarInt`, `VarLong`, and fixed-width
163/// integer types, implement that trait.
164#[proc_macro_derive(ProtocolEnum, attributes(protocol_enum))]
165pub fn derive_protocol_enum(input: TokenStream) -> TokenStream {
166    let input = parse_macro_input!(input as DeriveInput);
167    match expand_protocol_enum(&input) {
168        Ok(tokens) => tokens.into(),
169        Err(error) => error.into_compile_error().into(),
170    }
171}
172
173fn expand_protocol_enum(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
174    let repr = enum_repr(input)?;
175    let Data::Enum(data) = &input.data else {
176        return Err(Error::new_spanned(
177            input,
178            "ProtocolEnum can only be derived for enums",
179        ));
180    };
181    if data.variants.is_empty() {
182        return Err(Error::new_spanned(
183            input,
184            "ProtocolEnum requires at least one enum variant",
185        ));
186    }
187
188    let mut variants = Vec::with_capacity(data.variants.len());
189    for variant in &data.variants {
190        if !matches!(variant.fields, Fields::Unit) {
191            return Err(Error::new_spanned(
192                variant,
193                "ProtocolEnum only supports fieldless enum variants",
194            ));
195        }
196        variants.push(&variant.ident);
197    }
198
199    let name = &input.ident;
200    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
201
202    Ok(quote! {
203        impl #impl_generics ::mcproto_types::ProtocolEnum for #name #type_generics #where_clause {
204            type Repr = #repr;
205
206            fn discriminant(&self) -> i128 {
207                match self {
208                    #(Self::#variants => Self::#variants as i128,)*
209                }
210            }
211
212            fn to_repr(&self) -> ::std::option::Option<Self::Repr> {
213                <Self::Repr as ::mcproto_types::EnumRepr>::from_discriminant(
214                    <Self as ::mcproto_types::ProtocolEnum>::discriminant(self),
215                )
216            }
217
218            fn from_repr(repr: Self::Repr) -> ::std::option::Option<Self> {
219                let value = <Self::Repr as ::mcproto_types::EnumRepr>::discriminant(&repr);
220                match value {
221                    #(value if value == Self::#variants as i128 => ::std::option::Option::Some(Self::#variants),)*
222                    _ => ::std::option::Option::None,
223                }
224            }
225        }
226
227        impl #impl_generics ::mcproto_types::TypeCodec for #name #type_generics #where_clause {
228            fn encode(
229                &self,
230                writer: &mut impl ::std::io::Write,
231            ) -> ::std::result::Result<(), ::mcproto_types::__private::CodecError> {
232                ::mcproto_types::__private::encode_protocol_enum(self, writer)
233            }
234
235            fn decode(
236                reader: &mut impl ::std::io::Read,
237            ) -> ::std::result::Result<Self, ::mcproto_types::__private::CodecError> {
238                ::mcproto_types::__private::decode_protocol_enum(reader)
239            }
240        }
241    })
242}
243
244fn enum_repr(input: &DeriveInput) -> Result<Type> {
245    let mut repr = None;
246
247    for attribute in &input.attrs {
248        if !attribute.path().is_ident("protocol_enum") {
249            continue;
250        }
251
252        attribute.parse_nested_meta(|meta| {
253            if !meta.path.is_ident("repr") {
254                return Err(meta.error("expected `repr = Type`"));
255            }
256            if repr.is_some() {
257                return Err(meta.error("duplicate `repr` argument"));
258            }
259
260            repr = Some(meta.value()?.parse()?);
261            Ok(())
262        })?;
263    }
264
265    repr.ok_or_else(|| {
266        Error::new_spanned(
267            input,
268            "ProtocolEnum requires `#[protocol_enum(repr = Type)]`",
269        )
270    })
271}