specctra, specctra_derive: rewrite logic inspecting macro attributes

This allows parsing more complicated attribute syntax, here needed to
specify multiple names for a `Vec` field.
This commit is contained in:
Tomasz Cichoń 2024-10-02 11:09:50 +02:00
parent 9159312ea5
commit 336cea11e8
5 changed files with 109 additions and 65 deletions

View File

@ -1,5 +1,6 @@
use proc_macro::TokenStream;
use syn::{Attribute, DeriveInput, LitStr};
use syn::{Attribute, DeriveInput, LitStr, Meta, Token};
use syn::punctuated::Punctuated;
mod read;
mod write;
@ -16,16 +17,37 @@ pub fn derive_write(input: TokenStream) -> TokenStream {
write::impl_write(&input).into()
}
fn attr_present(attrs: &Vec<Attribute>, name: &str) -> bool {
attrs
.iter()
.find(|attr| attr.path().is_ident(name))
.is_some()
enum FieldType {
Anonymous,
AnonymousVec,
NamedVec(Vec<LitStr>),
NotSpecified,
}
fn attr_content(attrs: &Vec<Attribute>, name: &str) -> Option<String> {
attrs
fn parse_attributes(attrs: &Vec<Attribute>) -> FieldType {
for attr in attrs {
match &attr.meta {
Meta::Path(path) => {
if path.is_ident("anon") {
return FieldType::Anonymous;
} else if path.is_ident("anon_vec") {
return FieldType::AnonymousVec;
}
},
Meta::List(list) if list.path.is_ident("vec") => {
return FieldType::NamedVec(list
.parse_args_with(
Punctuated::<LitStr, Token![,]>::parse_terminated
)
.expect("#[vec(...)] must contain a list of string literals")
.iter()
.find(|attr| attr.path().is_ident(name))
.and_then(|attr| Some(attr.parse_args::<LitStr>().expect("string literal").value()))
.cloned()
.collect()
);
},
_ => (),
}
}
FieldType::NotSpecified
}

View File

@ -4,8 +4,8 @@ use syn::ext::IdentExt;
use syn::Type::Path;
use syn::{Data, DeriveInput, Field, Fields};
use crate::attr_content;
use crate::attr_present;
use crate::parse_attributes;
use crate::FieldType;
pub fn impl_read(input: &DeriveInput) -> TokenStream {
let name = &input.ident;
@ -46,27 +46,33 @@ fn impl_body(data: &Data) -> TokenStream {
fn impl_field(field: &Field) -> TokenStream {
let name = &field.ident;
let name_str = name.as_ref().expect("field name").unraw();
let field_type = parse_attributes(&field.attrs);
if attr_present(&field.attrs, "anon") {
match field_type {
FieldType::Anonymous => {
quote! {
#name: tokenizer.read_value()?,
}
} else if let Some(dsn_name) = attr_content(&field.attrs, "vec") {
quote! {
#name: tokenizer.read_named_array(#dsn_name)?,
}
} else if attr_present(&field.attrs, "anon_vec") {
},
FieldType::AnonymousVec => {
quote! {
#name: tokenizer.read_array()?,
}
} else {
},
FieldType::NamedVec(valid_aliases) => {
quote! {
#name: tokenizer.read_array_with_alias(&[#(#valid_aliases),*])?,
}
},
FieldType::NotSpecified => {
if let Path(type_path) = &field.ty {
let segments = &type_path.path.segments;
if segments.len() == 1 {
let ident = &segments.first().unwrap().ident;
if ident == "Option" {
return quote! {
#name: tokenizer.read_optional(stringify!(#name_str))?,
#name: tokenizer
.read_optional(stringify!(#name_str))?,
};
}
}
@ -76,4 +82,5 @@ fn impl_field(field: &Field) -> TokenStream {
#name: tokenizer.read_named(stringify!(#name_str))?,
}
}
}
}

View File

@ -4,8 +4,8 @@ use syn::ext::IdentExt;
use syn::Type::Path;
use syn::{Data, DeriveInput, Field, Fields};
use crate::attr_content;
use crate::attr_present;
use crate::parse_attributes;
use crate::FieldType;
pub fn impl_write(input: &DeriveInput) -> TokenStream {
let name = &input.ident;
@ -44,27 +44,34 @@ fn impl_body(data: &Data) -> TokenStream {
fn impl_field(field: &Field) -> TokenStream {
let name = &field.ident;
let name_str = name.as_ref().expect("field name").unraw();
let field_type = parse_attributes(&field.attrs);
if attr_present(&field.attrs, "anon") {
match field_type {
FieldType::Anonymous => {
quote! {
writer.write_value(&self.#name)?;
}
} else if let Some(dsn_name) = attr_content(&field.attrs, "vec") {
quote! {
writer.write_named_array(#dsn_name, &self.#name)?;
}
} else if attr_present(&field.attrs, "anon_vec") {
},
FieldType::AnonymousVec => {
quote! {
writer.write_array(&self.#name)?;
}
} else {
},
FieldType::NamedVec(valid_aliases) => {
let canonical_name = &valid_aliases[0];
quote! {
writer.write_named_array(#canonical_name, &self.#name)?;
}
},
FieldType::NotSpecified => {
if let Path(type_path) = &field.ty {
let segments = &type_path.path.segments;
if segments.len() == 1 {
let ident = &segments.first().unwrap().ident;
if ident == "Option" {
return quote! {
writer.write_optional(stringify!(#name_str), &self.#name)?;
writer
.write_optional(stringify!(#name_str), &self.#name)?;
};
}
}
@ -74,4 +81,5 @@ fn impl_field(field: &Field) -> TokenStream {
writer.write_named(stringify!(#name_str), &self.#name)?;
}
}
}
}

View File

@ -387,6 +387,13 @@ impl<R: std::io::BufRead> ListTokenizer<R> {
pub fn read_named_array<T: ReadDsn<R>>(
&mut self,
name: &'static str,
) -> Result<Vec<T>, ParseErrorContext> {
self.read_array_with_alias(&[name])
}
pub fn read_array_with_alias<T: ReadDsn<R>>(
&mut self,
valid_names: &[&'static str],
) -> Result<Vec<T>, ParseErrorContext> {
let mut array = Vec::<T>::new();
loop {
@ -395,7 +402,7 @@ impl<R: std::io::BufRead> ListTokenizer<R> {
name: ref actual_name,
} = input.token
{
if actual_name == name {
if valid_names.contains(&actual_name.to_ascii_lowercase().as_ref()) {
let value = self.read_value::<T>()?;
self.consume_token()?.expect_end()?;
array.push(value);

View File

@ -159,7 +159,7 @@ pub struct Grid {
#[derive(ReadDsn, WriteSes, Debug)]
pub struct StructureRule {
pub width: Option<f32>,
#[vec("clearance")]
#[vec("clearance", "clear")]
pub clearances: Vec<Clearance>,
}