lofty_attr-0.11.0/.cargo_vcs_info.json0000644000000001500000000000100132760ustar { "git": { "sha1": "714366ba3327593e3482201ca62562be308df715" }, "path_in_vcs": "lofty_attr" }lofty_attr-0.11.0/Cargo.toml0000644000000046630000000000100113110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "lofty_attr" version = "0.11.0" authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] build = false include = [ "src", "Cargo.toml", "../LICENSE-*", ] autobins = false autoexamples = false autotests = false autobenches = false description = "Macros for Lofty" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/Serial-ATA/lofty-rs" [lib] name = "lofty_attr" path = "src/lib.rs" proc-macro = true [dependencies.proc-macro2] version = "1.0.64" [dependencies.quote] version = "1.0.29" [dependencies.syn] version = "2.0.25" features = [ "full", "parsing", ] [lints.clippy] bool_to_int_with_if = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" dbg_macro = "forbid" doc_markdown = "allow" field_reassign_with_default = "allow" from_over_into = "allow" ignored_unit_patterns = "allow" into_iter_without_iter = "allow" len_without_is_empty = "allow" let_underscore_untyped = "allow" manual_range_patterns = "allow" match_wildcard_for_single_variants = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" needless_late_init = "allow" needless_return = "allow" no_effect_underscore_binding = "allow" redundant_guards = "allow" return_self_not_must_use = "allow" semicolon_if_nothing_returned = "allow" similar_names = "allow" single_match_else = "allow" string_to_string = "forbid" tabs_in_doc_comments = "allow" too_many_lines = "allow" type_complexity = "allow" uninlined_format_args = "allow" upper_case_acronyms = "allow" used_underscore_binding = "allow" [lints.clippy.all] level = "deny" priority = -1 [lints.clippy.pedantic] level = "deny" priority = -1 [lints.rust] explicit_outlives_requirements = "deny" missing_docs = "deny" rust_2018_idioms = "deny" trivial_casts = "deny" trivial_numeric_casts = "deny" unknown_lints = "allow" unused_import_braces = "deny" [lints.rustdoc] broken_intra_doc_links = "deny" lofty_attr-0.11.0/Cargo.toml.orig000064400000000000000000000007501046102023000147630ustar 00000000000000[package] name = "lofty_attr" version = "0.11.0" authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] edition = "2021" license = "MIT OR Apache-2.0" description = "Macros for Lofty" repository = "https://github.com/Serial-ATA/lofty-rs" readme = "README.md" include = ["src", "Cargo.toml", "../LICENSE-*"] [dependencies] syn = { version = "2.0.25", features = ["full", "parsing"] } quote = "1.0.29" proc-macro2 = "1.0.64" [lints] workspace = true [lib] proc-macro = true lofty_attr-0.11.0/README.md000064400000000000000000000001071046102023000133470ustar 00000000000000This crate provides macros for [Lofty](https://crates.io/crates/lofty).lofty_attr-0.11.0/src/attribute.rs000064400000000000000000000030151046102023000152310ustar 00000000000000use syn::parse::Parse; use syn::punctuated::Punctuated; use syn::{token, Attribute, Expr, Ident, LitStr, Token}; pub(crate) enum AttributeValue { /// `#[lofty(attribute_name)]` Path(Ident), /// `#[lofty(attribute_name = "value")]` NameValue(Ident, LitStr), #[allow(dead_code)] /// `#[lofty(attribute_name(value1, value2, value3))]` SingleList(Ident, Punctuated), } impl AttributeValue { pub(crate) fn from_attribute( expected_path: &str, attribute: &Attribute, ) -> syn::Result> { if !attribute.path().is_ident(expected_path) { return Ok(None); } let mut value = None; attribute.parse_nested_meta(|meta| { // `#[lofty(attribute_name)]` if meta.input.is_empty() { value = Some(AttributeValue::Path(meta.path.get_ident().unwrap().clone())); return Ok(()); } // `#[lofty(attribute_name = "value")]` if meta.input.peek(token::Eq) { let val = meta.value()?; let str_value: LitStr = val.parse()?; value = Some(AttributeValue::NameValue( meta.path.get_ident().unwrap().clone(), str_value, )); return Ok(()); } // `#[lofty(attribute_name(value1, value2, value3))]` if meta.input.peek(token::Paren) { return meta.parse_nested_meta(|meta| { let list = meta.input.parse_terminated(syn::Expr::parse, Token![,])?; value = Some(AttributeValue::SingleList( meta.path.get_ident().unwrap().clone(), list, )); Ok(()) }); } Err(meta.error("Unrecognized attribute format")) })?; Ok(value) } } lofty_attr-0.11.0/src/internal.rs000064400000000000000000000064751046102023000150570ustar 00000000000000// Items that only pertain to internal usage of lofty_attr use crate::lofty_file::FieldContents; use std::collections::HashMap; use quote::quote; pub(crate) fn opt_internal_file_type( struct_name: String, ) -> Option<(proc_macro2::TokenStream, bool)> { const LOFTY_FILE_TYPES: [&str; 12] = [ "Aac", "Aiff", "Ape", "Flac", "Mpeg", "Mp4", "Mpc", "Opus", "Vorbis", "Speex", "Wav", "WavPack", ]; const ID3V2_STRIPPABLE: [&str; 2] = ["Flac", "Ape"]; let stripped = struct_name.strip_suffix("File"); if let Some(prefix) = stripped { if let Some(pos) = LOFTY_FILE_TYPES .iter() .position(|p| p.eq_ignore_ascii_case(prefix)) { let file_ty = LOFTY_FILE_TYPES[pos]; let tt = file_ty.parse::().unwrap(); return Some((tt, ID3V2_STRIPPABLE.contains(&file_ty))); } } None } pub(crate) fn init_write_lookup( id3v2_strippable: bool, ) -> HashMap<&'static str, proc_macro2::TokenStream> { let mut map = HashMap::new(); macro_rules! insert { ($map:ident, $key:path, $val:block) => { $map.insert(stringify!($key), quote! { $val }) }; } insert!(map, Ape, { lofty::ape::tag::ApeTagRef { read_only: false, items: lofty::ape::tag::tagitems_into_ape(tag), } .write_to(file, write_options) }); insert!(map, Id3v1, { Into::>::into(tag).write_to(file, write_options) }); if id3v2_strippable { insert!(map, Id3v2, { lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(file, write_options) }); } else { insert!(map, Id3v2, { lofty::id3::v2::tag::Id3v2TagRef { flags: lofty::id3::v2::Id3v2TagFlags::default(), frames: lofty::id3::v2::tag::tag_frames(tag).peekable(), } .write_to(file, write_options) }); } insert!(map, RiffInfo, { lofty::iff::wav::tag::RIFFInfoListRef::new(lofty::iff::wav::tag::tagitems_into_riff( tag.items(), )) .write_to(file, write_options) }); insert!(map, AiffText, { lofty::iff::aiff::tag::AiffTextChunksRef { name: tag.get_string(&lofty::prelude::ItemKey::TrackTitle), author: tag.get_string(&lofty::prelude::ItemKey::TrackArtist), copyright: tag.get_string(&lofty::prelude::ItemKey::CopyrightMessage), annotations: Some(tag.get_strings(&lofty::prelude::ItemKey::Comment)), comments: None, } .write_to(file, write_options) }); map } pub(crate) fn write_module( fields: &[FieldContents], lookup: HashMap<&'static str, proc_macro2::TokenStream>, ) -> proc_macro2::TokenStream { let applicable_formats = fields.iter().map(|f| { let tag_ty = syn::parse_str::(&format!("::lofty::tag::TagType::{}", &f.tag_type)) .unwrap(); let cfg_features = f.get_cfg_features(); let block = lookup.get(&*tag_ty.segments[3].ident.to_string()).unwrap(); quote! { #( #cfg_features )* #tag_ty => #block, } }); quote! { pub(crate) mod write { #[allow(unused_variables)] pub(crate) fn write_to(file: &mut F, tag: &::lofty::tag::Tag, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> where F: ::lofty::io::FileLike, ::lofty::error::LoftyError: ::std::convert::From<::Error>, ::lofty::error::LoftyError: ::std::convert::From<::Error>, { match tag.tag_type() { #( #applicable_formats )* _ => crate::macros::err!(UnsupportedTag), } } } } } lofty_attr-0.11.0/src/lib.rs000064400000000000000000000037431046102023000140040ustar 00000000000000//! Macros for [Lofty](https://crates.io/crates/lofty) #![allow( unknown_lints, clippy::too_many_lines, clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::module_name_repetitions, clippy::must_use_candidate, clippy::doc_markdown, let_underscore_drop, clippy::match_wildcard_for_single_variants, clippy::semicolon_if_nothing_returned, clippy::new_without_default, clippy::from_over_into, clippy::upper_case_acronyms, clippy::single_match_else, clippy::similar_names, clippy::tabs_in_doc_comments, clippy::len_without_is_empty, clippy::needless_late_init, clippy::type_complexity, clippy::type_repetition_in_bounds, unused_qualifications, clippy::return_self_not_must_use, clippy::bool_to_int_with_if, clippy::uninlined_format_args, /* This should be changed for any normal "{}", but I'm not a fan of it for any debug or width specific formatting */ clippy::manual_let_else, clippy::struct_excessive_bools, clippy::match_bool, clippy::needless_pass_by_value )] mod attribute; mod internal; mod lofty_file; mod lofty_tag; mod util; use crate::lofty_file::LoftyFile; use crate::lofty_tag::{LoftyTag, LoftyTagAttribute}; use proc_macro::TokenStream; use syn::{parse_macro_input, ItemStruct}; /// Creates a file usable by Lofty /// /// See [here](https://github.com/Serial-ATA/lofty-rs/tree/main/examples/custom_resolver) for an example of how to use it. #[proc_macro_derive(LoftyFile, attributes(lofty))] pub fn lofty_file(input: TokenStream) -> TokenStream { let lofty_file = parse_macro_input!(input as LoftyFile); match lofty_file.emit() { Ok(ret) => ret, Err(e) => e.to_compile_error().into(), } } #[proc_macro_attribute] #[doc(hidden)] pub fn tag(args_input: TokenStream, input: TokenStream) -> TokenStream { let attribute = parse_macro_input!(args_input as LoftyTagAttribute); let input = parse_macro_input!(input as ItemStruct); let lofty_tag = LoftyTag::new(attribute, input); lofty_tag.emit() } lofty_attr-0.11.0/src/lofty_file.rs000064400000000000000000000351531046102023000153720ustar 00000000000000use crate::attribute::AttributeValue; use crate::{internal, util}; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::parse::Parse; use syn::spanned::Spanned; use syn::{Attribute, Data, DataStruct, DeriveInput, Field, Fields, Type}; #[derive(Default)] pub struct InternalFileDetails { pub(crate) has_internal_write_module: bool, pub(crate) has_internal_file_type: bool, pub(crate) id3v2_strippable: bool, } #[derive(Default)] pub(crate) struct FileFields { pub(crate) tags: Vec, pub(crate) properties: Option, } pub struct FileStructInfo { pub(crate) name: Ident, pub(crate) span: Span, pub(crate) fields: FileFields, } pub(crate) struct AudioFileImplFields { pub(crate) should_impl_audiofile: bool, pub(crate) read_fn: Option, pub(crate) write_fn: Option, } impl Default for AudioFileImplFields { fn default() -> Self { Self { should_impl_audiofile: true, read_fn: None, write_fn: None, } } } pub struct LoftyFile { pub(crate) struct_info: FileStructInfo, pub(crate) audiofile_impl: AudioFileImplFields, pub(crate) internal_details: InternalFileDetails, pub(crate) file_type: proc_macro2::TokenStream, pub(crate) should_impl_into_taggedfile: bool, } impl Parse for LoftyFile { fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { let input: DeriveInput = input.parse()?; let data_struct = match input.data { Data::Struct( ref data_struct @ DataStruct { fields: Fields::Named(_), .. }, ) => data_struct, _ => { return Err(util::err( input.ident.span(), "This macro can only be used on structs with named fields", )) }, }; let mut lofty_file = LoftyFile { struct_info: FileStructInfo { name: input.ident.clone(), span: input.ident.span(), fields: FileFields::default(), }, audiofile_impl: AudioFileImplFields::default(), should_impl_into_taggedfile: true, file_type: proc_macro2::TokenStream::new(), internal_details: InternalFileDetails::default(), }; let mut errors = Vec::new(); let mut has_internal_write_module = false; for attr in &input.attrs { if let Some(lofty_attr) = AttributeValue::from_attribute("lofty", attr)? { match lofty_attr { AttributeValue::Path(value) => match &*value.to_string() { "no_audiofile_impl" => { lofty_file.audiofile_impl.should_impl_audiofile = false }, "no_into_taggedfile_impl" => lofty_file.should_impl_into_taggedfile = false, "internal_write_module_do_not_use_anywhere_else" => { has_internal_write_module = true }, _ => errors.push(util::err(attr.span(), "Unknown attribute")), }, AttributeValue::NameValue(lhs, rhs) => match &*lhs.to_string() { "read_fn" => lofty_file.audiofile_impl.read_fn = Some(rhs.parse()?), "write_fn" => lofty_file.audiofile_impl.write_fn = Some(rhs.parse()?), "file_type" => lofty_file.file_type = rhs.parse()?, _ => errors.push(util::err(attr.span(), "Unknown attribute")), }, _ => errors.push(util::err(attr.span(), "Unknown attribute")), } } } let struct_name = input.ident.clone(); let opt_file_type = internal::opt_internal_file_type(struct_name.to_string()); let has_internal_file_type = opt_file_type.is_some(); if !has_internal_file_type && has_internal_write_module { // TODO: This is the best check we can do for now I think? // Definitely needs some work when a better solution comes out. return Err(crate::util::err( input.ident.span(), "Attempted to use an internal attribute externally", )); } lofty_file.internal_details.has_internal_write_module = has_internal_write_module; lofty_file.internal_details.has_internal_file_type = has_internal_file_type; // Internal files do not specify a `#[lofty(file_type = "...")]` if lofty_file.file_type.is_empty() && lofty_file.internal_details.has_internal_file_type { let Some((ft, id3v2_strip)) = opt_file_type else { return Err(util::err( input.ident.span(), "Unable to locate file type for internal file", )); }; lofty_file.internal_details.id3v2_strippable = id3v2_strip; lofty_file.file_type = ft; } let (tag_fields, properties_field) = match get_fields(&mut errors, data_struct)? { Some(fields) => fields, None => return Err(errors.remove(0)), }; if tag_fields.is_empty() { errors.push(util::err(input.ident.span(), "Struct has no tag fields")); } // We do not need to check for a `properties` field yet. lofty_file.struct_info.fields.tags = tag_fields; lofty_file.struct_info.fields.properties = properties_field.cloned(); Ok(lofty_file) } } impl LoftyFile { pub(crate) fn emit(self) -> syn::Result { // When implementing `AudioFile`, the struct must have: // * A `properties` field // * A `#[read_fn]` attribute // // Otherwise, we can simply ignore their absence. let mut audiofile_impl = proc_macro2::TokenStream::new(); if self.audiofile_impl.should_impl_audiofile { audiofile_impl = generate_audiofile_impl(&self)?; } // Assert all tag fields implement `TagExt` let assert_tag_impl_into = self .struct_info .fields .tags .iter() .enumerate() .map(|(i, f)| { let name = format_ident!("_AssertTagExt{}", i); let field_ty = &f.ty; quote_spanned! {field_ty.span()=> struct #name where #field_ty: ::lofty::prelude::TagExt; } }); let mut from_taggedfile_impl = proc_macro2::TokenStream::new(); if self.should_impl_into_taggedfile { from_taggedfile_impl = generate_from_taggedfile_impl(&self); } let getters = get_getters(&self.struct_info.fields.tags, &self.struct_info.name); let mut ret = quote! { #( #assert_tag_impl_into )* #audiofile_impl #from_taggedfile_impl #( #getters )* }; // Create `write` module if internal if self.internal_details.has_internal_write_module { let lookup = internal::init_write_lookup(self.internal_details.id3v2_strippable); let write_mod = internal::write_module(&self.struct_info.fields.tags, lookup); ret = quote! { #ret use crate::_this_is_internal; #write_mod } } Ok(TokenStream::from(ret)) } } pub(crate) struct FieldContents { name: Ident, pub(crate) attrs: Vec, needs_option: bool, getter_name: Option, ty: Type, pub(crate) tag_type: proc_macro2::TokenStream, } impl FieldContents { pub(crate) fn get_cfg_features(&self) -> impl Iterator { self.attrs.iter().filter(|a| a.path().is_ident("cfg")) } } fn get_fields<'a>( errors: &mut Vec, data: &'a DataStruct, ) -> syn::Result, Option<&'a syn::Field>)>> { let mut tag_fields = Vec::new(); let mut properties_field = None; for field in &data.fields { let name = field.ident.clone().unwrap(); if name == "properties" { properties_field = Some(field); } if !name.to_string().ends_with("_tag") { continue; } let mut tag_type = None; let mut getter_name = None; for attr in &field.attrs { if let Some(lofty_attr) = AttributeValue::from_attribute("lofty", attr)? { match lofty_attr { AttributeValue::NameValue(lhs, rhs) => match &*lhs.to_string() { "tag_type" => tag_type = Some(rhs.parse::()?), "getter" => getter_name = Some(rhs.parse::()?), _ => errors.push(util::err(attr.span(), "Unknown attribute")), }, _ => errors.push(util::err(attr.span(), "Unknown attribute")), } } } let Some(tag_type) = tag_type else { errors.push(util::err( field.ident.span(), "Expected a `#[lofty(tag_type = \"...\")]` attribute", )); return Ok(None); }; let other_attrs = field .attrs .iter() .filter(|a| !a.path().is_ident("lofty")) .cloned() .collect::>(); let option_unwrapped = util::extract_type_from_option(&field.ty); // `option_unwrapped` will be `Some` if the type was wrapped in an `Option` let needs_option = option_unwrapped.is_some(); let contents = FieldContents { name, attrs: other_attrs, getter_name, ty: option_unwrapped.unwrap_or_else(|| field.ty.clone()), tag_type, needs_option, }; tag_fields.push(contents); } Ok(Some((tag_fields, properties_field))) } fn get_getters<'a>( tag_fields: &'a [FieldContents], struct_name: &'a Ident, ) -> impl Iterator + 'a { tag_fields.iter().map(move |f| { let name = f.getter_name.clone().unwrap_or_else(|| { let name = f.name.to_string().strip_suffix("_tag").unwrap().to_string(); Ident::new(&name, f.name.span()).into_token_stream() }); let (ty_prefix, ty_suffix) = if f.needs_option { (quote! {Option<}, quote! {>}) } else { (quote! {}, quote! {}) }; let field_name = &f.name; let field_ty = &f.ty; let ref_access = if f.needs_option { quote! {self.#field_name.as_ref()} } else { quote! {&self.#field_name} }; let mut_ident = Ident::new(&format!("{}_mut", name), Span::call_site()); let mut_access = if f.needs_option { quote! {self.#field_name.as_mut()} } else { quote! {&mut self.#field_name} }; let set_ident = Ident::new(&format!("set_{}", name), Span::call_site()); let setter = if f.needs_option { quote! { let ret = self.#field_name.take(); self.#field_name = Some(tag); return ret; } } else { quote! { Some(::core::mem::replace(&mut self.#field_name, tag)) } }; let remove_ident = Ident::new(&format!("remove_{}", name), Span::call_site()); let remover = if f.needs_option { quote! { self.#field_name.take() } } else { let assert_field_ty_default = quote_spanned! {f.name.span()=> struct _AssertDefault where #field_ty: core::default::Default; }; quote! { #assert_field_ty_default ::core::mem::take(&mut self.#field_name) } }; let cfg_features = f.get_cfg_features(); quote! { #( #cfg_features )* impl #struct_name { /// Returns a reference to the tag pub fn #name(&self) -> #ty_prefix &#field_ty #ty_suffix { #ref_access } /// Returns a mutable reference to the tag pub fn #mut_ident(&mut self) -> #ty_prefix &mut #field_ty #ty_suffix { #mut_access } /// Sets the tag, returning the old one pub fn #set_ident(&mut self, tag: #field_ty) -> Option<#field_ty> { #setter } /// Removes the tag pub fn #remove_ident(&mut self) -> #ty_prefix #field_ty #ty_suffix { #remover } } } }) } fn generate_audiofile_impl(file: &LoftyFile) -> syn::Result { fn tag_exists_iter( tag_fields: &[FieldContents], ) -> impl Iterator + '_ { tag_fields.iter().map(|f| { let name = &f.name; if f.needs_option { quote! { self.#name.is_some() } } else { quote! { true } } }) } let Some(properties_field) = &file.struct_info.fields.properties else { return Err(util::err( file.struct_info.span, "Struct has no `properties` field, required for `AudioFile` impl", )); }; let Some(read_fn) = &file.audiofile_impl.read_fn else { return Err(util::err( file.struct_info.span, "Expected a `#[read_fn]` attribute", )); }; let tag_fields = &file.struct_info.fields.tags; let save_to_body = get_save_to_body(file.audiofile_impl.write_fn.as_ref(), tag_fields); let tag_exists = tag_exists_iter(tag_fields); let tag_exists_2 = tag_exists_iter(tag_fields); let tag_type = tag_fields.iter().map(|f| &f.tag_type); let properties_field_ty = &properties_field.ty; let assert_properties_impl = quote_spanned! {properties_field_ty.span()=> struct _AssertIntoFileProperties where #properties_field_ty: ::std::convert::Into<::lofty::properties::FileProperties>; }; let struct_name = &file.struct_info.name; let ret = quote! { #assert_properties_impl impl ::lofty::prelude::AudioFile for #struct_name { type Properties = #properties_field_ty; fn read_from(reader: &mut R, parse_options: ::lofty::config::ParseOptions) -> ::lofty::error::Result where R: std::io::Read + std::io::Seek, { #read_fn(reader, parse_options) } fn save_to(&self, file: &mut F, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> where F: ::lofty::io::FileLike, ::lofty::error::LoftyError: ::std::convert::From<::Error>, ::lofty::error::LoftyError: ::std::convert::From<::Error>, { use ::lofty::tag::TagExt as _; use ::std::io::Seek as _; #save_to_body } fn properties(&self) -> &Self::Properties { &self.properties } #[allow(unreachable_code)] fn contains_tag(&self) -> bool { #( #tag_exists )||* } #[allow(unreachable_code, unused_variables)] fn contains_tag_type(&self, tag_type: ::lofty::tag::TagType) -> bool { match tag_type { #( ::lofty::tag::TagType::#tag_type => { #tag_exists_2 } ),* _ => false } } } }; Ok(ret) } fn get_save_to_body( write_fn: Option<&proc_macro2::TokenStream>, tag_fields: &[FieldContents], ) -> proc_macro2::TokenStream { // Custom write fn if let Some(write_fn) = write_fn { return quote! { #write_fn(&self, file, write_options) }; } let tag_field_save = tag_fields.iter().map(|f| { let name = &f.name; if f.needs_option { quote! { if let Some(ref tag) = self.#name { file.rewind()?; tag.save_to(file, write_options)?; } } } else { quote! { file.rewind()?; self.#name.save_to(file, write_options)?; } } }); quote! { #(#tag_field_save)* Ok(()) } } fn generate_from_taggedfile_impl(file: &LoftyFile) -> proc_macro2::TokenStream { let tag_fields = &file.struct_info.fields.tags; let conditions = tag_fields.iter().map(|f| { let name = &f.name; if f.needs_option { quote! { if let Some(t) = input.#name { tags.push(t.into()); } } } else { quote! { tags.push(input.#name.into()); } } }); let file_type = &file.file_type; let file_type_variant = if file.internal_details.has_internal_file_type { quote! { ::lofty::file::FileType::#file_type } } else { let file_ty_str = file_type.to_string(); quote! { ::lofty::file::FileType::Custom(#file_ty_str) } }; let struct_name = &file.struct_info.name; quote! { impl ::std::convert::From<#struct_name> for ::lofty::file::TaggedFile { fn from(input: #struct_name) -> Self { use ::lofty::prelude::TaggedFileExt as _; ::lofty::file::TaggedFile::new( #file_type_variant, ::lofty::properties::FileProperties::from(input.properties), { let mut tags: Vec<::lofty::tag::Tag> = Vec::new(); #( #conditions )* tags } ) } } } } lofty_attr-0.11.0/src/lofty_tag.rs000064400000000000000000000101741046102023000152220ustar 00000000000000use 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, } impl Parse for LoftyTagAttribute { fn parse(input: ParseStream<'_>) -> Result { let mut description = None; let mut supported_formats = Vec::new(); let start_span = input.span(); let args = Punctuated::::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 { 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, ) -> 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(()) }) } lofty_attr-0.11.0/src/util.rs000064400000000000000000000025301046102023000142040ustar 00000000000000use std::fmt::Display; use proc_macro2::Span; use syn::Type; // https://stackoverflow.com/questions/55271857/how-can-i-get-the-t-from-an-optiont-when-using-syn pub(crate) fn extract_type_from_option(ty: &Type) -> Option { use syn::{GenericArgument, Path, PathArguments, PathSegment}; fn extract_type_path(ty: &Type) -> Option<&Path> { match *ty { Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path), _ => None, } } fn extract_option_segment(path: &Path) -> Option<&PathSegment> { let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| { acc.push_str(&v.ident.to_string()); acc.push('|'); acc }); vec!["Option|", "std|option|Option|", "core|option|Option|"] .into_iter() .find(|s| idents_of_path == *s) .and_then(|_| path.segments.last()) } extract_type_path(ty) .and_then(extract_option_segment) .and_then(|path_seg| { let type_params = &path_seg.arguments; // It should have only on angle-bracketed param (""): match *type_params { PathArguments::AngleBracketed(ref params) => params.args.first(), _ => None, } }) .and_then(|generic_arg| match *generic_arg { GenericArgument::Type(ref ty) => Some(ty.clone()), _ => None, }) } pub(crate) fn err(span: Span, error: T) -> syn::Error { syn::Error::new(span, error) }