mirror of
https://github.com/rm-dr/datapath.git
synced 2025-12-10 05:14: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]]
|
[[package]]
|
||||||
name = "datapath"
|
name = "datapath"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"datapath-macro",
|
"datapath-macro",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -24,7 +24,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "datapath-macro"
|
name = "datapath-macro"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ readme = "README.md"
|
|||||||
authors = ["rm-dr"]
|
authors = ["rm-dr"]
|
||||||
|
|
||||||
# Don't forget to bump datapath-macro below!
|
# Don't forget to bump datapath-macro below!
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unused_import_braces = "deny"
|
unused_import_braces = "deny"
|
||||||
@@ -70,7 +70,7 @@ cargo_common_metadata = "deny"
|
|||||||
#
|
#
|
||||||
|
|
||||||
[workspace.dependencies]
|
[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" }
|
datapath = { path = "crates/datapath" }
|
||||||
|
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
|
|||||||
@@ -343,13 +343,14 @@ fn generate_simple_datapath(
|
|||||||
segments: &[Segment],
|
segments: &[Segment],
|
||||||
attrs: &[syn::Attribute],
|
attrs: &[syn::Attribute],
|
||||||
) -> proc_macro2::TokenStream {
|
) -> 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_common_impls(struct_name, segments, attrs);
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#struct_def
|
#struct_def
|
||||||
#display_impl
|
#display_impl
|
||||||
#datapath_impl
|
#datapath_impl
|
||||||
|
#from_trait_impls
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +361,7 @@ fn generate_schema_datapath(
|
|||||||
schema_type: &Type,
|
schema_type: &Type,
|
||||||
attrs: &[syn::Attribute],
|
attrs: &[syn::Attribute],
|
||||||
) -> proc_macro2::TokenStream {
|
) -> 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_common_impls(struct_name, segments, attrs);
|
||||||
|
|
||||||
// Generate SchemaDatapath implementation
|
// Generate SchemaDatapath implementation
|
||||||
@@ -374,6 +375,7 @@ fn generate_schema_datapath(
|
|||||||
#struct_def
|
#struct_def
|
||||||
#display_impl
|
#display_impl
|
||||||
#datapath_impl
|
#datapath_impl
|
||||||
|
#from_trait_impls
|
||||||
#schema_datapath_impl
|
#schema_datapath_impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,6 +389,7 @@ fn generate_common_impls(
|
|||||||
proc_macro2::TokenStream,
|
proc_macro2::TokenStream,
|
||||||
proc_macro2::TokenStream,
|
proc_macro2::TokenStream,
|
||||||
proc_macro2::TokenStream,
|
proc_macro2::TokenStream,
|
||||||
|
proc_macro2::TokenStream,
|
||||||
) {
|
) {
|
||||||
// Extract typed fields
|
// Extract typed fields
|
||||||
let typed_fields: Vec<_> = segments
|
let typed_fields: Vec<_> = segments
|
||||||
@@ -404,21 +407,25 @@ fn generate_common_impls(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut doc_str = String::new();
|
// Build pattern string
|
||||||
for s in segments {
|
let pattern_str = {
|
||||||
if !doc_str.is_empty() {
|
let mut s = String::new();
|
||||||
doc_str.push('/');
|
for seg in segments {
|
||||||
}
|
if !s.is_empty() {
|
||||||
|
s.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
match s {
|
match seg {
|
||||||
Segment::Constant(x) => doc_str.push_str(x),
|
Segment::Constant(x) => s.push_str(x),
|
||||||
Segment::Typed { name, ty } => {
|
Segment::Typed { name, ty } => {
|
||||||
doc_str.push_str(&format!("{name}={}", ty.to_token_stream()))
|
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! {
|
let struct_def = quote! {
|
||||||
#(#attrs)*
|
#(#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
|
// Generate parse implementation
|
||||||
let mut parse_body = Vec::new();
|
let mut parse_body = Vec::new();
|
||||||
|
|
||||||
@@ -480,6 +553,23 @@ fn generate_common_impls(
|
|||||||
|
|
||||||
let datapath_impl = quote! {
|
let datapath_impl = quote! {
|
||||||
impl ::datapath::Datapath for #struct_name {
|
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> {
|
fn with_file(&self, file: impl ::core::convert::Into<::std::string::String>) -> ::datapath::DatapathFile<Self> {
|
||||||
::datapath::DatapathFile {
|
::datapath::DatapathFile {
|
||||||
path: self.clone(),
|
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.
|
/// 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:
|
Associate datapaths with schema types for type-safe data handling:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use datapath::datapath;
|
use datapath::{datapath, Datapath};
|
||||||
|
|
||||||
pub struct UserEvent {
|
pub struct UserEvent {
|
||||||
pub action: String,
|
pub action: String,
|
||||||
@@ -60,10 +60,46 @@ datapath! {
|
|||||||
// EventPath::Schema == UserEvent
|
// 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
|
## Examples
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use datapath::datapath;
|
use datapath::{datapath, Datapath};
|
||||||
|
|
||||||
pub struct MetricsSchema;
|
pub struct MetricsSchema;
|
||||||
|
|
||||||
@@ -86,4 +122,30 @@ datapath! {
|
|||||||
schema: MetricsSchema
|
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: Eq + PartialEq + Hash,
|
||||||
Self: Debug + Display,
|
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
|
/// Returns a [DatapathFile] with the given file at this datapath
|
||||||
fn with_file(&self, file: impl Into<String>) -> DatapathFile<Self>;
|
fn with_file(&self, file: impl Into<String>) -> DatapathFile<Self>;
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ pub use datapathfile::*;
|
|||||||
mod schema;
|
mod schema;
|
||||||
pub use schema::*;
|
pub use schema::*;
|
||||||
|
|
||||||
|
mod wildcardable;
|
||||||
|
pub use wildcardable::*;
|
||||||
|
|
||||||
pub use datapath_macro::datapath;
|
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