use crate::api::paths;
use crate::error::{Error, Result};
use crate::sequencer::Sequencer;
use crate::storage::TileStorage;
use crate::types::Entry;
use crate::vindex::VerifiableIndex;
use axum::{
body::Bytes,
extract::{Path, State},
http::{header, StatusCode},
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub storage: TileStorage,
pub sequencer: Sequencer,
pub vindex: Option<Arc<VerifiableIndex>>,
}
impl AppState {
pub fn new(storage: TileStorage, sequencer: Sequencer) -> Self {
Self {
storage,
sequencer,
vindex: None,
}
}
pub fn with_vindex(mut self, vindex: Arc<VerifiableIndex>) -> Self {
self.vindex = Some(vindex);
self
}
}
pub async fn add_entry(State(state): State<Arc<AppState>>, body: Bytes) -> Result<Response> {
if body.is_empty() {
return Err(Error::InvalidEntry("empty entry".into()));
}
let entry = Entry::new(body.to_vec());
let index = state.sequencer.add(entry).await?;
let response = (StatusCode::OK, index.to_string());
Ok(response.into_response())
}
pub async fn get_checkpoint(State(state): State<Arc<AppState>>) -> Result<Response> {
match state.storage.read_checkpoint().await? {
Some(data) => {
let response = (
StatusCode::OK,
[(header::CACHE_CONTROL, "no-cache")],
data.into_bytes(),
);
Ok(response.into_response())
}
None => Err(Error::NotFound("checkpoint not found".into())),
}
}
pub async fn get_tile(
State(state): State<Arc<AppState>>,
Path((level, tile_path)): Path<(String, String)>,
) -> Result<Response> {
let (level, index, partial) = paths::parse_tile_path(&level, &tile_path)?;
let path = paths::tile_path(level, index, partial);
match state.storage.read_raw(&path).await? {
Some(data) => {
let response = (
StatusCode::OK,
[
(header::CACHE_CONTROL, "max-age=31536000, immutable"),
(header::CONTENT_TYPE, "application/octet-stream"),
],
data,
);
Ok(response.into_response())
}
None => Err(Error::NotFound(format!("tile not found: {}", path))),
}
}
pub async fn get_entries(
State(state): State<Arc<AppState>>,
Path(entries_path): Path<String>,
) -> Result<Response> {
let (index, partial) = paths::parse_tile_index(&entries_path)?;
let path = paths::entries_path(index, partial);
match state.storage.read_raw(&path).await? {
Some(data) => {
let response = (
StatusCode::OK,
[(header::CONTENT_TYPE, "application/octet-stream")],
data,
);
Ok(response.into_response())
}
None => Err(Error::NotFound(format!("entries not found: {}", path))),
}
}
pub async fn health() -> &'static str {
"ok"
}
#[derive(Debug, Serialize)]
pub struct VindexProofNode {
pub label_bit_len: u32,
pub label_path: String,
pub hash: String,
}
#[derive(Debug, Serialize)]
pub struct VindexLookupResponse {
pub indices: Vec<u64>,
pub tree_size: u64,
pub found: bool,
pub proof: Vec<VindexProofNode>,
pub root_hash: String,
}
pub async fn vindex_lookup(
State(state): State<Arc<AppState>>,
Path(hash_str): Path<String>,
) -> Result<Response> {
let vindex = state
.vindex
.as_ref()
.ok_or_else(|| Error::Internal("vindex not enabled".into()))?;
let hash_bytes = hex::decode(&hash_str)
.map_err(|e| Error::InvalidEntry(format!("invalid hex hash: {}", e)))?;
if hash_bytes.len() != 32 {
return Err(Error::InvalidEntry(format!(
"hash must be 32 bytes, got {}",
hash_bytes.len()
)));
}
let mut key = [0u8; 32];
key.copy_from_slice(&hash_bytes);
let result = vindex.lookup(&key);
let root_hash = vindex.root_hash();
let response = VindexLookupResponse {
indices: result.indices.iter().map(|i| i.value()).collect(),
tree_size: result.tree_size,
found: result.found,
proof: result
.proof
.into_iter()
.map(|n| VindexProofNode {
label_bit_len: n.label_bit_len,
label_path: hex::encode(&n.label_path),
hash: hex::encode(n.hash),
})
.collect(),
root_hash: hex::encode(root_hash),
};
Ok((StatusCode::OK, Json(response)).into_response())
}
pub async fn vindex_lookup_key(
State(state): State<Arc<AppState>>,
Path(key): Path<String>,
) -> Result<Response> {
let vindex = state
.vindex
.as_ref()
.ok_or_else(|| Error::Internal("vindex not enabled".into()))?;
let result = vindex.lookup_string(&key);
let root_hash = vindex.root_hash();
let response = VindexLookupResponse {
indices: result.indices.iter().map(|i| i.value()).collect(),
tree_size: result.tree_size,
found: result.found,
proof: result
.proof
.into_iter()
.map(|n| VindexProofNode {
label_bit_len: n.label_bit_len,
label_path: hex::encode(&n.label_path),
hash: hex::encode(n.hash),
})
.collect(),
root_hash: hex::encode(root_hash),
};
Ok((StatusCode::OK, Json(response)).into_response())
}
#[derive(Debug, Serialize)]
pub struct VindexStatsResponse {
pub tree_size: u64,
pub key_count: usize,
pub root_hash: String,
}
pub async fn vindex_stats(State(state): State<Arc<AppState>>) -> Result<Response> {
let vindex = state
.vindex
.as_ref()
.ok_or_else(|| Error::Internal("vindex not enabled".into()))?;
let response = VindexStatsResponse {
tree_size: vindex.tree_size(),
key_count: vindex.key_count(),
root_hash: hex::encode(vindex.root_hash()),
};
Ok((StatusCode::OK, Json(response)).into_response())
}