Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edcode derive proc-macros for enum #38

Merged
merged 13 commits into from
Mar 23, 2024
18 changes: 18 additions & 0 deletions crates/util/edcode-derive-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "rimecraft-edcode-derive-tests"
version = "0.1.0"
edition = "2021"
authors = ["C191239 <[email protected]>"]
description = "Tests for rimecraft-edcode-derive"
repository = "https://github.com/rimecraft-rs/rimecraft/"
license = "AGPL-3.0-or-later"
categories = ["encoding"]

[badges]
maintenance = { status = "passively-maintained" }

[dev-dependencies]
rimecraft-edcode = { path = "../edcode", features = ["derive"] }

[lints]
workspace = true
23 changes: 23 additions & 0 deletions crates/util/edcode-derive-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Tests for `rimecraft-edcode-derive` crate.

#[cfg(test)]
mod tests {
use rimecraft_edcode::{bytes::BytesMut, Decode, Encode};

#[test]
#[allow(dead_code)]
fn derive_enum() {
#[derive(Encode, Decode, PartialEq, Eq)]
#[repr(u8)]
enum Topics {
Pearl = 15,
Lakers = 24,
Kim = 3,
Someone = 36,
}

let mut buf = BytesMut::new();
assert!(Topics::Someone.encode(&mut buf).is_ok());
assert!(Topics::decode(buf).is_ok_and(|x| x == Topics::Someone));
}
}
25 changes: 25 additions & 0 deletions crates/util/edcode-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "rimecraft-edcode-derive"
version = "0.1.0"
edition = "2021"
authors = ["C191239 <[email protected]>"]
description = "Derive macros for rimecraft-edcode"
repository = "https://github.com/rimecraft-rs/rimecraft/"
license = "AGPL-3.0-or-later"
categories = ["encoding"]

[badges]
maintenance = { status = "passively-maintained" }

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"

[lints]
workspace = true

[lib]
proc-macro = true
209 changes: 209 additions & 0 deletions crates/util/edcode-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//! Proc-macros for deriving `rimecraft_edcode` traits.
//!
//! __You shouldn't use this crate directly__, use `rimecraft_edcode` crate
//! with `derive` feature flag instead.

use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::quote;
use syn::{
parse_macro_input, spanned::Spanned, Attribute, Data, DataEnum, DeriveInput, Error, Expr,
Fields, Ident, Meta,
};

macro_rules! unsupported_object {
($tr:literal, $ty:literal) => {
concat!("deriving `", $tr, "` to `", $ty, "` is not supported")
};
}

macro_rules! fields_disallowed {
() => {
"variants with fields are not supported"
};
}

macro_rules! discriminant_required {
() => {
"variants must have explicit discriminant"
};
}

macro_rules! unsupported_repr {
() => {
"only `u*` and `i*` (excluding 128-bit types) repr is supported"
};
}

macro_rules! repr_required {
() => {
"must specify repr"
};
}

/// Common parsing code for deriving to `enum`.
fn parse_derive_enum(
ident: Ident,
attrs: Vec<Attribute>,
data: DataEnum,
) -> Result<(Ident, Ident, Vec<Ident>, Vec<Expr>), TokenStream> {
let mut enum_idents: Vec<Ident> = vec![];
let mut enum_vals: Vec<Expr> = vec![];
for var in data.variants.into_iter() {
if matches!(var.fields, Fields::Unit) {
enum_idents.push(var.ident.clone());
} else {
return Err(Error::new(var.fields.span(), fields_disallowed!())
.into_compile_error()
.into());
}
let has_disc_err = !var.discriminant.clone().is_some_and(|(_, e)| {
JieningYu marked this conversation as resolved.
Show resolved Hide resolved
enum_vals.push(e);
true
});
if has_disc_err {
return Err(Error::new(var.span(), discriminant_required!())
.into_compile_error()
.into());
}
}
let mut repr_type: Option<Ident> = None;
for attr in attrs {
if let Meta::List(meta) = attr.meta {
let is_repr = meta
.path
.require_ident()
.is_ok_and(|id| id == &Ident::new("repr", id.span()));
if is_repr {
let mut iter = meta.tokens.into_iter().peekable();
let span = iter.peek().span();
macro_rules! ident_helper {
($span:expr => $( $ty:ident ),*) => {
vec![
$( Ident::new(stringify!($ty), $span) ),*
]
};
}
let supported = iter.next().is_some_and(|x| {
if let TokenTree::Ident(id) = x {
if ident_helper!(span => u8, u16, u32, u64, i8, i16, i32, i64).contains(&id)
{
repr_type = Some(id);
true
} else {
false
}
} else {
false
}
});
if !supported {
return Err(Error::new(span, unsupported_repr!())
.into_compile_error()
.into());
}
}
}
}
let repr_type = repr_type.ok_or_else(|| {
std::convert::Into::<TokenStream>::into(
Error::new(ident.span(), repr_required!()).into_compile_error(),
)
})?;
Ok((ident, repr_type, enum_idents, enum_vals))
}

