1use proc_macro::TokenStream;
22use proc_macro2::Span;
23use quote::quote;
24use serde::Deserialize;
25use std::fs;
26use syn::{parse_macro_input, ItemMod, Lit, Meta};
27
28#[derive(Deserialize, Debug)]
30struct GenreData {
31 genres: Vec<Genre>,
32}
33
34#[derive(Deserialize, Debug, Clone)]
35struct Genre {
36 id: String,
37 subgenres: Option<Vec<SubGenre>>,
38}
39
40#[derive(Deserialize, Debug, Clone)]
41struct SubGenre {
42 id: String,
43}
44
45#[proc_macro_attribute]
53pub fn music_genres(args: TokenStream, input: TokenStream) -> TokenStream {
54 let input = parse_macro_input!(input as ItemMod);
55
56 let path = parse_path_from_args(args).unwrap_or_else(|err| {
58 panic!("music_genres macro error: {}", err);
59 });
60
61 let genre_data = load_genre_data(&path).unwrap_or_else(|err| {
63 panic!("Failed to load genre data from '{}': {}", path, err);
64 });
65
66 let generated_enum = generate_genre_enum(&genre_data);
68
69 let vis = &input.vis;
71 let mod_name = &input.ident;
72 let attrs = &input.attrs;
73
74 let expanded = quote! {
76 #(#attrs)*
77 #vis mod #mod_name {
78 #generated_enum
79 }
80 };
81
82 TokenStream::from(expanded)
83}
84
85fn parse_path_from_args(args: TokenStream) -> Result<String, String> {
86 if args.is_empty() {
87 return Err("path argument is required".to_string());
88 }
89
90 let args_parsed =
91 syn::parse::<Meta>(args).map_err(|e| format!("Failed to parse arguments: {}", e))?;
92
93 match args_parsed {
94 Meta::NameValue(nv) if nv.path.is_ident("path") => match nv.value {
95 syn::Expr::Lit(syn::ExprLit {
96 lit: Lit::Str(lit_str),
97 ..
98 }) => Ok(lit_str.value()),
99 _ => Err("path must be a string literal".to_string()),
100 },
101 _ => Err("Expected 'path = \"...\"' argument".to_string()),
102 }
103}
104
105fn load_genre_data(path: &str) -> Result<GenreData, Box<dyn std::error::Error>> {
106 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
108 let full_path = std::path::Path::new(&manifest_dir).join(path);
109
110 let final_path = if full_path.exists() {
111 full_path
112 } else {
113 std::path::PathBuf::from(path)
114 };
115
116 let content = fs::read_to_string(&final_path)
117 .map_err(|e| format!("Cannot read file {:?}: {}", final_path, e))?;
118 let genre_data: GenreData =
119 serde_json::from_str(&content).map_err(|e| format!("Cannot parse JSON: {}", e))?;
120 Ok(genre_data)
121}
122
123fn generate_genre_enum(genre_data: &GenreData) -> proc_macro2::TokenStream {
124 let mut variants = Vec::new();
125 let mut discriminant = 0u16;
126
127 let mut sorted_genres = genre_data.genres.clone();
129 sorted_genres.sort_by(|a, b| a.id.cmp(&b.id));
130
131 for genre in sorted_genres {
132 let main_genre_ident = format_ident(&genre.id);
134
135 variants.push(quote! {
136 #main_genre_ident = #discriminant
137 });
138 discriminant += 1;
139
140 if let Some(subgenres) = &genre.subgenres {
142 let mut sorted_subgenres = subgenres.clone();
143 sorted_subgenres.sort_by(|a, b| a.id.cmp(&b.id));
144
145 for subgenre in sorted_subgenres {
146 let subgenre_ident = format_ident(&subgenre.id);
147 variants.push(quote! {
148 #subgenre_ident = #discriminant
149 });
150 discriminant += 1;
151 }
152 }
153 }
154
155 quote! {
156 use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
157 use scale_info::TypeInfo;
158
159 #[cfg(feature = "std")]
160 use ts_rs::TS;
161
162 #[derive(
165 Clone,
166 Copy,
167 PartialEq,
168 Eq,
169 PartialOrd,
170 Ord,
171 Debug,
172 Encode,
173 Decode,
174 DecodeWithMemTracking,
175 TypeInfo,
176 MaxEncodedLen,
177 )]
178 #[cfg_attr(feature = "std", derive(TS), ts(export), ts(export_to = "shared/"))]
179 #[repr(u16)]
180 pub enum GenreId {
181 #(#variants,)*
182 }
183 }
184}
185
186fn format_ident(name: &str) -> syn::Ident {
187 let formatted = name
189 .split('_')
190 .map(|word| {
191 let mut chars = word.chars();
192 match chars.next() {
193 None => String::new(),
194 Some(first) => {
195 first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
196 }
197 }
198 })
199 .collect::<String>();
200
201 let cleaned = formatted
203 .replace(" ", "")
204 .replace("/", "")
205 .replace("-", "")
206 .replace("&", "And")
207 .replace("'", "")
208 .replace("‑", "")
209 .chars()
210 .filter(|c| c.is_alphanumeric())
211 .collect::<String>();
212
213 syn::Ident::new(&cleaned, Span::call_site())
214}