1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[cfg(feature = "proptest-arbitrary")]
12use proptest::prelude::{Arbitrary, BoxedStrategy, Just, Strategy, prop_oneof};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum RelationshipType {
18 ParentChild,
20 DependsOn,
22 Follows,
24 RelatedTo,
26 Blocks,
28 Duplicates,
30 References,
32}
33
34impl RelationshipType {
35 #[must_use]
37 pub fn is_directional(&self) -> bool {
38 matches!(
39 self,
40 Self::ParentChild | Self::DependsOn | Self::Follows | Self::Blocks
41 )
42 }
43
44 #[must_use]
46 pub fn inverse(&self) -> Option<Self> {
47 match self {
48 Self::ParentChild => Some(Self::ParentChild), Self::DependsOn => Some(Self::DependsOn), Self::Follows => Some(Self::Follows), Self::Blocks => Some(Self::Blocks), Self::RelatedTo => None, Self::Duplicates => None, Self::References => None, }
56 }
57
58 #[must_use]
60 pub fn requires_acyclic(&self) -> bool {
61 matches!(self, Self::DependsOn | Self::ParentChild | Self::Blocks)
62 }
63
64 #[must_use]
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 Self::ParentChild => "parent_child",
69 Self::DependsOn => "depends_on",
70 Self::Follows => "follows",
71 Self::RelatedTo => "related_to",
72 Self::Blocks => "blocks",
73 Self::Duplicates => "duplicates",
74 Self::References => "references",
75 }
76 }
77
78 pub fn parse(s: &str) -> Result<Self, String> {
84 match s {
85 "parent_child" => Ok(Self::ParentChild),
86 "depends_on" => Ok(Self::DependsOn),
87 "follows" => Ok(Self::Follows),
88 "related_to" => Ok(Self::RelatedTo),
89 "blocks" => Ok(Self::Blocks),
90 "duplicates" => Ok(Self::Duplicates),
91 "references" => Ok(Self::References),
92 _ => Err(format!("Unknown relationship type: {s}")),
93 }
94 }
95
96 #[must_use]
98 pub fn all() -> Vec<Self> {
99 vec![
100 Self::ParentChild,
101 Self::DependsOn,
102 Self::Follows,
103 Self::RelatedTo,
104 Self::Blocks,
105 Self::Duplicates,
106 Self::References,
107 ]
108 }
109}
110
111impl std::fmt::Display for RelationshipType {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "{}", self.as_str())
114 }
115}
116
117#[cfg(feature = "proptest-arbitrary")]
118impl Arbitrary for RelationshipType {
119 type Parameters = ();
120 type Strategy = BoxedStrategy<Self>;
121
122 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
123 prop_oneof![
124 Just(Self::ParentChild),
125 Just(Self::DependsOn),
126 Just(Self::Follows),
127 Just(Self::RelatedTo),
128 Just(Self::Blocks),
129 Just(Self::Duplicates),
130 Just(Self::References),
131 ]
132 .boxed()
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
138pub struct RelationshipMetadata {
139 pub reason: Option<String>,
141 pub created_by: Option<String>,
143 pub priority: Option<u8>,
145 pub weight: Option<f32>,
147 #[serde(default)]
149 pub custom_fields: HashMap<String, String>,
150}
151
152impl RelationshipMetadata {
153 #[must_use]
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 #[must_use]
161 pub fn with_reason(reason: String) -> Self {
162 Self {
163 reason: Some(reason),
164 ..Default::default()
165 }
166 }
167
168 #[must_use]
170 pub fn with_creator(reason: String, created_by: String) -> Self {
171 Self {
172 reason: Some(reason),
173 created_by: Some(created_by),
174 ..Default::default()
175 }
176 }
177
178 pub fn set_field(&mut self, key: String, value: String) {
180 self.custom_fields.insert(key, value);
181 }
182
183 #[must_use]
185 pub fn get_field(&self, key: &str) -> Option<&String> {
186 self.custom_fields.get(key)
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct EpisodeRelationship {
193 pub id: Uuid,
195 pub from_episode_id: Uuid,
197 pub to_episode_id: Uuid,
199 pub relationship_type: RelationshipType,
201 pub metadata: RelationshipMetadata,
203 pub created_at: DateTime<Utc>,
205}
206
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct EpisodePatternRelationship {
210 pub id: Uuid,
212 pub episode_id: Uuid,
214 pub pattern_id: Uuid,
216 pub relationship_type: RelationshipType,
218 pub metadata: RelationshipMetadata,
220 pub created_at: DateTime<Utc>,
222}
223
224impl EpisodePatternRelationship {
225 #[must_use]
227 pub fn new(
228 episode_id: Uuid,
229 pattern_id: Uuid,
230 relationship_type: RelationshipType,
231 metadata: RelationshipMetadata,
232 ) -> Self {
233 Self {
234 id: Uuid::new_v4(),
235 episode_id,
236 pattern_id,
237 relationship_type,
238 metadata,
239 created_at: Utc::now(),
240 }
241 }
242}
243
244impl EpisodeRelationship {
245 #[must_use]
247 pub fn new(
248 from_episode_id: Uuid,
249 to_episode_id: Uuid,
250 relationship_type: RelationshipType,
251 metadata: RelationshipMetadata,
252 ) -> Self {
253 Self {
254 id: Uuid::new_v4(),
255 from_episode_id,
256 to_episode_id,
257 relationship_type,
258 metadata,
259 created_at: Utc::now(),
260 }
261 }
262
263 #[must_use]
265 pub fn with_reason(
266 from_episode_id: Uuid,
267 to_episode_id: Uuid,
268 relationship_type: RelationshipType,
269 reason: String,
270 ) -> Self {
271 Self::new(
272 from_episode_id,
273 to_episode_id,
274 relationship_type,
275 RelationshipMetadata::with_reason(reason),
276 )
277 }
278
279 #[must_use]
281 pub fn is_directional(&self) -> bool {
282 self.relationship_type.is_directional()
283 }
284
285 #[must_use]
287 pub fn inverse(&self) -> Option<Self> {
288 self.relationship_type.inverse().map(|inv_type| Self {
289 id: Uuid::new_v4(),
290 from_episode_id: self.to_episode_id,
291 to_episode_id: self.from_episode_id,
292 relationship_type: inv_type,
293 metadata: self.metadata.clone(),
294 created_at: self.created_at,
295 })
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum Direction {
302 Outgoing,
304 Incoming,
306 Both,
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_relationship_type_directional() {
316 assert!(RelationshipType::ParentChild.is_directional());
317 assert!(RelationshipType::DependsOn.is_directional());
318 assert!(RelationshipType::Follows.is_directional());
319 assert!(RelationshipType::Blocks.is_directional());
320 assert!(!RelationshipType::RelatedTo.is_directional());
321 assert!(!RelationshipType::Duplicates.is_directional());
322 assert!(!RelationshipType::References.is_directional());
323 }
324
325 #[test]
326 fn test_relationship_type_acyclic() {
327 assert!(RelationshipType::DependsOn.requires_acyclic());
328 assert!(RelationshipType::ParentChild.requires_acyclic());
329 assert!(RelationshipType::Blocks.requires_acyclic());
330 assert!(!RelationshipType::Follows.requires_acyclic());
331 assert!(!RelationshipType::RelatedTo.requires_acyclic());
332 }
333
334 #[test]
335 fn test_relationship_type_str_conversion() {
336 for rel_type in RelationshipType::all() {
337 let s = rel_type.as_str();
338 let parsed = RelationshipType::parse(s).unwrap();
339 assert_eq!(rel_type, parsed);
340 }
341 }
342
343 #[test]
344 fn test_relationship_type_from_str_invalid() {
345 let result = RelationshipType::parse("invalid_type");
346 assert!(result.is_err());
347 assert!(result.unwrap_err().contains("Unknown relationship type"));
348 }
349
350 #[test]
351 fn test_relationship_creation() {
352 let from_id = Uuid::new_v4();
353 let to_id = Uuid::new_v4();
354 let metadata = RelationshipMetadata::with_reason("Subtask of parent".to_string());
355
356 let rel = EpisodeRelationship::new(
357 from_id,
358 to_id,
359 RelationshipType::ParentChild,
360 metadata.clone(),
361 );
362
363 assert_eq!(rel.from_episode_id, from_id);
364 assert_eq!(rel.to_episode_id, to_id);
365 assert_eq!(rel.relationship_type, RelationshipType::ParentChild);
366 assert_eq!(rel.metadata.reason, Some("Subtask of parent".to_string()));
367 }
368
369 #[test]
370 fn test_relationship_with_reason() {
371 let from_id = Uuid::new_v4();
372 let to_id = Uuid::new_v4();
373
374 let rel = EpisodeRelationship::with_reason(
375 from_id,
376 to_id,
377 RelationshipType::DependsOn,
378 "Requires API design".to_string(),
379 );
380
381 assert_eq!(rel.from_episode_id, from_id);
382 assert_eq!(rel.to_episode_id, to_id);
383 assert_eq!(rel.metadata.reason, Some("Requires API design".to_string()));
384 }
385
386 #[test]
387 fn test_relationship_inverse() {
388 let from_id = Uuid::new_v4();
389 let to_id = Uuid::new_v4();
390
391 let rel = EpisodeRelationship::with_reason(
392 from_id,
393 to_id,
394 RelationshipType::ParentChild,
395 "Child task".to_string(),
396 );
397
398 let inverse = rel.inverse().expect("Should have inverse");
399 assert_eq!(inverse.from_episode_id, to_id);
400 assert_eq!(inverse.to_episode_id, from_id);
401 assert_eq!(inverse.relationship_type, RelationshipType::ParentChild);
402 }
403
404 #[test]
405 fn test_relationship_metadata() {
406 let mut metadata = RelationshipMetadata::new();
407 assert!(metadata.reason.is_none());
408 assert!(metadata.created_by.is_none());
409 assert!(metadata.priority.is_none());
410
411 metadata.set_field("project".to_string(), "memory-system".to_string());
412 assert_eq!(
413 metadata.get_field("project"),
414 Some(&"memory-system".to_string())
415 );
416 }
417
418 #[test]
419 fn test_relationship_metadata_with_creator() {
420 let metadata =
421 RelationshipMetadata::with_creator("Bug fix".to_string(), "alice".to_string());
422
423 assert_eq!(metadata.reason, Some("Bug fix".to_string()));
424 assert_eq!(metadata.created_by, Some("alice".to_string()));
425 }
426
427 #[test]
428 fn test_relationship_serialization() {
429 let from_id = Uuid::new_v4();
430 let to_id = Uuid::new_v4();
431 let rel = EpisodeRelationship::with_reason(
432 from_id,
433 to_id,
434 RelationshipType::DependsOn,
435 "Test reason".to_string(),
436 );
437
438 let json = serde_json::to_string(&rel).unwrap();
439 let deserialized: EpisodeRelationship = serde_json::from_str(&json).unwrap();
440
441 assert_eq!(rel.from_episode_id, deserialized.from_episode_id);
442 assert_eq!(rel.to_episode_id, deserialized.to_episode_id);
443 assert_eq!(rel.relationship_type, deserialized.relationship_type);
444 assert_eq!(rel.metadata.reason, deserialized.metadata.reason);
445 }
446
447 #[test]
448 fn test_direction_enum() {
449 let _ = Direction::Outgoing;
451 let _ = Direction::Incoming;
452 let _ = Direction::Both;
453 }
454}