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::{ValidationError, ValidationReport, can_add_event, validate};
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 {
79 graph,
80 index,
81 config,
82 };
83 chronicle.build_edges();
84 Ok(chronicle)
85 }
86
87 pub fn validate(&self) -> ValidationReport {
92 validate(self)
93 }
94
95 pub fn can_add_event(&self, event: &Event) -> Result<(), Vec<ValidationError>> {
101 can_add_event(self, event)
102 }
103
104 fn build_edges(&mut self) {
106 let mut edges: Vec<(NodeIndex, String, Relationship)> = Vec::new();
108
109 for nx in self.graph.node_indices() {
110 let entity = &self.graph[nx];
111 match entity {
112 Entity::Actor(actor) => {
113 for aff in &actor.affiliations {
114 edges.push((nx, aff.clone(), Relationship::AffiliatedWith));
115 }
116 }
117 Entity::Event(event) => {
118 if let Some(loc) = &event.location {
119 edges.push((nx, loc.clone(), Relationship::OccurredAt));
120 }
121 for cause in &event.caused_by {
122 edges.push((nx, cause.clone(), Relationship::CausedBy));
123 }
124 for p in &event.participants {
125 edges.push((
126 nx,
127 p.actor.clone(),
128 Relationship::ParticipatedIn {
129 role: p.role.clone(),
130 sentiment: p.sentiment.clone(),
131 },
132 ));
133 }
134 for sc in &event.state_changes {
135 edges.push((
136 nx,
137 sc.entity.clone(),
138 Relationship::HasStateChange {
139 change: sc.change.clone(),
140 },
141 ));
142 }
143 }
144 Entity::Concept(concept) => {
145 if let Some(origin) = &concept.origin_event {
146 edges.push((nx, origin.clone(), Relationship::OriginatedFrom));
147 }
148 }
149 Entity::Account(account) => {
150 edges.push((nx, account.source.clone(), Relationship::AuthoredBy));
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 {
210 Entity::Actor(self)
211 }
212}
213impl IntoEntity for Place {
214 fn into_entity(self) -> Entity {
215 Entity::Place(self)
216 }
217}
218impl IntoEntity for Event {
219 fn into_entity(self) -> Entity {
220 Entity::Event(self)
221 }
222}
223impl IntoEntity for Concept {
224 fn into_entity(self) -> Entity {
225 Entity::Concept(self)
226 }
227}
228impl IntoEntity for Account {
229 fn into_entity(self) -> Entity {
230 Entity::Account(self)
231 }
232}
233
234trait HasId {
235 fn id(&self) -> &str;
236}
237
238impl HasId for Actor {
239 fn id(&self) -> &str {
240 &self.id
241 }
242}
243impl HasId for Place {
244 fn id(&self) -> &str {
245 &self.id
246 }
247}
248impl HasId for Event {
249 fn id(&self) -> &str {
250 &self.id
251 }
252}
253impl HasId for Concept {
254 fn id(&self) -> &str {
255 &self.id
256 }
257}
258impl HasId for Account {
259 fn id(&self) -> &str {
260 &self.id
261 }
262}
263
264fn load_dir<T>(
265 root: &Path,
266 subdir: &str,
267 graph: &mut StableGraph<Entity, Relationship>,
268 index: &mut HashMap<String, NodeIndex>,
269) -> Result<(), ChronicleError>
270where
271 T: serde::de::DeserializeOwned + IntoEntity + HasId,
272{
273 let dir = root.join(subdir);
274 if !dir.exists() {
275 return Ok(());
276 }
277 for entry in fs::read_dir(&dir)? {
278 let entry = entry?;
279 let path = entry.path();
280 if path.extension().is_some_and(|e| e == "ron") {
281 let contents = fs::read_to_string(&path)?;
282 let items: Vec<T> = ron::from_str(&contents)?;
283 for item in items {
284 let id = item.id().to_owned();
285 if index.contains_key(&id) {
286 return Err(ChronicleError::DuplicateId(id));
287 }
288 let nx = graph.add_node(item.into_entity());
289 index.insert(id, nx);
290 }
291 }
292 }
293 Ok(())
294}