use mant_ir::ResolvedContent;
use mant_protocol::{
MAX_SCOPE_DEPTH, MAX_SCOPE_DOCUMENT_LIMIT, ResolvedDocumentScope, ScopedDocument,
};
use std::{collections::BTreeSet, error::Error, fmt};
#[derive(Debug, Clone, Copy)]
pub struct QueryScopeView<'a> {
graph: &'a ResolvedDocumentScope,
documents: &'a [ResolvedContent],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopeInputError {
LengthMismatch,
TooManyDocuments,
AddressMismatch {
index: usize,
},
DuplicateAddress {
index: usize,
},
InvalidSource {
index: usize,
},
UnknownGraphAddress,
}
impl fmt::Display for ScopeInputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LengthMismatch => f.write_str("scope graph and content lengths do not match"),
Self::TooManyDocuments => f.write_str("scope content exceeds the document ceiling"),
Self::AddressMismatch { index } => {
write!(f, "scope content address does not match graph slot {index}")
}
Self::DuplicateAddress { index } => {
write!(f, "scope graph repeats an address at slot {index}")
}
Self::InvalidSource { index } => write!(
f,
"scope source order or coordinates are invalid at slot {index}"
),
Self::UnknownGraphAddress => {
f.write_str("scope graph refers to a document outside the loaded set")
}
}
}
}
impl Error for ScopeInputError {}
impl<'a> QueryScopeView<'a> {
pub fn new(
graph: &'a ResolvedDocumentScope,
documents: &'a [ResolvedContent],
) -> Result<Self, ScopeInputError> {
if graph.documents.len() != documents.len() {
return Err(ScopeInputError::LengthMismatch);
}
if documents.len() > MAX_SCOPE_DOCUMENT_LIMIT as usize {
return Err(ScopeInputError::TooManyDocuments);
}
let mut addresses = BTreeSet::new();
let mut depth = 0;
for (index, (source, content)) in graph.documents.iter().zip(documents).enumerate() {
if content.address.as_ref() != Some(&source.address) {
return Err(ScopeInputError::AddressMismatch { index });
}
if !addresses.insert(&source.address) {
return Err(ScopeInputError::DuplicateAddress { index });
}
if source.depth < depth
|| source.depth > MAX_SCOPE_DEPTH
|| source
.root_indices
.iter()
.any(|root| usize::from(*root) >= graph.query.documents.len())
|| (source.depth != 0 && !source.root_indices.is_empty())
{
return Err(ScopeInputError::InvalidSource { index });
}
depth = source.depth;
}
let known = |address: &mant_ir::DocumentAddress| addresses.contains(address);
if graph
.edges
.iter()
.any(|edge| !known(&edge.from) || !known(&edge.to))
|| graph
.documents
.iter()
.any(|source| source.reached_from.iter().any(|from| !known(from)))
|| graph.frontier.iter().any(|item| !known(&item.from))
|| graph
.unresolved
.iter()
.any(|item| item.from.as_ref().is_some_and(|from| !known(from)))
|| graph
.reference_limits
.iter()
.any(|item| !known(&item.document))
{
return Err(ScopeInputError::UnknownGraphAddress);
}
Ok(Self { graph, documents })
}
#[must_use]
pub const fn graph(self) -> &'a ResolvedDocumentScope {
self.graph
}
#[must_use]
pub const fn documents(self) -> &'a [ResolvedContent] {
self.documents
}
#[must_use]
pub fn iter(self) -> impl ExactSizeIterator<Item = (&'a ScopedDocument, &'a ResolvedContent)> {
self.graph.documents.iter().zip(self.documents)
}
}