feat: Add support for externally tagged enums
Also did a massive code refactor that helped simplify the implementation.
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
#![feature(iterator_try_collect)]
|
||||
#![feature(if_let_guard)]
|
||||
use std::ops::Deref;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{DeriveInput, LitStr, Token, parse_macro_input, spanned::Spanned};
|
||||
use syn::{DataEnum, DataStruct, DeriveInput, LitStr, Token, parse_macro_input, spanned::Spanned};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StructField {
|
||||
name: syn::Ident,
|
||||
ty: syn::Type,
|
||||
case: Option<Case<'static>>,
|
||||
default: bool,
|
||||
optional: bool,
|
||||
flatten: bool,
|
||||
}
|
||||
|
||||
@@ -29,9 +31,9 @@ impl ToTokens for StructField {
|
||||
<#ty as Typed>::generate_members().unwrap_or("".to_string())
|
||||
});
|
||||
} else {
|
||||
let default = if self.default { "?" } else { "" };
|
||||
let optional = if self.optional { "?" } else { "" };
|
||||
|
||||
let format = format!("---@field {} {{}}{}\n", name, default);
|
||||
let format = format!("---@field {} {{}}{}\n", name, optional);
|
||||
|
||||
tokens.extend(quote! {
|
||||
format!(#format, <#ty as Typed>::type_name())
|
||||
@@ -68,6 +70,28 @@ impl ToTokens for Struct {
|
||||
}
|
||||
}
|
||||
|
||||
impl Struct {
|
||||
fn from_data(
|
||||
data: DataStruct,
|
||||
name: syn::Ident,
|
||||
case: Option<convert_case::Case<'static>>,
|
||||
) -> syn::Result<Self> {
|
||||
if data.fields.iter().any(|field| field.ident.is_none()) {
|
||||
return Err(syn::Error::new(
|
||||
data.fields.span(),
|
||||
"Tuple structs are not supported by Typed",
|
||||
));
|
||||
}
|
||||
|
||||
let fields = parse_fields(data.fields, case)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_owned(),
|
||||
fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct BasicEnumVariant {
|
||||
name: syn::Ident,
|
||||
case: Option<Case<'static>>,
|
||||
@@ -117,14 +141,196 @@ impl ToTokens for BasicEnum {
|
||||
}
|
||||
}
|
||||
|
||||
impl BasicEnum {
|
||||
fn from_data(
|
||||
data: DataEnum,
|
||||
name: syn::Ident,
|
||||
case: Option<convert_case::Case<'static>>,
|
||||
) -> syn::Result<Self> {
|
||||
if let Some(err) = data
|
||||
.variants
|
||||
.iter()
|
||||
.filter(|variant| !variant.fields.is_empty())
|
||||
.map(|variant| {
|
||||
syn::Error::new(
|
||||
variant.fields.span(),
|
||||
"Typed supports enum variants with fields only when the enum is externally tagged",
|
||||
)
|
||||
})
|
||||
.reduce(|mut acc, e| {
|
||||
acc.combine(e);
|
||||
acc
|
||||
})
|
||||
{
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_owned(),
|
||||
variants: data
|
||||
.variants
|
||||
.into_iter()
|
||||
.map(|variant| BasicEnumVariant {
|
||||
name: variant.ident,
|
||||
case,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ExtTaggedEnumVariant {
|
||||
name: syn::Ident,
|
||||
fields: Vec<StructField>,
|
||||
case: Option<Case<'static>>,
|
||||
}
|
||||
|
||||
impl ToTokens for ExtTaggedEnumVariant {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let fields = &self.fields;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#(
|
||||
output += &#fields;
|
||||
)*
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExtTaggedEnumVariant> for BasicEnumVariant {
|
||||
fn from(value: ExtTaggedEnumVariant) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
case: value.case,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtTaggedEnum {
|
||||
name: syn::Ident,
|
||||
tag: LitStr,
|
||||
variants: Vec<ExtTaggedEnumVariant>,
|
||||
}
|
||||
|
||||
impl ToTokens for ExtTaggedEnum {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = &self.name;
|
||||
let tag_variants: Vec<BasicEnumVariant> =
|
||||
self.variants.iter().cloned().map(Into::into).collect();
|
||||
let variants = &self.variants;
|
||||
|
||||
let tag = format!("---@field {}\n", self.tag.value());
|
||||
|
||||
tokens.extend(quote! {
|
||||
fn generate_header() -> Option<String> {
|
||||
Some(format!("---@class {}\n", <#name as Typed>::type_name()))
|
||||
}
|
||||
|
||||
fn generate_members() -> Option<String> {
|
||||
let mut output = String::new();
|
||||
|
||||
output += #tag;
|
||||
#(
|
||||
output += &#tag_variants;
|
||||
)*
|
||||
|
||||
#(
|
||||
#variants
|
||||
)*
|
||||
|
||||
Some(output)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtTaggedEnum {
|
||||
fn from_data(
|
||||
data: DataEnum,
|
||||
name: syn::Ident,
|
||||
tag: LitStr,
|
||||
case: Option<convert_case::Case<'static>>,
|
||||
) -> syn::Result<Self> {
|
||||
Ok(Self {
|
||||
name: name.to_owned(),
|
||||
tag,
|
||||
variants: data
|
||||
.variants
|
||||
.into_iter()
|
||||
.map(|variant| -> syn::Result<_> {
|
||||
let mut fields = parse_fields(variant.fields, case)?;
|
||||
|
||||
// Force each field to be optional as they might not be used depending on the
|
||||
// variant selected
|
||||
fields.iter_mut().for_each(|field| field.optional = true);
|
||||
|
||||
Ok(ExtTaggedEnumVariant {
|
||||
name: variant.ident,
|
||||
case,
|
||||
fields,
|
||||
})
|
||||
})
|
||||
.try_collect()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_fields(
|
||||
input: syn::Fields,
|
||||
case: Option<convert_case::Case<'static>>,
|
||||
) -> syn::Result<Vec<StructField>> {
|
||||
let mut fields = Vec::new();
|
||||
for field in input {
|
||||
let mut default = false;
|
||||
let mut flatten = false;
|
||||
for attr in &field.attrs {
|
||||
if attr.path().is_ident("serde")
|
||||
&& let Err(err) = attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("default") {
|
||||
default = true;
|
||||
if meta.input.peek(Token![=]) {
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
meta.input.parse::<LitStr>()?;
|
||||
}
|
||||
} else if meta.path.is_ident("flatten") {
|
||||
flatten = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
{
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
fields.push(StructField {
|
||||
name: field.ident.expect("We already checked that ident is some"),
|
||||
ty: field.ty,
|
||||
case,
|
||||
optional: default,
|
||||
flatten,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(fields)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Typed, attributes(serde))]
|
||||
pub fn typed(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let name = &ast.ident;
|
||||
match typed_inner(ast) {
|
||||
Ok(ts) => ts.into(),
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn typed_inner(ast: DeriveInput) -> syn::Result<TokenStream2> {
|
||||
let name = ast.ident;
|
||||
|
||||
let mut case = None;
|
||||
|
||||
let mut tag = None;
|
||||
for attr in &ast.attrs {
|
||||
if attr.path().is_ident("serde")
|
||||
&& let Err(err) = attr.parse_nested_meta(|meta| {
|
||||
@@ -148,10 +354,15 @@ pub fn typed(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
}?);
|
||||
}
|
||||
|
||||
if meta.path.is_ident("tag") {
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
tag = Some(meta.input.parse::<LitStr>()?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
{
|
||||
return err.into_compile_error().into();
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,103 +374,29 @@ pub fn typed(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
|
||||
let test: TokenStream2 = match ast.data {
|
||||
syn::Data::Struct(data_struct) => {
|
||||
if data_struct.fields.iter().any(|field| field.ident.is_none()) {
|
||||
return syn::Error::new(
|
||||
data_struct.fields.span(),
|
||||
"Tuple structs are not supported by Typed",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
|
||||
let mut fields = Vec::new();
|
||||
for field in data_struct.fields {
|
||||
let mut default = false;
|
||||
let mut flatten = false;
|
||||
for attr in &field.attrs {
|
||||
if attr.path().is_ident("serde")
|
||||
&& let Err(err) = attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("default") {
|
||||
default = true;
|
||||
if meta.input.peek(Token![=]) {
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
meta.input.parse::<LitStr>()?;
|
||||
}
|
||||
} else if meta.path.is_ident("flatten") {
|
||||
flatten = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
{
|
||||
return err.into_compile_error().into();
|
||||
}
|
||||
}
|
||||
|
||||
fields.push(StructField {
|
||||
name: field.ident.expect("We already checked that ident is some"),
|
||||
ty: field.ty,
|
||||
case,
|
||||
default,
|
||||
flatten,
|
||||
});
|
||||
}
|
||||
|
||||
Struct {
|
||||
name: name.to_owned(),
|
||||
fields,
|
||||
}
|
||||
.into_token_stream()
|
||||
Struct::from_data(data_struct, name.clone(), case)?.into_token_stream()
|
||||
}
|
||||
syn::Data::Enum(data_enum) if let Some(tag) = tag => {
|
||||
ExtTaggedEnum::from_data(data_enum, name.clone(), tag, case)?.into_token_stream()
|
||||
}
|
||||
syn::Data::Enum(data_enum) => {
|
||||
let errors: TokenStream2 = data_enum
|
||||
.variants
|
||||
.iter()
|
||||
.filter(|variant| !variant.fields.is_empty())
|
||||
.map(|variant| {
|
||||
syn::Error::new(
|
||||
variant.fields.span(),
|
||||
"Only basic enums are supported by Typed",
|
||||
)
|
||||
.into_compile_error()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !errors.is_empty() {
|
||||
return errors.into();
|
||||
}
|
||||
|
||||
BasicEnum {
|
||||
name: name.to_owned(),
|
||||
variants: data_enum
|
||||
.variants
|
||||
.into_iter()
|
||||
.map(|variant| BasicEnumVariant {
|
||||
name: variant.ident,
|
||||
case,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
.into_token_stream()
|
||||
BasicEnum::from_data(data_enum, name.clone(), case)?.into_token_stream()
|
||||
}
|
||||
syn::Data::Union(data_union) => {
|
||||
return syn::Error::new(
|
||||
return Err(syn::Error::new(
|
||||
data_union.union_token.span,
|
||||
"Unions are not supported by Typed",
|
||||
)
|
||||
.into_compile_error()
|
||||
.into();
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
Ok(quote! {
|
||||
impl ::lua_typed::Typed for #name {
|
||||
#type_name_fn
|
||||
|
||||
#test
|
||||
}
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lua_typed::Typed;
|
||||
|
||||
#[derive(Typed)]
|
||||
#[serde(rename_all = "snake_case", tag = "action")]
|
||||
pub enum ActionType {
|
||||
Broadcast { extras: HashMap<String, String> },
|
||||
// View,
|
||||
// Http
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_type() {
|
||||
insta::assert_snapshot!(<ActionType as Typed>::generate_full().unwrap(), @r#"
|
||||
---@class ActionType
|
||||
---@field action
|
||||
---| "broadcast"
|
||||
---@field extras table<string, string>?
|
||||
"#);
|
||||
}
|
||||
|
||||
#[derive(Typed)]
|
||||
#[repr(u8)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -25,22 +45,23 @@ fn priority() {
|
||||
|
||||
#[derive(Typed)]
|
||||
pub struct Action {
|
||||
// #[serde(flatten)]
|
||||
// pub action: ActionType,
|
||||
#[serde(flatten)]
|
||||
pub action: ActionType,
|
||||
pub label: String,
|
||||
pub clear: Option<bool>,
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn action() {
|
||||
// insta::assert_snapshot!(<Action as Typed>::generate_full().unwrap(), @r#"
|
||||
// ---@class Action
|
||||
// ---@field action "broadcast"
|
||||
// ---@field extras table<string, string>?
|
||||
// ---@field label string?
|
||||
// ---@clear clear bool?
|
||||
// "#);
|
||||
// }
|
||||
#[test]
|
||||
fn action() {
|
||||
insta::assert_snapshot!(<Action as Typed>::generate_full().unwrap(), @r#"
|
||||
---@class Action
|
||||
---@field action
|
||||
---| "broadcast"
|
||||
---@field extras table<string, string>?
|
||||
---@field label string
|
||||
---@field clear boolean?
|
||||
"#);
|
||||
}
|
||||
|
||||
#[derive(Typed)]
|
||||
pub struct Notification {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
error: Only basic enums are supported by Typed
|
||||
error: Typed supports enum variants with fields only when the enum is externally tagged
|
||||
--> tests/ui/complex_struct.rs:6:6
|
||||
|
|
||||
6 | B(String),
|
||||
| ^^^^^^^^
|
||||
|
||||
error: Only basic enums are supported by Typed
|
||||
error: Typed supports enum variants with fields only when the enum is externally tagged
|
||||
--> tests/ui/complex_struct.rs:7:6
|
||||
|
|
||||
7 | C(u8),
|
||||
| ^^^^
|
||||
|
||||
error: Only basic enums are supported by Typed
|
||||
error: Typed supports enum variants with fields only when the enum is externally tagged
|
||||
--> tests/ui/complex_struct.rs:8:7
|
||||
|
|
||||
8 | D { test: f32 },
|
||||
|
||||
9
tests/ui/tuple_enum_untagged.rs
Normal file
9
tests/ui/tuple_enum_untagged.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use lua_typed::Typed;
|
||||
|
||||
#[derive(Typed)]
|
||||
enum Test {
|
||||
A,
|
||||
D { test: f32 },
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
5
tests/ui/tuple_enum_untagged.stderr
Normal file
5
tests/ui/tuple_enum_untagged.stderr
Normal file
@@ -0,0 +1,5 @@
|
||||
error: Typed supports enum variants with fields only when the enum is externally tagged
|
||||
--> tests/ui/tuple_enum_untagged.rs:6:7
|
||||
|
|
||||
6 | D { test: f32 },
|
||||
| ^^^^^^^^^^^^^
|
||||
Reference in New Issue
Block a user