lofty_attr: Add tag attribute macro to generate some docs

This commit is contained in:
Serial 2022-10-11 19:47:54 -04:00
parent f3d9176a48
commit efab7ca276
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
9 changed files with 137 additions and 115 deletions

View file

@ -5,26 +5,13 @@ mod util;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields};
use syn::{parse_macro_input, AttributeArgs, Data, DataStruct, DeriveInput, Fields, 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 {
act(input, lofty_file::parse)
}
#[proc_macro_derive(LoftyTag, attributes(lofty))]
#[doc(hidden)]
pub fn lofty_tag(input: TokenStream) -> TokenStream {
act(input, lofty_tag::parse)
}
fn act(
input: TokenStream,
func: impl Fn(&DeriveInput, &DataStruct, &mut Vec<syn::Error>) -> proc_macro2::TokenStream,
) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let data_struct = match input.data {
@ -46,8 +33,24 @@ fn act(
};
let mut errors = Vec::new();
let ret = func(&input, data_struct, &mut errors);
let ret = lofty_file::parse(&input, data_struct, &mut errors);
finish(ret, errors)
}
#[proc_macro_attribute]
#[doc(hidden)]
pub fn tag(args_input: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args_input as AttributeArgs);
let input = parse_macro_input!(input as ItemStruct);
let mut errors = Vec::new();
let ret = lofty_tag::parse(args, input, &mut errors);
finish(ret, errors)
}
fn finish(ret: proc_macro2::TokenStream, errors: Vec<syn::Error>) -> TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
TokenStream::from(quote! {

View file

@ -1,46 +1,110 @@
use crate::util::{self, bail};
use crate::util::bail;
use proc_macro2::Span;
use quote::quote;
use syn::{DataStruct, DeriveInput, Meta, NestedMeta};
use syn::spanned::Spanned;
use syn::{AttributeArgs, ItemStruct, Lit, Meta, NestedMeta, Path};
enum SupportedFormat {
Full(Path),
ReadOnly(Path),
}
pub(crate) fn parse(
input: &DeriveInput,
_data_struct: &DataStruct,
attr: AttributeArgs,
input: ItemStruct,
errors: &mut Vec<syn::Error>,
) -> proc_macro2::TokenStream {
let mut supported_file_types_attr = None;
for attr in &input.attrs {
if let Some(list) = util::get_attr_list("lofty", attr) {
if let Some(NestedMeta::Meta(Meta::List(ml))) = list.nested.first() {
if ml
.path
.segments
.first()
.expect("path shouldn't be empty")
.ident == "supported_formats"
{
supported_file_types_attr = Some(ml.clone());
let mut desc = None;
let mut supported_formats = Vec::new();
let mut read_only_encountered = false;
for nested_meta in attr {
match nested_meta {
NestedMeta::Meta(Meta::NameValue(mnv)) if mnv.path.is_ident("description") => {
if desc.is_some() {
bail!(errors, mnv.span(), "Duplicate `description` entry");
}
}
if let Lit::Str(s) = &mnv.lit {
desc = Some(s.value());
continue;
} else {
bail!(
errors,
Span::call_site(),
"Invalid `description` entry, expected string value"
);
}
},
NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("supported_formats") => {
for nested in list.nested {
if let NestedMeta::Meta(meta) = nested {
match meta {
Meta::Path(path) => {
supported_formats.push(SupportedFormat::Full(path.clone()));
continue;
},
Meta::List(list) if list.path.is_ident("read_only") => {
if read_only_encountered {
bail!(errors, list.path.span(), "Duplicate `read_only` entry");
}
read_only_encountered = true;
for item in list.nested.iter() {
if let NestedMeta::Meta(Meta::Path(path)) = item {
supported_formats
.push(SupportedFormat::ReadOnly(path.clone()));
continue;
}
}
continue;
},
_ => {},
}
}
}
continue;
},
_ => {
bail!(
errors,
nested_meta.span(),
"Unexpected input, check the format of the arguments"
);
},
}
}
if supported_file_types_attr.is_none() {
bail!(
errors,
input.ident.span(),
"Tag has no #[lofty(supported_formats)] attribute"
);
}
let ident = &input.ident;
let supported_file_types_attr = supported_file_types_attr.unwrap();
let file_types_iter = supported_file_types_attr.nested.iter();
let supported_types_iter = supported_formats.iter().map(|format| match format {
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()
),
});
let flattened_file_types = supported_formats.iter().map(|format| match format {
SupportedFormat::Full(path) | SupportedFormat::ReadOnly(path) => path,
});
quote! {
#[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::FileType] = &[
#( lofty::FileType:: #file_types_iter ),*
#( lofty::FileType:: #flattened_file_types ),*
];
}
}

View file

@ -13,7 +13,7 @@ use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
macro_rules! impl_accessor {
($($name:ident => $($key:literal)|+;)+) => {
@ -49,14 +49,6 @@ macro_rules! impl_accessor {
}
}
/// An `APE` tag
///
/// ## Supported file types
///
/// * [`FileType::APE`](crate::FileType::APE)
/// * [`FileType::MPEG`](crate::FileType::MPEG)
/// * [`FileType::WavPack`](crate::FileType::WavPack)
///
/// ## Item storage
///
/// `APE` isn't a very strict format. An [`ApeItem`] only restricted by its name, meaning it can use
@ -72,8 +64,8 @@ macro_rules! impl_accessor {
///
/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded.
/// For items, see [`ApeItem::new`].
#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)]
#[lofty(supported_formats(APE, MPEG, WavPack))]
#[derive(Default, Debug, PartialEq, Eq, Clone)]
#[tag(description = "An `APE` tag", supported_formats(APE, MPEG, WavPack))]
pub struct ApeTag {
/// Whether or not to mark the tag as read only
pub read_only: bool,

View file

@ -8,7 +8,7 @@ use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
macro_rules! impl_accessor {
($($name:ident,)+) => {
@ -30,8 +30,6 @@ macro_rules! impl_accessor {
}
}
/// An ID3v1 tag
///
/// ID3v1 is a severely limited format, with each field
/// being incredibly small in size. All fields have been
/// commented with their maximum sizes and any other additional
@ -40,12 +38,6 @@ macro_rules! impl_accessor {
/// Attempting to write a field greater than the maximum size
/// will **not** error, it will just be shrunk.
///
/// ## Supported file types
///
/// * [`FileType::APE`](crate::FileType::APE)
/// * [`FileType::MP3`](crate::FileType::MPEG)
/// * [`FileType::WavPack`](crate::FileType::WavPack)
///
/// ## Conversions
///
/// ### From `Tag`
@ -54,8 +46,8 @@ macro_rules! impl_accessor {
///
/// * [`GENRES`] contains the string
/// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8`
#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)]
#[lofty(supported_formats(APE, MPEG, WavPack))]
#[derive(Default, Debug, PartialEq, Eq, Clone)]
#[tag(description = "An ID3v1 tag", supported_formats(APE, MPEG, WavPack))]
pub struct ID3v1Tag {
/// Track title, 30 bytes max
pub title: Option<String>,

View file

@ -18,7 +18,7 @@ use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
macro_rules! impl_accessor {
($($name:ident => $id:literal;)+) => {
@ -61,15 +61,6 @@ macro_rules! impl_accessor {
}
}
/// An `ID3v2` tag
///
/// ## Supported file types
///
/// * [`FileType::MPEG`](crate::FileType::MPEG)
/// * [`FileType::WAV`](crate::FileType::WAV)
/// * [`FileType::AIFF`](crate::FileType::AIFF)
/// * [`FileType::APE`](crate::FileType::APE) **(READ ONLY)**
///
/// ## Conversions
///
/// ⚠ **Warnings** ⚠
@ -98,8 +89,11 @@ macro_rules! impl_accessor {
/// and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively, and converted back to binary with
/// [`GeneralEncapsulatedObject::as_bytes`](crate::id3::v2::GeneralEncapsulatedObject::as_bytes) and
/// [`SynchronizedText::as_bytes`](crate::id3::v2::SynchronizedText::as_bytes) for writing.
#[derive(LoftyTag, PartialEq, Eq, Debug, Clone)]
#[lofty(supported_formats(APE, AIFF, FLAC, MPEG, WAV))]
#[derive(PartialEq, Eq, Debug, Clone)]
#[tag(
description = "An `ID3v2` tag",
supported_formats(AIFF, MPEG, WAV, read_only(FLAC, APE))
)]
pub struct ID3v2Tag {
flags: ID3v2TagFlags,
pub(super) original_version: ID3v2Version,

View file

@ -11,7 +11,7 @@ use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use byteorder::BigEndian;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
/// Represents an AIFF `COMT` chunk
///
@ -34,12 +34,6 @@ pub struct Comment {
pub text: String,
}
/// `AIFF` text chunks
///
/// ## Supported file types
///
/// * [`FileType::AIFF`](crate::FileType::AIFF)
///
/// ## Item storage
///
/// `AIFF` has a few chunks for storing basic metadata, all of
@ -61,8 +55,8 @@ pub struct Comment {
/// * [`ItemKey::Comment`](crate::ItemKey::Comment)
///
/// When converting [Comment]s, only the `text` field will be preserved.
#[derive(LoftyTag, Default, Clone, Debug, PartialEq, Eq)]
#[lofty(supported_formats(AIFF))]
#[derive(Default, Clone, Debug, PartialEq, Eq)]
#[tag(description = "`AIFF` text chunks", supported_formats(AIFF))]
pub struct AIFFTextChunks {
/// The name of the piece
pub name: Option<String>,

View file

@ -10,7 +10,7 @@ use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
macro_rules! impl_accessor {
($($name:ident => $key:literal;)+) => {
@ -32,12 +32,6 @@ macro_rules! impl_accessor {
}
}
/// A RIFF INFO LIST
///
/// ## Supported file types
///
/// * [`FileType::WAV`](crate::FileType::WAV)
///
/// ## Conversions
///
/// ## From `Tag`
@ -46,8 +40,8 @@ macro_rules! impl_accessor {
///
/// * The [`TagItem`] has a value other than [`ItemValue::Binary`](crate::ItemValue::Binary)
/// * It has a key that is 4 bytes in length and within the ASCII range
#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)]
#[lofty(supported_formats(WAV))]
#[derive(Default, Debug, PartialEq, Eq, Clone)]
#[tag(description = "A RIFF INFO LIST", supported_formats(WAV))]
pub struct RIFFInfoList {
/// A collection of chunk-value pairs
pub(crate) items: Vec<(String, String)>,

View file

@ -18,7 +18,7 @@ use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
const ARTIST: AtomIdent = AtomIdent::Fourcc(*b"\xa9ART");
const TITLE: AtomIdent = AtomIdent::Fourcc(*b"\xa9nam");
@ -55,12 +55,6 @@ macro_rules! impl_accessor {
}
}
/// An MP4 ilst atom
///
/// ## Supported file types
///
/// * [`FileType::MP4`](crate::FileType::MP4)
///
/// ## Pictures
///
/// Unlike other formats, ilst does not store a [`PictureType`]. All pictures will have
@ -81,8 +75,8 @@ macro_rules! impl_accessor {
/// well as pictures, will be preserved.
///
/// An attempt will be made to create the `TrackNumber/TrackTotal` (trkn) and `DiscNumber/DiscTotal` (disk) pairs.
#[derive(LoftyTag, Default, PartialEq, Debug, Clone)]
#[lofty(supported_formats(MP4))]
#[derive(Default, PartialEq, Debug, Clone)]
#[tag(description = "An MP4 ilst atom", supported_formats(MP4))]
pub struct Ilst {
pub(crate) atoms: Vec<Atom>,
}

View file

@ -12,7 +12,7 @@ use std::fs::{File, OpenOptions};
use std::io::{Cursor, Write};
use std::path::Path;
use lofty_attr::LoftyTag;
use lofty_attr::tag;
macro_rules! impl_accessor {
($($name:ident => $key:literal;)+) => {
@ -34,16 +34,11 @@ macro_rules! impl_accessor {
}
}
/// Vorbis comments
///
/// ## Supported file types
///
/// * [`FileType::FLAC`](crate::FileType::FLAC)
/// * [`FileType::Opus`](crate::FileType::Opus)
/// * [`FileType::Speex`](crate::FileType::Speex)
/// * [`FileType::Vorbis`](crate::FileType::Vorbis)
#[derive(LoftyTag, Default, PartialEq, Eq, Debug, Clone)]
#[lofty(supported_formats(FLAC, Opus, Speex, Vorbis))]
#[derive(Default, PartialEq, Eq, Debug, Clone)]
#[tag(
description = "Vorbis comments",
supported_formats(FLAC, Opus, Speex, Vorbis)
)]
pub struct VorbisComments {
/// An identifier for the encoding software
pub(crate) vendor: String,