Skip to content

Commit

Permalink
Merge pull request #38 from rimecraft-rs/enum-edcode-macro
Browse files Browse the repository at this point in the history
Edcode derive proc-macros for `enum`
  • Loading branch information
JieningYu authored Mar 23, 2024
2 parents df3380c + 54b31f9 commit b4f67a2
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 0 deletions.
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)| {
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,
};
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;
}
};
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(),
}
}
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

0 comments on commit b4f67a2

Please sign in to comment.