global: snapshot

This commit is contained in:
nym21
2026-01-16 23:49:49 +01:00
parent 3b00a92fa4
commit 6bb1a2a311
34 changed files with 2600 additions and 5071 deletions

View File

@@ -6,11 +6,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::Result;
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
Result,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
@@ -18,22 +18,10 @@ use crate::{
use super::AppState;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
State(AppState { query, cache, .. }): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;

View File

@@ -6,11 +6,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::Result;
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
Result,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
@@ -18,22 +18,10 @@ use crate::{
use super::AppState;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
State(AppState { query, cache, .. }): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;

View File

@@ -6,11 +6,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::Result;
use brk_types::{Format, MetricSelection, OutputLegacy};
use quick_cache::sync::GuardResult;
use crate::{
Result,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
@@ -18,22 +18,10 @@ use crate::{
use super::AppState;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
State(AppState { query, cache, .. }): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;

View File

@@ -170,6 +170,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state,
)
.await
.into_response()
},
|op| op
.id("get_metric")
@@ -188,7 +189,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.api_route(
"/api/metrics/bulk",
get_with(
bulk::handler,
|uri, headers, query, state| async move {
bulk::handler(uri, headers, query, state).await.into_response()
},
|op| op
.id("get_metrics")
.metrics_tag()
@@ -225,7 +228,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
Metrics::from(split.collect::<Vec<_>>().join(separator)),
range,
));
legacy::handler(uri, headers, Query(params), state).await
legacy::handler(uri, headers, Query(params), state)
.await
.into_response()
},
|op| op
.metrics_tag()
@@ -250,7 +255,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state: State<AppState>|
-> Response {
let params: MetricSelection = params.into();
legacy::handler(uri, headers, Query(params), state).await
legacy::handler(uri, headers, Query(params), state)
.await
.into_response()
},
|op| op
.metrics_tag()

View File

@@ -48,7 +48,7 @@ impl ApiRoutes for ApiRouter<AppState> {
.add_server_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.api_route(
"/api.json",
"/openapi.json",
get_with(
async |headers: HeaderMap,
Extension(api): Extension<Arc<OpenApi>>|
@@ -62,7 +62,7 @@ impl ApiRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api.trimmed.json",
"/api.json",
get_with(
async |headers: HeaderMap,
Extension(api_trimmed): Extension<Arc<String>>|
@@ -72,12 +72,13 @@ impl ApiRoutes for ApiRouter<AppState> {
Response::static_json(&headers, &value)
},
|op| {
op.id("get_openapi_trimmed")
op.id("get_api")
.server_tag()
.summary("Trimmed OpenAPI specification")
.summary("Compact OpenAPI specification")
.description(
"Compact OpenAPI specification optimized for LLM consumption. \
Removes redundant fields while preserving essential API information.",
Removes redundant fields while preserving essential API information. \
Full spec available at `/openapi.json`.",
)
.ok_response::<serde_json::Value>()
},

View File

@@ -29,7 +29,7 @@ pub fn create_openapi() -> OpenApi {
- **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.)
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format
- **Multiple formats**: JSON and CSV output
- **LLM-optimized**: Compact OpenAPI spec at [`/api.trimmed.json`](/api.trimmed.json) for AI tools
- **LLM-optimized**: Compact OpenAPI spec at [`/api.json`](/api.json) for AI tools (full spec at [`/openapi.json`](/openapi.json))
### Client Libraries

View File

@@ -18,7 +18,7 @@
<script>
Scalar.createApiReference("#app", {
url: "/api.json",
url: "/openapi.json",
hideClientButton: true,
telemetry: false,
// showToolbar: "never",

View File

@@ -0,0 +1,58 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use brk_error::Error as BrkError;
/// Server result type with Error that implements IntoResponse.
pub type Result<T> = std::result::Result<T, Error>;
/// Server error type that maps to HTTP status codes.
pub struct Error(StatusCode, String);
impl Error {
pub fn bad_request(msg: impl Into<String>) -> Self {
Self(StatusCode::BAD_REQUEST, msg.into())
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self(StatusCode::FORBIDDEN, msg.into())
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self(StatusCode::NOT_FOUND, msg.into())
}
pub fn internal(msg: impl Into<String>) -> Self {
Self(StatusCode::INTERNAL_SERVER_ERROR, msg.into())
}
}
impl From<BrkError> for Error {
fn from(e: BrkError) -> Self {
let status = match &e {
BrkError::InvalidTxid
| BrkError::InvalidNetwork
| BrkError::InvalidAddress
| BrkError::UnsupportedType(_)
| BrkError::Parse(_)
| BrkError::NoMetrics
| BrkError::MetricUnsupportedIndex { .. }
| BrkError::WeightExceeded { .. } => StatusCode::BAD_REQUEST,
BrkError::UnknownAddress
| BrkError::UnknownTxid
| BrkError::NotFound(_)
| BrkError::MetricNotFound { .. } => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
Self(status, e.to_string())
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
(self.0, self.1).into_response()
}
}

View File

@@ -7,30 +7,30 @@ use std::{
use axum::{
body::Body,
extract::{self, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
http::HeaderMap,
response::Response,
};
use brk_error::Result;
use quick_cache::sync::GuardResult;
use tracing::{error, info};
use crate::{
AppState, EMBEDDED_WEBSITE, HeaderMapExtended, ModifiedState, ResponseExtended, WebsiteSource,
AppState, EMBEDDED_WEBSITE, Error, HeaderMapExtended, ModifiedState, ResponseExtended, Result,
WebsiteSource,
};
pub async fn file_handler(
headers: HeaderMap,
State(state): State<AppState>,
path: extract::Path<String>,
) -> Response {
) -> Result<Response> {
any_handler(headers, state, Some(path.0))
}
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Result<Response> {
any_handler(headers, state, None)
}
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Response {
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Result<Response> {
match &state.website {
WebsiteSource::Disabled => unreachable!("routes not added when disabled"),
WebsiteSource::Embedded => embedded_handler(&state, path),
@@ -92,7 +92,7 @@ fn build_response(state: &AppState, path: &Path, content: Vec<u8>, cache_key: &s
response
}
fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
fn embedded_handler(state: &AppState, path: Option<String>) -> Result<Response> {
let path = path.unwrap_or_else(|| "index.html".to_string());
let sanitized = sanitize_path(&path);
@@ -113,17 +113,15 @@ fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
});
let Some(file) = file else {
let response: Response<Body> =
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
return response;
return Err(Error::not_found("File not found"));
};
build_response(
Ok(build_response(
state,
Path::new(file.path()),
file.contents().to_vec(),
&file.path().to_string_lossy(),
)
))
}
fn filesystem_handler(
@@ -131,7 +129,7 @@ fn filesystem_handler(
state: &AppState,
files_path: &Path,
path: Option<String>,
) -> Response {
) -> Result<Response> {
let path = if let Some(path) = path {
let sanitized = sanitize_path(&path);
let mut path = files_path.join(&sanitized);
@@ -145,9 +143,7 @@ fn filesystem_handler(
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
return response;
return Err(Error::forbidden("Access denied"));
}
}
@@ -162,12 +158,7 @@ fn filesystem_handler(
// SPA fallback
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
return response;
return Err(Error::not_found("File doesn't exist"));
} else {
path = files_path.join("index.html");
}
@@ -181,14 +172,7 @@ fn filesystem_handler(
path_to_response(&headers, state, &path)
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());

View File

@@ -17,7 +17,6 @@ use axum::{
routing::get,
serve,
};
use brk_error::Result;
use brk_query::AsyncQuery;
use include_dir::{Dir, include_dir};
use quick_cache::sync::Cache;
@@ -48,12 +47,14 @@ impl WebsiteSource {
mod api;
pub mod cache;
mod error;
mod extended;
mod files;
mod state;
use api::*;
pub use cache::{CacheParams, CacheStrategy};
pub use error::{Error, Result};
use extended::*;
use files::FilesRoutes;
use state::*;
@@ -75,7 +76,7 @@ impl Server {
})
}
pub async fn serve(self) -> Result<()> {
pub async fn serve(self) -> brk_error::Result<()> {
let state = self.0;
let compression_layer = CompressionLayer::new()