From f51162478bbabbf99369d4d427556817551b3719 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:23:36 -0800 Subject: [PATCH] v0.0.2 --- Cargo.lock | 4 +- Cargo.toml | 4 +- crates/datapath-macro/src/lib.rs | 142 +++++++++++++++++++++++++--- crates/datapath/README.md | 66 ++++++++++++- crates/datapath/src/datapath.rs | 15 ++- crates/datapath/src/lib.rs | 3 + crates/datapath/src/wildcardable.rs | 77 +++++++++++++++ 7 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 crates/datapath/src/wildcardable.rs diff --git a/Cargo.lock b/Cargo.lock index fc7016f..1cdb44d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "datapath" -version = "0.0.1" +version = "0.0.2" dependencies = [ "datapath-macro", "uuid", @@ -24,7 +24,7 @@ dependencies = [ [[package]] name = "datapath-macro" -version = "0.0.1" +version = "0.0.2" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5f667c8..a054c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" authors = ["rm-dr"] # Don't forget to bump datapath-macro below! -version = "0.0.1" +version = "0.0.2" [workspace.lints.rust] unused_import_braces = "deny" @@ -70,7 +70,7 @@ cargo_common_metadata = "deny" # [workspace.dependencies] -datapath-macro = { path = "crates/datapath-macro", version = "0.0.1" } +datapath-macro = { path = "crates/datapath-macro", version = "0.0.2" } datapath = { path = "crates/datapath" } chrono = "0.4.42" diff --git a/crates/datapath-macro/src/lib.rs b/crates/datapath-macro/src/lib.rs index dde1a1b..76c28f7 100644 --- a/crates/datapath-macro/src/lib.rs +++ b/crates/datapath-macro/src/lib.rs @@ -343,13 +343,14 @@ fn generate_simple_datapath( segments: &[Segment], attrs: &[syn::Attribute], ) -> proc_macro2::TokenStream { - let (struct_def, display_impl, datapath_impl) = + let (struct_def, display_impl, datapath_impl, from_trait_impls) = generate_common_impls(struct_name, segments, attrs); quote! { #struct_def #display_impl #datapath_impl + #from_trait_impls } } @@ -360,7 +361,7 @@ fn generate_schema_datapath( schema_type: &Type, attrs: &[syn::Attribute], ) -> proc_macro2::TokenStream { - let (struct_def, display_impl, datapath_impl) = + let (struct_def, display_impl, datapath_impl, from_trait_impls) = generate_common_impls(struct_name, segments, attrs); // Generate SchemaDatapath implementation @@ -374,6 +375,7 @@ fn generate_schema_datapath( #struct_def #display_impl #datapath_impl + #from_trait_impls #schema_datapath_impl } } @@ -387,6 +389,7 @@ fn generate_common_impls( proc_macro2::TokenStream, proc_macro2::TokenStream, proc_macro2::TokenStream, + proc_macro2::TokenStream, ) { // Extract typed fields let typed_fields: Vec<_> = segments @@ -404,21 +407,25 @@ fn generate_common_impls( } }); - let mut doc_str = String::new(); - for s in segments { - if !doc_str.is_empty() { - doc_str.push('/'); - } + // Build pattern string + let pattern_str = { + let mut s = String::new(); + for seg in segments { + if !s.is_empty() { + s.push('/'); + } - match s { - Segment::Constant(x) => doc_str.push_str(x), - Segment::Typed { name, ty } => { - doc_str.push_str(&format!("{name}={}", ty.to_token_stream())) + match seg { + Segment::Constant(x) => s.push_str(x), + Segment::Typed { name, ty } => { + s.push_str(&format!("{name}={}", ty.to_token_stream())) + } } } - } + s + }; - let doc_str = format!("\n\nDatapath pattern: `{doc_str}`"); + let doc_str = format!("\n\nDatapath pattern: `{pattern_str}`"); let struct_def = quote! { #(#attrs)* @@ -444,6 +451,72 @@ fn generate_common_impls( } }; + // Generate tuple types + let tuple_type = if typed_fields.is_empty() { + quote! { () } + } else { + let field_types = typed_fields.iter().map(|(_, ty)| ty); + quote! { (#(#field_types,)*) } + }; + + let wildcardable_tuple_type = if typed_fields.is_empty() { + quote! { () } + } else { + let wildcardable_types = typed_fields.iter().map(|(_, ty)| { + quote! { ::datapath::Wildcardable<#ty> } + }); + quote! { (#(#wildcardable_types,)*) } + }; + + // Generate from_tuple implementation + let from_tuple_body = if typed_fields.is_empty() { + quote! { Self {} } + } else { + let field_assignments = typed_fields.iter().enumerate().map(|(idx, (name, _))| { + let index = syn::Index::from(idx); + quote! { #name: tuple.#index } + }); + + quote! { + Self { + #(#field_assignments),* + } + } + }; + + // Generate to_tuple implementation + let to_tuple_body = if typed_fields.is_empty() { + quote! { () } + } else { + let field_names = typed_fields.iter().map(|(name, _)| name); + quote! { (#(self.#field_names,)*) } + }; + + // Generate from_wildcardable implementation + let from_wildcardable_body = { + let mut parts = Vec::new(); + let mut field_idx = 0; + + for seg in segments { + match seg { + Segment::Constant(s) => { + parts.push(quote! { #s.to_string() }); + } + Segment::Typed { name, .. } => { + let idx = syn::Index::from(field_idx); + field_idx += 1; + parts.push(quote! { + format!("{}={}", stringify!(#name), tuple.#idx) + }); + } + } + } + + quote! { + vec![#(#parts),*].join("/") + } + }; + // Generate parse implementation let mut parse_body = Vec::new(); @@ -480,6 +553,23 @@ fn generate_common_impls( let datapath_impl = quote! { impl ::datapath::Datapath for #struct_name { + const PATTERN: &'static str = #pattern_str; + + type Tuple = #tuple_type; + type WildcardableTuple = #wildcardable_tuple_type; + + fn from_tuple(tuple: Self::Tuple) -> Self { + #from_tuple_body + } + + fn to_tuple(self) -> Self::Tuple { + #to_tuple_body + } + + fn from_wildcardable(tuple: Self::WildcardableTuple) -> ::std::string::String { + #from_wildcardable_body + } + fn with_file(&self, file: impl ::core::convert::Into<::std::string::String>) -> ::datapath::DatapathFile { ::datapath::DatapathFile { path: self.clone(), @@ -513,7 +603,31 @@ fn generate_common_impls( } }; - (struct_def, display_impl, datapath_impl) + // Generate From for Struct + let from_tuple_impl = quote! { + impl ::core::convert::From<#tuple_type> for #struct_name { + fn from(value: #tuple_type) -> Self { + ::from_tuple(value) + } + } + }; + + // Generate From for Tuple + let from_struct_impl = quote! { + impl ::core::convert::From<#struct_name> for #tuple_type { + fn from(value: #struct_name) -> Self { + <#struct_name as ::datapath::Datapath>::to_tuple(value) + } + } + }; + + // Combine both implementations + let from_trait_impls = quote! { + #from_tuple_impl + #from_struct_impl + }; + + (struct_def, display_impl, datapath_impl, from_trait_impls) } /// The `datapath!` macro generates datapath struct definitions with parsing and formatting logic. diff --git a/crates/datapath/README.md b/crates/datapath/README.md index 8bedf31..7559c26 100644 --- a/crates/datapath/README.md +++ b/crates/datapath/README.md @@ -42,7 +42,7 @@ match parsed { Associate datapaths with schema types for type-safe data handling: ```rust -use datapath::datapath; +use datapath::{datapath, Datapath}; pub struct UserEvent { pub action: String, @@ -60,10 +60,46 @@ datapath! { // EventPath::Schema == UserEvent ``` +## Pattern Introspection and Wildcards + +Access the pattern string and work with wildcarded paths: + +```rust +use datapath::{datapath, Datapath, Wildcardable}; +use uuid::Uuid; + +datapath! { + struct Metrics(metrics/service=String/timestamp=i64/v1); +} + +// Access the pattern string +// (use for logging/debug) +assert_eq!(Metrics::PATTERN, "metrics/service=String/timestamp=i64/v1"); + +// Convert to/from tuples +let metrics = Metrics { + service: "api".to_string(), + timestamp: 1234567890, +}; +let tuple = metrics.clone().to_tuple(); +assert_eq!(tuple, ("api".to_string(), 1234567890i64)); + +let recreated = Metrics::from_tuple(tuple); +assert_eq!(recreated.service, "api"); +assert_eq!(recreated.timestamp, 1234567890); + +// Create wildcarded paths for querying +let all_services = Metrics::from_wildcardable(( + Wildcardable::Star, + Wildcardable::Value(1234567890i64), +)); +assert_eq!(all_services, "metrics/service=*/timestamp=1234567890/v1"); +``` + ## Examples ```rust -use datapath::datapath; +use datapath::{datapath, Datapath}; pub struct MetricsSchema; @@ -86,4 +122,30 @@ datapath! { schema: MetricsSchema }; } +``` + +### Constant-Only Paths + +Paths with no typed fields work correctly with empty tuples: + +```rust +use datapath::{datapath, Datapath}; + +datapath! { + struct ConstantPath(assets/data/"v1.0"); +} + +// PATTERN works for constant-only paths +assert_eq!(ConstantPath::PATTERN, "assets/data/v1.0"); + +// Tuple type is unit () +let empty_tuple = ConstantPath {}.to_tuple(); +assert_eq!(empty_tuple, ()); + +let path = ConstantPath::from_tuple(()); +assert_eq!(format!("{}", path), "assets/data/v1.0"); + +// from_wildcardable also works (no wildcards possible) +let path_str = ConstantPath::from_wildcardable(()); +assert_eq!(path_str, "assets/data/v1.0"); ``` \ No newline at end of file diff --git a/crates/datapath/src/datapath.rs b/crates/datapath/src/datapath.rs index d01b82a..ff42aff 100644 --- a/crates/datapath/src/datapath.rs +++ b/crates/datapath/src/datapath.rs @@ -12,7 +12,20 @@ where Self: Eq + PartialEq + Hash, Self: Debug + Display, { - // Default + /// The exact pattern string passed to the macro that generated this struct + const PATTERN: &'static str; + + /// A tuple of this path's parameter types, in the order they appear in the pattern + type Tuple; + + /// [Datapath::Tuple], but each type is wrapped in a [crate::Wildcardable]. + type WildcardableTuple; + + fn from_tuple(tuple: Self::Tuple) -> Self; + fn to_tuple(self) -> Self::Tuple; + + /// Return a string where wildcarded partitions are `*`. + fn from_wildcardable(tuple: Self::WildcardableTuple) -> String; /// Returns a [DatapathFile] with the given file at this datapath fn with_file(&self, file: impl Into) -> DatapathFile; diff --git a/crates/datapath/src/lib.rs b/crates/datapath/src/lib.rs index 58b7f2d..e0da51a 100644 --- a/crates/datapath/src/lib.rs +++ b/crates/datapath/src/lib.rs @@ -16,4 +16,7 @@ pub use datapathfile::*; mod schema; pub use schema::*; +mod wildcardable; +pub use wildcardable::*; + pub use datapath_macro::datapath; diff --git a/crates/datapath/src/wildcardable.rs b/crates/datapath/src/wildcardable.rs new file mode 100644 index 0000000..e96ac36 --- /dev/null +++ b/crates/datapath/src/wildcardable.rs @@ -0,0 +1,77 @@ +use std::{ + fmt::{Debug, Display}, + hash::Hash, + str::FromStr, +}; + +/// A wrapper for wildcardable partition values. +/// Allows us to specify, for example, `ts=1337` and `ts=*`. +#[derive(Debug, PartialEq, Eq, Hash, Default)] +pub enum Wildcardable { + /// This value is wildcarded with a star, + /// as in `ts=*` + #[default] + Star, + + /// This value is explicitly given, + /// as in `ts=1337` + Value(T), +} + +impl Wildcardable { + pub fn inner(&self) -> Option<&T> { + match self { + Self::Star => None, + Self::Value(x) => Some(x), + } + } + + pub fn into_inner(self) -> Option { + match self { + Self::Star => None, + Self::Value(x) => Some(x), + } + } +} + +impl Copy for Wildcardable {} + +impl Clone for Wildcardable { + fn clone(&self) -> Self { + match self { + Self::Star => Self::Star, + Self::Value(x) => Self::Value(x.clone()), + } + } +} + +impl Display for Wildcardable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Star => write!(f, "*"), + Self::Value(x) => write!(f, "{x}"), + } + } +} + +impl FromStr for Wildcardable { + type Err = (); + fn from_str(s: &str) -> Result { + return Ok(match s { + "*" => Self::Star, + value => Self::Value(value.parse().map_err(|_err| ())?), + }); + } +} + +impl From for Wildcardable { + fn from(value: T) -> Self { + Self::Value(value) + } +} + +impl From> for Option { + fn from(value: Wildcardable) -> Self { + value.into_inner() + } +}