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, from_trait_impls) =
347 generate_common_impls(struct_name, segments, attrs);
348
349 quote! {
350 #struct_def
351 #display_impl
352 #datapath_impl
353 #from_trait_impls
354 }
355}
356
357fn generate_schema_datapath(
359 struct_name: &Ident,
360 segments: &[Segment],
361 schema_type: &Type,
362 attrs: &[syn::Attribute],
363) -> proc_macro2::TokenStream {
364 let (struct_def, display_impl, datapath_impl, from_trait_impls) =
365 generate_common_impls(struct_name, segments, attrs);
366
367 let schema_datapath_impl = quote! {
369 impl ::datapath::SchemaDatapath for #struct_name {
370 type Schema = #schema_type;
371 }
372 };
373
374 quote! {
375 #struct_def
376 #display_impl
377 #datapath_impl
378 #from_trait_impls
379 #schema_datapath_impl
380 }
381}
382
383fn generate_common_impls(
385 struct_name: &Ident,
386 segments: &[Segment],
387 attrs: &[syn::Attribute],
388) -> (
389 proc_macro2::TokenStream,
390 proc_macro2::TokenStream,
391 proc_macro2::TokenStream,
392 proc_macro2::TokenStream,
393) {
394 let typed_fields: Vec<_> = segments
396 .iter()
397 .filter_map(|seg| match seg {
398 Segment::Typed { name, ty } => Some((name, ty)),
399 _ => None,
400 })
401 .collect();
402
403 let struct_fields = typed_fields.iter().map(|(name, ty)| {
405 quote! {
406 pub #name: #ty
407 }
408 });
409
410 let pattern_str = {
412 let mut s = String::new();
413 for seg in segments {
414 if !s.is_empty() {
415 s.push('/');
416 }
417
418 match seg {
419 Segment::Constant(x) => s.push_str(x),
420 Segment::Typed { name, ty } => {
421 s.push_str(&format!("{name}={}", ty.to_token_stream()))
422 }
423 }
424 }
425 s
426 };
427
428 let doc_str = format!("\n\nDatapath pattern: `{pattern_str}`");
429
430 let struct_def = quote! {
431 #(#attrs)*
432 #[allow(non_camel_case_types)]
433 #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::cmp::PartialEq, ::core::cmp::Eq, ::core::hash::Hash)]
434 #[doc = #doc_str]
435 pub struct #struct_name {
436 #(#struct_fields),*
437 }
438 };
439
440 let display_parts = segments.iter().map(|seg| match seg {
442 Segment::Constant(s) => quote! { #s.to_string() },
443 Segment::Typed { name, .. } => quote! { format!("{}={}", stringify!(#name), self.#name) },
444 });
445
446 let display_impl = quote! {
447 impl ::core::fmt::Display for #struct_name {
448 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
449 write!(f, "{}", vec![#(#display_parts),*].join("/"))
450 }
451 }
452 };
453
454 let tuple_type = if typed_fields.is_empty() {
456 quote! { () }
457 } else {
458 let field_types = typed_fields.iter().map(|(_, ty)| ty);
459 quote! { (#(#field_types,)*) }
460 };
461
462 let wildcardable_tuple_type = if typed_fields.is_empty() {
463 quote! { () }
464 } else {
465 let wildcardable_types = typed_fields.iter().map(|(_, ty)| {
466 quote! { ::datapath::Wildcardable<#ty> }
467 });
468 quote! { (#(#wildcardable_types,)*) }
469 };
470
471 let from_tuple_body = if typed_fields.is_empty() {
473 quote! { Self {} }
474 } else {
475 let field_assignments = typed_fields.iter().enumerate().map(|(idx, (name, _))| {
476 let index = syn::Index::from(idx);
477 quote! { #name: tuple.#index }
478 });
479
480 quote! {
481 Self {
482 #(#field_assignments),*
483 }
484 }
485 };
486
487 let to_tuple_body = if typed_fields.is_empty() {
489 quote! { () }
490 } else {
491 let field_names = typed_fields.iter().map(|(name, _)| name);
492 quote! { (#(self.#field_names,)*) }
493 };
494
495 let from_wildcardable_body = {
497 let mut parts = Vec::new();
498 let mut field_idx = 0;
499
500 for seg in segments {
501 match seg {
502 Segment::Constant(s) => {
503 parts.push(quote! { #s.to_string() });
504 }
505 Segment::Typed { name, .. } => {
506 let idx = syn::Index::from(field_idx);
507 field_idx += 1;
508 parts.push(quote! {
509 format!("{}={}", stringify!(#name), tuple.#idx)
510 });
511 }
512 }
513 }
514
515 quote! {
516 vec![#(#parts),*].join("/")
517 }
518 };
519
520 let mut parse_body = Vec::new();
522
523 for seg in segments {
524 match seg {
525 Segment::Constant(s) => {
526 parse_body.push(quote! {
527 {
528 match parts.next() {
529 Option::Some(#s) => {}
530 _ => return Option::None,
531 }
532 }
533 });
534 }
535 Segment::Typed { name, ty } => {
536 let name_str = name.to_string();
537 parse_body.push(quote! {
538 let #name: #ty = {
539 let x = match parts.next() {
540 Option::Some(x) => x.strip_prefix(concat!(#name_str, "="))?,
541 _ => return Option::None,
542 };
543
544 ::core::str::FromStr::from_str(x).ok()?
545 };
546 });
547 }
548 }
549 }
550
551 let field_names: Vec<_> = typed_fields.iter().map(|(name, _)| name).collect();
553
554 let datapath_impl = quote! {
555 impl ::datapath::Datapath for #struct_name {
556 const PATTERN: &'static str = #pattern_str;
557
558 type Tuple = #tuple_type;
559 type WildcardableTuple = #wildcardable_tuple_type;
560
561 fn from_tuple(tuple: Self::Tuple) -> Self {
562 #from_tuple_body
563 }
564
565 fn to_tuple(self) -> Self::Tuple {
566 #to_tuple_body
567 }
568
569 fn from_wildcardable(tuple: Self::WildcardableTuple) -> ::std::string::String {
570 #from_wildcardable_body
571 }
572
573 fn with_file(&self, file: impl ::core::convert::Into<::std::string::String>) -> ::datapath::DatapathFile<Self> {
574 ::datapath::DatapathFile {
575 path: self.clone(),
576 file: file.into(),
577 }
578 }
579
580 fn parse(path: &str) -> Option<::datapath::DatapathFile<Self>> {
581 if path.contains("\n") {
582 return Option::None;
583 }
584
585 let mut parts = path.split("/");
586
587 #(#parse_body)*
588
589 let mut file = ::std::string::String::new();
590 if let Option::Some(first) = parts.next() {
591 file.push_str(first);
592 for part in parts {
593 file.push_str("/");
594 file.push_str(part);
595 }
596 }
597
598 Option::Some(::datapath::DatapathFile {
599 path: Self { #(#field_names),* },
600 file,
601 })
602 }
603
604 fn field(&self, name: &str) -> Option<::std::string::String> {
605 match name {
606 #(stringify!(#field_names) => Some(self.#field_names.to_string()),)*
607 _ => None,
608 }
609 }
610 }
611 };
612
613 let from_tuple_impl = quote! {
615 impl ::core::convert::From<#tuple_type> for #struct_name {
616 fn from(value: #tuple_type) -> Self {
617 <Self as ::datapath::Datapath>::from_tuple(value)
618 }
619 }
620 };
621
622 let from_struct_impl = quote! {
624 impl ::core::convert::From<#struct_name> for #tuple_type {
625 fn from(value: #struct_name) -> Self {
626 <#struct_name as ::datapath::Datapath>::to_tuple(value)
627 }
628 }
629 };
630
631 let from_trait_impls = quote! {
633 #from_tuple_impl
634 #from_struct_impl
635 };
636
637 (struct_def, display_impl, datapath_impl, from_trait_impls)
638}
639
640#[proc_macro]
653pub fn datapath(input: TokenStream) -> TokenStream {
654 let defs =
655 parse_macro_input!(input with Punctuated::<DatapathDef, Token![;]>::parse_terminated);
656
657 let generated = defs.into_iter().map(generate_datapath_code);
658
659 let output = quote! {
660 #(#generated)*
661 };
662
663 output.into()
664}