use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{
Error, Expr, ExprLit, ItemStruct, Lit, LitStr, Meta, MetaList, MetaNameValue, Path, Result,
Token,
};
enum SupportedFormat {
Full(Path),
ReadOnly(Path),
}
impl SupportedFormat {
fn emit_doc_comment(&self) -> String {
match self {
SupportedFormat::Full(path) => format!(
"* [`FileType::{ft}`](crate::FileType::{ft})\n",
ft = path.get_ident().unwrap()
),
SupportedFormat::ReadOnly(path) => format!(
"* [`FileType::{ft}`](crate::FileType::{ft}) **(READ ONLY)**\n",
ft = path.get_ident().unwrap()
),
}
}
fn path(&self) -> &Path {
match self {
SupportedFormat::Full(path) | SupportedFormat::ReadOnly(path) => path,
}
}
fn read_only(&self) -> Option<&Path> {
match self {
SupportedFormat::ReadOnly(path) => Some(path),
_ => None,
}
}
}
pub(crate) struct LoftyTag {
attribute: LoftyTagAttribute,
input: ItemStruct,
}
impl LoftyTag {
pub(crate) fn new(attribute: LoftyTagAttribute, input: ItemStruct) -> Self {
LoftyTag { attribute, input }
}
pub(crate) fn emit(&self) -> TokenStream {
let ident = &self.input.ident;
let desc = &self.attribute.description;
let supported_types_iter = self
.attribute
.supported_formats
.iter()
.map(SupportedFormat::emit_doc_comment);
let flattened_file_types = self
.attribute
.supported_formats
.iter()
.map(SupportedFormat::path);
let read_only_file_types = self
.attribute
.supported_formats
.iter()
.filter_map(SupportedFormat::read_only);
let input = &self.input;
TokenStream::from(quote! {
use ::lofty::_this_is_internal;
#[doc = #desc]
#[doc = "\n"]
#[doc = "## Supported file types\n\n"]
#( #[doc = #supported_types_iter] )*
#[doc = "\n"]
#input
impl #ident {
pub(crate) const SUPPORTED_FORMATS: &'static [::lofty::file::FileType] = &[
#( ::lofty::file::FileType:: #flattened_file_types ),*
];
pub(crate) const READ_ONLY_FORMATS: &'static [::lofty::file::FileType] = &[
#( ::lofty::file::FileType:: #read_only_file_types ),*
];
}
})
}
}
pub(crate) struct LoftyTagAttribute {
description: LitStr,
supported_formats: Vec<SupportedFormat>,
}
impl Parse for LoftyTagAttribute {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut description = None;
let mut supported_formats = Vec::new();
let start_span = input.span();
let args = Punctuated::<Meta, Token![,]>::parse_separated_nonempty(input)?;
for nested_meta in args {
match nested_meta {
Meta::NameValue(mnv) if mnv.path.is_ident("description") => {
if description.is_some() {
return Err(Error::new(mnv.span(), "Duplicate `description` entry"));
}
description = Some(parse_description(mnv)?);
},
Meta::List(list) if list.path.is_ident("supported_formats") => {
parse_supported_formats(list, &mut supported_formats)?;
},
_ => {
return Err(Error::new(
nested_meta.span(),
"Unexpected input, check the format of the arguments",
))
},
}
}
if description.is_none() {
return Err(Error::new(start_span, "No description provided"));
}
Ok(Self {
description: description.unwrap(),
supported_formats,
})
}
}
fn parse_description(name_value: MetaNameValue) -> Result<LitStr> {
match name_value.value {
Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
}) => Ok(lit_str),
_ => Err(Error::new(
name_value.span(),
"Invalid `description` entry, expected string value",
)),
}
}
fn parse_supported_formats(
meta_list: MetaList,
supported_formats: &mut Vec<SupportedFormat>,
) -> Result<()> {
let mut read_only_encountered = false;
meta_list.parse_nested_meta(|meta| {
if meta.path.is_ident("read_only") {
if read_only_encountered {
return Err(meta.error("Duplicate `read_only` entry"));
}
read_only_encountered = true;
meta.parse_nested_meta(|nested_meta| {
supported_formats.push(SupportedFormat::ReadOnly(nested_meta.path));
Ok(())
})?;
} else {
supported_formats.push(SupportedFormat::Full(meta.path));
}
Ok(())
})
}