use crate::middleware::{is_in_namespace, RequestNamespace};
use crate::state::AppState;
use aingle_graph::{NodeId, Value};
use axum::{
extract::{Path, State},
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub struct ConsistencyResponse {
pub score: f64,
pub total: usize,
pub verified: usize,
}
#[derive(Deserialize, Debug)]
pub struct BatchVerifyAssertionsRequest {
pub assertions: Vec<AssertionRef>,
}
#[derive(Deserialize, Debug)]
pub struct AssertionRef {
pub subject: String,
pub predicate: String,
}
#[derive(Serialize, Debug)]
pub struct AssertionVerifyResult {
pub subject: String,
pub predicate: String,
pub verified: bool,
}
#[derive(Serialize, Debug)]
pub struct BatchVerifyAssertionsResponse {
pub results: Vec<AssertionVerifyResult>,
}
pub async fn get_agent_consistency(
State(state): State<AppState>,
ns_ext: Option<axum::Extension<RequestNamespace>>,
Path(agent_id): Path<String>,
) -> impl IntoResponse {
let ns_prefix = ns_ext
.as_ref()
.and_then(|axum::Extension(RequestNamespace(ns))| ns.clone())
.unwrap_or_else(|| "mayros".to_string());
let (owned_subject_triples, prefixed_triples) = {
let graph = state.graph.read().await;
let agent_node = Value::node(NodeId::named(format!("{}:agent:{}", ns_prefix, agent_id)));
let mut owned = Vec::new();
if let Ok(triples) = graph.get_object(&agent_node) {
for triple in &triples {
let pred_str = triple.predicate.as_str();
if pred_str.ends_with(":assertedBy") || pred_str.ends_with(":ownedBy") {
let subject_triples = graph.get_subject(&triple.subject).unwrap_or_default();
owned.push(subject_triples);
}
}
}
let agent_prefix = format!("{}:agent:{}:", ns_prefix, agent_id);
let mut prefixed = Vec::new();
if let Ok(prefixed_subjects) = graph.subjects_with_prefix(&agent_prefix) {
for subj in &prefixed_subjects {
if let Ok(subj_triples) = graph.get_subject(subj) {
let filtered: Vec<_> = subj_triples
.into_iter()
.filter(|t| {
let p = t.predicate.as_str();
!p.ends_with(":assertedBy") && !p.ends_with(":ownedBy")
})
.collect();
prefixed.push(filtered);
}
}
}
(owned, prefixed)
};
let logic = state.logic.read().await;
let mut total: usize = 0;
let mut verified: usize = 0;
for subject_triples in &owned_subject_triples {
total += 1;
let any_valid = subject_triples.iter().any(|t| logic.validate(t).is_valid);
if any_valid {
verified += 1;
}
}
for triples in &prefixed_triples {
for t in triples {
total += 1;
if logic.validate(t).is_valid {
verified += 1;
}
}
}
drop(logic);
let score = if total > 0 {
verified as f64 / total as f64
} else {
0.0
};
Json(ConsistencyResponse {
score,
total,
verified,
})
}
pub async fn batch_verify_assertions(
State(state): State<AppState>,
ns_ext: Option<axum::Extension<RequestNamespace>>,
Json(req): Json<BatchVerifyAssertionsRequest>,
) -> impl IntoResponse {
let ns_filter = ns_ext.and_then(|axum::Extension(RequestNamespace(ns))| ns);
let assertion_triples: Vec<_> = {
let graph = state.graph.read().await;
req.assertions
.iter()
.map(|assertion| {
if let Some(ref ns) = ns_filter {
if !is_in_namespace(&assertion.subject, ns) {
return None;
}
}
let subj = NodeId::named(&assertion.subject);
let triples = graph.get_subject(&subj).unwrap_or_default();
triples
.into_iter()
.find(|t| t.predicate.as_str() == assertion.predicate)
})
.collect()
};
let logic = state.logic.read().await;
let results: Vec<AssertionVerifyResult> = req
.assertions
.iter()
.zip(assertion_triples.iter())
.map(|(assertion, maybe_triple)| {
let verified = maybe_triple
.as_ref()
.map(|t| logic.validate(t).is_valid)
.unwrap_or(false);
AssertionVerifyResult {
subject: assertion.subject.clone(),
predicate: assertion.predicate.clone(),
verified,
}
})
.collect();
drop(logic);
Json(BatchVerifyAssertionsResponse { results })
}
pub fn reputation_router() -> axum::Router<AppState> {
axum::Router::new()
.route(
"/api/v1/agents/{id}/consistency",
axum::routing::get(get_agent_consistency),
)
.route(
"/api/v1/assertions/verify-batch",
axum::routing::post(batch_verify_assertions),
)
}