1
0
mirror of https://github.com/rm-dr/datapath.git synced 2025-12-07 20:04:13 -08:00
This commit is contained in:
2025-12-02 22:23:36 -08:00
committed by Mark
parent dff5acc737
commit f51162478b
7 changed files with 290 additions and 21 deletions

4
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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<Self> {
::datapath::DatapathFile {
path: self.clone(),
@@ -513,7 +603,31 @@ fn generate_common_impls(
}
};
(struct_def, display_impl, datapath_impl)
// Generate From<Tuple> for Struct
let from_tuple_impl = quote! {
impl ::core::convert::From<#tuple_type> for #struct_name {
fn from(value: #tuple_type) -> Self {
<Self as ::datapath::Datapath>::from_tuple(value)
}
}
};
// Generate From<Struct> 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.

View File

@@ -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");
```

View File

@@ -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<String>) -> DatapathFile<Self>;

View File

@@ -16,4 +16,7 @@ pub use datapathfile::*;
mod schema;
pub use schema::*;
mod wildcardable;
pub use wildcardable::*;
pub use datapath_macro::datapath;

View File

@@ -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<T: FromStr + Display + Debug + Eq + PartialEq + Hash> {
/// This value is wildcarded with a star,
/// as in `ts=*`
#[default]
Star,
/// This value is explicitly given,
/// as in `ts=1337`
Value(T),
}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash> Wildcardable<T> {
pub fn inner(&self) -> Option<&T> {
match self {
Self::Star => None,
Self::Value(x) => Some(x),
}
}
pub fn into_inner(self) -> Option<T> {
match self {
Self::Star => None,
Self::Value(x) => Some(x),
}
}
}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash + Copy> Copy for Wildcardable<T> {}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash + Clone> Clone for Wildcardable<T> {
fn clone(&self) -> Self {
match self {
Self::Star => Self::Star,
Self::Value(x) => Self::Value(x.clone()),
}
}
}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash> Display for Wildcardable<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Star => write!(f, "*"),
Self::Value(x) => write!(f, "{x}"),
}
}
}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash> FromStr for Wildcardable<T> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
return Ok(match s {
"*" => Self::Star,
value => Self::Value(value.parse().map_err(|_err| ())?),
});
}
}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash> From<T> for Wildcardable<T> {
fn from(value: T) -> Self {
Self::Value(value)
}
}
impl<T: FromStr + Display + Debug + Eq + PartialEq + Hash> From<Wildcardable<T>> for Option<T> {
fn from(value: Wildcardable<T>) -> Self {
value.into_inner()
}
}