Skip to main content

spotify_rs_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{Field, ItemStruct, LitStr, parse_macro_input};
4
5#[proc_macro_attribute]
6pub fn docs(attr: TokenStream, item: TokenStream) -> TokenStream {
7    docs_impl(attr, item)
8}
9
10// Macro to return a Some when formatting strings.
11macro_rules! format_some {
12    ($($arg:tt)*) => {{
13        let res = format!($($arg)*);
14
15        let res = quote! {
16            #[doc = #res]
17        };
18
19        Some(res)
20    }}
21}
22
23pub(crate) fn docs_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
24    let ast: ItemStruct = syn::parse(item.clone()).unwrap();
25    let ItemStruct {
26        vis,
27        ident,
28        fields,
29        attrs,
30        ..
31    } = ast;
32
33    // Set the item name to the struct name
34    let mut item_name = ident.to_string().to_lowercase();
35
36    // If there's an argument "name", set the item_name to it instead,
37    // otherwise return an error.
38    let parser = syn::meta::parser(|meta| {
39        if meta.path.is_ident("name") {
40            let value: LitStr = meta.value()?.parse()?;
41            item_name = value.value();
42
43            Ok(())
44        } else {
45            Err(meta.error("unsupported property"))
46        }
47    });
48
49    parse_macro_input!(attr with parser);
50
51    let (docs, fields): (Vec<_>, Vec<Field>) = fields
52        .into_iter()
53        .map(|f| {
54            let field_name = f.ident.as_ref().unwrap().to_string();
55            let struct_name = &item_name;
56
57            // Generate the comment based on the field name,
58            // including the struct name in the comment as well.
59            let doc = field_documentation(&field_name, struct_name);
60
61            (doc, f)
62        })
63        .collect();
64
65    // Reconstruct the struct with the documentation applied.
66    quote! {
67        #(#attrs)*
68        #vis struct #ident {
69            #(
70                #docs
71                #fields
72
73            ),*
74        }
75
76    }
77    .into()
78}
79
80fn field_documentation(field_name: &str, name: &str) -> Option<proc_macro2::TokenStream> {
81    match field_name {
82        "available_markets" => format_some!(
83            "The [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes for the markets in which the {name} is available."
84        ),
85
86        "external_urls" => format_some!("Known external URLs for the {name}."),
87
88        "href" => format_some!(
89            "A link to the Spotify Web API endpoint providing full details of the {name}."
90        ),
91
92        "id" => format_some!(
93            "The [Spotify ID](https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids) for the {name}."
94        ),
95
96        "r#type" | "type" => format_some!("The object type. Allowed values: `{name}`."),
97
98        "uri" => format_some!(
99            "The [Spotify URI](https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids) for the {name}."
100        ),
101
102        "copyrights" => format_some!("The copyright statements of the {name}."),
103
104        "external_ids" => format_some!("Known external IDs for the {name}."),
105
106        "images" => {
107            format_some!("The cover art for the {name} in various sizes, with the widest first.")
108        }
109
110        "name" => format_some!("The name of the {name}."),
111
112        "release_date" => format_some!("The date the {name} was first released."),
113
114        "release_date_precision" => {
115            format_some!("The precision with which the `release_date` field is known.")
116        }
117
118        "description" => format_some!("A text description of the {name}."),
119
120        "html_description" => {
121            format_some!("A description of the {name} that may contain HTML tags.")
122        }
123
124        "restrictions" => {
125            format_some!("Included in the response when a content restriction is applied.")
126        }
127
128        "explicit" => format_some!(
129            "Whether or not the {name} contains explicit content.\n\nNote: `false` can also mean *unknown*."
130        ),
131
132        "languages" => format_some!(
133            "A list of [ISO 639](https://en.wikipedia.org/wiki/ISO_639) codes for the languages spoken in the {name}."
134        ),
135
136        "media_type" => format_some!("The type of the media of the {name}."),
137
138        "publisher" => format_some!("The publisher of the {name}."),
139
140        "duration_ms" => format_some!("The duration of the {name} in miliseconds."),
141
142        "is_playable" => {
143            format_some!("Indicates whether or not the {name} is playable in the current market.")
144        }
145
146        "resume_point" => format_some!(
147            "The user's latest position in the {name}.\n\nNote: this field is only available if the user is authorised with the `user-read-playback-position` scope."
148        ),
149
150        _ => None,
151    }
152}