Skip to main content

mollendorff_ref/
schema.rs

1//! references.yaml schema v1.0.0
2//!
3//! Central schema for reference tracking and verification.
4
5use serde::{Deserialize, Serialize};
6
7/// Root structure for references.yaml
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ReferencesFile {
10    pub meta: Meta,
11    pub references: Vec<Reference>,
12}
13
14/// Metadata about the references file
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Meta {
17    /// ISO date when file was created
18    pub created: String,
19    /// ISO datetime of last verification run (null if never)
20    pub last_verified: Option<String>,
21    /// Tool used for verification
22    pub tool: String,
23    /// Total number of references
24    pub total_links: usize,
25}
26
27/// A single reference entry
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Reference {
30    /// URL to verify
31    pub url: String,
32    /// Human-readable title
33    pub title: String,
34    /// Categories for filtering (e.g., `["research", "wikipedia"]`)
35    pub categories: Vec<String>,
36    /// Files that cite this reference
37    pub cited_in: Vec<String>,
38    /// Verification status
39    pub status: Status,
40    /// ISO datetime of last verification (null if pending)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub verified: Option<String>,
43    /// Notes (redirect target URL, error message, etc.)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub notes: Option<String>,
46}
47
48/// Reference verification status
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum Status {
52    /// Not yet verified
53    Pending,
54    /// 200 response, content accessible
55    Ok,
56    /// 404, 5xx, DNS failure, connection error
57    Dead,
58    /// Ended up on different domain (link rot)
59    Redirect,
60    /// 200 but content blocked by paywall
61    Paywall,
62    /// 200 but login required to view content
63    Login,
64}
65
66impl std::fmt::Display for Status {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Status::Pending => write!(f, "pending"),
70            Status::Ok => write!(f, "ok"),
71            Status::Dead => write!(f, "dead"),
72            Status::Redirect => write!(f, "redirect"),
73            Status::Paywall => write!(f, "paywall"),
74            Status::Login => write!(f, "login"),
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_status_display() {
85        assert_eq!(Status::Pending.to_string(), "pending");
86        assert_eq!(Status::Ok.to_string(), "ok");
87        assert_eq!(Status::Dead.to_string(), "dead");
88        assert_eq!(Status::Redirect.to_string(), "redirect");
89        assert_eq!(Status::Paywall.to_string(), "paywall");
90        assert_eq!(Status::Login.to_string(), "login");
91    }
92
93    #[test]
94    fn test_deserialize_status() {
95        let json = r#""ok""#;
96        let status: Status = serde_json::from_str(json).unwrap();
97        assert_eq!(status, Status::Ok);
98    }
99
100    #[test]
101    fn test_serialize_reference() {
102        let reference = Reference {
103            url: "https://example.com".to_string(),
104            title: "Example".to_string(),
105            categories: vec!["test".to_string()],
106            cited_in: vec!["README.md".to_string()],
107            status: Status::Pending,
108            verified: None,
109            notes: None,
110        };
111        let yaml = serde_yaml::to_string(&reference).unwrap();
112        assert!(yaml.contains("url: https://example.com"));
113        assert!(yaml.contains("status: pending"));
114        // Optional fields should not appear when None
115        assert!(!yaml.contains("verified:"));
116        assert!(!yaml.contains("notes:"));
117    }
118
119    #[test]
120    fn test_full_file_roundtrip() {
121        let file = ReferencesFile {
122            meta: Meta {
123                created: "2025-12-15".to_string(),
124                last_verified: None,
125                tool: "ref".to_string(),
126                total_links: 1,
127            },
128            references: vec![Reference {
129                url: "https://example.com".to_string(),
130                title: "Example".to_string(),
131                categories: vec!["test".to_string()],
132                cited_in: vec!["README.md".to_string()],
133                status: Status::Ok,
134                verified: Some("2025-12-15T10:00:00Z".to_string()),
135                notes: None,
136            }],
137        };
138        let yaml = serde_yaml::to_string(&file).unwrap();
139        let parsed: ReferencesFile = serde_yaml::from_str(&yaml).unwrap();
140        assert_eq!(parsed.meta.total_links, 1);
141        assert_eq!(parsed.references[0].status, Status::Ok);
142    }
143}