mirror of
https://github.com/rm-dr/datapath.git
synced 2025-12-07 20:04:13 -08:00
Initial version: v0.0.1
This commit is contained in:
184
Cargo.lock
generated
Normal file
184
Cargo.lock
generated
Normal file
@@ -0,0 +1,184 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "datapath"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"datapath-macro",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "datapath-macro"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.178"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
81
Cargo.toml
Normal file
81
Cargo.toml
Normal file
@@ -0,0 +1,81 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.90.0"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/rm-dr/datapath"
|
||||
readme = "README.md"
|
||||
authors = ["rm-dr"]
|
||||
|
||||
# Don't forget to bump datapath-macro below!
|
||||
version = "0.0.1"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unused_import_braces = "deny"
|
||||
unit_bindings = "deny"
|
||||
single_use_lifetimes = "deny"
|
||||
non_ascii_idents = "deny"
|
||||
macro_use_extern_crate = "deny"
|
||||
elided_lifetimes_in_paths = "deny"
|
||||
absolute_paths_not_starting_with_crate = "deny"
|
||||
explicit_outlives_requirements = "warn"
|
||||
unused_crate_dependencies = "warn"
|
||||
redundant_lifetimes = "warn"
|
||||
missing_docs = "allow"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
todo = "deny"
|
||||
uninlined_format_args = "allow"
|
||||
result_large_err = "allow"
|
||||
too_many_arguments = "allow"
|
||||
upper_case_acronyms = "deny"
|
||||
needless_return = "allow"
|
||||
new_without_default = "allow"
|
||||
tabs_in_doc_comments = "allow"
|
||||
dbg_macro = "deny"
|
||||
allow_attributes = "deny"
|
||||
create_dir = "deny"
|
||||
filetype_is_file = "deny"
|
||||
integer_division = "allow"
|
||||
lossy_float_literal = "deny"
|
||||
map_err_ignore = "deny"
|
||||
mutex_atomic = "deny"
|
||||
needless_raw_strings = "deny"
|
||||
str_to_string = "deny"
|
||||
string_add = "deny"
|
||||
use_debug = "allow"
|
||||
verbose_file_reads = "deny"
|
||||
large_types_passed_by_value = "deny"
|
||||
wildcard_dependencies = "deny"
|
||||
negative_feature_names = "deny"
|
||||
redundant_feature_names = "deny"
|
||||
multiple_crate_versions = "deny"
|
||||
missing_safety_doc = "warn"
|
||||
identity_op = "allow"
|
||||
print_stderr = "deny"
|
||||
print_stdout = "deny"
|
||||
comparison_chain = "allow"
|
||||
unimplemented = "deny"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
type_complexity = "allow"
|
||||
cargo_common_metadata = "deny"
|
||||
|
||||
|
||||
#
|
||||
# MARK: dependencies
|
||||
#
|
||||
|
||||
[workspace.dependencies]
|
||||
datapath-macro = { path = "crates/datapath-macro", version = "0.0.1" }
|
||||
datapath = { path = "crates/datapath" }
|
||||
|
||||
chrono = "0.4.42"
|
||||
proc-macro2 = "1.0.103"
|
||||
quote = "1.0.42"
|
||||
syn = "2.0.111"
|
||||
|
||||
uuid = "1.19.0"
|
||||
23
crates/datapath-macro/Cargo.toml
Normal file
23
crates/datapath-macro/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "datapath-macro"
|
||||
description = "Type-safe structured paths with partitions, parsing, and schema associations. Dependency for the `datapath` crate."
|
||||
keywords = ["path", "filesystem", "parsing", "typed", "schema"]
|
||||
categories = ["filesystem", "data-structures", "parsing"]
|
||||
version = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
readme = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
543
crates/datapath-macro/src/lib.rs
Normal file
543
crates/datapath-macro/src/lib.rs
Normal file
@@ -0,0 +1,543 @@
|
||||
//! This crate provides a declarative macro for defining datapaths.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{
|
||||
Ident, Token, Type,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_macro_input,
|
||||
punctuated::Punctuated,
|
||||
};
|
||||
|
||||
/// Represents a single datapath definition
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
enum DatapathDef {
|
||||
/// Simple syntax: `struct Name(path/segments);`
|
||||
Simple {
|
||||
struct_name: Ident,
|
||||
segments: Vec<Segment>,
|
||||
attrs: Vec<syn::Attribute>,
|
||||
},
|
||||
/// Schema syntax: `struct Name { pattern: path/segments, schema: Type }`
|
||||
WithSchema {
|
||||
struct_name: Ident,
|
||||
segments: Vec<Segment>,
|
||||
schema_type: Type,
|
||||
attrs: Vec<syn::Attribute>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Represents a segment in a datapath: either a constant or a typed field
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
enum Segment {
|
||||
Constant(String),
|
||||
Typed { name: Ident, ty: Type },
|
||||
}
|
||||
|
||||
impl Parse for DatapathDef {
|
||||
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
||||
// Parse attributes (like #[doc = "..."])
|
||||
let attrs = input.call(syn::Attribute::parse_outer)?;
|
||||
|
||||
// Parse: struct Name
|
||||
input.parse::<Token![struct]>()?;
|
||||
let struct_name: Ident = input.parse()?;
|
||||
|
||||
// Check if next is '(' or '{'
|
||||
let lookahead = input.lookahead1();
|
||||
|
||||
if lookahead.peek(syn::token::Paren) {
|
||||
// Simple syntax: struct Name(...)
|
||||
let content;
|
||||
syn::parenthesized!(content in input);
|
||||
let segments = parse_pattern(&content)?;
|
||||
|
||||
Ok(DatapathDef::Simple {
|
||||
struct_name,
|
||||
segments,
|
||||
attrs,
|
||||
})
|
||||
} else if lookahead.peek(syn::token::Brace) {
|
||||
// Schema syntax: struct Name { pattern: ..., schema: ... }
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
|
||||
// Parse fields in any order
|
||||
let mut segments = None;
|
||||
let mut schema_type = None;
|
||||
|
||||
while !content.is_empty() {
|
||||
let field_name: Ident = content.parse()?;
|
||||
content.parse::<Token![:]>()?;
|
||||
|
||||
match field_name.to_string().as_str() {
|
||||
"pattern" => {
|
||||
if segments.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
field_name,
|
||||
"duplicate 'pattern' field",
|
||||
));
|
||||
}
|
||||
// Find the next field keyword to know where pattern ends
|
||||
let next_keyword = find_next_keyword(&content);
|
||||
segments = Some(if let Some(kw) = next_keyword {
|
||||
parse_pattern_until_keyword(&content, &kw)?
|
||||
} else {
|
||||
parse_pattern(&content)?
|
||||
});
|
||||
}
|
||||
"schema" => {
|
||||
if schema_type.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
field_name,
|
||||
"duplicate 'schema' field",
|
||||
));
|
||||
}
|
||||
schema_type = Some(content.parse()?);
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
field_name,
|
||||
"unknown field, expected 'pattern' or 'schema'",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure required fields are present
|
||||
let segments = segments.ok_or_else(|| {
|
||||
syn::Error::new(content.span(), "missing required field 'pattern'")
|
||||
})?;
|
||||
let schema_type = schema_type.ok_or_else(|| {
|
||||
syn::Error::new(content.span(), "missing required field 'schema'")
|
||||
})?;
|
||||
|
||||
Ok(DatapathDef::WithSchema {
|
||||
struct_name,
|
||||
segments,
|
||||
schema_type,
|
||||
attrs,
|
||||
})
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the next field keyword in the input stream
|
||||
fn find_next_keyword(input: ParseStream<'_>) -> Option<String> {
|
||||
let fork = input.fork();
|
||||
|
||||
// Skip through tokens until we find an identifier that could be a keyword
|
||||
while !fork.is_empty() {
|
||||
if fork.peek(Ident) {
|
||||
if let Ok(ident) = fork.parse::<Ident>() {
|
||||
let ident_str = ident.to_string();
|
||||
if ident_str == "schema" {
|
||||
return Some(ident_str);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to advance past the current token
|
||||
let _ = fork.parse::<proc_macro2::TokenTree>();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse a complete pattern (used when the entire input is the pattern)
|
||||
fn parse_pattern(input: ParseStream<'_>) -> syn::Result<Vec<Segment>> {
|
||||
let mut segments = Vec::new();
|
||||
let mut current_token = String::new();
|
||||
|
||||
while !input.is_empty() {
|
||||
parse_next_segment(input, &mut segments, &mut current_token)?;
|
||||
}
|
||||
|
||||
// Add remaining constant if any
|
||||
if !current_token.is_empty() {
|
||||
segments.push(Segment::Constant(current_token));
|
||||
}
|
||||
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
/// Parse pattern until we encounter a specific keyword (like "schema")
|
||||
fn parse_pattern_until_keyword(
|
||||
input: ParseStream<'_>,
|
||||
stop_keyword: &str,
|
||||
) -> syn::Result<Vec<Segment>> {
|
||||
let mut segments = Vec::new();
|
||||
let mut current_token = String::new();
|
||||
|
||||
while !input.is_empty() {
|
||||
// Check if next token is the stop keyword
|
||||
if input.peek(Ident) {
|
||||
let fork = input.fork();
|
||||
if let Ok(ident) = fork.parse::<Ident>()
|
||||
&& ident == stop_keyword
|
||||
{
|
||||
// Found the stop keyword, finalize and return
|
||||
if !current_token.is_empty() {
|
||||
segments.push(Segment::Constant(current_token));
|
||||
}
|
||||
return Ok(segments);
|
||||
}
|
||||
}
|
||||
|
||||
parse_next_segment(input, &mut segments, &mut current_token)?;
|
||||
}
|
||||
|
||||
// Add remaining constant if any
|
||||
if !current_token.is_empty() {
|
||||
segments.push(Segment::Constant(current_token));
|
||||
}
|
||||
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
/// Parse the next segment in a pattern
|
||||
fn parse_next_segment(
|
||||
input: ParseStream<'_>,
|
||||
segments: &mut Vec<Segment>,
|
||||
current_token: &mut String,
|
||||
) -> syn::Result<()> {
|
||||
// Try to parse as string literal first (for quoted keys or constants)
|
||||
if input.peek(syn::LitStr) {
|
||||
let lit: syn::LitStr = input.parse()?;
|
||||
let lit_value = lit.value();
|
||||
|
||||
// Check if next token is '=' (quoted partition key)
|
||||
if input.peek(Token![=]) {
|
||||
input.parse::<Token![=]>()?;
|
||||
let ty: Type = input.parse()?;
|
||||
|
||||
// Create an Ident from the string literal value, replacing '-' with '_'
|
||||
let ident_str = lit_value.replace('-', "_");
|
||||
let ident = Ident::new(&ident_str, lit.span());
|
||||
|
||||
segments.push(Segment::Typed { name: ident, ty });
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
}
|
||||
} else {
|
||||
// This is a constant segment
|
||||
if !current_token.is_empty() {
|
||||
current_token.push('/');
|
||||
}
|
||||
current_token.push_str(&lit_value);
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
segments.push(Segment::Constant(current_token.clone()));
|
||||
current_token.clear();
|
||||
}
|
||||
}
|
||||
} else if let Ok(ident) = input.parse::<Ident>() {
|
||||
let ident_str = ident.to_string();
|
||||
|
||||
// Check if next token is '='
|
||||
if input.peek(Token![=]) {
|
||||
input.parse::<Token![=]>()?;
|
||||
let ty: Type = input.parse()?;
|
||||
|
||||
segments.push(Segment::Typed {
|
||||
name: ident.clone(),
|
||||
ty,
|
||||
});
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
}
|
||||
} else {
|
||||
// This is a constant segment
|
||||
if !current_token.is_empty() {
|
||||
current_token.push('/');
|
||||
}
|
||||
current_token.push_str(&ident_str);
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
segments.push(Segment::Constant(current_token.clone()));
|
||||
current_token.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to parse as literal (for version numbers, string literals, or plain integers)
|
||||
let lookahead = input.lookahead1();
|
||||
|
||||
if lookahead.peek(syn::LitStr) {
|
||||
// String literal segment like "dashed-path-segment"
|
||||
let lit: syn::LitStr = input.parse()?;
|
||||
if !current_token.is_empty() {
|
||||
current_token.push('/');
|
||||
}
|
||||
current_token.push_str(&lit.value());
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
segments.push(Segment::Constant(current_token.clone()));
|
||||
current_token.clear();
|
||||
}
|
||||
} else if lookahead.peek(syn::LitFloat) {
|
||||
let lit: syn::LitFloat = input.parse()?;
|
||||
if !current_token.is_empty() {
|
||||
current_token.push('/');
|
||||
}
|
||||
current_token.push_str(&lit.to_string());
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
segments.push(Segment::Constant(current_token.clone()));
|
||||
current_token.clear();
|
||||
}
|
||||
} else if lookahead.peek(syn::LitInt) {
|
||||
let lit: syn::LitInt = input.parse()?;
|
||||
if !current_token.is_empty() {
|
||||
current_token.push('/');
|
||||
}
|
||||
current_token.push_str(&lit.to_string());
|
||||
|
||||
// Check for '/' separator
|
||||
if input.peek(Token![/]) {
|
||||
input.parse::<Token![/]>()?;
|
||||
segments.push(Segment::Constant(current_token.clone()));
|
||||
current_token.clear();
|
||||
}
|
||||
} else {
|
||||
return Err(lookahead.error());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate code for a datapath definition
|
||||
fn generate_datapath_code(def: DatapathDef) -> proc_macro2::TokenStream {
|
||||
match def {
|
||||
DatapathDef::Simple {
|
||||
struct_name,
|
||||
segments,
|
||||
attrs,
|
||||
} => generate_simple_datapath(&struct_name, &segments, &attrs),
|
||||
DatapathDef::WithSchema {
|
||||
struct_name,
|
||||
segments,
|
||||
schema_type,
|
||||
attrs,
|
||||
} => generate_schema_datapath(&struct_name, &segments, &schema_type, &attrs),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate code for simple datapath (without schema)
|
||||
fn generate_simple_datapath(
|
||||
struct_name: &Ident,
|
||||
segments: &[Segment],
|
||||
attrs: &[syn::Attribute],
|
||||
) -> proc_macro2::TokenStream {
|
||||
let (struct_def, display_impl, datapath_impl) =
|
||||
generate_common_impls(struct_name, segments, attrs);
|
||||
|
||||
quote! {
|
||||
#struct_def
|
||||
#display_impl
|
||||
#datapath_impl
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate code for datapath with schema
|
||||
fn generate_schema_datapath(
|
||||
struct_name: &Ident,
|
||||
segments: &[Segment],
|
||||
schema_type: &Type,
|
||||
attrs: &[syn::Attribute],
|
||||
) -> proc_macro2::TokenStream {
|
||||
let (struct_def, display_impl, datapath_impl) =
|
||||
generate_common_impls(struct_name, segments, attrs);
|
||||
|
||||
// Generate SchemaDatapath implementation
|
||||
let schema_datapath_impl = quote! {
|
||||
impl ::datapath::SchemaDatapath for #struct_name {
|
||||
type Schema = #schema_type;
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#struct_def
|
||||
#display_impl
|
||||
#datapath_impl
|
||||
#schema_datapath_impl
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate common implementations shared by both variants
|
||||
fn generate_common_impls(
|
||||
struct_name: &Ident,
|
||||
segments: &[Segment],
|
||||
attrs: &[syn::Attribute],
|
||||
) -> (
|
||||
proc_macro2::TokenStream,
|
||||
proc_macro2::TokenStream,
|
||||
proc_macro2::TokenStream,
|
||||
) {
|
||||
// Extract typed fields
|
||||
let typed_fields: Vec<_> = segments
|
||||
.iter()
|
||||
.filter_map(|seg| match seg {
|
||||
Segment::Typed { name, ty } => Some((name, ty)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Generate struct fields
|
||||
let struct_fields = typed_fields.iter().map(|(name, ty)| {
|
||||
quote! {
|
||||
pub #name: #ty
|
||||
}
|
||||
});
|
||||
|
||||
let mut doc_str = String::new();
|
||||
for s in segments {
|
||||
if !doc_str.is_empty() {
|
||||
doc_str.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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let doc_str = format!("\n\nDatapath pattern: `{doc_str}`");
|
||||
|
||||
let struct_def = quote! {
|
||||
#(#attrs)*
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::cmp::PartialEq, ::core::cmp::Eq, ::core::hash::Hash)]
|
||||
#[doc = #doc_str]
|
||||
pub struct #struct_name {
|
||||
#(#struct_fields),*
|
||||
}
|
||||
};
|
||||
|
||||
// Generate Display implementation
|
||||
let display_parts = segments.iter().map(|seg| match seg {
|
||||
Segment::Constant(s) => quote! { #s.to_string() },
|
||||
Segment::Typed { name, .. } => quote! { format!("{}={}", stringify!(#name), self.#name) },
|
||||
});
|
||||
|
||||
let display_impl = quote! {
|
||||
impl ::core::fmt::Display for #struct_name {
|
||||
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
|
||||
write!(f, "{}", vec![#(#display_parts),*].join("/"))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate parse implementation
|
||||
let mut parse_body = Vec::new();
|
||||
|
||||
for seg in segments {
|
||||
match seg {
|
||||
Segment::Constant(s) => {
|
||||
parse_body.push(quote! {
|
||||
{
|
||||
match parts.next() {
|
||||
Option::Some(#s) => {}
|
||||
_ => return Option::None,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Segment::Typed { name, ty } => {
|
||||
let name_str = name.to_string();
|
||||
parse_body.push(quote! {
|
||||
let #name: #ty = {
|
||||
let x = match parts.next() {
|
||||
Option::Some(x) => x.strip_prefix(concat!(#name_str, "="))?,
|
||||
_ => return Option::None,
|
||||
};
|
||||
|
||||
::core::str::FromStr::from_str(x).ok()?
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract just the field names for struct construction
|
||||
let field_names = typed_fields.iter().map(|(name, _)| name);
|
||||
|
||||
let datapath_impl = quote! {
|
||||
impl ::datapath::Datapath for #struct_name {
|
||||
fn with_file(&self, file: impl ::core::convert::Into<::std::string::String>) -> ::datapath::DatapathFile<Self> {
|
||||
::datapath::DatapathFile {
|
||||
path: self.clone(),
|
||||
file: file.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(path: &str) -> Option<::datapath::DatapathFile<Self>> {
|
||||
if path.contains("\n") {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
let mut parts = path.split("/");
|
||||
|
||||
#(#parse_body)*
|
||||
|
||||
let mut file = ::std::string::String::new();
|
||||
if let Option::Some(first) = parts.next() {
|
||||
file.push_str(first);
|
||||
for part in parts {
|
||||
file.push_str("/");
|
||||
file.push_str(part);
|
||||
}
|
||||
}
|
||||
|
||||
Option::Some(::datapath::DatapathFile {
|
||||
path: Self { #(#field_names),* },
|
||||
file,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(struct_def, display_impl, datapath_impl)
|
||||
}
|
||||
|
||||
/// The `datapath!` macro generates datapath struct definitions with parsing and formatting logic.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// datapath! {
|
||||
/// struct CaptureRaw_2_0(capture/user_id=Uuid/ts=i64/raw/2.0);
|
||||
/// struct OtherPath {
|
||||
/// pattern: web/domain=String/ts=i64/raw/2.0
|
||||
/// schema: MySchema
|
||||
/// };
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro]
|
||||
pub fn datapath(input: TokenStream) -> TokenStream {
|
||||
let defs =
|
||||
parse_macro_input!(input with Punctuated::<DatapathDef, Token![;]>::parse_terminated);
|
||||
|
||||
let generated = defs.into_iter().map(generate_datapath_code);
|
||||
|
||||
let output = quote! {
|
||||
#(#generated)*
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
21
crates/datapath/Cargo.toml
Normal file
21
crates/datapath/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "datapath"
|
||||
description = "Type-safe structured paths with partitions, parsing, and schema associations"
|
||||
keywords = ["path", "filesystem", "parsing", "typed", "schema"]
|
||||
categories = ["filesystem", "data-structures", "parsing"]
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
version = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
readme = "README.md"
|
||||
authors = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
datapath-macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
23
crates/datapath/src/datapath.rs
Normal file
23
crates/datapath/src/datapath.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use crate::DatapathFile;
|
||||
|
||||
pub trait Datapath
|
||||
where
|
||||
Self: Send + Sync + 'static,
|
||||
Self: Clone + Sized,
|
||||
Self: Eq + PartialEq + Hash,
|
||||
Self: Debug + Display,
|
||||
{
|
||||
// Default
|
||||
|
||||
/// Returns a [DatapathFile] with the given file at this datapath
|
||||
fn with_file(&self, file: impl Into<String>) -> DatapathFile<Self>;
|
||||
|
||||
/// Parse a string as this datapath with a (possibly empty-string)
|
||||
/// file, returning `None` if this string is invalid.
|
||||
fn parse(path: &str) -> Option<DatapathFile<Self>>;
|
||||
}
|
||||
30
crates/datapath/src/datapathfile.rs
Normal file
30
crates/datapath/src/datapathfile.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
hash::Hash,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::Datapath;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DatapathFile<D: Datapath> {
|
||||
pub path: D,
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
impl<D: Datapath> FromStr for DatapathFile<D> {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Datapath::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Datapath> Display for DatapathFile<D> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.file.is_empty() {
|
||||
write!(f, "{}", self.path)
|
||||
} else {
|
||||
write!(f, "{}/{}", self.path, self.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
crates/datapath/src/lib.rs
Normal file
14
crates/datapath/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// silence linter, used in README
|
||||
#[cfg(test)]
|
||||
use uuid as _;
|
||||
|
||||
mod datapath;
|
||||
pub use datapath::*;
|
||||
|
||||
mod datapathfile;
|
||||
pub use datapathfile::*;
|
||||
|
||||
mod schema;
|
||||
pub use schema::*;
|
||||
|
||||
pub use datapath_macro::datapath;
|
||||
15
crates/datapath/src/schema.rs
Normal file
15
crates/datapath/src/schema.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::Datapath;
|
||||
|
||||
/// A datapath with an associated schema.
|
||||
///
|
||||
/// Provides [AssociatedDatapath::Schema],
|
||||
/// which is the schema that is available at this path.
|
||||
///
|
||||
/// A datapath can have at most one schema, but the same
|
||||
/// schema may be used in an arbitrary number of datapaths.
|
||||
pub trait SchemaDatapath
|
||||
where
|
||||
Self: Datapath,
|
||||
{
|
||||
type Schema;
|
||||
}
|
||||
Reference in New Issue
Block a user