1use std::io::Write;
11
12use anyhow::Result;
13use clap::ValueEnum;
14
15pub use crate::cli::format::{
16 output_as, write_items_jsonl, write_scalar_jsonl, JsonlSerialize, OutputFormat,
17};
18
19use crate::atlassian::confluence_types::{
20 ConfluenceAttachmentPage, ConfluenceSpacePage, PageSummaryPage,
21};
22use crate::atlassian::confluence_types::{
23 ConfluenceSearchResults, ConfluenceUserGetResults, ConfluenceUserSearchResults,
24};
25use crate::atlassian::jira_types::{
26 AgileBoardList, AgileSprintList, CreateMeta, JiraDevStatus, JiraDevStatusSummary,
27 JiraProjectList, JiraProjectVersionList, JiraSearchResult, JiraUserGetResults,
28 JiraUserSearchResults, JiraWatcherList, JiraWorklogList,
29};
30
31#[derive(Clone, Debug, Default, ValueEnum)]
33pub enum ContentFormat {
34 #[default]
36 Jfm,
37 Adf,
39}
40
41impl JsonlSerialize for AgileBoardList {
42 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
43 write_items_jsonl(self.boards.iter(), out)
44 }
45}
46
47impl JsonlSerialize for AgileSprintList {
48 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
49 write_items_jsonl(self.sprints.iter(), out)
50 }
51}
52
53impl JsonlSerialize for JiraSearchResult {
54 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
55 write_items_jsonl(self.issues.iter(), out)
56 }
57}
58
59impl JsonlSerialize for JiraProjectList {
60 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
61 write_items_jsonl(self.projects.iter(), out)
62 }
63}
64
65impl JsonlSerialize for CreateMeta {
66 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
67 write_items_jsonl(self.fields.iter(), out)
68 }
69}
70
71impl JsonlSerialize for JiraProjectVersionList {
72 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
73 write_items_jsonl(self.versions.iter(), out)
74 }
75}
76
77impl JsonlSerialize for JiraWatcherList {
78 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
79 write_items_jsonl(self.watchers.iter(), out)
80 }
81}
82
83impl JsonlSerialize for JiraWorklogList {
84 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
85 write_items_jsonl(self.worklogs.iter(), out)
86 }
87}
88
89impl JsonlSerialize for ConfluenceSearchResults {
90 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
91 write_items_jsonl(self.results.iter(), out)
92 }
93}
94
95impl JsonlSerialize for ConfluenceUserSearchResults {
96 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
97 write_items_jsonl(self.users.iter(), out)
98 }
99}
100
101impl JsonlSerialize for ConfluenceAttachmentPage {
102 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
103 write_items_jsonl(self.results.iter(), out)
104 }
105}
106
107impl JsonlSerialize for ConfluenceSpacePage {
108 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
109 write_items_jsonl(self.results.iter(), out)
110 }
111}
112
113impl JsonlSerialize for PageSummaryPage {
114 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
115 write_items_jsonl(self.results.iter(), out)
116 }
117}
118
119impl JsonlSerialize for JiraUserSearchResults {
120 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
121 write_items_jsonl(self.users.iter(), out)
122 }
123}
124
125impl JsonlSerialize for JiraUserGetResults {
126 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
127 write_items_jsonl(self.users.iter(), out)
128 }
129}
130
131impl JsonlSerialize for ConfluenceUserGetResults {
132 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
133 write_items_jsonl(self.users.iter(), out)
134 }
135}
136
137impl JsonlSerialize for JiraDevStatus {
138 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
139 write_scalar_jsonl(self, out)
140 }
141}
142
143impl JsonlSerialize for JiraDevStatusSummary {
144 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
145 write_scalar_jsonl(self, out)
146 }
147}
148
149#[cfg(test)]
150#[allow(clippy::unwrap_used)]
151mod tests {
152 use super::*;
153 use crate::atlassian::confluence_types::{ConfluenceSearchResult, ConfluenceUserSearchResult};
154 use crate::atlassian::jira_types::{
155 AgileBoard, AgileSprint, JiraComment, JiraDevStatusCount, JiraIssue, JiraProject,
156 JiraProjectVersion, JiraUser, JiraWorklog,
157 };
158 use crate::cli::format::write_output;
159
160 #[test]
163 fn default_is_jfm() {
164 let format = ContentFormat::default();
165 assert!(matches!(format, ContentFormat::Jfm));
166 }
167
168 #[test]
169 fn jfm_variant() {
170 let format = ContentFormat::Jfm;
171 assert!(matches!(format, ContentFormat::Jfm));
172 }
173
174 #[test]
175 fn adf_variant() {
176 let format = ContentFormat::Adf;
177 assert!(matches!(format, ContentFormat::Adf));
178 }
179
180 #[test]
181 fn debug_format() {
182 assert_eq!(format!("{:?}", ContentFormat::Jfm), "Jfm");
183 assert_eq!(format!("{:?}", ContentFormat::Adf), "Adf");
184 }
185
186 #[test]
187 fn clone() {
188 let format = ContentFormat::Adf;
189 let cloned = format;
190 assert!(matches!(cloned, ContentFormat::Adf));
191 }
192
193 fn sample_board(id: u64, name: &str) -> AgileBoard {
196 AgileBoard {
197 id,
198 name: name.to_string(),
199 board_type: "scrum".to_string(),
200 project_key: None,
201 }
202 }
203
204 fn sample_sprint(id: u64, name: &str) -> AgileSprint {
205 AgileSprint {
206 id,
207 name: name.to_string(),
208 state: "active".to_string(),
209 goal: None,
210 start_date: None,
211 end_date: None,
212 }
213 }
214
215 fn sample_issue(key: &str) -> JiraIssue {
216 JiraIssue {
217 key: key.to_string(),
218 summary: "s".to_string(),
219 description_adf: None,
220 status: None,
221 issue_type: None,
222 assignee: None,
223 priority: None,
224 labels: vec![],
225 custom_fields: vec![],
226 }
227 }
228
229 fn sample_project(key: &str) -> JiraProject {
230 JiraProject {
231 key: key.to_string(),
232 id: "1".to_string(),
233 name: "Project".to_string(),
234 project_type: None,
235 lead: None,
236 }
237 }
238
239 fn sample_project_version(id: &str, name: &str) -> JiraProjectVersion {
240 JiraProjectVersion {
241 id: id.to_string(),
242 name: name.to_string(),
243 description: None,
244 project_key: "PROJ".to_string(),
245 released: false,
246 archived: false,
247 release_date: None,
248 start_date: None,
249 }
250 }
251
252 fn sample_user(name: &str) -> JiraUser {
253 JiraUser {
254 account_id: name.to_string(),
255 display_name: name.to_string(),
256 email_address: None,
257 }
258 }
259
260 fn sample_worklog(id: &str) -> JiraWorklog {
261 JiraWorklog {
262 id: id.to_string(),
263 author: "alice".to_string(),
264 time_spent: "1h".to_string(),
265 time_spent_seconds: 3600,
266 started: "2025-01-01T00:00:00Z".to_string(),
267 comment: None,
268 }
269 }
270
271 fn sample_confluence_result(id: &str) -> ConfluenceSearchResult {
272 ConfluenceSearchResult {
273 id: id.to_string(),
274 title: "Title".to_string(),
275 space_key: "ENG".to_string(),
276 }
277 }
278
279 fn sample_confluence_user(id: &str) -> ConfluenceUserSearchResult {
280 ConfluenceUserSearchResult {
281 account_id: Some(id.to_string()),
282 display_name: "Name".to_string(),
283 email: None,
284 }
285 }
286
287 fn sample_comment(id: &str, _body: &str) -> JiraComment {
288 JiraComment {
289 id: id.to_string(),
290 author: "alice".to_string(),
291 created: "2025-01-01T00:00:00Z".to_string(),
292 body_adf: None,
293 updated: None,
294 }
295 }
296
297 fn jsonl_string<T: JsonlSerialize>(value: &T) -> String {
300 let mut buf = Vec::new();
301 value.write_jsonl(&mut buf).unwrap();
302 String::from_utf8(buf).unwrap()
303 }
304
305 #[test]
306 fn vec_of_refs_jsonl() {
307 let comment = sample_comment("c1", "hello");
308 let comments = vec![&comment];
309 let mut buf = Vec::new();
310 comments.write_jsonl(&mut buf).unwrap();
311 let out = String::from_utf8(buf).unwrap();
312 assert!(out.starts_with('{'));
313 assert!(out.ends_with('\n'));
314 assert_eq!(out.lines().count(), 1);
315 }
316
317 #[test]
318 fn agile_board_list_jsonl() {
319 let list = AgileBoardList {
320 boards: vec![sample_board(1, "B1"), sample_board(2, "B2")],
321 total: 2,
322 };
323 let out = jsonl_string(&list);
324 assert_eq!(out.lines().count(), 2);
325 assert!(out.contains("\"id\":1"));
326 assert!(out.contains("\"id\":2"));
327 }
328
329 #[test]
330 fn agile_board_list_jsonl_empty() {
331 let list = AgileBoardList {
332 boards: vec![],
333 total: 0,
334 };
335 assert_eq!(jsonl_string(&list), "");
336 }
337
338 #[test]
339 fn agile_sprint_list_jsonl() {
340 let list = AgileSprintList {
341 sprints: vec![sample_sprint(1, "Sprint 1")],
342 total: 1,
343 };
344 let out = jsonl_string(&list);
345 assert_eq!(out.lines().count(), 1);
346 assert!(out.contains("\"id\":1"));
347 }
348
349 #[test]
350 fn jira_search_result_jsonl() {
351 let result = JiraSearchResult {
352 issues: vec![sample_issue("A-1"), sample_issue("A-2")],
353 total: 2,
354 };
355 let out = jsonl_string(&result);
356 assert_eq!(out.lines().count(), 2);
357 assert!(out.contains("\"key\":\"A-1\""));
358 assert!(out.contains("\"key\":\"A-2\""));
359 }
360
361 #[test]
362 fn jira_project_list_jsonl() {
363 let list = JiraProjectList {
364 projects: vec![sample_project("P")],
365 total: 1,
366 };
367 let out = jsonl_string(&list);
368 assert_eq!(out.lines().count(), 1);
369 assert!(out.contains("\"key\":\"P\""));
370 }
371
372 #[test]
373 fn jira_project_version_list_jsonl() {
374 let list = JiraProjectVersionList {
375 versions: vec![
376 sample_project_version("10", "1.0.0"),
377 sample_project_version("11", "1.1.0"),
378 ],
379 total: 2,
380 };
381 let out = jsonl_string(&list);
382 assert_eq!(out.lines().count(), 2);
383 assert!(out.contains("\"id\":\"10\""));
384 assert!(out.contains("\"id\":\"11\""));
385 }
386
387 #[test]
388 fn jira_watcher_list_jsonl() {
389 let list = JiraWatcherList {
390 watchers: vec![sample_user("alice"), sample_user("bob")],
391 watch_count: 2,
392 };
393 let out = jsonl_string(&list);
394 assert_eq!(out.lines().count(), 2);
395 }
396
397 #[test]
398 fn jira_worklog_list_jsonl() {
399 let list = JiraWorklogList {
400 worklogs: vec![sample_worklog("w1")],
401 total: 1,
402 };
403 let out = jsonl_string(&list);
404 assert_eq!(out.lines().count(), 1);
405 assert!(out.contains("\"id\":\"w1\""));
406 }
407
408 #[test]
409 fn confluence_search_results_jsonl() {
410 let list = ConfluenceSearchResults {
411 results: vec![sample_confluence_result("r1")],
412 total: 1,
413 };
414 let out = jsonl_string(&list);
415 assert_eq!(out.lines().count(), 1);
416 assert!(out.contains("\"id\":\"r1\""));
417 }
418
419 #[test]
420 fn confluence_user_search_results_jsonl() {
421 let list = ConfluenceUserSearchResults {
422 users: vec![sample_confluence_user("u1")],
423 total: 1,
424 };
425 let out = jsonl_string(&list);
426 assert_eq!(out.lines().count(), 1);
427 assert!(out.contains("\"accountId\":\"u1\"") || out.contains("\"account_id\":\"u1\""));
428 }
429
430 #[test]
431 fn confluence_attachment_page_jsonl() {
432 use crate::atlassian::confluence_types::{ConfluenceAttachment, ConfluenceAttachmentPage};
433 let page = ConfluenceAttachmentPage {
434 results: vec![
435 ConfluenceAttachment {
436 id: "a1".to_string(),
437 title: "one.png".to_string(),
438 media_type: Some("image/png".to_string()),
439 file_size: Some(100),
440 download_url: None,
441 version: None,
442 page_id: None,
443 file_id: None,
444 },
445 ConfluenceAttachment {
446 id: "a2".to_string(),
447 title: "two.pdf".to_string(),
448 media_type: None,
449 file_size: None,
450 download_url: None,
451 version: None,
452 page_id: None,
453 file_id: None,
454 },
455 ],
456 next_cursor: Some("NEXT".to_string()),
457 };
458 let out = jsonl_string(&page);
459 assert_eq!(out.lines().count(), 2);
460 assert!(out.contains("\"id\":\"a1\""));
461 assert!(out.contains("\"id\":\"a2\""));
462 }
463
464 #[test]
465 fn confluence_attachment_page_jsonl_empty() {
466 use crate::atlassian::confluence_types::ConfluenceAttachmentPage;
467 let page = ConfluenceAttachmentPage {
468 results: vec![],
469 next_cursor: None,
470 };
471 assert_eq!(jsonl_string(&page), "");
472 }
473
474 #[test]
475 fn confluence_space_page_jsonl() {
476 use crate::atlassian::confluence_types::{ConfluenceSpace, ConfluenceSpacePage};
477 let page = ConfluenceSpacePage {
478 results: vec![
479 ConfluenceSpace {
480 id: "100".to_string(),
481 key: "ENG".to_string(),
482 name: "Engineering".to_string(),
483 type_: "global".to_string(),
484 status: "current".to_string(),
485 homepage_id: Some("200".to_string()),
486 },
487 ConfluenceSpace {
488 id: "101".to_string(),
489 key: "OPS".to_string(),
490 name: "Operations".to_string(),
491 type_: "global".to_string(),
492 status: "archived".to_string(),
493 homepage_id: None,
494 },
495 ],
496 next_cursor: Some("NEXT".to_string()),
497 };
498 let out = jsonl_string(&page);
499 assert_eq!(out.lines().count(), 2);
500 assert!(out.contains("\"id\":\"100\""));
501 assert!(out.contains("\"key\":\"ENG\""));
502 assert!(out.contains("\"homepageId\":\"200\""));
503 }
504
505 #[test]
506 fn confluence_space_page_jsonl_empty() {
507 use crate::atlassian::confluence_types::ConfluenceSpacePage;
508 let page = ConfluenceSpacePage {
509 results: vec![],
510 next_cursor: None,
511 };
512 assert_eq!(jsonl_string(&page), "");
513 }
514
515 #[test]
516 fn jira_user_search_results_jsonl() {
517 use crate::atlassian::jira_types::{JiraUserSearchResult, JiraUserSearchResults};
518 let list = JiraUserSearchResults {
519 users: vec![
520 JiraUserSearchResult {
521 account_id: "u1".to_string(),
522 display_name: Some("Alice".to_string()),
523 email_address: Some("alice@example.com".to_string()),
524 active: true,
525 account_type: Some("atlassian".to_string()),
526 },
527 JiraUserSearchResult {
528 account_id: "u2".to_string(),
529 display_name: None,
530 email_address: None,
531 active: false,
532 account_type: None,
533 },
534 ],
535 count: 2,
536 };
537 let out = jsonl_string(&list);
538 assert_eq!(out.lines().count(), 2);
539 assert!(out.contains("\"account_id\":\"u1\""));
540 assert!(out.contains("\"account_id\":\"u2\""));
541 }
542
543 #[test]
544 fn jira_user_get_results_jsonl() {
545 use crate::atlassian::jira_types::{JiraUserGetResults, JiraUserRecord};
546 let list = JiraUserGetResults {
547 users: vec![
548 JiraUserRecord {
549 account_id: "u1".to_string(),
550 display_name: Some("Alice".to_string()),
551 email_address: Some("alice@example.com".to_string()),
552 active: Some(true),
553 account_type: Some("atlassian".to_string()),
554 error: None,
555 },
556 JiraUserRecord {
557 account_id: "bad".to_string(),
558 display_name: None,
559 email_address: None,
560 active: None,
561 account_type: None,
562 error: Some("HTTP 404".to_string()),
563 },
564 ],
565 };
566 let out = jsonl_string(&list);
567 assert_eq!(out.lines().count(), 2);
568 assert!(out.contains("\"account_id\":\"u1\""));
569 assert!(out.contains("\"error\":\"HTTP 404\""));
570 }
571
572 #[test]
573 fn confluence_user_get_results_jsonl() {
574 use crate::atlassian::confluence_types::{ConfluenceUserGetResults, ConfluenceUserRecord};
575 let list = ConfluenceUserGetResults {
576 users: vec![ConfluenceUserRecord {
577 account_id: "abc".to_string(),
578 display_name: Some("Alice".to_string()),
579 email: Some("a@x.com".to_string()),
580 account_type: Some("atlassian".to_string()),
581 active: None,
582 error: None,
583 }],
584 };
585 let out = jsonl_string(&list);
586 assert_eq!(out.lines().count(), 1);
587 assert!(out.contains("\"account_id\":\"abc\""));
588 }
589
590 #[test]
591 fn jira_dev_status_jsonl_single_line() {
592 let status = JiraDevStatus {
593 pull_requests: vec![],
594 branches: vec![],
595 repositories: vec![],
596 };
597 let out = jsonl_string(&status);
598 assert_eq!(out.lines().count(), 1);
599 }
600
601 #[test]
602 fn jira_dev_status_summary_jsonl_single_line() {
603 let summary = JiraDevStatusSummary {
604 pullrequest: JiraDevStatusCount {
605 count: 0,
606 providers: vec![],
607 },
608 branch: JiraDevStatusCount {
609 count: 0,
610 providers: vec![],
611 },
612 repository: JiraDevStatusCount {
613 count: 0,
614 providers: vec![],
615 },
616 };
617 let out = jsonl_string(&summary);
618 assert_eq!(out.lines().count(), 1);
619 }
620
621 #[test]
624 fn output_as_jsonl_wrapper_returns_true() {
625 let list = AgileBoardList {
626 boards: vec![sample_board(1, "b")],
627 total: 1,
628 };
629 assert!(output_as(&list, &OutputFormat::Jsonl).unwrap());
630 }
631
632 #[test]
633 fn write_output_jsonl_wrapper() {
634 let list = AgileBoardList {
635 boards: vec![sample_board(1, "b1"), sample_board(2, "b2")],
636 total: 2,
637 };
638 let mut buf = Vec::new();
639 let wrote = write_output(&list, &OutputFormat::Jsonl, &mut buf).unwrap();
640 assert!(wrote);
641 let out = String::from_utf8(buf).unwrap();
642 assert_eq!(out.lines().count(), 2);
643 assert!(out.contains("\"id\":1"));
644 assert!(out.contains("\"id\":2"));
645 }
646
647 #[test]
648 fn write_output_json_wrapper_includes_total_field() {
649 let list = AgileBoardList {
650 boards: vec![sample_board(1, "b1")],
651 total: 42,
652 };
653 let mut buf = Vec::new();
654 write_output(&list, &OutputFormat::Json, &mut buf).unwrap();
655 let out = String::from_utf8(buf).unwrap();
656 assert!(out.contains("\"total\": 42"));
657 }
658
659 #[test]
660 fn write_output_yaml_wrapper_includes_total_field() {
661 let list = AgileBoardList {
662 boards: vec![],
663 total: 0,
664 };
665 let mut buf = Vec::new();
666 write_output(&list, &OutputFormat::Yaml, &mut buf).unwrap();
667 let out = String::from_utf8(buf).unwrap();
668 assert!(out.contains("total: 0"));
669 }
670}