lean_ctx/core/web/
citation.rs1use super::url_guard;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Citation {
14 pub url: String,
15 pub title: Option<String>,
16 pub site: Option<String>,
17 pub fetched_at: String,
19}
20
21impl Citation {
22 pub fn new(url: &str, title: Option<String>) -> Self {
23 Self {
24 url: url.to_string(),
25 title,
26 site: url_guard::validate(url).ok().map(|u| u.host),
27 fetched_at: chrono::Utc::now().to_rfc3339(),
28 }
29 }
30
31 pub fn footer(&self) -> String {
33 let headline = match &self.title {
34 Some(t) if !t.is_empty() => format!("{t} — {}", self.url),
35 _ => self.url.clone(),
36 };
37 let site = self.site.as_deref().unwrap_or("unknown");
38 format!(
39 "\n\n---\nSource: {headline}\nSite: {site} · Retrieved: {}",
40 self.fetched_at
41 )
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn derives_site_from_url() {
51 let c = Citation::new("https://en.wikipedia.org/wiki/Rust", Some("Rust".into()));
52 assert_eq!(c.site.as_deref(), Some("en.wikipedia.org"));
53 }
54
55 #[test]
56 fn footer_contains_source_and_retrieval() {
57 let c = Citation::new("https://x.com/a", Some("Title".into()));
58 let footer = c.footer();
59 assert!(footer.contains("Source: Title — https://x.com/a"));
60 assert!(footer.contains("Site: x.com"));
61 assert!(footer.contains("Retrieved:"));
62 }
63
64 #[test]
65 fn footer_without_title_uses_url() {
66 let c = Citation::new("https://x.com/a", None);
67 assert!(c.footer().contains("Source: https://x.com/a"));
68 }
69}