#[cfg(test)]
mod tests;
use anda_core::{BoxError, Json};
use anda_kip::{
AggregationFunction, BeliefStatus, Command, FindExpression, MetaCommand, OperationStatus,
Projection, Request, Response, TopLevelStatus, WhereClause,
};
use serde::Deserialize;
pub fn single_read_result(response: &Response) -> Result<&Json, BoxError> {
if response.kip != "2.0"
|| response.status != TopLevelStatus::Succeeded
|| response.error.is_some()
|| response.results.len() != 1
{
return Err(format!("read envelope did not succeed: {response:?}").into());
}
let operation = &response.results[0];
if operation.status != OperationStatus::Succeeded || operation.error.is_some() {
return Err(format!("read operation did not succeed: {operation:?}").into());
}
operation
.result
.as_ref()
.ok_or_else(|| "read operation returned no result".into())
}
#[derive(Debug)]
pub struct SearchObservation<'a> {
pub hits: &'a [Json],
pub exhaustive: Option<bool>,
}
pub fn search_observation(response: &Response) -> Result<SearchObservation<'_>, BoxError> {
let result = single_read_result(response)?;
let hits = result
.get("hits")
.and_then(Json::as_array)
.ok_or("SEARCH result is missing its hits array")?;
let _context = result
.get("search_context")
.and_then(Json::as_object)
.ok_or("SEARCH result is missing its search_context")?;
for hit in hits {
let id = hit.get("id").and_then(Json::as_str);
let element = hit.get("element").and_then(Json::as_object);
if id.is_none()
|| element.is_none()
|| element
.and_then(|element| element.get("id"))
.and_then(Json::as_str)
!= id
{
return Err("SEARCH hit is missing its matching element".into());
}
}
let exhaustive = match result.get("exhaustive") {
None => None,
Some(Json::Bool(value)) => Some(*value),
Some(_) => return Err("SEARCH exhaustive coverage must be a boolean".into()),
};
Ok(SearchObservation {
hits,
exhaustive: if has_cursor(response) {
Some(false)
} else {
exhaustive
},
})
}
#[derive(Debug)]
pub enum ProbeObservation {
Rows { count: usize, complete: bool },
Search { hit_count: usize },
Belief(Box<Projection>),
}
impl ProbeObservation {
pub fn hit_count(&self) -> usize {
match self {
Self::Rows { count, .. } => *count,
Self::Search { hit_count } => *hit_count,
Self::Belief(_) => 1,
}
}
pub fn raw_presence(&self) -> Option<bool> {
match self {
Self::Rows { count, complete } if *count > 0 || *complete => Some(*count > 0),
_ => None,
}
}
pub fn belief_holds(&self) -> Option<bool> {
match self {
Self::Belief(projection) => match projection.status {
BeliefStatus::Accepted => Some(true),
BeliefStatus::Rejected => Some(false),
_ => None,
},
_ => None,
}
}
}
pub fn probe_observation(
request: &Request,
response: &Response,
) -> Result<ProbeObservation, BoxError> {
request.validate()?;
if request.operations.len() != 1 {
return Err("one expectation requires exactly one read operation".into());
}
let result = single_read_result(response)?;
match request.operations[0].parse()? {
Command::Meta(MetaCommand::Search(_)) => Ok(ProbeObservation::Search {
hit_count: search_observation(response)?.hits.len(),
}),
Command::Kql(query) => {
if query.where_clauses.iter().any(has_belief) {
let [FindExpression::Variable(projected)] =
query.find_clause.expressions.as_slice()
else {
return Err(
"a belief expectation must return one full BELIEF projection".into(),
);
};
if !projected.path.is_empty()
|| !query.where_clauses.iter().any(|clause| {
matches!(clause,
WhereClause::Belief { variable, .. } if variable == &projected.var)
})
{
return Err(
"a belief expectation must directly project its BELIEF variable".into(),
);
}
if has_cursor(response) {
return Err(
"BELIEF probe has additional candidate rows; its target is ambiguous"
.into(),
);
}
let rows = result
.as_array()
.ok_or("BELIEF query did not return rows")?;
let [row] = rows.as_slice() else {
return Err(
"BELIEF query did not return one unambiguous projection; state is unknown"
.into(),
);
};
let projection = Projection::deserialize(row)?;
if projection
.basis
.as_ref()
.is_none_or(|basis| basis.policy.id.is_empty())
|| projection
.slot_status
.is_some_and(|status| status != projection.status)
{
return Err("BELIEF projection is missing its basis or carries inconsistent final status".into());
}
return Ok(ProbeObservation::Belief(Box::new(projection)));
}
let rows = result
.as_array()
.ok_or("raw FIND query did not return rows")?;
let count = if matches!(
query.find_clause.expressions.as_slice(),
[FindExpression::Aggregation {
func: AggregationFunction::Count,
..
}]
) {
let [count] = rows.as_slice() else {
return Err("COUNT query did not return one count".into());
};
usize::try_from(
count
.as_u64()
.ok_or("COUNT query did not return a nonnegative integer")?,
)?
} else {
rows.len()
};
Ok(ProbeObservation::Rows {
count,
complete: !has_cursor(response),
})
}
_ => Err(
"an existence expectation requires raw FIND; a truth expectation requires BELIEF"
.into(),
),
}
}
fn has_belief(clause: &WhereClause) -> bool {
match clause {
WhereClause::Belief { .. } | WhereClause::BeliefSlot { .. } => true,
WhereClause::Not(clauses)
| WhereClause::Optional(clauses)
| WhereClause::Union(clauses) => clauses.iter().any(has_belief),
_ => false,
}
}
fn has_cursor(response: &Response) -> bool {
response.next_cursor.is_some()
|| response
.results
.iter()
.any(|operation| operation.next_cursor.is_some())
}