Skip to main content

datapath_macro/
lib.rs

1//! This crate provides a declarative macro for defining datapaths.
2
3use 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/// Represents a single datapath definition
13#[expect(clippy::large_enum_variant)]
14enum DatapathDef {
15	/// Simple syntax: `struct Name(path/segments);`
16	Simple {
17		struct_name: Ident,
18		segments: Vec<Segment>,
19		attrs: Vec<syn::Attribute>,
20	},
21	/// Schema syntax: `struct Name { pattern: path/segments, schema: Type }`
22	WithSchema {
23		struct_name: Ident,
24		segments: Vec<Segment>,
25		schema_type: Type,
26		attrs: Vec<syn::Attribute>,
27	},
28}
29
30/// Represents a segment in a datapath: either a constant or a typed field
31#[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		// Parse attributes (like #[doc = "..."])
40		let attrs = input.call(syn::Attribute::parse_outer)?;
41
42		// Parse: struct Name
43		input.parse::<Token![struct]>()?;
44		let struct_name: Ident = input.parse()?;
45
46		// Check if next is '(' or '{'
47		let lookahead = input.lookahead1();
48
49		if lookahead.peek(syn::token::Paren) {
50			// Simple syntax: struct Name(...)
51			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			// Schema syntax: struct Name { pattern: ..., schema: ... }
62			let content;
63			syn::braced!(content in input);
64
65			// Parse fields in any order
66			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						// Find the next field keyword to know where pattern ends
82						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			// Ensure required fields are present
108			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
127/// Find the next field keyword in the input stream
128fn find_next_keyword(input: ParseStream<'_>) -> Option<String> {
129	let fork = input.fork();
130
131	// Skip through tokens until we find an identifier that could be a keyword
132	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			// Try to advance past the current token
142			let _ = fork.parse::<proc_macro2::TokenTree>();
143		}
144	}
145
146	None
147}
148
149/// Parse a complete pattern (used when the entire input is the pattern)
150fn 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	// Add remaining constant if any
159	if !current_token.is_empty() {
160		segments.push(Segment::Constant(current_token));
161	}
162
163	Ok(segments)
164}
165
166/// Parse pattern until we encounter a specific keyword (like "schema")
167fn 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		// Check if next token is the stop keyword
176		if input.peek(Ident) {
177			let fork = input.fork();
178			if let Ok(ident) = fork.parse::<Ident>()
179				&& ident == stop_keyword
180			{
181				// Found the stop keyword, finalize and return
182				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	// Add remaining constant if any
193	if !current_token.is_empty() {
194		segments.push(Segment::Constant(current_token));
195	}
196
197	Ok(segments)
198}
199
200/// Parse the next segment in a pattern
201fn parse_next_segment(
202	input: ParseStream<'_>,
203	segments: &mut Vec<Segment>,
204	current_token: &mut String,
205) -> syn::Result<()> {
206	// Try to parse as string literal first (for quoted keys or constants)
207	if input.peek(syn::LitStr) {
208		let lit: syn::LitStr = input.parse()?;
209		let lit_value = lit.value();
210
211		// Check if next token is '=' (quoted partition key)
212		if input.peek(Token![=]) {
213			input.parse::<Token![=]>()?;
214			let ty: Type = input.parse()?;
215
216			// Create an Ident from the string literal value, replacing '-' with '_'
217			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			// Check for '/' separator
223			if input.peek(Token![/]) {
224				input.parse::<Token![/]>()?;
225			}
226		} else {
227			// This is a constant segment
228			if !current_token.is_empty() {
229				current_token.push('/');
230			}
231			current_token.push_str(&lit_value);
232
233			// Check for '/' separator
234			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		// Check if next token is '='
244		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			// Check for '/' separator
254			if input.peek(Token![/]) {
255				input.parse::<Token![/]>()?;
256			}
257		} else {
258			// This is a constant segment
259			if !current_token.is_empty() {
260				current_token.push('/');
261			}
262			current_token.push_str(&ident_str);
263
264			// Check for '/' separator
265			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		// Try to parse as literal (for version numbers, string literals, or plain integers)
273		let lookahead = input.lookahead1();
274
275		if lookahead.peek(syn::LitStr) {
276			// String literal segment like "dashed-path-segment"
277			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			// Check for '/' separator
284			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			// Check for '/' separator
297			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			// Check for '/' separator
310			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
323/// Generate code for a datapath definition
324fn 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
340/// Generate code for simple datapath (without schema)
341fn 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
357/// Generate code for datapath with schema
358fn 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	// Generate SchemaDatapath implementation
368	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
383/// Generate common implementations shared by both variants
384fn 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	// Extract typed fields
395	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	// Generate struct fields
404	let struct_fields = typed_fields.iter().map(|(name, ty)| {
405		quote! {
406			pub #name: #ty
407		}
408	});
409
410	// Build pattern string
411	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	// Generate Display implementation
441	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	// Generate tuple types
455	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	// Generate from_tuple implementation
472	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	// Generate to_tuple implementation
488	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	// Generate from_wildcardable implementation
496	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	// Generate parse implementation
521	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	// Extract just the field names for struct construction
552	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	// Generate From<Tuple> for Struct
614	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	// Generate From<Struct> for Tuple
623	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	// Combine both implementations
632	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/// The `datapath!` macro generates datapath struct definitions with parsing and formatting logic.
641///
642/// # Example
643/// ```ignore
644/// datapath! {
645///     struct CaptureRaw_2_0(capture/user_id=Uuid/ts=i64/raw/2.0);
646///     struct OtherPath {
647///         pattern: web/domain=String/ts=i64/raw/2.0
648///         schema: MySchema
649///     };
650/// }
651/// ```
652#[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}