use crate::error::{Error, Result};
use crate::rest::dag::{
action_to_dto, DagActionDto, DagStatsResponse, DagTipsResponse, PruneRequest, PruneResponse,
};
use crate::state::AppState;
pub(crate) const DEFAULT_HISTORY_LIMIT: usize = 50;
pub async fn history_by_subject(
state: &AppState,
subject: &str,
limit: usize,
) -> Result<Vec<DagActionDto>> {
let graph = state.graph.read().await;
let actions = graph
.dag_history_by_subject(subject, limit)
.map_err(|e| Error::Internal(e.to_string()))?;
Ok(actions.iter().map(action_to_dto).collect())
}
pub async fn tips(state: &AppState) -> Result<DagTipsResponse> {
let graph = state.graph.read().await;
let dag_store = graph
.dag_store()
.ok_or_else(|| Error::Internal("DAG not enabled".into()))?;
let tips = dag_store
.tips()
.map_err(|e| Error::Internal(e.to_string()))?;
let tip_strings: Vec<String> = tips.iter().map(|h| h.to_hex()).collect();
let count = tip_strings.len();
Ok(DagTipsResponse {
tips: tip_strings,
count,
})
}
pub async fn action(state: &AppState, hash: &str) -> Result<DagActionDto> {
let action_hash = aingle_graph::dag::DagActionHash::from_hex(hash)
.ok_or_else(|| Error::InvalidInput(format!("Invalid DAG action hash: {}", hash)))?;
let graph = state.graph.read().await;
let dag_store = graph
.dag_store()
.ok_or_else(|| Error::Internal("DAG not enabled".into()))?;
let action = dag_store
.get(&action_hash)
.map_err(|e| Error::Internal(e.to_string()))?
.ok_or_else(|| Error::NotFound(format!("DAG action {} not found", hash)))?;
Ok(action_to_dto(&action))
}
pub async fn chain(state: &AppState, author: &str, limit: usize) -> Result<Vec<DagActionDto>> {
let author = aingle_graph::NodeId::named(author);
let graph = state.graph.read().await;
let dag_store = graph
.dag_store()
.ok_or_else(|| Error::Internal("DAG not enabled".into()))?;
let actions = dag_store
.chain(&author, limit)
.map_err(|e| Error::Internal(e.to_string()))?;
Ok(actions.iter().map(action_to_dto).collect())
}
pub async fn stats(state: &AppState) -> Result<DagStatsResponse> {
let graph = state.graph.read().await;
let dag_store = graph
.dag_store()
.ok_or_else(|| Error::Internal("DAG not enabled".into()))?;
let action_count = dag_store.action_count();
let tip_count = dag_store
.tip_count()
.map_err(|e| Error::Internal(e.to_string()))?;
Ok(DagStatsResponse {
action_count,
tip_count,
})
}
pub async fn prune(state: &AppState, req: PruneRequest) -> Result<PruneResponse> {
let policy = match req.policy.as_str() {
"keep_all" => aingle_graph::dag::RetentionPolicy::KeepAll,
"keep_since" => aingle_graph::dag::RetentionPolicy::KeepSince { seconds: req.value },
"keep_last" => aingle_graph::dag::RetentionPolicy::KeepLast(req.value as usize),
"keep_depth" => aingle_graph::dag::RetentionPolicy::KeepDepth(req.value as usize),
other => return Err(Error::InvalidInput(format!("Unknown policy: {}", other))),
};
let graph = state.graph.read().await;
let result = graph
.dag_prune(&policy, req.create_checkpoint)
.map_err(|e| Error::Internal(e.to_string()))?;
Ok(PruneResponse {
pruned_count: result.pruned_count,
retained_count: result.retained_count,
checkpoint_hash: result.checkpoint_hash.map(|h| h.to_hex()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn history_of_unknown_subject_is_empty() {
let state = AppState::with_db_path(":memory:", None).unwrap();
{
let mut graph = state.graph.write().await;
graph.enable_dag();
}
let h = history_by_subject(&state, "ex:nobody", 10).await.unwrap();
assert!(h.is_empty());
}
async fn enabled_state() -> AppState {
let state = AppState::with_db_path(":memory:", None).unwrap();
{
let mut graph = state.graph.write().await;
graph.enable_dag();
}
state
}
#[tokio::test]
async fn tips_of_empty_dag() {
let state = enabled_state().await;
let resp = tips(&state).await.unwrap();
assert_eq!(resp.count, resp.tips.len());
}
#[tokio::test]
async fn action_with_invalid_hash_is_invalid_input() {
let state = enabled_state().await;
let err = action(&state, "not-a-hash").await.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[tokio::test]
async fn chain_of_unknown_author_is_empty() {
let state = enabled_state().await;
let c = chain(&state, "node:nobody", 10).await.unwrap();
assert!(c.is_empty());
}
#[tokio::test]
async fn stats_of_empty_dag() {
let state = enabled_state().await;
let s = stats(&state).await.unwrap();
assert_eq!(s.action_count, 0);
}
#[tokio::test]
async fn prune_keep_all_prunes_nothing() {
let state = enabled_state().await;
let resp = prune(
&state,
PruneRequest {
policy: "keep_all".into(),
value: 0,
create_checkpoint: false,
},
)
.await
.unwrap();
assert_eq!(resp.pruned_count, 0);
}
#[tokio::test]
async fn prune_unknown_policy_is_invalid_input() {
let state = enabled_state().await;
let err = prune(
&state,
PruneRequest {
policy: "bogus".into(),
value: 0,
create_checkpoint: false,
},
)
.await
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
}