use aide::axum::{ApiRouter, routing::get_with}; use axum::{ extract::{Path, Query, State}, http::{HeaderMap, Uri}, response::{IntoResponse, Response}, }; use brk_query::{ DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination, }; use brk_traversable::TreeNode; use brk_types::{ Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam, MetricWithIndex, Metrics, }; use crate::{CacheStrategy, extended::TransformResponseExtended}; use super::AppState; mod bulk; mod data; mod legacy; /// Maximum allowed request weight in bytes (650KB) const MAX_WEIGHT: usize = 65 * 10_000; pub trait ApiMetricsRoutes { fn add_metrics_routes(self) -> Self; } impl ApiMetricsRoutes for ApiRouter { fn add_metrics_routes(self) -> Self { self.api_route( "/api/metrics", get_with( async |headers: HeaderMap, State(state): State| { state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metrics_catalog().clone())).await }, |op| op .id("get_metrics_tree") .metrics_tag() .summary("Metrics catalog") .description( "Returns the complete hierarchical catalog of available metrics organized as a tree structure. \ Metrics are grouped by categories and subcategories." ) .ok_response::() .not_modified(), ), ) .api_route( "/api/metrics/count", get_with( async | headers: HeaderMap, State(state): State | { state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metric_count())).await }, |op| op .id("get_metrics_count") .metrics_tag() .summary("Metric count") .description("Returns the number of metrics available per index type.") .ok_response::>() .not_modified(), ), ) .api_route( "/api/metrics/indexes", get_with( async | headers: HeaderMap, State(state): State | { state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.indexes().to_vec())).await }, |op| op .id("get_indexes") .metrics_tag() .summary("List available indexes") .description( "Returns all available indexes with their accepted query aliases. Use any alias when querying metrics." ) .ok_response::>() .not_modified(), ), ) .api_route( "/api/metrics/list", get_with( async | headers: HeaderMap, State(state): State, Query(pagination): Query | { state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.metrics(pagination))).await }, |op| op .id("list_metrics") .metrics_tag() .summary("Metrics list") .description("Paginated flat list of all available metric names. Use `page` query param for pagination.") .ok_response::() .not_modified(), ), ) .api_route( "/api/metrics/search/{metric}", get_with( async | headers: HeaderMap, State(state): State, Path(path): Path, Query(query): Query | { state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.match_metric(&path.metric, query.limit))).await }, |op| op .id("search_metrics") .metrics_tag() .summary("Search metrics") .description("Fuzzy search for metrics by name. Supports partial matches and typos.") .ok_response::>() .not_modified() .server_error(), ), ) .api_route( "/api/metric/{metric}", get_with( async | headers: HeaderMap, State(state): State, Path(path): Path | { state.cached_json(&headers, CacheStrategy::Static, move |q| { if let Some(indexes) = q.metric_to_indexes(path.metric.clone()) { return Ok(indexes.clone()) } Err(q.metric_not_found_error(&path.metric)) }).await }, |op| op .id("get_metric_info") .metrics_tag() .summary("Get supported indexes for a metric") .description( "Returns the list of indexes supported by the specified metric. \ For example, `realized_price` might be available on dateindex, weekindex, and monthindex." ) .ok_response::>() .not_modified() .not_found() .server_error(), ), ) .api_route( "/api/metric/{metric}/{index}", get_with( async |uri: Uri, headers: HeaderMap, state: State, Path(path): Path, Query(range): Query| -> Response { data::handler( uri, headers, Query(MetricSelection::from((path.index, path.metric, range))), state, ) .await }, |op| op .id("get_metric") .metrics_tag() .summary("Get metric data") .description( "Fetch data for a specific metric at the given index. \ Use query parameters to filter by date range and format (json/csv)." ) .ok_response::() .csv_response() .not_modified() .not_found(), ), ) .api_route( "/api/metrics/bulk", get_with( bulk::handler, |op| op .id("get_metrics") .metrics_tag() .summary("Bulk metric data") .description( "Fetch multiple metrics in a single request. Supports filtering by index and date range. \ Returns an array of MetricData objects. For a single metric, use `get_metric` instead." ) .ok_response::>() .csv_response() .not_modified(), ), ) .api_route( "/api/vecs/{variant}", get_with( async |uri: Uri, headers: HeaderMap, Path(variant): Path, Query(range): Query, state: State| -> Response { let separator = "_to_"; let variant = variant.replace("-", "_"); let mut split = variant.split(separator); let ser_index = split.next().unwrap(); let Ok(index) = Index::try_from(ser_index) else { return format!("Index {ser_index} doesn't exist").into_response(); }; let params = MetricSelection::from(( index, Metrics::from(split.collect::>().join(separator)), range, )); legacy::handler(uri, headers, Query(params), state).await }, |op| op .metrics_tag() .summary("Legacy variant endpoint") .description( "**DEPRECATED** - Use `/api/metric/{metric}/{index}` instead.\n\n\ Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\ Legacy endpoint for querying metrics by variant path (e.g., `dateindex_to_price`). \ Returns raw data without the MetricData wrapper." ) .deprecated() .ok_response::() .not_modified(), ), ) .api_route( "/api/vecs/query", get_with( async |uri: Uri, headers: HeaderMap, Query(params): Query, state: State| -> Response { legacy::handler(uri, headers, Query(params.into()), state).await }, |op| op .metrics_tag() .summary("Legacy query endpoint") .description( "**DEPRECATED** - Use `/api/metric/{metric}/{index}` or `/api/metrics/bulk` instead.\n\n\ Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\ Legacy endpoint for querying metrics. Returns raw data without the MetricData wrapper." ) .deprecated() .ok_response::() .not_modified(), ), ) } }