1use serde::{Deserialize, Serialize};
201use std::collections::BTreeSet;
202
203pub use structable_derive::StructTable;
204
205#[derive(Clone, Debug, Default, Deserialize, Serialize)]
209pub struct OutputConfig {
210 #[serde(default)]
212 pub fields: BTreeSet<String>,
213 #[serde(default)]
215 pub wide: bool,
216 #[serde(default)]
218 pub pretty: bool,
219}
220
221pub trait StructTableOptions {
225 fn wide_mode(&self) -> bool;
227
228 fn pretty_mode(&self) -> bool;
230
231 fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool;
233
234 fn field_data_json_pointer<S: AsRef<str>>(&self, _field: S) -> Option<String> {
237 None
238 }
239}
240
241impl StructTableOptions for OutputConfig {
242 fn wide_mode(&self) -> bool {
243 self.wide
244 }
245
246 fn pretty_mode(&self) -> bool {
247 self.pretty
248 }
249
250 fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool {
251 if !is_wide_field {
252 self.fields.is_empty()
253 || self
254 .fields
255 .iter()
256 .any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
257 } else {
258 (self.fields.is_empty() && self.wide_mode())
259 || self
260 .fields
261 .iter()
262 .any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
263 }
264 }
265}
266
267pub trait StructTable {
269 fn class_headers<O: StructTableOptions>(_config: &O) -> Option<Vec<String>> {
272 None
273 }
274
275 fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
278 None
279 }
280
281 fn data<O: StructTableOptions>(&self, config: &O) -> Vec<Option<String>>;
283
284 fn status(&self) -> Option<String> {
286 None
287 }
288}
289
290pub fn build_table<T, O>(data: &T, options: &O) -> (Vec<String>, Vec<Vec<String>>)
296where
297 T: StructTable,
298 O: StructTableOptions,
299{
300 let headers = Vec::from(["Attribute".into(), "Value".into()]);
301 let mut rows: Vec<Vec<String>> = Vec::new();
302 let col_headers = T::class_headers(options).or_else(|| data.instance_headers(options));
303 if let Some(hdr) = col_headers {
304 for (a, v) in hdr.iter().zip(data.data(options).iter()) {
305 if let Some(data) = v {
306 rows.push(Vec::from([a.to_string(), data.to_string()]));
307 }
308 }
309 }
310 (headers, rows)
311}
312
313pub fn build_list_table<I, T, O>(data: I, options: &O) -> (Vec<String>, Vec<Vec<String>>)
318where
319 I: Iterator<Item = T>,
320 T: StructTable,
321 O: StructTableOptions,
322{
323 if let Some(headers) = T::class_headers(options) {
324 let rows: Vec<Vec<String>> = Vec::from_iter(data.map(|item| {
325 item.data(options)
326 .into_iter()
327 .map(|el| el.unwrap_or_else(|| String::from(" ")))
328 .collect::<Vec<String>>()
329 }));
330 (headers, rows)
331 } else {
332 (Vec::new(), Vec::new())
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use serde_json::{json, Value};
340 use std::collections::BTreeMap;
341
342 use super::*;
343
344 #[derive(Default, Deserialize, Serialize, StructTable)]
345 struct User {
346 #[structable(title = "ID")]
347 id: u64,
348 first_name: String,
349 last_name: String,
350 #[structable(title = "Long", wide)]
351 extra: String,
352 #[structable(optional, serialize, wide)]
353 complex_data: Option<Value>,
354 #[structable(optional)]
355 dummy: Option<String>,
356 }
357
358 #[derive(Deserialize, Serialize, StructTable)]
359 struct StatusStruct {
360 #[structable(status)]
361 status: String,
362 }
363
364 #[derive(Clone, Deserialize, Serialize)]
365 enum Status {
366 Dummy,
367 }
368
369 #[derive(Deserialize, Serialize, StructTable)]
370 struct SerializeStatusStruct {
371 #[structable(serialize, status)]
372 status: Status,
373 }
374
375 #[derive(Deserialize, Serialize, StructTable)]
376 struct SerializeOptionStatusStruct {
377 #[structable(optional, serialize, status)]
378 status: Option<Status>,
379 }
380
381 #[derive(Deserialize, Serialize, StructTable)]
382 struct OptionStatusStruct {
383 #[structable(status, optional)]
384 status: Option<String>,
385 }
386
387 #[test]
388 fn test_single() {
389 let config = OutputConfig::default();
390 let user = User {
391 id: 1,
392 first_name: "Scooby".into(),
393 last_name: "Doo".into(),
394 extra: "XYZ".into(),
395 complex_data: Some(json!({"a": "b", "c": "d"})),
396 dummy: None,
397 };
398 assert_eq!(
399 build_table(&user, &config),
400 (
401 vec!["Attribute".into(), "Value".into()],
402 vec![
403 vec!["ID".into(), "1".into()],
404 vec!["first_name".into(), "Scooby".into()],
405 vec!["last_name".into(), "Doo".into()],
406 ]
407 )
408 );
409 }
410
411 #[test]
412 fn test_single_wide() {
413 let config = OutputConfig {
414 wide: true,
415 ..Default::default()
416 };
417 let user = User {
418 id: 1,
419 first_name: "Scooby".into(),
420 last_name: "Doo".into(),
421 extra: "XYZ".into(),
422 complex_data: Some(json!({"a": "b", "c": "d"})),
423 dummy: None,
424 };
425 assert_eq!(
426 build_table(&user, &config),
427 (
428 vec!["Attribute".into(), "Value".into()],
429 vec![
430 vec!["ID".into(), "1".into()],
431 vec!["first_name".into(), "Scooby".into()],
432 vec!["last_name".into(), "Doo".into()],
433 vec!["Long".into(), "XYZ".into()],
434 vec![
435 "complex_data".into(),
436 "{\"a\":\"b\",\"c\":\"d\"}".to_string()
437 ],
438 ]
439 )
440 );
441 }
442
443 #[test]
444 fn test_single_wide_column() {
445 let config = OutputConfig {
446 fields: BTreeSet::from(["Long".into()]),
447 ..Default::default()
448 };
449 let user = User {
450 id: 1,
451 first_name: "Scooby".into(),
452 last_name: "Doo".into(),
453 extra: "XYZ".into(),
454 complex_data: Some(json!({"a": "b", "c": "d"})),
455 dummy: None,
456 };
457 assert_eq!(
458 build_table(&user, &config),
459 (
460 vec!["Attribute".into(), "Value".into()],
461 vec![vec!["Long".into(), "XYZ".into()],]
462 )
463 );
464 }
465
466 #[test]
467 fn test_single_wide_column_wide_mode() {
468 let config = OutputConfig {
469 fields: BTreeSet::from(["Long".into()]),
470 wide: true,
471 ..Default::default()
472 };
473 let user = User {
474 id: 1,
475 first_name: "Scooby".into(),
476 last_name: "Doo".into(),
477 extra: "XYZ".into(),
478 complex_data: Some(json!({"a": "b", "c": "d"})),
479 dummy: None,
480 };
481 assert_eq!(
482 build_table(&user, &config),
483 (
484 vec!["Attribute".into(), "Value".into()],
485 vec![vec!["Long".into(), "XYZ".into()],]
486 )
487 );
488 }
489
490 #[test]
491 fn test_single_wide_pretty() {
492 let config = OutputConfig {
493 wide: true,
494 pretty: true,
495 ..Default::default()
496 };
497 let user = User {
498 id: 1,
499 first_name: "Scooby".into(),
500 last_name: "Doo".into(),
501 extra: "XYZ".into(),
502 complex_data: Some(json!({"a": "b", "c": "d"})),
503 dummy: None,
504 };
505 assert_eq!(
506 build_table(&user, &config),
507 (
508 vec!["Attribute".into(), "Value".into()],
509 vec![
510 vec!["ID".into(), "1".into()],
511 vec!["first_name".into(), "Scooby".into()],
512 vec!["last_name".into(), "Doo".into()],
513 vec!["Long".into(), "XYZ".into()],
514 vec![
515 "complex_data".into(),
516 "{\n \"a\": \"b\",\n \"c\": \"d\"\n}".to_string()
517 ],
518 ]
519 )
520 );
521 }
522
523 #[test]
524 fn test_single_status() {
525 assert_eq!(
526 StatusStruct {
527 status: "foo".into(),
528 }
529 .status(),
530 Some("foo".into())
531 );
532 }
533
534 #[test]
535 fn test_single_no_status() {
536 assert_eq!(User::default().status(), None);
537 }
538
539 #[test]
540 fn test_single_option_status() {
541 assert_eq!(
542 OptionStatusStruct {
543 status: Some("foo".into()),
544 }
545 .status(),
546 Some("foo".into())
547 );
548 }
549
550 #[test]
551 fn test_complex_status() {
552 assert_eq!(
553 SerializeStatusStruct {
554 status: Status::Dummy,
555 }
556 .status(),
557 Some("Dummy".into())
558 );
559
560 assert_eq!(
561 SerializeOptionStatusStruct {
562 status: Some(Status::Dummy),
563 }
564 .status(),
565 Some("Dummy".into())
566 );
567
568 let (_, rows) = build_table(
569 &SerializeOptionStatusStruct {
570 status: Some(Status::Dummy),
571 },
572 &OutputConfig::default(),
573 );
574 assert_eq!(vec![vec!["status".to_string(), "Dummy".to_string()]], rows);
575
576 let (_, rows) = build_list_table(
577 [SerializeOptionStatusStruct {
578 status: Some(Status::Dummy),
579 }]
580 .iter(),
581 &OutputConfig::default(),
582 );
583 assert_eq!(vec![vec!["Dummy".to_string()]], rows);
584 }
585
586 #[test]
587 fn test_status() {
588 #[derive(Deserialize, Serialize, StructTable)]
589 struct StatusStruct {
590 #[structable(title = "ID")]
591 id: u64,
592 #[structable(status)]
593 status: String,
594 }
595 }
596
597 #[test]
598 fn test_list() {
599 let config = OutputConfig::default();
600 let users = vec![
601 User {
602 id: 1,
603 first_name: "Scooby".into(),
604 last_name: "Doo".into(),
605 extra: "Foo".into(),
606 complex_data: Some(json!({"a": "b", "c": "d"})),
607 dummy: None,
608 },
609 User {
610 id: 2,
611 first_name: "John".into(),
612 last_name: "Cena".into(),
613 extra: "Bar".into(),
614 complex_data: None,
615 dummy: None,
616 },
617 ];
618
619 assert_eq!(
620 build_list_table(users.iter(), &config),
621 (
622 vec![
623 "ID".into(),
624 "first_name".into(),
625 "last_name".into(),
626 "dummy".into()
627 ],
628 vec![
629 vec!["1".into(), "Scooby".into(), "Doo".into(), " ".into()],
630 vec!["2".into(), "John".into(), "Cena".into(), " ".into()],
631 ]
632 )
633 );
634 }
635
636 #[test]
637 fn test_list_wide_column() {
638 let config = OutputConfig {
639 fields: BTreeSet::from(["Long".into()]),
640 ..Default::default()
641 };
642 let users = vec![
643 User {
644 id: 1,
645 first_name: "Scooby".into(),
646 last_name: "Doo".into(),
647 extra: "Foo".into(),
648 complex_data: Some(json!({"a": "b", "c": "d"})),
649 dummy: None,
650 },
651 User {
652 id: 2,
653 first_name: "John".into(),
654 last_name: "Cena".into(),
655 extra: "Bar".into(),
656 complex_data: None,
657 dummy: Some("foo".into()),
658 },
659 ];
660
661 assert_eq!(
662 build_list_table(users.iter(), &config),
663 (
664 vec!["Long".into(),],
665 vec![vec!["Foo".into(),], vec!["Bar".into(),],]
666 )
667 );
668 }
669
670 #[test]
671 fn test_list_wide_column_wide_mode() {
672 let config = OutputConfig {
673 fields: BTreeSet::from(["Long".into()]),
674 wide: true,
675 pretty: false,
676 };
677 let users = vec![
678 User {
679 id: 1,
680 first_name: "Scooby".into(),
681 last_name: "Doo".into(),
682 extra: "Foo".into(),
683 complex_data: Some(json!({"a": "b", "c": "d"})),
684 dummy: None,
685 },
686 User {
687 id: 2,
688 first_name: "John".into(),
689 last_name: "Cena".into(),
690 extra: "Bar".into(),
691 complex_data: None,
692 dummy: Some("foo".into()),
693 },
694 ];
695
696 assert_eq!(
697 build_list_table(users.iter(), &config),
698 (
699 vec!["Long".into(),],
700 vec![vec!["Foo".into(),], vec!["Bar".into(),],]
701 )
702 );
703 }
704
705 #[test]
706 fn test_list_wide() {
707 let config = OutputConfig {
708 fields: BTreeSet::new(),
709 wide: true,
710 pretty: false,
711 };
712 let users = vec![
713 User {
714 id: 1,
715 first_name: "Scooby".into(),
716 last_name: "Doo".into(),
717 extra: "Foo".into(),
718 complex_data: Some(json!({"a": "b", "c": "d"})),
719 dummy: None,
720 },
721 User {
722 id: 2,
723 first_name: "John".into(),
724 last_name: "Cena".into(),
725 extra: "Bar".into(),
726 complex_data: None,
727 dummy: Some("foo".into()),
728 },
729 ];
730
731 assert_eq!(
732 build_list_table(users.iter(), &config),
733 (
734 vec![
735 "ID".into(),
736 "first_name".into(),
737 "last_name".into(),
738 "Long".into(),
739 "complex_data".into(),
740 "dummy".into()
741 ],
742 vec![
743 vec![
744 "1".into(),
745 "Scooby".into(),
746 "Doo".into(),
747 "Foo".into(),
748 "{\"a\":\"b\",\"c\":\"d\"}".to_string(),
749 " ".to_string()
750 ],
751 vec![
752 "2".into(),
753 "John".into(),
754 "Cena".into(),
755 "Bar".into(),
756 " ".to_string(),
757 "foo".into()
758 ],
759 ]
760 )
761 );
762 }
763
764 #[test]
765 fn test_deser() {
766 #[derive(Deserialize, Serialize, StructTable)]
767 struct Foo {
768 #[structable(title = "ID")]
769 id: u64,
770 #[structable(optional)]
771 foo: Option<String>,
772 #[structable(optional)]
773 bar: Option<bool>,
774 }
775
776 let foo: Foo = serde_json::from_value(json!({"id": 1})).expect("Foo object");
777
778 assert_eq!(
779 build_table(&foo, &OutputConfig::default()),
780 (
781 vec!["Attribute".into(), "Value".into()],
782 vec![vec!["ID".into(), "1".into()],]
783 )
784 );
785 }
786
787 #[test]
788 fn test_output_config() {
789 let config = OutputConfig {
790 fields: BTreeSet::from(["Foo".into(), "bAr".into(), "BAZ".into(), "a:b-c".into()]),
791 ..Default::default()
792 };
793
794 assert!(config.should_return_field("Foo", false));
795 assert!(config.should_return_field("FOO", false));
796 assert!(config.should_return_field("bar", false));
797 assert!(config.should_return_field("baz", false));
798 assert!(config.should_return_field("a:b-c", false));
799 }
800
801 #[test]
802 fn test_instance_headers() {
803 struct Sot(BTreeMap<String, String>);
804
805 impl StructTable for Sot {
806 fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
807 Some(self.0.keys().map(Into::into).collect())
808 }
809 fn data<O: StructTableOptions>(&self, _config: &O) -> Vec<Option<String>> {
810 Vec::from_iter(self.0.values().map(|x| Some(x.to_string())))
811 }
812 }
813
814 let sot = Sot(BTreeMap::from([
815 ("a".into(), "1".into()),
816 ("b".into(), "2".into()),
817 ("c".into(), "3".into()),
818 ]));
819
820 assert_eq!(
821 build_table(&sot, &OutputConfig::default()),
822 (
823 vec!["Attribute".into(), "Value".into()],
824 vec![
825 vec!["a".into(), "1".into()],
826 vec!["b".into(), "2".into()],
827 vec!["c".into(), "3".into()]
828 ]
829 )
830 );
831 }
832
833 #[test]
834 fn test_json_pointer() {
835 struct CustomConfig {
836 jp: Option<String>,
837 }
838
839 impl StructTableOptions for CustomConfig {
840 fn wide_mode(&self) -> bool {
841 true
842 }
843
844 fn pretty_mode(&self) -> bool {
845 true
846 }
847
848 fn should_return_field<S: AsRef<str>>(&self, _field: S, _is_wide_field: bool) -> bool {
849 true
850 }
851
852 fn field_data_json_pointer<S: AsRef<str>>(&self, _field: S) -> Option<String> {
853 self.jp.clone()
854 }
855 }
856
857 #[derive(StructTable)]
858 struct Data {
859 #[structable(serialize)]
860 a: Value,
861 #[structable(optional, serialize)]
862 b: Option<Value>,
863 #[structable(serialize)]
864 c: NestedData,
865 }
866
867 #[derive(Clone, Deserialize, Serialize)]
868 struct NestedData {
869 b: NestedData2,
870 }
871 #[derive(Clone, Deserialize, Serialize)]
872 struct NestedData2 {
873 c: String,
874 }
875
876 let config = CustomConfig {
877 jp: Some("/b/c".to_string()),
878 };
879 let sot = Data {
880 a: json!({"b": {"c": "d", "e": "f"}}),
881 b: Some(json!({"b": {"c": "x", "e": "f"}})),
882 c: NestedData {
883 b: NestedData2 { c: "x".to_string() },
884 },
885 };
886 assert_eq!(
887 build_table(&sot, &config),
888 (
889 vec!["Attribute".to_string(), "Value".to_string()],
890 vec![
891 vec!["a".to_string(), "d".to_string()],
892 vec!["b".to_string(), "x".to_string()],
893 vec!["c".to_string(), "x".to_string()],
894 ]
895 ),
896 );
897 }
898}