From 0028651f70458e3da65ce0cf82e5689c39346a3e Mon Sep 17 00:00:00 2001 From: Serial Date: Tue, 18 May 2021 22:21:23 -0400 Subject: [PATCH] Create impl_tag attribute proc macro --- .gitignore | 2 +- Cargo.toml | 4 + lofty-attr/Cargo.lock | 46 +++++++++++ lofty-attr/Cargo.toml | 17 ++++ lofty-attr/src/lib.rs | 177 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 lofty-attr/Cargo.lock create mode 100644 lofty-attr/Cargo.toml create mode 100644 lofty-attr/src/lib.rs diff --git a/.gitignore b/.gitignore index 7c6c77f1..9bd75286 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Generated by Cargo # will have compiled files and executables -/target/ +**/target/ # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index 0069c68c..7b661652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,10 @@ thiserror = "1.0.24" base64 = "0.13.0" byteorder = "1.4.3" +[dependencies.lofty_attr] +path = "./lofty-attr" +version = "0.1.0" + [features] default = ["full"] full = ["all_tags", "duration"] diff --git a/lofty-attr/Cargo.lock b/lofty-attr/Cargo.lock new file mode 100644 index 00000000..a39aa7a5 --- /dev/null +++ b/lofty-attr/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "lofty_attr" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" diff --git a/lofty-attr/Cargo.toml b/lofty-attr/Cargo.toml new file mode 100644 index 00000000..dd6cae71 --- /dev/null +++ b/lofty-attr/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "lofty_attr" +version = "0.1.0" +authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/Serial-ATA/lofty-rs" +edition = "2018" + +[dependencies] +quote = "1.0.9" + +[dependencies.syn] +version = "1.0.72" +features = ["full"] + +[lib] +proc-macro = true diff --git a/lofty-attr/src/lib.rs b/lofty-attr/src/lib.rs new file mode 100644 index 00000000..60ce9453 --- /dev/null +++ b/lofty-attr/src/lib.rs @@ -0,0 +1,177 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, AttributeArgs, Error, ItemStruct, Meta, NestedMeta}; + +#[proc_macro_attribute] +#[allow(clippy::too_many_lines)] +pub fn impl_tag(args: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + let args = parse_macro_input!(args as AttributeArgs); + + if args.len() != 2 { + return Error::new( + input.ident.span(), + "impl_tag requires an inner tag and TagType", + ) + .to_compile_error() + .into(); + } + + if let (NestedMeta::Meta(Meta::Path(inner)), NestedMeta::Meta(tag_type)) = + (args[0].clone(), args[1].clone()) + { + if let Some(inner) = inner.get_ident() { + let input_ident = input.ident; + + let expanded = quote! { + #[doc(hidden)] + pub struct #input_ident { + inner: #inner, + #[cfg(feature = "duration")] + duration: Option + } + + impl Default for #input_ident { + fn default() -> Self { + Self { + inner: #inner::default(), + #[cfg(feature = "duration")] + duration: None, + } + } + } + + impl #input_ident { + /// Creates a new default tag + pub fn new() -> Self { + Self::default() + } + } + + use std::any::Any; + + impl ToAnyTag for #input_ident { + fn to_anytag(&self) -> AnyTag<'_> { + self.into() + } + } + + impl ToAny for #input_ident { + fn to_any(&self) -> &dyn Any { + self + } + fn to_any_mut(&mut self) -> &mut dyn Any { + self + } + } + + impl AudioTag for #input_ident {} + + // From wrapper to inner (same type) + impl From<#input_ident> for #inner { + fn from(inp: #input_ident) -> Self { + inp.inner + } + } + + // From inner to wrapper (same type) + impl From<#inner> for #input_ident { + fn from(inp: #inner) -> Self { + Self { + inner: inp, + #[cfg(feature = "duration")] + duration: None, + } + } + } + + impl<'a> From<&'a #input_ident> for AnyTag<'a> { + fn from(inp: &'a #input_ident) -> Self { + Self { + title: inp.title(), + artists: inp.artists_vec(), + year: inp.year().map(|y| y as i32), + album: Album::new( + inp.album_title(), + inp.album_artists_vec(), + inp.album_covers(), + ), + track_number: inp.track_number(), + total_tracks: inp.total_tracks(), + disc_number: inp.disc_number(), + total_discs: inp.total_discs(), + comments: None, // TODO + date: inp.date(), + } + } + } + + impl<'a> From> for #input_ident { + fn from(inp: AnyTag<'a>) -> Self { + let mut tag = #input_ident::default(); + + if let Some(v) = inp.title() { + tag.set_title(v) + } + if let Some(v) = inp.artists_as_string() { + tag.set_artist(&v) + } + if let Some(v) = inp.year { + tag.set_year(v) + } + if let Some(v) = inp.album().title { + tag.set_album_title(v) + } + if let Some(v) = inp.album().artists { + tag.set_album_artist(&v.join("/")) + } + if let Some(v) = inp.track_number() { + tag.set_track_number(v) + } + if let Some(v) = inp.total_tracks() { + tag.set_total_tracks(v) + } + if let Some(v) = inp.disc_number() { + tag.set_disc_number(v) + } + if let Some(v) = inp.total_discs() { + tag.set_total_discs(v) + } + + tag + } + } + + // From dyn AudioTag to wrapper (any type) + impl From> for #input_ident { + fn from(inp: Box) -> Self { + let mut inp = inp; + if let Some(t_refmut) = inp.to_any_mut().downcast_mut::<#input_ident>() { + let t = std::mem::replace(t_refmut, #input_ident::new()); // TODO: can we avoid creating the dummy tag? + t + } else { + let mut t = inp.to_dyn_tag(#tag_type); + let t_refmut = t.to_any_mut().downcast_mut::<#input_ident>().unwrap(); + let t = std::mem::replace(t_refmut, #input_ident::new()); + t + } + } + } + + // From dyn AudioTag to inner (any type) + impl From> for #inner { + fn from(inp: Box) -> Self { + let t: #input_ident = inp.into(); + t.into() + } + } + }; + + return TokenStream::from(expanded); + } + } + + Error::new(input.ident.span(), "impl_tag provided invalid arguments") + .to_compile_error() + .into() +}