1use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7use petgraph::stable_graph::{NodeIndex, StableGraph};
8
9use crate::error::ChronicleError;
10use crate::model::*;
11use crate::validation::{ValidationReport, ValidationError, validate, can_add_event};
12
13pub struct Chronicle {
26 pub graph: StableGraph<Entity, Relationship>,
30
31 pub index: HashMap<String, NodeIndex>,
34
35 pub config: ValidationConfig,
38}
39
40impl Chronicle {
41 pub fn from_directory(path: &Path) -> Result<Self, ChronicleError> {
51 Self::from_directory_with_config(path, ValidationConfig::default())
52 }
53
54 pub fn from_directory_with_config(
66 path: &Path,
67 config: ValidationConfig,
68 ) -> Result<Self, ChronicleError> {
69 let mut graph = StableGraph::new();
70 let mut index = HashMap::new();
71
72 load_dir::<Actor>(path, "actors", &mut graph, &mut index)?;
73 load_dir::<Place>(path, "places", &mut graph, &mut index)?;
74 load_dir::<Event>(path, "events", &mut graph, &mut index)?;
75 load_dir::<Concept>(path, "concepts", &mut graph, &mut index)?;
76 load_dir::<Account>(path, "accounts", &mut graph, &mut index)?;
77
78 let mut chronicle = Self { graph, index, config };
79 chronicle.build_edges();
80 Ok(chronicle)
81 }
82
83 pub fn validate(&self) -> ValidationReport {
88 validate(self)
89 }
90
91 pub fn can_add_event(&self, event: &Event) -> Result<(), Vec<ValidationError>> {
97 can_add_event(self, event)
98 }
99
100 fn build_edges(&mut self) {
102 let mut edges: Vec<(NodeIndex, String, Relationship)> = Vec::new();
104
105 for nx in self.graph.node_indices() {
106 let entity = &self.graph[nx];
107 match entity {
108 Entity::Actor(actor) => {
109 for aff in &actor.affiliations {
110 edges.push((nx, aff.clone(), Relationship::AffiliatedWith));
111 }
112 }
113 Entity::Event(event) => {
114 if let Some(loc) = &event.location {
115 edges.push((nx, loc.clone(), Relationship::OccurredAt));
116 }
117 for cause in &event.caused_by {
118 edges.push((nx, cause.clone(), Relationship::CausedBy));
119 }
120 for p in &event.participants {
121 edges.push((
122 nx,
123 p.actor.clone(),
124 Relationship::ParticipatedIn {
125 role: p.role.clone(),
126 sentiment: p.sentiment.clone(),
127 },
128 ));
129 }
130 for sc in &event.state_changes {
131 edges.push((
132 nx,
133 sc.entity.clone(),
134 Relationship::HasStateChange {
135 change: sc.change.clone(),
136 },
137 ));
138 }
139 }
140 Entity::Concept(concept) => {
141 if let Some(origin) = &concept.origin_event {
142 edges.push((nx, origin.clone(), Relationship::OriginatedFrom));
143 }
144 }
145 Entity::Account(account) => {
146 edges.push((
147 nx,
148 account.source.clone(),
149 Relationship::AuthoredBy,
150 ));
151 for ev in &account.event_refs {
152 edges.push((nx, ev.clone(), Relationship::AccountOf));
153 }
154 for mention in parse_references(&account.text) {
155 edges.push((nx, mention, Relationship::Mentions));
156 }
157 }
158 Entity::Place(place) => {
159 if let Some(r) = &place.region {
160 edges.push((nx, r.clone(), Relationship::LocatedIn));
161 }
162 }
163 }
164 }
165
166 for (from, target_id, rel) in edges {
167 if let Some(&to) = self.index.get(&target_id) {
168 self.graph.add_edge(from, to, rel);
169 }
170 }
172 }
173}
174
175pub fn parse_references(text: &str) -> Vec<String> {
189 let mut refs = Vec::new();
190 let mut chars = text.chars().peekable();
191 while let Some(ch) = chars.next() {
192 if ch == '{' {
193 let id: String = chars.by_ref().take_while(|&c| c != '}').collect();
194 if !id.is_empty() {
195 refs.push(id);
196 }
197 }
198 }
199 refs
200}
201
202trait IntoEntity {
205 fn into_entity(self) -> Entity;
206}
207
208impl IntoEntity for Actor {
209 fn into_entity(self) -> Entity { Entity::Actor(self) }
210}
211impl IntoEntity for Place {
212 fn into_entity(self) -> Entity { Entity::Place(self) }
213}
214impl IntoEntity for Event {
215 fn into_entity(self) -> Entity { Entity::Event(self) }
216}
217impl IntoEntity for Concept {
218 fn into_entity(self) -> Entity { Entity::Concept(self) }
219}
220impl IntoEntity for Account {
221 fn into_entity(self) -> Entity { Entity::Account(self) }
222}
223
224trait HasId {
225 fn id(&self) -> &str;
226}
227
228impl HasId for Actor { fn id(&self) -> &str { &self.id } }
229impl HasId for Place { fn id(&self) -> &str { &self.id } }
230impl HasId for Event { fn id(&self) -> &str { &self.id } }
231impl HasId for Concept { fn id(&self) -> &str { &self.id } }
232impl HasId for Account { fn id(&self) -> &str { &self.id } }
233
234fn load_dir<T>(
235 root: &Path,
236 subdir: &str,
237 graph: &mut StableGraph<Entity, Relationship>,
238 index: &mut HashMap<String, NodeIndex>,
239) -> Result<(), ChronicleError>
240where
241 T: serde::de::DeserializeOwned + IntoEntity + HasId,
242{
243 let dir = root.join(subdir);
244 if !dir.exists() {
245 return Ok(());
246 }
247 for entry in fs::read_dir(&dir)? {
248 let entry = entry?;
249 let path = entry.path();
250 if path.extension().is_some_and(|e| e == "ron") {
251 let contents = fs::read_to_string(&path)?;
252 let items: Vec<T> = ron::from_str(&contents)?;
253 for item in items {
254 let id = item.id().to_owned();
255 if index.contains_key(&id) {
256 return Err(ChronicleError::DuplicateId(id));
257 }
258 let nx = graph.add_node(item.into_entity());
259 index.insert(id, nx);
260 }
261 }
262 }
263 Ok(())
264}