1use std::collections::BTreeMap;
24
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
63#[non_exhaustive]
64pub struct Metadata {
65 pub title: Option<String>,
67 pub author: Option<String>,
69 pub subject: Option<String>,
71 pub description: Option<String>,
78 pub last_saved_by: Option<String>,
85 pub keywords: Vec<String>,
90 pub created: Option<String>,
92 pub modified: Option<String>,
94 pub extras: BTreeMap<String, String>,
98}
99
100impl Metadata {
101 #[must_use]
107 pub fn new() -> Self {
108 Self::default()
109 }
110
111 #[must_use]
113 pub fn with_title(mut self, value: impl Into<String>) -> Self {
114 self.title = Some(value.into());
115 self
116 }
117
118 #[must_use]
120 pub fn with_author(mut self, value: impl Into<String>) -> Self {
121 self.author = Some(value.into());
122 self
123 }
124
125 #[must_use]
127 pub fn with_subject(mut self, value: impl Into<String>) -> Self {
128 self.subject = Some(value.into());
129 self
130 }
131
132 #[must_use]
134 pub fn with_description(mut self, value: impl Into<String>) -> Self {
135 self.description = Some(value.into());
136 self
137 }
138
139 #[must_use]
141 pub fn with_last_saved_by(mut self, value: impl Into<String>) -> Self {
142 self.last_saved_by = Some(value.into());
143 self
144 }
145
146 #[must_use]
148 pub fn with_keywords<I, S>(mut self, values: I) -> Self
149 where
150 I: IntoIterator<Item = S>,
151 S: Into<String>,
152 {
153 self.keywords = values.into_iter().map(Into::into).collect();
154 self
155 }
156
157 #[must_use]
159 pub fn with_created(mut self, value: impl Into<String>) -> Self {
160 self.created = Some(value.into());
161 self
162 }
163
164 #[must_use]
166 pub fn with_modified(mut self, value: impl Into<String>) -> Self {
167 self.modified = Some(value.into());
168 self
169 }
170
171 pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
174 self.extras.insert(key.into(), value.into());
175 self
176 }
177}
178
179impl std::fmt::Display for Metadata {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 match &self.title {
182 Some(t) => write!(f, "Metadata(\"{}\")", t),
183 None => write!(f, "Metadata(untitled)"),
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn default_is_all_none_or_empty() {
194 let m = Metadata::default();
195 assert!(m.title.is_none());
196 assert!(m.author.is_none());
197 assert!(m.subject.is_none());
198 assert!(m.description.is_none());
199 assert!(m.last_saved_by.is_none());
200 assert!(m.keywords.is_empty());
201 assert!(m.created.is_none());
202 assert!(m.modified.is_none());
203 assert!(m.extras.is_empty());
204 }
205
206 #[test]
207 fn struct_literal_construction() {
208 let mut extras = BTreeMap::new();
209 extras.insert("category".to_string(), "Test".to_string());
210 let m = Metadata {
211 title: Some("Test".to_string()),
212 author: Some("Author".to_string()),
213 subject: Some("Subject".to_string()),
214 description: Some("Long form description".to_string()),
215 last_saved_by: Some("Editor".to_string()),
216 keywords: vec!["rust".to_string(), "hwp".to_string()],
217 created: Some("2026-02-07T00:00:00Z".to_string()),
218 modified: Some("2026-02-07T12:00:00Z".to_string()),
219 extras,
220 };
221 assert_eq!(m.title.as_deref(), Some("Test"));
222 assert_eq!(m.keywords.len(), 2);
223 assert_eq!(m.description.as_deref(), Some("Long form description"));
224 assert_eq!(m.last_saved_by.as_deref(), Some("Editor"));
225 assert_eq!(m.extras.get("category").map(String::as_str), Some("Test"));
226 }
227
228 #[test]
229 fn extras_btree_ordering_is_deterministic() {
230 let mut m = Metadata::default();
233 m.extras.insert("zeta".to_string(), "z".to_string());
234 m.extras.insert("alpha".to_string(), "a".to_string());
235 m.extras.insert("mu".to_string(), "m".to_string());
236 let keys: Vec<&str> = m.extras.keys().map(String::as_str).collect();
237 assert_eq!(keys, vec!["alpha", "mu", "zeta"]);
238 }
239
240 #[test]
241 fn partial_construction_with_defaults() {
242 let m = Metadata { title: Some("Report".to_string()), ..Metadata::default() };
243 assert_eq!(m.title.as_deref(), Some("Report"));
244 assert!(m.author.is_none());
245 }
246
247 #[test]
248 fn display_with_title() {
249 let m = Metadata { title: Some("My Doc".to_string()), ..Metadata::default() };
250 assert_eq!(m.to_string(), "Metadata(\"My Doc\")");
251 }
252
253 #[test]
254 fn display_without_title() {
255 let m = Metadata::default();
256 assert_eq!(m.to_string(), "Metadata(untitled)");
257 }
258
259 #[test]
260 fn equality() {
261 let a = Metadata { title: Some("A".to_string()), ..Metadata::default() };
262 let b = Metadata { title: Some("A".to_string()), ..Metadata::default() };
263 let c = Metadata { title: Some("B".to_string()), ..Metadata::default() };
264 assert_eq!(a, b);
265 assert_ne!(a, c);
266 }
267
268 #[test]
269 fn clone_independence() {
270 let m = Metadata { title: Some("Original".to_string()), ..Metadata::default() };
271 let mut cloned = m.clone();
272 cloned.title = Some("Modified".to_string());
273 assert_eq!(m.title.as_deref(), Some("Original"));
274 }
275
276 #[test]
277 fn korean_text() {
278 let m = Metadata {
279 title: Some("분기 보고서".to_string()),
280 author: Some("김철수".to_string()),
281 keywords: vec!["한글".to_string(), "보고서".to_string()],
282 ..Metadata::default()
283 };
284 assert_eq!(m.title.as_deref(), Some("분기 보고서"));
285 }
286
287 #[test]
288 fn serde_roundtrip() {
289 let mut extras = BTreeMap::new();
290 extras.insert("category".to_string(), "draft".to_string());
291 let m = Metadata {
292 title: Some("Test".to_string()),
293 author: Some("Author".to_string()),
294 subject: None,
295 description: Some("body".to_string()),
296 last_saved_by: Some("Editor".to_string()),
297 keywords: vec!["a".to_string(), "b".to_string()],
298 created: Some("2026-02-07T00:00:00Z".to_string()),
299 modified: None,
300 extras,
301 };
302 let json = serde_json::to_string(&m).unwrap();
303 let back: Metadata = serde_json::from_str(&json).unwrap();
304 assert_eq!(m, back);
305 }
306
307 #[test]
308 fn serde_default_roundtrip() {
309 let m = Metadata::default();
310 let json = serde_json::to_string(&m).unwrap();
311 let back: Metadata = serde_json::from_str(&json).unwrap();
312 assert_eq!(m, back);
313 }
314
315 #[test]
316 fn empty_keywords_serializes_as_empty_array() {
317 let m = Metadata::default();
318 let json = serde_json::to_string(&m).unwrap();
319 assert!(json.contains("\"keywords\":[]"), "json: {json}");
320 }
321}