server: api doc part 5

This commit is contained in:
nym21
2025-10-08 20:32:27 +02:00
parent 83d74da556
commit 6ad15221de
11 changed files with 418 additions and 308 deletions
@@ -25,9 +25,10 @@ const VERSION: Version = Version::ZERO;
pub struct Vecs {
starting_height: Option<Height>,
#[vecs(skip)]
#[traversable(skip)]
pub state: Option<AddressCohortState>,
#[traversable(flatten)]
pub inner: common::Vecs,
pub height_to_addr_count: EagerVec<Height, StoredU64>,
@@ -17,9 +17,10 @@ use crate::{
pub struct Vecs {
state_starting_height: Option<Height>,
#[vecs(skip)]
#[traversable(skip)]
pub state: Option<UTXOCohortState>,
#[traversable(flatten)]
pub inner: common::Vecs,
}
+1 -1
View File
@@ -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()
@@ -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 {
+2 -4
View File
@@ -91,16 +91,14 @@ fn req_to_response_res(
match interface.format(vecs, &params.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()
}
+1 -4
View File
@@ -103,10 +103,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
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();
+23 -4
View File
@@ -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<AppState> {
.route(
"/api.json",
get(
async |Extension(api): Extension<Arc<OpenApi>>| -> Json<Arc<OpenApi>> {
Json(api)
async |headers: HeaderMap,
Extension(api): Extension<Arc<OpenApi>>|
-> 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
},
),
)
@@ -11,6 +11,7 @@ where
Self: Sized,
{
fn new_not_modified() -> Self;
fn new_json_from_bytes(bytes: Vec<u8>) -> Self;
}
impl ResponseExtended for Response<Body> {
@@ -20,4 +21,11 @@ impl ResponseExtended for Response<Body> {
headers.insert_cors();
response
}
fn new_json_from_bytes(bytes: Vec<u8>) -> Self {
Response::builder()
.header("content-type", "application/json")
.body(bytes.into())
.unwrap()
}
}
+314 -225
View File
@@ -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<Item = &dyn vecdb::AnyCollectableVec> {
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::<Vec<_>>()
} 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<Item = &dyn vecdb::AnyCollectableVec> {
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::<syn::Ident>()
.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<Item = &dyn vecdb::AnyCollectableVec> {
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<FieldInfo<'a>>, 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(&param) = 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<FieldAttr> {
for attr in &field.attrs {
if attr.path().is_ident("traversable")
&& let Ok(ident) = attr.parse_args::<syn::Ident>()
{
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<String, brk_traversable::TreeNode> =
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<Item = &dyn vecdb::AnyCollectableVec> {
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 = &regular_fields[1..];
(
quote! {
let mut regular_iter: Box<dyn Iterator<Item = &dyn vecdb::AnyCollectableVec>> =
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<dyn Iterator<Item = &dyn vecdb::AnyCollectableVec>> =
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<Item = &dyn vecdb::AnyCollectableVec> {
#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<Item = &dyn vecdb::AnyCollectableVec> {
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<Item = &dyn vecdb::AnyCollectableVec> {
std::iter::empty()
}
};
}
let (init_part, chain_part) = if let Some((&first, rest)) = regular_fields.split_first() {
(
quote! {
let mut regular_iter: Box<dyn Iterator<Item = &dyn vecdb::AnyCollectableVec>> =
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<dyn Iterator<Item = &dyn vecdb::AnyCollectableVec>> =
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<Item = &dyn vecdb::AnyCollectableVec> {
#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
}