-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from rimecraft-rs/enum-edcode-macro
Edcode derive proc-macros for `enum`
- Loading branch information
Showing
6 changed files
with
280 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters