use chrono::{DateTime, TimeZone, Utc};
use klieo_core::error::MemoryError;
use klieo_core::ids::FactId;
use serde::{Deserialize, Serialize};
const RESERVED_ENTITY_LABELS: &[&str] = &[
"Ticket",
"Policy",
"Agent",
"Member",
"Concept",
"Article",
"Obligation",
"Deadline",
];
pub fn known_epoch() -> DateTime<Utc> {
Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0)
.single()
.expect("Unix epoch is a valid UTC datetime")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EntityType {
Ticket,
Policy,
Agent,
Member,
Concept,
Article,
Obligation,
Deadline,
Other {
hint: String,
},
}
impl EntityType {
pub fn as_str(&self) -> &str {
match self {
Self::Ticket => "Ticket",
Self::Policy => "Policy",
Self::Agent => "Agent",
Self::Member => "Member",
Self::Concept => "Concept",
Self::Article => "Article",
Self::Obligation => "Obligation",
Self::Deadline => "Deadline",
Self::Other { hint } => hint.as_str(),
}
}
pub fn from_label(label: &str) -> Self {
match label {
"Ticket" => Self::Ticket,
"Policy" => Self::Policy,
"Agent" => Self::Agent,
"Member" => Self::Member,
"Concept" => Self::Concept,
"Article" => Self::Article,
"Obligation" => Self::Obligation,
"Deadline" => Self::Deadline,
other => Self::Other {
hint: other.to_string(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EntityRef {
pub entity_type: EntityType,
pub name: String,
}
impl EntityRef {
pub fn try_new(entity_type: EntityType, name: impl Into<String>) -> Result<Self, MemoryError> {
if let EntityType::Other { hint } = &entity_type {
if RESERVED_ENTITY_LABELS.iter().any(|label| label == hint) {
return Err(MemoryError::Store(format!(
"EntityType::Other {{ hint: {hint:?} }} collides with reserved typed label; use the typed variant"
)));
}
}
Ok(Self {
entity_type,
name: name.into().to_ascii_lowercase(),
})
}
pub fn new(entity_type: EntityType, name: impl Into<String>) -> Self {
Self::try_new(entity_type, name).expect("EntityRef::new violated reserved-label constraint")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GraphNode {
Entity(EntityRef),
Fact(FactId),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EdgeKind {
MentionedIn,
CoOccurs,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GraphEdge {
#[allow(missing_docs)]
pub from: GraphNode,
#[allow(missing_docs)]
pub to: GraphNode,
#[allow(missing_docs)]
pub kind: EdgeKind,
}
impl GraphEdge {
pub fn new(from: GraphNode, to: GraphNode, kind: EdgeKind) -> Self {
Self { from, to, kind }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GraphView {
#[allow(missing_docs)]
pub nodes: Vec<GraphNode>,
#[allow(missing_docs)]
pub edges: Vec<GraphEdge>,
pub truncated: bool,
#[serde(default)]
pub fact_texts: std::collections::HashMap<FactId, String>,
}
impl GraphView {
pub fn complete(nodes: Vec<GraphNode>, edges: Vec<GraphEdge>) -> Self {
Self {
nodes,
edges,
truncated: false,
fact_texts: std::collections::HashMap::new(),
}
}
pub fn truncated(nodes: Vec<GraphNode>, edges: Vec<GraphEdge>) -> Self {
Self {
nodes,
edges,
truncated: true,
fact_texts: std::collections::HashMap::new(),
}
}
pub fn with_fact_texts(
mut self,
fact_texts: std::collections::HashMap<FactId, String>,
) -> Self {
self.fact_texts = fact_texts;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_label_covers_every_reserved_label() {
for label in RESERVED_ENTITY_LABELS {
let resolved = EntityType::from_label(label);
assert!(
!matches!(resolved, EntityType::Other { .. }),
"reserved label {label:?} routed to Other — \
add an arm to EntityType::from_label",
);
assert_eq!(resolved.as_str(), *label);
}
}
}