structable/lib.rs
1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! Representing data to the user (i.e. in CLI or TUI) usually requires converting data into vector
16//! of vector of strings with the data. Further this data is being passed to tools like
17//! `comfy_table`, `cli-table`or similar. Preparing such data is a tedious job. This is where
18//! StructTable is coming to help.
19//!
20//! For a structure like:
21//!
22//! ```rust
23//! use serde::Serialize;
24//! use serde_json::Value;
25//! use structable::{StructTable, StructTableOptions};
26//!
27//! #[derive(Serialize, StructTable)]
28//! struct User {
29//! #[structable(title = "ID")]
30//! id: u64,
31//! first_name: String,
32//! last_name: String,
33//! #[structable(title = "Long", wide)]
34//! extra: String,
35//! #[structable(optional, serialize, wide)]
36//! complex_data: Option<Value>,
37//! #[structable(optional)]
38//! dummy: Option<String>,
39//! }
40//! ```
41//!
42//! What you get is:
43//!
44//! ```rust
45//! # use serde::Serialize;
46//! # use serde_json::Value;
47//! # use structable::{StructTable, StructTableOptions};
48//! # #[derive(Serialize)]
49//! # struct User {
50//! # id: u64,
51//! # first_name: String,
52//! # last_name: String,
53//! # extra: String,
54//! # complex_data: Option<Value>,
55//! # dummy: Option<String>,
56//! # }
57//! impl StructTable for User {
58//! fn class_headers<O: StructTableOptions>(
59//! options: &O,
60//! ) -> Option<Vec<String>> {
61//! let mut headers: Vec<String> = Vec::new();
62//! if options.should_return_field("ID", false) {
63//! headers.push("ID".to_string());
64//! }
65//! if options.should_return_field("first_name", false) {
66//! headers.push("first_name".to_string());
67//! }
68//! if options.should_return_field("last_name", false) {
69//! headers.push("last_name".to_string());
70//! }
71//! if options.should_return_field("Long", true) {
72//! headers.push("Long".to_string());
73//! }
74//! if options.should_return_field("complex_data", true) {
75//! headers.push("complex_data".to_string());
76//! }
77//! if options.should_return_field("dummy", false) {
78//! headers.push("dummy".to_string());
79//! }
80//! Some(headers)
81//! }
82//!
83//! fn data<O: StructTableOptions>(
84//! &self,
85//! options: &O,
86//! ) -> ::std::vec::Vec<Option<::std::string::String>> {
87//! let mut row: Vec<Option<String>> = Vec::new();
88//! if options.should_return_field("ID", false) {
89//! row.push(Some(self.id.to_string()));
90//! }
91//! if options.should_return_field("first_name", false) {
92//! row.push(Some(self.first_name.to_string()));
93//! }
94//! if options.should_return_field("last_name", false) {
95//! row.push(Some(self.last_name.to_string()));
96//! }
97//! if options.should_return_field("Long", true) {
98//! row.push(Some(self.extra.to_string()));
99//! }
100//! if options.should_return_field("complex_data", true) {
101//! row.push(
102//! self
103//! .complex_data
104//! .clone()
105//! .map(|v| {
106//! if options.pretty_mode() {
107//! serde_json::to_string_pretty(&v)
108//! } else {
109//! serde_json::to_string(&v)
110//! }
111//! .unwrap_or_else(|_| String::from(
112//! "<ERROR SERIALIZING DATA>",
113//! ))
114//! }),
115//! );
116//! }
117//! if options.should_return_field("dummy", false) {
118//! row.push(self.dummy.clone().map(|x| x.to_string()));
119//! }
120//! row
121//! }
122//! fn status(&self) -> Option<String> {
123//! None
124//! }
125//! }
126//! ```
127//! ## Field parameters
128//!
129//! - `title` column name to be returned. When unset field name is used.
130//!
131//! - `wide` return field only in the `wide` mode, or when explicitly requested through `fields`
132//!
133//! - `serialize` serialize field value to the json. When `pretty` mode is requested uses
134//! `to_pretty_string()`
135//!
136//!
137//! ## Example
138//!
139//! ```rust
140//! # use std::collections::BTreeSet;
141//! # use serde_json::{json, Value};
142//! # use serde::Serialize;
143//! use structable::{build_table, build_list_table};
144//! use structable::{OutputConfig, StructTable, StructTableOptions};
145//!
146//! #[derive(Serialize, StructTable)]
147//! struct User {
148//! #[structable(title = "ID")]
149//! id: u64,
150//! first_name: &'static str,
151//! last_name: &'static str,
152//! #[structable(title = "Long(only in wide mode)", wide)]
153//! extra: &'static str,
154//! #[structable(optional, pretty)]
155//! complex_data: Option<Value>
156//! }
157//!
158//! let users = vec![
159//! User {
160//! id: 1,
161//! first_name: "Scooby",
162//! last_name: "Doo",
163//! extra: "Foo",
164//! complex_data: Some(json!({"a": "b", "c": "d"}))
165//! },
166//! User {
167//! id: 2,
168//! first_name: "John",
169//! last_name: "Cena",
170//! extra: "Bar",
171//! complex_data: None
172//! },
173//! ];
174//! let user = User {
175//! id: 1,
176//! first_name: "Scooby",
177//! last_name: "Doo",
178//! extra: "XYZ",
179//! complex_data: Some(json!({"a": "b", "c": "d"}))
180//! };
181//!
182//! let config = OutputConfig {
183//! fields: BTreeSet::from(["Last Name".to_string()]),
184//! wide: false,
185//! pretty: false
186//! };
187//!
188//! let data = build_table(&user, &config);
189//! println!("Single user {:?} => {:?}", data.0, data.1);
190//! let data2 = build_list_table(users.iter(), &config);
191//! println!("multiple users {:?} => {:?}", data2.0, data2.1);
192//!
193//! ```
194//!
195//! ```text
196//! Single user ["Attribute", "Value"] => [["id", "1"], ["first_name", "Scooby"], ["last_name", "Doo"], ["long_only", "XYZ"]]
197//! multiple user ["id", "first_name", "last_name", "long_only"] => [["1", "Scooby", "Doo", "Foo"], ["2", "John", "Cena", "Bar"]]
198//! ```
199//!
200use serde::{Deserialize, Serialize};
201use std::collections::BTreeSet;
202
203pub use structable_derive::StructTable;
204
205/// Output configuration
206///
207/// This structure is controlling how the table table is being built for a structure.
208#[derive(Clone, Debug, Default, Deserialize, Serialize)]
209pub struct OutputConfig {
210 /// Limit fields (their titles) to be returned
211 #[serde(default)]
212 pub fields: BTreeSet<String>,
213 /// Wide mode (additional fields requested)
214 #[serde(default)]
215 pub wide: bool,
216 /// Pretty-print
217 #[serde(default)]
218 pub pretty: bool,
219}
220
221/// StructTable output configuration trait
222///
223/// When OutputConfig can not be used you can implement this trait on you structure.
224pub trait StructTableOptions {
225 /// Whether to return fields marked as `wide`-only
226 fn wide_mode(&self) -> bool;
227
228 /// Whether to serialize values using `to_pretty_string`
229 fn pretty_mode(&self) -> bool;
230
231 /// Whether the attribute should be returned
232 fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool;
233}
234
235impl StructTableOptions for OutputConfig {
236 fn wide_mode(&self) -> bool {
237 self.wide
238 }
239
240 fn pretty_mode(&self) -> bool {
241 self.pretty
242 }
243
244 fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool {
245 if !is_wide_field {
246 self.fields.is_empty()
247 || self
248 .fields
249 .iter()
250 .any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
251 } else {
252 (self.fields.is_empty() && self.wide_mode())
253 || self
254 .fields
255 .iter()
256 .any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
257 }
258 }
259}
260
261/// Trait for building tables out of structures
262pub trait StructTable {
263 /// Return Vector of table headers (attribute titles to be returned) that are not instance
264 /// specific (i.e. struct)
265 fn class_headers<O: StructTableOptions>(_config: &O) -> Option<Vec<String>> {
266 None
267 }
268
269 /// Return Vector of table headers (attribute titles to be returned) from the instance that are
270 /// instance specific (i.e. HashMap)
271 fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
272 None
273 }
274
275 /// Return vector of selected fields as `Option<String>`
276 fn data<O: StructTableOptions>(&self, config: &O) -> Vec<Option<String>>;
277
278 /// Return structure status property
279 fn status(&self) -> Option<String> {
280 None
281 }
282}
283
284/// Build a table for a single structure
285///
286/// Returns a vector with first row being column headers ["Attribute", "Value"]. All other rows
287/// represent transposed table with first value in the vector being an attribute name and second
288/// value being the value itself. The optional attribute, which is `None` is not being returned.
289pub fn build_table<T, O>(data: &T, options: &O) -> (Vec<String>, Vec<Vec<String>>)
290where
291 T: StructTable,
292 O: StructTableOptions,
293{
294 let headers = Vec::from(["Attribute".into(), "Value".into()]);
295 let mut rows: Vec<Vec<String>> = Vec::new();
296 let col_headers = T::class_headers(options).or_else(|| data.instance_headers(options));
297 if let Some(hdr) = col_headers {
298 for (a, v) in hdr.iter().zip(data.data(options).iter()) {
299 if let Some(data) = v {
300 rows.push(Vec::from([a.to_string(), data.to_string()]));
301 }
302 }
303 }
304 (headers, rows)
305}
306
307/// Build a table for list of entries
308///
309/// Returns vector of vector of strings with first row being table headers and all other rows are
310/// the values themselves.
311pub fn build_list_table<I, T, O>(data: I, options: &O) -> (Vec<String>, Vec<Vec<String>>)
312where
313 I: Iterator<Item = T>,
314 T: StructTable,
315 O: StructTableOptions,
316{
317 if let Some(headers) = T::class_headers(options) {
318 let rows: Vec<Vec<String>> = Vec::from_iter(data.map(|item| {
319 item.data(options)
320 .into_iter()
321 .map(|el| el.unwrap_or_else(|| String::from(" ")))
322 .collect::<Vec<String>>()
323 }));
324 (headers, rows)
325 } else {
326 // TODO: Make method returning result
327 (Vec::new(), Vec::new())
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use serde_json::{json, Value};
334 use std::collections::BTreeMap;
335
336 use super::*;
337
338 #[derive(Default, Deserialize, Serialize, StructTable)]
339 struct User {
340 #[structable(title = "ID")]
341 id: u64,
342 first_name: String,
343 last_name: String,
344 #[structable(title = "Long", wide)]
345 extra: String,
346 #[structable(optional, serialize, wide)]
347 complex_data: Option<Value>,
348 #[structable(optional)]
349 dummy: Option<String>,
350 }
351
352 #[derive(Deserialize, Serialize, StructTable)]
353 struct StatusStruct {
354 #[structable(status)]
355 status: String,
356 }
357
358 #[derive(Clone, Deserialize, Serialize)]
359 enum Status {
360 Dummy,
361 }
362
363 #[derive(Deserialize, Serialize, StructTable)]
364 struct SerializeStatusStruct {
365 #[structable(serialize, status)]
366 status: Status,
367 }
368
369 #[derive(Deserialize, Serialize, StructTable)]
370 struct SerializeOptionStatusStruct {
371 #[structable(optional, serialize, status)]
372 status: Option<Status>,
373 }
374
375 #[derive(Deserialize, Serialize, StructTable)]
376 struct OptionStatusStruct {
377 #[structable(status, optional)]
378 status: Option<String>,
379 }
380
381 #[test]
382 fn test_single() {
383 let config = OutputConfig::default();
384 let user = User {
385 id: 1,
386 first_name: "Scooby".into(),
387 last_name: "Doo".into(),
388 extra: "XYZ".into(),
389 complex_data: Some(json!({"a": "b", "c": "d"})),
390 dummy: None,
391 };
392 assert_eq!(
393 build_table(&user, &config),
394 (
395 vec!["Attribute".into(), "Value".into()],
396 vec![
397 vec!["ID".into(), "1".into()],
398 vec!["first_name".into(), "Scooby".into()],
399 vec!["last_name".into(), "Doo".into()],
400 ]
401 )
402 );
403 }
404
405 #[test]
406 fn test_single_wide() {
407 let config = OutputConfig {
408 wide: true,
409 ..Default::default()
410 };
411 let user = User {
412 id: 1,
413 first_name: "Scooby".into(),
414 last_name: "Doo".into(),
415 extra: "XYZ".into(),
416 complex_data: Some(json!({"a": "b", "c": "d"})),
417 dummy: None,
418 };
419 assert_eq!(
420 build_table(&user, &config),
421 (
422 vec!["Attribute".into(), "Value".into()],
423 vec![
424 vec!["ID".into(), "1".into()],
425 vec!["first_name".into(), "Scooby".into()],
426 vec!["last_name".into(), "Doo".into()],
427 vec!["Long".into(), "XYZ".into()],
428 vec![
429 "complex_data".into(),
430 "{\"a\":\"b\",\"c\":\"d\"}".to_string()
431 ],
432 ]
433 )
434 );
435 }
436
437 #[test]
438 fn test_single_wide_column() {
439 let config = OutputConfig {
440 fields: BTreeSet::from(["Long".into()]),
441 ..Default::default()
442 };
443 let user = User {
444 id: 1,
445 first_name: "Scooby".into(),
446 last_name: "Doo".into(),
447 extra: "XYZ".into(),
448 complex_data: Some(json!({"a": "b", "c": "d"})),
449 dummy: None,
450 };
451 assert_eq!(
452 build_table(&user, &config),
453 (
454 vec!["Attribute".into(), "Value".into()],
455 vec![vec!["Long".into(), "XYZ".into()],]
456 )
457 );
458 }
459
460 #[test]
461 fn test_single_wide_column_wide_mode() {
462 let config = OutputConfig {
463 fields: BTreeSet::from(["Long".into()]),
464 wide: true,
465 ..Default::default()
466 };
467 let user = User {
468 id: 1,
469 first_name: "Scooby".into(),
470 last_name: "Doo".into(),
471 extra: "XYZ".into(),
472 complex_data: Some(json!({"a": "b", "c": "d"})),
473 dummy: None,
474 };
475 assert_eq!(
476 build_table(&user, &config),
477 (
478 vec!["Attribute".into(), "Value".into()],
479 vec![vec!["Long".into(), "XYZ".into()],]
480 )
481 );
482 }
483
484 #[test]
485 fn test_single_wide_pretty() {
486 let config = OutputConfig {
487 wide: true,
488 pretty: true,
489 ..Default::default()
490 };
491 let user = User {
492 id: 1,
493 first_name: "Scooby".into(),
494 last_name: "Doo".into(),
495 extra: "XYZ".into(),
496 complex_data: Some(json!({"a": "b", "c": "d"})),
497 dummy: None,
498 };
499 assert_eq!(
500 build_table(&user, &config),
501 (
502 vec!["Attribute".into(), "Value".into()],
503 vec![
504 vec!["ID".into(), "1".into()],
505 vec!["first_name".into(), "Scooby".into()],
506 vec!["last_name".into(), "Doo".into()],
507 vec!["Long".into(), "XYZ".into()],
508 vec![
509 "complex_data".into(),
510 "{\n \"a\": \"b\",\n \"c\": \"d\"\n}".to_string()
511 ],
512 ]
513 )
514 );
515 }
516
517 #[test]
518 fn test_single_status() {
519 assert_eq!(
520 StatusStruct {
521 status: "foo".into(),
522 }
523 .status(),
524 Some("foo".into())
525 );
526 }
527
528 #[test]
529 fn test_single_no_status() {
530 assert_eq!(User::default().status(), None);
531 }
532
533 #[test]
534 fn test_single_option_status() {
535 assert_eq!(
536 OptionStatusStruct {
537 status: Some("foo".into()),
538 }
539 .status(),
540 Some("foo".into())
541 );
542 }
543
544 #[test]
545 fn test_complex_status() {
546 assert_eq!(
547 SerializeStatusStruct {
548 status: Status::Dummy,
549 }
550 .status(),
551 Some("Dummy".into())
552 );
553
554 assert_eq!(
555 SerializeOptionStatusStruct {
556 status: Some(Status::Dummy),
557 }
558 .status(),
559 Some("Dummy".into())
560 );
561
562 let (_, rows) = build_table(
563 &SerializeOptionStatusStruct {
564 status: Some(Status::Dummy),
565 },
566 &OutputConfig::default(),
567 );
568 assert_eq!(vec![vec!["status".to_string(), "Dummy".to_string()]], rows);
569
570 let (_, rows) = build_list_table(
571 [SerializeOptionStatusStruct {
572 status: Some(Status::Dummy),
573 }]
574 .iter(),
575 &OutputConfig::default(),
576 );
577 assert_eq!(vec![vec!["Dummy".to_string()]], rows);
578 }
579
580 #[test]
581 fn test_status() {
582 #[derive(Deserialize, Serialize, StructTable)]
583 struct StatusStruct {
584 #[structable(title = "ID")]
585 id: u64,
586 #[structable(status)]
587 status: String,
588 }
589 }
590
591 #[test]
592 fn test_list() {
593 let config = OutputConfig::default();
594 let users = vec![
595 User {
596 id: 1,
597 first_name: "Scooby".into(),
598 last_name: "Doo".into(),
599 extra: "Foo".into(),
600 complex_data: Some(json!({"a": "b", "c": "d"})),
601 dummy: None,
602 },
603 User {
604 id: 2,
605 first_name: "John".into(),
606 last_name: "Cena".into(),
607 extra: "Bar".into(),
608 complex_data: None,
609 dummy: None,
610 },
611 ];
612
613 assert_eq!(
614 build_list_table(users.iter(), &config),
615 (
616 vec![
617 "ID".into(),
618 "first_name".into(),
619 "last_name".into(),
620 "dummy".into()
621 ],
622 vec![
623 vec!["1".into(), "Scooby".into(), "Doo".into(), " ".into()],
624 vec!["2".into(), "John".into(), "Cena".into(), " ".into()],
625 ]
626 )
627 );
628 }
629
630 #[test]
631 fn test_list_wide_column() {
632 let config = OutputConfig {
633 fields: BTreeSet::from(["Long".into()]),
634 ..Default::default()
635 };
636 let users = vec![
637 User {
638 id: 1,
639 first_name: "Scooby".into(),
640 last_name: "Doo".into(),
641 extra: "Foo".into(),
642 complex_data: Some(json!({"a": "b", "c": "d"})),
643 dummy: None,
644 },
645 User {
646 id: 2,
647 first_name: "John".into(),
648 last_name: "Cena".into(),
649 extra: "Bar".into(),
650 complex_data: None,
651 dummy: Some("foo".into()),
652 },
653 ];
654
655 assert_eq!(
656 build_list_table(users.iter(), &config),
657 (
658 vec!["Long".into(),],
659 vec![vec!["Foo".into(),], vec!["Bar".into(),],]
660 )
661 );
662 }
663
664 #[test]
665 fn test_list_wide_column_wide_mode() {
666 let config = OutputConfig {
667 fields: BTreeSet::from(["Long".into()]),
668 wide: true,
669 pretty: false,
670 };
671 let users = vec![
672 User {
673 id: 1,
674 first_name: "Scooby".into(),
675 last_name: "Doo".into(),
676 extra: "Foo".into(),
677 complex_data: Some(json!({"a": "b", "c": "d"})),
678 dummy: None,
679 },
680 User {
681 id: 2,
682 first_name: "John".into(),
683 last_name: "Cena".into(),
684 extra: "Bar".into(),
685 complex_data: None,
686 dummy: Some("foo".into()),
687 },
688 ];
689
690 assert_eq!(
691 build_list_table(users.iter(), &config),
692 (
693 vec!["Long".into(),],
694 vec![vec!["Foo".into(),], vec!["Bar".into(),],]
695 )
696 );
697 }
698
699 #[test]
700 fn test_list_wide() {
701 let config = OutputConfig {
702 fields: BTreeSet::new(),
703 wide: true,
704 pretty: false,
705 };
706 let users = vec![
707 User {
708 id: 1,
709 first_name: "Scooby".into(),
710 last_name: "Doo".into(),
711 extra: "Foo".into(),
712 complex_data: Some(json!({"a": "b", "c": "d"})),
713 dummy: None,
714 },
715 User {
716 id: 2,
717 first_name: "John".into(),
718 last_name: "Cena".into(),
719 extra: "Bar".into(),
720 complex_data: None,
721 dummy: Some("foo".into()),
722 },
723 ];
724
725 assert_eq!(
726 build_list_table(users.iter(), &config),
727 (
728 vec![
729 "ID".into(),
730 "first_name".into(),
731 "last_name".into(),
732 "Long".into(),
733 "complex_data".into(),
734 "dummy".into()
735 ],
736 vec![
737 vec![
738 "1".into(),
739 "Scooby".into(),
740 "Doo".into(),
741 "Foo".into(),
742 "{\"a\":\"b\",\"c\":\"d\"}".to_string(),
743 " ".to_string()
744 ],
745 vec![
746 "2".into(),
747 "John".into(),
748 "Cena".into(),
749 "Bar".into(),
750 " ".to_string(),
751 "foo".into()
752 ],
753 ]
754 )
755 );
756 }
757
758 #[test]
759 fn test_deser() {
760 #[derive(Deserialize, Serialize, StructTable)]
761 struct Foo {
762 #[structable(title = "ID")]
763 id: u64,
764 #[structable(optional)]
765 foo: Option<String>,
766 #[structable(optional)]
767 bar: Option<bool>,
768 }
769
770 let foo: Foo = serde_json::from_value(json!({"id": 1})).expect("Foo object");
771
772 assert_eq!(
773 build_table(&foo, &OutputConfig::default()),
774 (
775 vec!["Attribute".into(), "Value".into()],
776 vec![vec!["ID".into(), "1".into()],]
777 )
778 );
779 }
780
781 #[test]
782 fn test_output_config() {
783 let config = OutputConfig {
784 fields: BTreeSet::from(["Foo".into(), "bAr".into(), "BAZ".into(), "a:b-c".into()]),
785 ..Default::default()
786 };
787
788 assert!(config.should_return_field("Foo", false));
789 assert!(config.should_return_field("FOO", false));
790 assert!(config.should_return_field("bar", false));
791 assert!(config.should_return_field("baz", false));
792 assert!(config.should_return_field("a:b-c", false));
793 }
794
795 #[test]
796 fn test_instance_headers() {
797 struct Sot(BTreeMap<String, String>);
798
799 impl StructTable for Sot {
800 fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
801 Some(self.0.keys().map(Into::into).collect())
802 }
803 fn data<O: StructTableOptions>(&self, _config: &O) -> Vec<Option<String>> {
804 Vec::from_iter(self.0.values().map(|x| Some(x.to_string())))
805 }
806 }
807
808 let sot = Sot(BTreeMap::from([
809 ("a".into(), "1".into()),
810 ("b".into(), "2".into()),
811 ("c".into(), "3".into()),
812 ]));
813
814 assert_eq!(
815 build_table(&sot, &OutputConfig::default()),
816 (
817 vec!["Attribute".into(), "Value".into()],
818 vec![
819 vec!["a".into(), "1".into()],
820 vec!["b".into(), "2".into()],
821 vec!["c".into(), "3".into()]
822 ]
823 )
824 );
825 }
826}