1use crate::ExportThread;
2use serde::Serialize;
3use std::collections::HashSet;
4
5#[derive(Serialize)]
6struct JsonThread {
7 thread_id: String,
8 subject: String,
9 participants: Vec<String>,
10 message_count: usize,
11 messages: Vec<JsonMessage>,
12}
13
14#[derive(Serialize)]
15struct JsonMessage {
16 id: String,
17 from: JsonAddress,
18 to: Vec<String>,
19 date: String,
20 subject: String,
21 body_text: Option<String>,
22 attachments: Vec<JsonAttachment>,
23}
24
25#[derive(Serialize)]
26struct JsonAddress {
27 name: Option<String>,
28 email: String,
29}
30
31#[derive(Serialize)]
32struct JsonAttachment {
33 filename: String,
34 size_bytes: u64,
35}
36
37pub fn export_json(thread: &ExportThread) -> String {
38 let participants: Vec<String> = thread
39 .messages
40 .iter()
41 .map(|m| m.from_email.clone())
42 .collect::<HashSet<_>>()
43 .into_iter()
44 .collect();
45
46 let json_thread = JsonThread {
47 thread_id: thread.thread_id.clone(),
48 subject: thread.subject.clone(),
49 message_count: thread.messages.len(),
50 participants,
51 messages: thread
52 .messages
53 .iter()
54 .map(|m| JsonMessage {
55 id: m.id.clone(),
56 from: JsonAddress {
57 name: m.from_name.clone(),
58 email: m.from_email.clone(),
59 },
60 to: m.to.clone(),
61 date: m.date.to_rfc3339(),
62 subject: m.subject.clone(),
63 body_text: m.body_text.clone(),
64 attachments: m
65 .attachments
66 .iter()
67 .map(|a| JsonAttachment {
68 filename: a.filename.clone(),
69 size_bytes: a.size_bytes,
70 })
71 .collect(),
72 })
73 .collect(),
74 };
75
76 serde_json::to_string_pretty(&json_thread).unwrap_or_default()
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::tests::{empty_body_thread, sample_thread, single_message_thread};
83
84 #[test]
85 fn json_is_valid_and_parseable() {
86 let result = export_json(&sample_thread());
87 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
88 assert!(parsed.is_object());
89 }
90
91 #[test]
92 fn json_has_correct_message_count() {
93 let result = export_json(&sample_thread());
94 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
95 assert_eq!(parsed["message_count"], 2);
96 }
97
98 #[test]
99 fn json_preserves_thread_metadata() {
100 let result = export_json(&sample_thread());
101 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
102 assert_eq!(parsed["thread_id"], "thread_abc");
103 assert_eq!(parsed["subject"], "Deployment rollback plan");
104 }
105
106 #[test]
107 fn json_messages_have_rfc3339_dates() {
108 let result = export_json(&sample_thread());
109 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
110 let date = parsed["messages"][0]["date"].as_str().unwrap();
111 assert!(date.contains('T'));
113 assert!(date.ends_with("+00:00") || date.ends_with('Z'));
114 }
115
116 #[test]
117 fn json_includes_attachments_with_size() {
118 let result = export_json(&sample_thread());
119 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
120 let atts = &parsed["messages"][1]["attachments"];
121 assert_eq!(atts.as_array().unwrap().len(), 1);
122 assert_eq!(atts[0]["filename"], "runbook.pdf");
123 assert_eq!(atts[0]["size_bytes"], 245_760);
124 }
125
126 #[test]
127 fn json_from_address_has_name_and_email() {
128 let result = export_json(&sample_thread());
129 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
130 let from = &parsed["messages"][0]["from"];
131 assert_eq!(from["name"], "Alice");
132 assert_eq!(from["email"], "alice@example.com");
133 }
134
135 #[test]
136 fn json_from_name_is_null_when_missing() {
137 let result = export_json(&single_message_thread());
138 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
139 assert!(parsed["messages"][0]["from"]["name"].is_null());
140 }
141
142 #[test]
143 fn json_body_text_is_null_when_missing() {
144 let result = export_json(&empty_body_thread());
145 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
146 assert!(parsed["messages"][0]["body_text"].is_null());
147 }
148
149 #[test]
150 fn json_includes_to_recipients() {
151 let result = export_json(&sample_thread());
152 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
153 let to = parsed["messages"][0]["to"].as_array().unwrap();
154 assert_eq!(to[0], "team@example.com");
155 }
156
157 #[test]
158 fn json_roundtrip_preserves_message_ids() {
159 let result = export_json(&sample_thread());
160 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
161 let ids: Vec<&str> = parsed["messages"]
162 .as_array()
163 .unwrap()
164 .iter()
165 .map(|m| m["id"].as_str().unwrap())
166 .collect();
167 assert_eq!(ids, vec!["msg_1", "msg_2"]);
168 }
169}