mirror of
https://github.com/rm-dr/datapath.git
synced 2025-12-07 20:04:13 -08:00
v0.0.2
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
```
|
||||
@@ -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>;
|
||||
|
||||
@@ -16,4 +16,7 @@ pub use datapathfile::*;
|
||||
mod schema;
|
||||
pub use schema::*;
|
||||
|
||||
mod wildcardable;
|
||||
pub use wildcardable::*;
|
||||
|
||||
pub use datapath_macro::datapath;
|
||||
|
||||
77
crates/datapath/src/wildcardable.rs
Normal file
77
crates/datapath/src/wildcardable.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user