From d2aa33d83096f628fe90e1e79f71c88618463035 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:47:26 -0800 Subject: [PATCH] Initial version: `v0.0.1` --- Cargo.lock | 184 ++++++++++ Cargo.toml | 81 +++++ crates/datapath-macro/Cargo.toml | 23 ++ crates/datapath-macro/src/lib.rs | 543 ++++++++++++++++++++++++++++ crates/datapath/Cargo.toml | 21 ++ crates/datapath/src/datapath.rs | 23 ++ crates/datapath/src/datapathfile.rs | 30 ++ crates/datapath/src/lib.rs | 14 + crates/datapath/src/schema.rs | 15 + 9 files changed, 934 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/datapath-macro/Cargo.toml create mode 100644 crates/datapath-macro/src/lib.rs create mode 100644 crates/datapath/Cargo.toml create mode 100644 crates/datapath/src/datapath.rs create mode 100644 crates/datapath/src/datapathfile.rs create mode 100644 crates/datapath/src/lib.rs create mode 100644 crates/datapath/src/schema.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fc7016f --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5f667c8 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/crates/datapath-macro/Cargo.toml b/crates/datapath-macro/Cargo.toml new file mode 100644 index 0000000..1431e96 --- /dev/null +++ b/crates/datapath-macro/Cargo.toml @@ -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 } diff --git a/crates/datapath-macro/src/lib.rs b/crates/datapath-macro/src/lib.rs new file mode 100644 index 0000000..dde1a1b --- /dev/null +++ b/crates/datapath-macro/src/lib.rs @@ -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, + attrs: Vec, + }, + /// Schema syntax: `struct Name { pattern: path/segments, schema: Type }` + WithSchema { + struct_name: Ident, + segments: Vec, + schema_type: Type, + attrs: Vec, + }, +} + +/// 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 { + // Parse attributes (like #[doc = "..."]) + let attrs = input.call(syn::Attribute::parse_outer)?; + + // Parse: struct Name + input.parse::()?; + 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::()?; + + 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 { + 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::() { + 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::(); + } + } + + None +} + +/// Parse a complete pattern (used when the entire input is the pattern) +fn parse_pattern(input: ParseStream<'_>) -> syn::Result> { + 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> { + 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 == 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, + 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::()?; + 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::()?; + } + } 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::()?; + segments.push(Segment::Constant(current_token.clone())); + current_token.clear(); + } + } + } else if let Ok(ident) = input.parse::() { + let ident_str = ident.to_string(); + + // Check if next token is '=' + if input.peek(Token![=]) { + input.parse::()?; + let ty: Type = input.parse()?; + + segments.push(Segment::Typed { + name: ident.clone(), + ty, + }); + + // Check for '/' separator + if input.peek(Token![/]) { + input.parse::()?; + } + } 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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 { + ::datapath::DatapathFile { + path: self.clone(), + file: file.into(), + } + } + + fn parse(path: &str) -> Option<::datapath::DatapathFile> { + 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::::parse_terminated); + + let generated = defs.into_iter().map(generate_datapath_code); + + let output = quote! { + #(#generated)* + }; + + output.into() +} diff --git a/crates/datapath/Cargo.toml b/crates/datapath/Cargo.toml new file mode 100644 index 0000000..1e67666 --- /dev/null +++ b/crates/datapath/Cargo.toml @@ -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"] } diff --git a/crates/datapath/src/datapath.rs b/crates/datapath/src/datapath.rs new file mode 100644 index 0000000..d01b82a --- /dev/null +++ b/crates/datapath/src/datapath.rs @@ -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) -> DatapathFile; + + /// Parse a string as this datapath with a (possibly empty-string) + /// file, returning `None` if this string is invalid. + fn parse(path: &str) -> Option>; +} diff --git a/crates/datapath/src/datapathfile.rs b/crates/datapath/src/datapathfile.rs new file mode 100644 index 0000000..6d25de0 --- /dev/null +++ b/crates/datapath/src/datapathfile.rs @@ -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 { + pub path: D, + pub file: String, +} + +impl FromStr for DatapathFile { + type Err = (); + fn from_str(s: &str) -> Result { + Datapath::parse(s).ok_or(()) + } +} + +impl Display for DatapathFile { + 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) + } + } +} diff --git a/crates/datapath/src/lib.rs b/crates/datapath/src/lib.rs new file mode 100644 index 0000000..e833fbd --- /dev/null +++ b/crates/datapath/src/lib.rs @@ -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; diff --git a/crates/datapath/src/schema.rs b/crates/datapath/src/schema.rs new file mode 100644 index 0000000..423c030 --- /dev/null +++ b/crates/datapath/src/schema.rs @@ -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; +}