diff --git a/Cargo.lock b/Cargo.lock index 82b60329a..43fee5675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1636,7 +1636,7 @@ checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" dependencies = [ "dispatch", "nix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1768,7 +1768,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1878,7 +1878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3760,13 +3760,13 @@ dependencies = [ [[package]] name = "quick_cache" -version = "0.6.16" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad6644cb07b7f3488b9f3d2fde3b4c0a7fa367cafefb39dff93a659f76eb786" +checksum = "ba15f5bccfb18c666351668b97bbff66da5093f96757ca15299e4e594fe1316e" dependencies = [ "ahash", "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "parking_lot 0.12.5", ] @@ -4080,7 +4080,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -4197,7 +4197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -4588,7 +4588,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -5282,7 +5282,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -5293,9 +5293,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e6c4a1f363c8210c6f77ba24f645c61c6fb941eccf013da691f7e09515b8ac" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", "windows-core", @@ -5305,18 +5305,18 @@ dependencies = [ [[package]] name = "windows-collections" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123e712f464a8a60ce1a13f4c446d2d43ab06464cb5842ff68f5c71b6fb7852e" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", ] [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -5327,9 +5327,9 @@ dependencies = [ [[package]] name = "windows-future" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f3db6b24b120200d649cd4811b4947188ed3a8d2626f7075146c5d178a9a4a" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", "windows-link", @@ -5338,9 +5338,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5349,9 +5349,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5360,15 +5360,15 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", "windows-link", @@ -5376,18 +5376,18 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -5416,14 +5416,14 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] @@ -5446,26 +5446,26 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows-threading" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", ] @@ -5478,9 +5478,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -5490,9 +5490,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -5502,9 +5502,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -5514,9 +5514,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -5526,9 +5526,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -5538,9 +5538,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -5550,9 +5550,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -5562,9 +5562,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index c68b63f8b..b65eb3b34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ panic = "abort" debug-assertions = false [workspace.dependencies] -aide = { version = "0.15.1", features = ["axum-json", "axum-query"], package = "brk-aide" } +aide = { version = "0.15.2", features = ["axum-json", "axum-query"], package = "brk-aide" } allocative = { version = "0.3.4", features = ["parking_lot"] } axum = "0.8.6" bitcoin = { version = "0.32.7", features = ["serde"] } @@ -70,7 +70,7 @@ jiff = "0.2.15" log = "0.4.28" minreq = { version = "2.14.1", features = ["https", "serde_json"] } parking_lot = "0.12.5" -quick_cache = "0.6.16" +quick_cache = "0.6.17" rayon = "1.11.0" schemars = "1.0.4" serde = "1.0.228" diff --git a/crates/brk_computer/src/stateful/address_cohort.rs b/crates/brk_computer/src/stateful/address_cohort.rs index b5ba67333..2ac068a9c 100644 --- a/crates/brk_computer/src/stateful/address_cohort.rs +++ b/crates/brk_computer/src/stateful/address_cohort.rs @@ -25,9 +25,10 @@ const VERSION: Version = Version::ZERO; pub struct Vecs { starting_height: Option, - #[vecs(skip)] + #[traversable(skip)] pub state: Option, + #[traversable(flatten)] pub inner: common::Vecs, pub height_to_addr_count: EagerVec, diff --git a/crates/brk_computer/src/stateful/utxo_cohort.rs b/crates/brk_computer/src/stateful/utxo_cohort.rs index f0ed7fd49..75a7c39d5 100644 --- a/crates/brk_computer/src/stateful/utxo_cohort.rs +++ b/crates/brk_computer/src/stateful/utxo_cohort.rs @@ -17,9 +17,10 @@ use crate::{ pub struct Vecs { state_starting_height: Option, - #[vecs(skip)] + #[traversable(skip)] pub state: Option, + #[traversable(flatten)] pub inner: common::Vecs, } diff --git a/crates/brk_logger/src/lib.rs b/crates/brk_logger/src/lib.rs index 140a32f76..9301e9da5 100644 --- a/crates/brk_logger/src/lib.rs +++ b/crates/brk_logger/src/lib.rs @@ -23,7 +23,7 @@ pub fn init(path: Option<&Path>) -> io::Result<()> { }); Builder::from_env(Env::default().default_filter_or( - "info,bitcoin=off,bitcoincore-rpc=off,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off,rmcp=off,brk_rmcp=off,tracing=off", + "info,bitcoin=off,bitcoincore-rpc=off,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off,rmcp=off,brk_rmcp=off,tracing=off,aide=off,brk_aide=off", )) .format(move |buf, record| { let date_time = Timestamp::now() diff --git a/crates/brk_server/src/api/chain/transactions.rs b/crates/brk_server/src/api/chain/transactions.rs index 98708839b..80cb355c2 100644 --- a/crates/brk_server/src/api/chain/transactions.rs +++ b/crates/brk_server/src/api/chain/transactions.rs @@ -21,7 +21,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::VecIterator; -use crate::extended::TransformResponseExtended; +use crate::extended::{ResponseExtended, TransformResponseExtended}; use super::AppState; @@ -140,10 +140,7 @@ async fn get_transaction_info( let bytes = sonic_rs::to_vec(&tx_info).unwrap(); - Ok(Response::builder() - .header("content-type", "application/json") - .body(bytes.into()) - .unwrap()) + Ok(Response::new_json_from_bytes(bytes)) } fn get_transaction_info_docs(op: TransformOperation) -> TransformOperation { diff --git a/crates/brk_server/src/api/metrics/data.rs b/crates/brk_server/src/api/metrics/data.rs index f96ef83f5..d021fa3a5 100644 --- a/crates/brk_server/src/api/metrics/data.rs +++ b/crates/brk_server/src/api/metrics/data.rs @@ -91,16 +91,14 @@ fn req_to_response_res( match interface.format(vecs, ¶ms.rest)? { Output::CSV(s) => { if let GuardResult::Guard(g) = guard_res { - g.insert(s.clone().into()) - .map_err(|_| Error::QuickCacheError)?; + let _ = g.insert(s.clone().into()); } s.into_response() } Output::Json(v) => { let json = v.to_vec(); if let GuardResult::Guard(g) = guard_res { - g.insert(json.clone().into()) - .map_err(|_| Error::QuickCacheError)?; + let _ = g.insert(json.clone().into()); } json.into_response() } diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index f74568710..3b0cc41ec 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -103,10 +103,7 @@ impl ApiMetricsRoutes for ApiRouter { let bytes = sonic_rs::to_vec(&app_state.interface.get_metrics_catalog()).unwrap(); - let mut response = Response::builder() - .header("content-type", "application/json") - .body(bytes.into()) - .unwrap(); + let mut response = Response::new_json_from_bytes(bytes); let headers = response.headers_mut(); headers.insert_cors(); diff --git a/crates/brk_server/src/api/mod.rs b/crates/brk_server/src/api/mod.rs index 4c0acac0c..f4134fa72 100644 --- a/crates/brk_server/src/api/mod.rs +++ b/crates/brk_server/src/api/mod.rs @@ -6,7 +6,8 @@ use aide::{ }; use axum::{ Extension, Json, - response::{Html, Redirect}, + http::HeaderMap, + response::{Html, Redirect, Response}, routing::get, }; use schemars::JsonSchema; @@ -15,7 +16,7 @@ use serde::Serialize; use crate::{ VERSION, api::{chain::ChainRoutes, metrics::ApiMetricsRoutes}, - extended::TransformResponseExtended, + extended::{HeaderMapExtended, ResponseExtended, TransformResponseExtended}, }; use super::AppState; @@ -76,8 +77,26 @@ impl ApiRoutes for ApiRouter { .route( "/api.json", get( - async |Extension(api): Extension>| -> Json> { - Json(api) + async |headers: HeaderMap, + Extension(api): Extension>| + -> Response { + let etag = VERSION; + + if headers + .get_if_none_match() + .is_some_and(|prev_etag| etag == prev_etag) + { + return Response::new_not_modified(); + } + + let mut response = + Response::new_json_from_bytes(sonic_rs::to_vec(&api).unwrap()); + + let headers = response.headers_mut(); + headers.insert_cors(); + headers.insert_etag(etag); + + response }, ), ) diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index 9fef0a496..26bbe1702 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -11,6 +11,7 @@ where Self: Sized, { fn new_not_modified() -> Self; + fn new_json_from_bytes(bytes: Vec) -> Self; } impl ResponseExtended for Response { @@ -20,4 +21,11 @@ impl ResponseExtended for Response { headers.insert_cors(); response } + + fn new_json_from_bytes(bytes: Vec) -> Self { + Response::builder() + .header("content-type", "application/json") + .body(bytes.into()) + .unwrap() + } } diff --git a/crates/brk_traversable_derive/src/lib.rs b/crates/brk_traversable_derive/src/lib.rs index 19cb74b1a..19b9780e2 100644 --- a/crates/brk_traversable_derive/src/lib.rs +++ b/crates/brk_traversable_derive/src/lib.rs @@ -2,263 +2,352 @@ use proc_macro::TokenStream; use quote::quote; use syn::{Data, DeriveInput, Fields, Type, parse_macro_input}; -#[proc_macro_derive(Traversable, attributes(vecs))] +#[proc_macro_derive(Traversable, attributes(traversable))] pub fn derive_traversable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; let generics = &input.generics; let (impl_generics, ty_generics, _) = generics.split_for_impl(); - let traverse_impl = match &input.data { - Data::Struct(data) => { - match &data.fields { - Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { - // Special case for single-field tuple structs - just delegate - let generic_params: Vec<_> = generics.type_params().map(|p| &p.ident).collect(); - let original_predicates = - &generics.where_clause.as_ref().map(|w| &w.predicates); - - let where_clause = - if original_predicates.is_some() || !generic_params.is_empty() { - quote! { - where - #(#generic_params: Send + Sync,)* - #original_predicates - } - } else { - quote! {} - }; - - return TokenStream::from(quote! { - impl #impl_generics Traversable for #name #ty_generics - #where_clause - { - fn to_tree_node(&self) -> brk_traversable::TreeNode { - self.0.to_tree_node() - } - - fn iter_any_collectable(&self) -> impl Iterator { - self.0.iter_any_collectable() - } - } - }); - } - _ => { - // Normal struct with named fields - let field_traversals = generate_field_traversals(&data.fields); - let iterator_impl = generate_iterator_impl(&data.fields); - - let generic_params: Vec<_> = generics.type_params().map(|p| &p.ident).collect(); - - let generics_needing_traversable = - if let Fields::Named(named_fields) = &data.fields { - let mut used = std::collections::BTreeSet::new(); - - for field in named_fields.named.iter() { - if !should_process_field(field) { - continue; - } - - if let Type::Path(type_path) = &field.ty - && type_path.path.segments.len() == 1 - && let Some(seg) = type_path.path.segments.first() - && seg.arguments.is_empty() - && let Some(pos) = - generic_params.iter().position(|g| g == &&seg.ident) - { - used.insert(generic_params[pos]); - } - } - used.into_iter().collect::>() - } else { - Vec::new() - }; - - let original_predicates = - &generics.where_clause.as_ref().map(|w| &w.predicates); - - let where_clause = if !generics_needing_traversable.is_empty() - || original_predicates.is_some() - || !generic_params.is_empty() - { - quote! { - where - #(#generics_needing_traversable: brk_traversable::Traversable,)* - #(#generic_params: Send + Sync,)* - #original_predicates - } - } else { - quote! {} - }; - - quote! { - impl #impl_generics Traversable for #name #ty_generics - #where_clause - { - fn to_tree_node(&self) -> brk_traversable::TreeNode { - #field_traversals - } - - #iterator_impl - } - } - } - } - } - _ => { - return syn::Error::new_spanned( - &input.ident, - "Traversable can only be derived for structs", - ) - .to_compile_error() - .into(); - } + let Data::Struct(data) = &input.data else { + return syn::Error::new_spanned( + &input.ident, + "Traversable can only be derived for structs", + ) + .to_compile_error() + .into(); }; - TokenStream::from(traverse_impl) -} - -fn should_process_field(field: &syn::Field) -> bool { - matches!(field.vis, syn::Visibility::Public(_)) && !has_skip_attribute(field) -} - -fn generate_field_traversals(fields: &Fields) -> proc_macro2::TokenStream { - match fields { - Fields::Named(fields) => { - let entries = fields.named.iter().filter_map(|f| { - let field_name = f.ident.as_ref()?; - let field_name_str = field_name.to_string(); - - if !should_process_field(f) { - return None; + // Handle single-field tuple struct delegation + if let Fields::Unnamed(fields) = &data.fields + && fields.unnamed.len() == 1 + { + let where_clause = build_where_clause(generics, &[]); + return TokenStream::from(quote! { + impl #impl_generics Traversable for #name #ty_generics #where_clause { + fn to_tree_node(&self) -> brk_traversable::TreeNode { + self.0.to_tree_node() } - if get_option_inner_type(&f.ty).is_some() { - Some(quote! { - self.#field_name.as_ref().map(|nested| (String::from(#field_name_str), nested.to_tree_node())) - }) - } else { - Some(quote! { - Some((String::from(#field_name_str), self.#field_name.to_tree_node())) - }) + fn iter_any_collectable(&self) -> impl Iterator { + self.0.iter_any_collectable() } - }); - - quote! { - let collected: std::collections::BTreeMap<_, _> = [#(#entries,)*] - .into_iter() - .flatten() - .collect(); - - // return if collected.len() == 1 { - // collected.into_values().next().unwrap() - // } else { - brk_traversable::TreeNode::Branch(collected) - // }; } - } - _ => quote! {}, + }); } -} -fn has_skip_attribute(field: &syn::Field) -> bool { - field.attrs.iter().any(|attr| { - attr.path().is_ident("vecs") - && attr - .parse_args::() - .map(|ident| ident == "skip") - .unwrap_or(false) + // Handle named fields + let Fields::Named(named_fields) = &data.fields else { + return TokenStream::from(quote! { + impl #impl_generics Traversable for #name #ty_generics { + fn to_tree_node(&self) -> brk_traversable::TreeNode { + brk_traversable::TreeNode::Branch(std::collections::BTreeMap::new()) + } + + fn iter_any_collectable(&self) -> impl Iterator { + std::iter::empty() + } + } + }); + }; + + let generic_params: Vec<_> = generics.type_params().map(|p| &p.ident).collect(); + + let (field_infos, generics_needing_traversable) = analyze_fields(named_fields, &generic_params); + + let field_traversals = generate_field_traversals(&field_infos); + let iterator_impl = generate_iterator_impl(&field_infos); + let where_clause = build_where_clause(generics, &generics_needing_traversable); + + TokenStream::from(quote! { + impl #impl_generics Traversable for #name #ty_generics #where_clause { + fn to_tree_node(&self) -> brk_traversable::TreeNode { + #field_traversals + } + + #iterator_impl + } }) } -fn generate_iterator_impl(fields: &Fields) -> proc_macro2::TokenStream { - match fields { - Fields::Named(fields) => { - let mut regular_fields = Vec::new(); - let mut option_fields = Vec::new(); +enum FieldAttr { + Normal, + Flatten, +} - for field in fields.named.iter() { - if let Some(field_name) = &field.ident { - if !should_process_field(field) { - continue; - } +struct FieldInfo<'a> { + name: &'a syn::Ident, + is_option: bool, + attr: FieldAttr, +} - if get_option_inner_type(&field.ty).is_some() { - option_fields.push(field_name); - } else { - regular_fields.push(field_name); - } +fn analyze_fields<'a>( + fields: &'a syn::FieldsNamed, + generic_params: &[&'a syn::Ident], +) -> (Vec>, Vec<&'a syn::Ident>) { + let mut field_infos = Vec::new(); + let mut generics_set = std::collections::BTreeSet::new(); + + for field in &fields.named { + let field_attr = get_field_attr(field); + + // Skip attribute means don't process at all + if field_attr.is_none() { + continue; + } + + if !matches!(field.vis, syn::Visibility::Public(_)) { + continue; + } + + let Some(field_name) = &field.ident else { + continue; + }; + + if let Type::Path(type_path) = &field.ty + && type_path.path.segments.len() == 1 + && let Some(seg) = type_path.path.segments.first() + && seg.arguments.is_empty() + && let Some(¶m) = generic_params.iter().find(|&&g| g == &seg.ident) + { + generics_set.insert(param); + } + + field_infos.push(FieldInfo { + name: field_name, + is_option: is_option_type(&field.ty), + attr: field_attr.unwrap(), + }); + } + + (field_infos, generics_set.into_iter().collect()) +} + +/// Returns None for skip, Some(attr) for normal/flatten +fn get_field_attr(field: &syn::Field) -> Option { + for attr in &field.attrs { + if attr.path().is_ident("traversable") + && let Ok(ident) = attr.parse_args::() + { + return match ident.to_string().as_str() { + "skip" => None, + "flatten" => Some(FieldAttr::Flatten), + _ => Some(FieldAttr::Normal), + }; + } + } + Some(FieldAttr::Normal) +} + +fn is_option_type(ty: &Type) -> bool { + matches!( + ty, + Type::Path(type_path) + if type_path.path.segments.last() + .is_some_and(|seg| seg.ident == "Option") + ) +} + +fn generate_field_traversals(infos: &[FieldInfo]) -> proc_macro2::TokenStream { + let has_flatten = infos.iter().any(|i| matches!(i.attr, FieldAttr::Flatten)); + let has_normal = infos.iter().any(|i| matches!(i.attr, FieldAttr::Normal)); + + if !has_flatten { + // Fast path: no flatten, simple collection + let entries = infos.iter().map(|info| { + let field_name = info.name; + let field_name_str = field_name.to_string(); + + if info.is_option { + quote! { + self.#field_name.as_ref().map(|nested| (String::from(#field_name_str), nested.to_tree_node())) + } + } else { + quote! { + Some((String::from(#field_name_str), self.#field_name.to_tree_node())) } } + }); - if regular_fields.is_empty() && option_fields.is_empty() { + return quote! { + let collected: std::collections::BTreeMap<_, _> = [#(#entries,)*] + .into_iter() + .flatten() + .collect(); + + brk_traversable::TreeNode::Branch(collected) + }; + } + + // Has flatten fields + if !has_normal { + // Only flatten fields, no normal fields - need explicit type annotation + let flatten_entries = infos.iter() + .filter(|i| matches!(i.attr, FieldAttr::Flatten)) + .map(|info| { + let field_name = info.name; + + if info.is_option { + quote! { + if let Some(ref nested) = self.#field_name { + if let brk_traversable::TreeNode::Branch(map) = nested.to_tree_node() { + collected.extend(map); + } + } + } + } else { + quote! { + if let brk_traversable::TreeNode::Branch(map) = self.#field_name.to_tree_node() { + collected.extend(map); + } + } + } + }); + + return quote! { + let mut collected: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + + #(#flatten_entries)* + + brk_traversable::TreeNode::Branch(collected) + }; + } + + // Has both normal and flatten fields + let normal_entries = infos.iter() + .filter(|i| matches!(i.attr, FieldAttr::Normal)) + .map(|info| { + let field_name = info.name; + let field_name_str = field_name.to_string(); + + if info.is_option { quote! { - fn iter_any_collectable(&self) -> impl Iterator { - std::iter::empty() + self.#field_name.as_ref().map(|nested| (String::from(#field_name_str), nested.to_tree_node())) + } + } else { + quote! { + Some((String::from(#field_name_str), self.#field_name.to_tree_node())) + } + } + }); + + let flatten_entries = infos.iter() + .filter(|i| matches!(i.attr, FieldAttr::Flatten)) + .map(|info| { + let field_name = info.name; + + if info.is_option { + quote! { + if let Some(ref nested) = self.#field_name { + if let brk_traversable::TreeNode::Branch(map) = nested.to_tree_node() { + collected.extend(map); + } } } } else { - let (init_part, chain_part) = if !regular_fields.is_empty() { - let first = regular_fields.first().unwrap(); - let rest = ®ular_fields[1..]; - ( - quote! { - let mut regular_iter: Box> = - Box::new(self.#first.iter_any_collectable()); - }, - quote! { - #(regular_iter = Box::new(regular_iter.chain(self.#rest.iter_any_collectable()));)* - }, - ) - } else { - ( - quote! { - let mut regular_iter: Box> = - Box::new(std::iter::empty()); - }, - quote! {}, - ) - }; - - let option_part = if !option_fields.is_empty() { - let chains = option_fields.iter().map(|f| { - quote! { - if let Some(ref x) = self.#f { - regular_iter = Box::new(regular_iter.chain(x.iter_any_collectable())); - } - } - }); - quote! { #(#chains)* } - } else { - quote! {} - }; - quote! { - fn iter_any_collectable(&self) -> impl Iterator { - #init_part - #chain_part - #option_part - regular_iter + if let brk_traversable::TreeNode::Branch(map) = self.#field_name.to_tree_node() { + collected.extend(map); } } } - } - _ => quote! { - fn iter_any_collectable(&self) -> impl Iterator { - std::iter::empty() - } - }, + }); + + quote! { + let mut collected: std::collections::BTreeMap<_, _> = [#(#normal_entries,)*] + .into_iter() + .flatten() + .collect(); + + #(#flatten_entries)* + + brk_traversable::TreeNode::Branch(collected) } } -fn get_option_inner_type(ty: &Type) -> Option<&Type> { - if let Type::Path(type_path) = ty - && let Some(segment) = type_path.path.segments.last() - && segment.ident == "Option" - && let syn::PathArguments::AngleBracketed(args) = &segment.arguments - && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() - { - return Some(inner_ty); +fn generate_iterator_impl(infos: &[FieldInfo]) -> proc_macro2::TokenStream { + let regular_fields: Vec<_> = infos + .iter() + .filter(|i| !i.is_option) + .map(|i| i.name) + .collect(); + + let option_fields: Vec<_> = infos + .iter() + .filter(|i| i.is_option) + .map(|i| i.name) + .collect(); + + if regular_fields.is_empty() && option_fields.is_empty() { + return quote! { + fn iter_any_collectable(&self) -> impl Iterator { + std::iter::empty() + } + }; + } + + let (init_part, chain_part) = if let Some((&first, rest)) = regular_fields.split_first() { + ( + quote! { + let mut regular_iter: Box> = + Box::new(self.#first.iter_any_collectable()); + }, + quote! { + #(regular_iter = Box::new(regular_iter.chain(self.#rest.iter_any_collectable()));)* + }, + ) + } else { + ( + quote! { + let mut regular_iter: Box> = + Box::new(std::iter::empty()); + }, + quote! {}, + ) + }; + + let option_part = if !option_fields.is_empty() { + let chains = option_fields.iter().map(|f| { + quote! { + if let Some(ref x) = self.#f { + regular_iter = Box::new(regular_iter.chain(x.iter_any_collectable())); + } + } + }); + quote! { #(#chains)* } + } else { + quote! {} + }; + + quote! { + fn iter_any_collectable(&self) -> impl Iterator { + #init_part + #chain_part + #option_part + regular_iter + } + } +} + +fn build_where_clause( + generics: &syn::Generics, + generics_needing_traversable: &[&syn::Ident], +) -> proc_macro2::TokenStream { + let generic_params: Vec<_> = generics.type_params().map(|p| &p.ident).collect(); + let original_predicates = generics.where_clause.as_ref().map(|w| &w.predicates); + + if generics_needing_traversable.is_empty() + && generic_params.is_empty() + && original_predicates.is_none() + { + return quote! {}; + } + + quote! { + where + #(#generics_needing_traversable: brk_traversable::Traversable,)* + #(#generic_params: Send + Sync,)* + #original_predicates } - None }