1use proc_macro::TokenStream;
4use quote::{ToTokens, quote};
5use syn::{
6 Ident, Token, Type,
7 parse::{Parse, ParseStream},
8 parse_macro_input,
9 punctuated::Punctuated,
10};
11
12#[expect(clippy::large_enum_variant)]
14enum DatapathDef {
15 Simple {
17 struct_name: Ident,
18 segments: Vec<Segment>,
19 attrs: Vec<syn::Attribute>,
20 },
21 WithSchema {
23 struct_name: Ident,
24 segments: Vec<Segment>,
25 schema_type: Type,
26 attrs: Vec<syn::Attribute>,
27 },
28}
29
30#[expect(clippy::large_enum_variant)]
32enum Segment {
33 Constant(String),
34 Typed { name: Ident, ty: Type },
35}
36
37impl Parse for DatapathDef {
38 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
39 let attrs = input.call(syn::Attribute::parse_outer)?;
41
42 input.parse::<Token![struct]>()?;
44 let struct_name: Ident = input.parse()?;
45
46 let lookahead = input.lookahead1();
48
49 if lookahead.peek(syn::token::Paren) {
50 let content;
52 syn::parenthesized!(content in input);
53 let segments = parse_pattern(&content)?;
54
55 Ok(DatapathDef::Simple {
56 struct_name,
57 segments,
58 attrs,
59 })
60 } else if lookahead.peek(syn::token::Brace) {
61 let content;
63 syn::braced!(content in input);
64
65 let mut segments = None;
67 let mut schema_type = None;
68
69 while !content.is_empty() {
70 let field_name: Ident = content.parse()?;
71 content.parse::<Token![:]>()?;
72
73 match field_name.to_string().as_str() {
74 "pattern" => {
75 if segments.is_some() {
76 return Err(syn::Error::new_spanned(
77 field_name,
78 "duplicate 'pattern' field",
79 ));
80 }
81 let next_keyword = find_next_keyword(&content);
83 segments = Some(if let Some(kw) = next_keyword {
84 parse_pattern_until_keyword(&content, &kw)?
85 } else {
86 parse_pattern(&content)?
87 });
88 }
89 "schema" => {
90 if schema_type.is_some() {
91 return Err(syn::Error::new_spanned(
92 field_name,
93 "duplicate 'schema' field",
94 ));
95 }
96 schema_type = Some(content.parse()?);
97 }
98 _ => {
99 return Err(syn::Error::new_spanned(
100 field_name,
101 "unknown field, expected 'pattern' or 'schema'",
102 ));
103 }
104 }
105 }
106
107 let segments = segments.ok_or_else(|| {
109 syn::Error::new(content.span(), "missing required field 'pattern'")
110 })?;
111 let schema_type = schema_type.ok_or_else(|| {
112 syn::Error::new(content.span(), "missing required field 'schema'")
113 })?;
114
115 Ok(DatapathDef::WithSchema {
116 struct_name,
117 segments,
118 schema_type,
119 attrs,
120 })
121 } else {
122 Err(lookahead.error())
123 }
124 }
125}
126
127fn find_next_keyword(input: ParseStream<'_>) -> Option<String> {
129 let fork = input.fork();
130
131 while !fork.is_empty() {
133 if fork.peek(Ident) {
134 if let Ok(ident) = fork.parse::<Ident>() {
135 let ident_str = ident.to_string();
136 if ident_str == "schema" {
137 return Some(ident_str);
138 }
139 }
140 } else {
141 let _ = fork.parse::<proc_macro2::TokenTree>();
143 }
144 }
145
146 None
147}
148
149fn parse_pattern(input: ParseStream<'_>) -> syn::Result<Vec<Segment>> {
151 let mut segments = Vec::new();
152 let mut current_token = String::new();
153
154 while !input.is_empty() {
155 parse_next_segment(input, &mut segments, &mut current_token)?;
156 }
157
158 if !current_token.is_empty() {
160 segments.push(Segment::Constant(current_token));
161 }
162
163 Ok(segments)
164}
165
166fn parse_pattern_until_keyword(
168 input: ParseStream<'_>,
169 stop_keyword: &str,
170) -> syn::Result<Vec<Segment>> {
171 let mut segments = Vec::new();
172 let mut current_token = String::new();
173
174 while !input.is_empty() {
175 if input.peek(Ident) {
177 let fork = input.fork();
178 if let Ok(ident) = fork.parse::<Ident>()
179 && ident == stop_keyword
180 {
181 if !current_token.is_empty() {
183 segments.push(Segment::Constant(current_token));
184 }
185 return Ok(segments);
186 }
187 }
188
189 parse_next_segment(input, &mut segments, &mut current_token)?;
190 }
191
192 if !current_token.is_empty() {
194 segments.push(Segment::Constant(current_token));
195 }
196
197 Ok(segments)
198}
199
200fn parse_next_segment(
202 input: ParseStream<'_>,
203 segments: &mut Vec<Segment>,
204 current_token: &mut String,
205) -> syn::Result<()> {
206 if input.peek(syn::LitStr) {
208 let lit: syn::LitStr = input.parse()?;
209 let lit_value = lit.value();
210
211 if input.peek(Token![=]) {
213 input.parse::<Token![=]>()?;
214 let ty: Type = input.parse()?;
215
216 let ident_str = lit_value.replace('-', "_");
218 let ident = Ident::new(&ident_str, lit.span());
219
220 segments.push(Segment::Typed { name: ident, ty });
221
222 if input.peek(Token![/]) {
224 input.parse::<Token![/]>()?;
225 }
226 } else {
227 if !current_token.is_empty() {
229 current_token.push('/');
230 }
231 current_token.push_str(&lit_value);
232
233 if input.peek(Token![/]) {
235 input.parse::<Token![/]>()?;
236 segments.push(Segment::Constant(current_token.clone()));
237 current_token.clear();
238 }
239 }
240 } else if let Ok(ident) = input.parse::<Ident>() {
241 let ident_str = ident.to_string();
242
243 if input.peek(Token![=]) {
245 input.parse::<Token![=]>()?;
246 let ty: Type = input.parse()?;
247
248 segments.push(Segment::Typed {
249 name: ident.clone(),
250 ty,
251 });
252
253 if input.peek(Token![/]) {
255 input.parse::<Token![/]>()?;
256 }
257 } else {
258 if !current_token.is_empty() {
260 current_token.push('/');
261 }
262 current_token.push_str(&ident_str);
263
264 if input.peek(Token![/]) {
266 input.parse::<Token![/]>()?;
267 segments.push(Segment::Constant(current_token.clone()));
268 current_token.clear();
269 }
270 }
271 } else {
272 let lookahead = input.lookahead1();
274
275 if lookahead.peek(syn::LitStr) {
276 let lit: syn::LitStr = input.parse()?;
278 if !current_token.is_empty() {
279 current_token.push('/');
280 }
281 current_token.push_str(&lit.value());
282
283 if input.peek(Token![/]) {
285 input.parse::<Token![/]>()?;
286 segments.push(Segment::Constant(current_token.clone()));
287 current_token.clear();
288 }
289 } else if lookahead.peek(syn::LitFloat) {
290 let lit: syn::LitFloat = input.parse()?;
291 if !current_token.is_empty() {
292 current_token.push('/');
293 }
294 current_token.push_str(&lit.to_string());
295
296 if input.peek(Token![/]) {
298 input.parse::<Token![/]>()?;
299 segments.push(Segment::Constant(current_token.clone()));
300 current_token.clear();
301 }
302 } else if lookahead.peek(syn::LitInt) {
303 let lit: syn::LitInt = input.parse()?;
304 if !current_token.is_empty() {
305 current_token.push('/');
306 }
307 current_token.push_str(&lit.to_string());
308
309 if input.peek(Token![/]) {
311 input.parse::<Token![/]>()?;
312 segments.push(Segment::Constant(current_token.clone()));
313 current_token.clear();
314 }
315 } else {
316 return Err(lookahead.error());
317 }
318 }
319
320 Ok(())
321}
322
323fn generate_datapath_code(def: DatapathDef) -> proc_macro2::TokenStream {
325 match def {
326 DatapathDef::Simple {
327 struct_name,
328 segments,
329 attrs,
330 } => generate_simple_datapath(&struct_name, &segments, &attrs),
331 DatapathDef::WithSchema {
332 struct_name,
333 segments,
334 schema_type,
335 attrs,
336 } => generate_schema_datapath(&struct_name, &segments, &schema_type, &attrs),
337 }
338}
339
340fn generate_simple_datapath(
342 struct_name: &Ident,
343 segments: &[Segment],
344 attrs: &[syn::Attribute],
345) -> proc_macro2::TokenStream {
346 let (struct_def, display_impl, datapath_impl) =
347 generate_common_impls(struct_name, segments, attrs);
348
349 quote! {
350 #struct_def
351 #display_impl
352 #datapath_impl
353 }
354}
355
356fn generate_schema_datapath(
358 struct_name: &Ident,
359 segments: &[Segment],
360 schema_type: &Type,
361 attrs: &[syn::Attribute],
362) -> proc_macro2::TokenStream {
363 let (struct_def, display_impl, datapath_impl) =
364 generate_common_impls(struct_name, segments, attrs);
365
366 let schema_datapath_impl = quote! {
368 impl ::datapath::SchemaDatapath for #struct_name {
369 type Schema = #schema_type;
370 }
371 };
372
373 quote! {
374 #struct_def
375 #display_impl
376 #datapath_impl
377 #schema_datapath_impl
378 }
379}
380
381fn generate_common_impls(
383 struct_name: &Ident,
384 segments: &[Segment],
385 attrs: &[syn::Attribute],
386) -> (
387 proc_macro2::TokenStream,
388 proc_macro2::TokenStream,
389 proc_macro2::TokenStream,
390) {
391 let typed_fields: Vec<_> = segments
393 .iter()
394 .filter_map(|seg| match seg {
395 Segment::Typed { name, ty } => Some((name, ty)),
396 _ => None,
397 })
398 .collect();
399
400 let struct_fields = typed_fields.iter().map(|(name, ty)| {
402 quote! {
403 pub #name: #ty
404 }
405 });
406
407 let mut doc_str = String::new();
408 for s in segments {
409 if !doc_str.is_empty() {
410 doc_str.push('/');
411 }
412
413 match s {
414 Segment::Constant(x) => doc_str.push_str(x),
415 Segment::Typed { name, ty } => {
416 doc_str.push_str(&format!("{name}={}", ty.to_token_stream()))
417 }
418 }
419 }
420
421 let doc_str = format!("\n\nDatapath pattern: `{doc_str}`");
422
423 let struct_def = quote! {
424 #(#attrs)*
425 #[allow(non_camel_case_types)]
426 #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::cmp::PartialEq, ::core::cmp::Eq, ::core::hash::Hash)]
427 #[doc = #doc_str]
428 pub struct #struct_name {
429 #(#struct_fields),*
430 }
431 };
432
433 let display_parts = segments.iter().map(|seg| match seg {
435 Segment::Constant(s) => quote! { #s.to_string() },
436 Segment::Typed { name, .. } => quote! { format!("{}={}", stringify!(#name), self.#name) },
437 });
438
439 let display_impl = quote! {
440 impl ::core::fmt::Display for #struct_name {
441 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
442 write!(f, "{}", vec![#(#display_parts),*].join("/"))
443 }
444 }
445 };
446
447 let mut parse_body = Vec::new();
449
450 for seg in segments {
451 match seg {
452 Segment::Constant(s) => {
453 parse_body.push(quote! {
454 {
455 match parts.next() {
456 Option::Some(#s) => {}
457 _ => return Option::None,
458 }
459 }
460 });
461 }
462 Segment::Typed { name, ty } => {
463 let name_str = name.to_string();
464 parse_body.push(quote! {
465 let #name: #ty = {
466 let x = match parts.next() {
467 Option::Some(x) => x.strip_prefix(concat!(#name_str, "="))?,
468 _ => return Option::None,
469 };
470
471 ::core::str::FromStr::from_str(x).ok()?
472 };
473 });
474 }
475 }
476 }
477
478 let field_names = typed_fields.iter().map(|(name, _)| name);
480
481 let datapath_impl = quote! {
482 impl ::datapath::Datapath for #struct_name {
483 fn with_file(&self, file: impl ::core::convert::Into<::std::string::String>) -> ::datapath::DatapathFile<Self> {
484 ::datapath::DatapathFile {
485 path: self.clone(),
486 file: file.into(),
487 }
488 }
489
490 fn parse(path: &str) -> Option<::datapath::DatapathFile<Self>> {
491 if path.contains("\n") {
492 return Option::None;
493 }
494
495 let mut parts = path.split("/");
496
497 #(#parse_body)*
498
499 let mut file = ::std::string::String::new();
500 if let Option::Some(first) = parts.next() {
501 file.push_str(first);
502 for part in parts {
503 file.push_str("/");
504 file.push_str(part);
505 }
506 }
507
508 Option::Some(::datapath::DatapathFile {
509 path: Self { #(#field_names),* },
510 file,
511 })
512 }
513 }
514 };
515
516 (struct_def, display_impl, datapath_impl)
517}
518
519#[proc_macro]
532pub fn datapath(input: TokenStream) -> TokenStream {
533 let defs =
534 parse_macro_input!(input with Punctuated::<DatapathDef, Token![;]>::parse_terminated);
535
536 let generated = defs.into_iter().map(generate_datapath_code);
537
538 let output = quote! {
539 #(#generated)*
540 };
541
542 output.into()
543}