global: snapshot

This commit is contained in:
nym21
2026-03-12 01:30:50 +01:00
parent 71dd7e9852
commit b97f32f86e
51 changed files with 916 additions and 652 deletions
+18 -18
View File
@@ -6,7 +6,7 @@ use axum::{
routing::get,
};
use brk_types::{
AddressParam, AddressStats, AddressTxidsParam, AddressValidation, Txid, Utxo,
AddressParam, AddressStats, AddressTxidsParam, AddressValidation, Transaction, Txid, Utxo,
ValidateAddressParam,
};
@@ -53,13 +53,13 @@ impl AddressRoutes for ApiRouter<AppState> {
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txs(path.address, params.after_txid, 25)).await
}, |op| op
.id("get_address_txs")
.addresses_tag()
.summary("Address transaction IDs")
.description("Get transaction IDs for an address, newest first. Use after_txid for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*")
.ok_response::<Vec<Txid>>()
.summary("Address transactions")
.description("Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*")
.ok_response::<Vec<Transaction>>()
.not_modified()
.bad_request()
.not_found()
@@ -67,20 +67,21 @@ impl AddressRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/address/{address}/utxo",
"/api/address/{address}/txs/chain",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_utxos(path.address)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txs(path.address, params.after_txid, 25)).await
}, |op| op
.id("get_address_utxos")
.id("get_address_confirmed_txs")
.addresses_tag()
.summary("Address UTXOs")
.description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*")
.ok_response::<Vec<Utxo>>()
.summary("Address confirmed transactions")
.description("Get confirmed transactions for an address, 25 per page. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*")
.ok_response::<Vec<Transaction>>()
.not_modified()
.bad_request()
.not_found()
@@ -109,21 +110,20 @@ impl AddressRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api/address/{address}/txs/chain",
"/api/address/{address}/utxo",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, 25)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_utxos(path.address)).await
}, |op| op
.id("get_address_confirmed_txs")
.id("get_address_utxos")
.addresses_tag()
.summary("Address confirmed transactions")
.description("Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*")
.ok_response::<Vec<Txid>>()
.summary("Address UTXOs")
.description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*")
.ok_response::<Vec<Utxo>>()
.not_modified()
.bad_request()
.not_found()
+69 -69
View File
@@ -38,6 +38,53 @@ impl BlockRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/blocks/{height}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await
},
|op| {
op.id("get_blocks_from_height")
.blocks_tag()
.summary("Blocks from height")
.description(
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*",
)
.ok_response::<Vec<BlockInfo>>()
.not_modified()
.bad_request()
.server_error()
},
),
)
.api_route(
"/api/block-height/{height}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await
},
|op| {
op.id("get_block_by_height")
.blocks_tag()
.summary("Block by height")
.description(
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*",
)
.ok_response::<BlockInfo>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}",
get_with(
@@ -86,53 +133,6 @@ impl BlockRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/block-height/{height}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await
},
|op| {
op.id("get_block_by_height")
.blocks_tag()
.summary("Block by height")
.description(
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*",
)
.ok_response::<BlockInfo>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/blocks/{height}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await
},
|op| {
op.id("get_blocks_from_height")
.blocks_tag()
.summary("Blocks from height")
.description(
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*",
)
.ok_response::<Vec<BlockInfo>>()
.not_modified()
.bad_request()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}/txids",
get_with(
@@ -206,28 +206,6 @@ impl BlockRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/v1/mining/blocks/timestamp/{timestamp}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<TimestampParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.id("get_block_by_timestamp")
.blocks_tag()
.summary("Block by timestamp")
.description("Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*")
.ok_response::<BlockTimestamp>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/block/{hash}/raw",
get_with(
@@ -252,5 +230,27 @@ impl BlockRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/v1/mining/blocks/timestamp/{timestamp}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<TimestampParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.id("get_block_by_timestamp")
.blocks_tag()
.summary("Block by timestamp")
.description("Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*")
.ok_response::<BlockTimestamp>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
),
)
}
}
+16 -16
View File
@@ -51,22 +51,6 @@ impl MempoolRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/v1/fees/recommended",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await
},
|op| {
op.id("get_recommended_fees")
.mempool_tag()
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets based on current mempool state.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
.ok_response::<RecommendedFees>()
.server_error()
},
),
)
.api_route(
"/api/mempool/price",
get_with(
@@ -87,6 +71,22 @@ impl MempoolRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/v1/fees/recommended",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await
},
|op| {
op.id("get_recommended_fees")
.mempool_tag()
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets based on current mempool state.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
.ok_response::<RecommendedFees>()
.server_error()
},
),
)
.api_route(
"/api/v1/fees/mempool-blocks",
get_with(
+140 -139
View File
@@ -171,6 +171,74 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.server_error(),
),
)
.api_route(
"/api/metric/{metric}/{index}",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<MetricWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::handler(
uri,
headers,
addr,
Query(MetricSelection::from((path.index, path.metric, range))),
state,
)
.await
.into_response()
},
|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::<MetricData>()
.csv_response()
.not_modified()
.not_found(),
),
)
.api_route(
"/api/metric/{metric}/{index}/data",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<MetricWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::raw_handler(
uri,
headers,
addr,
Query(MetricSelection::from((path.index, path.metric, range))),
state,
)
.await
.into_response()
},
|op| op
.id("get_metric_data")
.metrics_tag()
.summary("Get raw metric data")
.description(
"Returns just the data array without the MetricData wrapper. \
Supports the same range and format parameters as the standard endpoint."
)
.ok_response::<Vec<serde_json::Value>>()
.csv_response()
.not_modified()
.not_found(),
),
)
.api_route(
"/api/metric/{metric}/{index}/latest",
get_with(
@@ -239,74 +307,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.not_found(),
),
)
.api_route(
"/api/metric/{metric}/{index}/data",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<MetricWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::raw_handler(
uri,
headers,
addr,
Query(MetricSelection::from((path.index, path.metric, range))),
state,
)
.await
.into_response()
},
|op| op
.id("get_metric_data")
.metrics_tag()
.summary("Get raw metric data")
.description(
"Returns just the data array without the MetricData wrapper. \
Supports the same range and format parameters as the standard endpoint."
)
.ok_response::<Vec<serde_json::Value>>()
.csv_response()
.not_modified()
.not_found(),
),
)
.api_route(
"/api/metric/{metric}/{index}",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<MetricWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::handler(
uri,
headers,
addr,
Query(MetricSelection::from((path.index, path.metric, range))),
state,
)
.await
.into_response()
},
|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::<MetricData>()
.csv_response()
.not_modified()
.not_found(),
),
)
.api_route(
"/api/metrics/bulk",
get_with(
@@ -326,77 +326,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.not_modified(),
),
)
.api_route(
"/api/vecs/{variant}",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Path(variant): Path<String>,
Query(range): Query<DataRangeFormat>,
state: State<AppState>|
-> 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 Error::not_found(
format!("Index '{ser_index}' doesn't exist")
).into_response();
};
let params = MetricSelection::from((
index,
Metrics::from(split.collect::<Vec<_>>().join(separator)),
range,
));
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
},
|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., `day1_to_price`). \
Returns raw data without the MetricData wrapper."
)
.deprecated()
.ok_response::<serde_json::Value>()
.not_modified(),
),
)
.api_route(
"/api/vecs/query",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Query(params): Query<MetricSelectionLegacy>,
state: State<AppState>|
-> Response {
let params: MetricSelection = params.into();
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
},
|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::<serde_json::Value>()
.not_modified(),
),
)
// Cost basis distribution endpoints
.api_route(
"/api/metrics/cost-basis",
@@ -475,5 +404,77 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
},
),
)
// Deprecated endpoints
.api_route(
"/api/vecs/{variant}",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Path(variant): Path<String>,
Query(range): Query<DataRangeFormat>,
state: State<AppState>|
-> 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 Error::not_found(
format!("Index '{ser_index}' doesn't exist")
).into_response();
};
let params = MetricSelection::from((
index,
Metrics::from(split.collect::<Vec<_>>().join(separator)),
range,
));
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
},
|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., `day1_to_price`). \
Returns raw data without the MetricData wrapper."
)
.deprecated()
.ok_response::<serde_json::Value>()
.not_modified(),
),
)
.api_route(
"/api/vecs/query",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Query(params): Query<MetricSelectionLegacy>,
state: State<AppState>|
-> Response {
let params: MetricSelection = params.into();
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
},
|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::<serde_json::Value>()
.not_modified(),
),
)
}
}
+5 -8
View File
@@ -2,7 +2,7 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::{HeaderMap, Uri},
response::Redirect,
response::{IntoResponse, Redirect, Response},
routing::get,
};
use brk_types::{
@@ -11,7 +11,7 @@ use brk_types::{
RewardStats, TimePeriodParam,
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use crate::{CacheStrategy, Error, extended::TransformResponseExtended};
use super::AppState;
@@ -200,18 +200,15 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/fee-rates/{time_period}",
get_with(
async |Path(_path): Path<TimePeriodParam>| {
axum::Json(serde_json::json!({
"status": "wip",
"message": "This endpoint is work in progress. Percentile fields are not yet available."
}))
async |Path(_path): Path<TimePeriodParam>| -> Response {
Error::not_implemented("Fee rate percentiles are not yet available").into_response()
},
|op| {
op.id("get_block_fee_rates")
.mining_tag()
.summary("Block fee rates (WIP)")
.description("**Work in progress.** Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
.ok_response::<serde_json::Value>()
.server_error()
},
),
)
+8 -5
View File
@@ -12,6 +12,7 @@ use axum::{
};
use crate::{
Error,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes,
@@ -39,13 +40,13 @@ pub trait ApiRoutes {
impl ApiRoutes for ApiRouter<AppState> {
fn add_api_routes(self) -> Self {
self.add_addresses_routes()
self.add_server_routes()
.add_metrics_routes()
.add_block_routes()
.add_tx_routes()
.add_addresses_routes()
.add_mempool_routes()
.add_mining_routes()
.add_tx_routes()
.add_metrics_routes()
.add_server_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.api_route(
"/openapi.json",
@@ -99,7 +100,9 @@ impl ApiRoutes for ApiRouter<AppState> {
)
.route(
"/api/{*path}",
get(|| async { Redirect::permanent("/api") }),
get(|| async {
Error::not_found("Unknown API endpoint. See /api for documentation.")
}),
)
}
}
+1 -30
View File
@@ -14,7 +14,7 @@ mod compact;
pub use compact::ApiJson;
use aide::openapi::{Contact, Info, License, OpenApi, Server, ServerVariable, Tag};
use aide::openapi::{Contact, Info, License, OpenApi, Tag};
use crate::VERSION;
@@ -159,38 +159,9 @@ All errors return structured JSON with a consistent format:
},
];
let servers = vec![Server {
url: "{scheme}://{host}".into(),
description: Some("BRK server".into()),
variables: [
(
"scheme".into(),
ServerVariable {
enumeration: vec!["https".into(), "http".into()],
default: "https".into(),
description: Some("Protocol".into()),
..Default::default()
},
),
(
"host".into(),
ServerVariable {
default: "bitview.space".into(),
description: Some(
"Server address (e.g. bitview.space or localhost:3110)".into(),
),
..Default::default()
},
),
]
.into(),
..Default::default()
}];
OpenApi {
info,
tags,
servers,
..OpenApi::default()
}
}
+49 -49
View File
@@ -18,6 +18,55 @@ pub trait ServerRoutes {
impl ServerRoutes for ApiRouter<AppState> {
fn add_server_routes(self) -> Self {
self.api_route(
"/health",
get_with(
async |State(state): State<AppState>| -> axum::Json<Health> {
let uptime = state.started_instant.elapsed();
let tip_height = state.client.get_last_height();
let sync = state.sync(|q| {
let tip_height = tip_height.unwrap_or(q.indexed_height());
q.sync_status(tip_height)
});
axum::Json(Health {
status: Cow::Borrowed("healthy"),
service: Cow::Borrowed("brk"),
version: Cow::Borrowed(VERSION),
timestamp: jiff::Timestamp::now().to_string(),
started_at: state.started_at.to_string(),
uptime_seconds: uptime.as_secs(),
sync,
})
},
|op| {
op.id("get_health")
.server_tag()
.summary("Health check")
.description("Returns the health status of the API server, including uptime information.")
.ok_response::<Health>()
},
),
)
.api_route(
"/version",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, &uri, |_| {
Ok(env!("CARGO_PKG_VERSION"))
})
.await
},
|op| {
op.id("get_version")
.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
.not_modified()
},
),
)
.api_route(
"/api/server/sync",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
@@ -67,55 +116,6 @@ impl ServerRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/health",
get_with(
async |State(state): State<AppState>| -> axum::Json<Health> {
let uptime = state.started_instant.elapsed();
let tip_height = state.client.get_last_height();
let sync = state.sync(|q| {
let tip_height = tip_height.unwrap_or(q.indexed_height());
q.sync_status(tip_height)
});
axum::Json(Health {
status: Cow::Borrowed("healthy"),
service: Cow::Borrowed("brk"),
version: Cow::Borrowed(VERSION),
timestamp: jiff::Timestamp::now().to_string(),
started_at: state.started_at.to_string(),
uptime_seconds: uptime.as_secs(),
sync,
})
},
|op| {
op.id("get_health")
.server_tag()
.summary("Health check")
.description("Returns the health status of the API server, including uptime information.")
.ok_response::<Health>()
},
),
)
.api_route(
"/version",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, &uri, |_| {
Ok(env!("CARGO_PKG_VERSION"))
})
.await
},
|op| {
op.id("get_version")
.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
.not_modified()
},
),
)
}
}