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}