use crate::{
AuthorNodeCard, NodeBodyDescriptor, NodeCard, NodeCardExpectation, NodeCardPresentation,
NodeCardRejection, NodeCardStatus, NodeProjection, temporal_instant_nanos,
};
const MEMORY_ABOUT: &str = "memory_about";
const ENTRY_LABEL: &str = "entry";
const EVIDENCE_KINDS: [&str; 2] = ["memory_evidence", "evidence"];
pub const MAX_CARD_BYTES: usize = 4_096;
pub fn describes(card: &NodeCard, descriptor: &NodeBodyDescriptor) -> bool {
card.source_revision == descriptor.revision
&& card.source_record_digest == descriptor.record_digest
}
pub fn admit(
command: &AuthorNodeCard,
node: Option<&NodeProjection>,
descriptor: Option<&NodeBodyDescriptor>,
existing: Option<&NodeCard>,
cut_nanos: Option<i128>,
) -> Result<NodeCard, NodeCardRejection> {
let condensable = node.is_some_and(|node| {
node.properties.get(MEMORY_ABOUT).map(String::as_str) == Some(command.about.as_str())
&& (node.labels.iter().any(|label| label == ENTRY_LABEL)
|| EVIDENCE_KINDS.contains(&node.node_kind.as_str()))
});
if !condensable {
return Err(NodeCardRejection::UnknownRef {
node_id: command.node_id.clone(),
about: command.about.clone(),
});
}
let Some(descriptor) = descriptor else {
return Err(NodeCardRejection::NoBody {
node_id: command.node_id.clone(),
});
};
if descriptor.revision != command.source_revision
|| descriptor.record_digest != command.source_record_digest
{
return Err(NodeCardRejection::SourceMoved {
declared_revision: command.source_revision,
actual_revision: descriptor.revision,
declared_record_digest: command.source_record_digest.clone(),
actual_record_digest: descriptor.record_digest.clone(),
});
}
if let Some(cut) = cut_nanos
&& temporal_instant_nanos(&command.authored_at).is_none_or(|at| at > cut)
{
return Err(NodeCardRejection::AuthoredAfterCut {
authored_at: command.authored_at.clone(),
});
}
if command.text.trim().is_empty() {
return Err(NodeCardRejection::EmptyCard);
}
if command.text.len() > MAX_CARD_BYTES {
return Err(NodeCardRejection::CardTooLarge {
card_bytes: command.text.len(),
limit: MAX_CARD_BYTES,
});
}
if command.text.len() as u64 >= descriptor.body_bytes {
return Err(NodeCardRejection::NotCompact {
card_bytes: command.text.len(),
body_bytes: descriptor.body_bytes,
});
}
match (command.expect, existing) {
(NodeCardExpectation::Absent, Some(stored)) => {
return Err(NodeCardRejection::CardAlreadyExists {
actual_card_revision: stored.card_revision,
});
}
(NodeCardExpectation::CardRevision(declared), None) => {
return Err(NodeCardRejection::CardAbsent {
declared_card_revision: declared,
});
}
(NodeCardExpectation::CardRevision(declared), Some(stored))
if declared != stored.card_revision =>
{
return Err(NodeCardRejection::CardMoved {
declared_card_revision: declared,
actual_card_revision: stored.card_revision,
});
}
_ => {}
}
Ok(NodeCard {
node_id: command.node_id.clone(),
language: command.language.clone(),
text: command.text.clone(),
source_revision: descriptor.revision,
source_content_hash: descriptor.content_hash.clone(),
source_record_digest: descriptor.record_digest.clone(),
source_body_bytes: descriptor.body_bytes,
authored_by: command.authored_by.clone(),
authored_at: command.authored_at.clone(),
card_revision: existing.map_or(1, |stored| stored.card_revision + 1),
})
}
pub fn presentation(
card: Option<&NodeCard>,
descriptor: Option<&NodeBodyDescriptor>,
cut_nanos: Option<i128>,
) -> NodeCardPresentation {
let Some(card) = card else {
return NodeCardPresentation::absent();
};
let stamp = Some(card.stamp());
if let Some(cut) = cut_nanos {
let authored = temporal_instant_nanos(&card.authored_at);
if authored.is_none_or(|authored| authored > cut) {
return NodeCardPresentation {
status: NodeCardStatus::AfterCut,
text: None,
stored: stamp,
};
}
}
if descriptor.is_some_and(|descriptor| describes(card, descriptor)) {
NodeCardPresentation {
status: NodeCardStatus::Valid,
text: Some(card.text.clone()),
stored: stamp,
}
} else {
NodeCardPresentation {
status: NodeCardStatus::Stale,
text: None,
stored: stamp,
}
}
}
#[cfg(test)]
#[path = "node_card_policy_tests.rs"]
mod tests;