/// Derive `rimecraft_edcode::Encode` to objects.
///
/// # Enum
///
/// ## Requirements:
/// - All variants must be field-less.
/// - Enum must explicitly specify its representation through `#[repr()]`, and
/// only `u*` and `i*` (excluding 128-bit types) repr are allowed.
/// - All variants must have explicit discriminant.
#[proc_macro_derive(Encode)]
pub fn derive_encode(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match input.data {
Data::Enum(data) => {
let (ident, repr_type, enum_idents, enum_vals) =
match parse_derive_enum(input.ident, input.attrs, data) {
Ok(x) => x,
Err(err) => return err,
};
JieningYu marked this conversation as resolved.
Show resolved Hide resolved
let expanded = quote! {
impl ::rimecraft_edcode::Encode for #ident {
fn encode<B>(&self, mut buf: B) -> Result<(), std::io::Error>
where
B: ::rimecraft_edcode::bytes::BufMut,
{
let x:#repr_type = match self {
#( Self::#enum_idents => #enum_vals, )*
};
::rimecraft_edcode::Encode::encode(&x, &mut buf)?;
Ok(())
}
}
};
expanded.into()
}
Data::Struct(data) => Error::new(
data.struct_token.span,
unsupported_object!("Encode", "struct"),
)
.to_compile_error()
.into(),
Data::Union(data) => Error::new(
data.union_token.span,
unsupported_object!("Encode", "union"),
)
.to_compile_error()
.into(),
}
}

/// Derive `rimecraft_edcode::Decode` to objects.
#[proc_macro_derive(Decode)]
pub fn derive_decode(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match input.data {
Data::Enum(data) => {
let (ident, repr_type, enum_idents, enum_vals) =
match parse_derive_enum(input.ident, input.attrs, data) {
Ok(x) => x,
Err(err) => {
return err;
}
};
JieningYu marked this conversation as resolved.
Show resolved Hide resolved
let expanded = quote! {
impl ::rimecraft_edcode::Decode for #ident {
fn decode<B>(mut buf: B) -> Result<Self, std::io::Error>
where
B: ::rimecraft_edcode::bytes::Buf,
{
let x:#repr_type = ::rimecraft_edcode::Decode::decode(&mut buf)?;
let var = match x {
#( #enum_vals => Self::#enum_idents, )*
unknown => return Err(std::io::Error::other(format!("unknown variant {}", unknown))),
};
Ok(var)
}
}
};
expanded.into()
}
Data::Struct(data) => Error::new(
data.struct_token.span,
unsupported_object!("Decode", "struct"),
)
.to_compile_error()
.into(),
Data::Union(data) => Error::new(
data.union_token.span,
unsupported_object!("Decode", "union"),
)
.into_compile_error()
.into(),
JieningYu marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 2 additions & 0 deletions crates/util/edcode/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ maintenance = { status = "passively-maintained" }
[dependencies]
bytes = "1.5"
serde = { version = "1.0", optional = true }
rimecraft-edcode-derive = { path = "../edcode-derive", optional = true }
# custom formats
fastnbt = { version = "2.4", optional = true }
serde_json = { version = "1.0", optional = true }
Expand All @@ -24,6 +25,7 @@ glam = { version = "0.25", optional = true }
[features]
# default = ["serde", "nbt", "json", "uuid", "glam"]
serde = ["dep:serde"]
derive = ["dep:rimecraft-edcode-derive"]
# custom formats
fastnbt = ["serde", "dep:fastnbt"]
json = ["serde", "dep:serde_json"]
Expand Down
3 changes: 3 additions & 0 deletions crates/util/edcode/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use std::io;

pub use bytes;

#[cfg(feature = "derive")]
pub use rimecraft_edcode_derive::{Decode, Encode};

/// Describes types that can be encoded into a packet buffer.
pub trait Encode {
/// Encode into a buffer.
Expand Down