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    #[test]
528    fn test_single_no_status() {
529        assert_eq!(User::default().status(), None);
530    }
531    #[test]
532    fn test_single_option_status() {
533        assert_eq!(
534            OptionStatusStruct {
535                status: Some("foo".into()),
536            }
537            .status(),
538            Some("foo".into())
539        );
540    }
541
542    #[test]
543    fn test_complex_status() {
544        assert_eq!(
545            SerializeStatusStruct {
546                status: Status::Dummy,
547            }
548            .status(),
549            Some("Dummy".into())
550        );
551
552        assert_eq!(
553            SerializeOptionStatusStruct {
554                status: Some(Status::Dummy),
555            }
556            .status(),
557            Some("Dummy".into())
558        );
559    }
560    #[test]
561    fn test_status() {
562        #[derive(Deserialize, Serialize, StructTable)]
563        struct StatusStruct {
564            #[structable(title = "ID")]
565            id: u64,
566            #[structable(status)]
567            status: String,
568        }
569    }
570
571    #[test]
572    fn test_list() {
573        let config = OutputConfig::default();
574        let users = vec![
575            User {
576                id: 1,
577                first_name: "Scooby".into(),
578                last_name: "Doo".into(),
579                extra: "Foo".into(),
580                complex_data: Some(json!({"a": "b", "c": "d"})),
581                dummy: None,
582            },
583            User {
584                id: 2,
585                first_name: "John".into(),
586                last_name: "Cena".into(),
587                extra: "Bar".into(),
588                complex_data: None,
589                dummy: None,
590            },
591        ];
592
593        assert_eq!(
594            build_list_table(users.iter(), &config),
595            (
596                vec![
597                    "ID".into(),
598                    "first_name".into(),
599                    "last_name".into(),
600                    "dummy".into()
601                ],
602                vec![
603                    vec!["1".into(), "Scooby".into(), "Doo".into(), " ".into()],
604                    vec!["2".into(), "John".into(), "Cena".into(), " ".into()],
605                ]
606            )
607        );
608    }
609
610    #[test]
611    fn test_list_wide_column() {
612        let config = OutputConfig {
613            fields: BTreeSet::from(["Long".into()]),
614            ..Default::default()
615        };
616        let users = vec![
617            User {
618                id: 1,
619                first_name: "Scooby".into(),
620                last_name: "Doo".into(),
621                extra: "Foo".into(),
622                complex_data: Some(json!({"a": "b", "c": "d"})),
623                dummy: None,
624            },
625            User {
626                id: 2,
627                first_name: "John".into(),
628                last_name: "Cena".into(),
629                extra: "Bar".into(),
630                complex_data: None,
631                dummy: Some("foo".into()),
632            },
633        ];
634
635        assert_eq!(
636            build_list_table(users.iter(), &config),
637            (
638                vec!["Long".into(),],
639                vec![vec!["Foo".into(),], vec!["Bar".into(),],]
640            )
641        );
642    }
643
644    #[test]
645    fn test_list_wide_column_wide_mode() {
646        let config = OutputConfig {
647            fields: BTreeSet::from(["Long".into()]),
648            wide: true,
649            pretty: false,
650        };
651        let users = vec![
652            User {
653                id: 1,
654                first_name: "Scooby".into(),
655                last_name: "Doo".into(),
656                extra: "Foo".into(),
657                complex_data: Some(json!({"a": "b", "c": "d"})),
658                dummy: None,
659            },
660            User {
661                id: 2,
662                first_name: "John".into(),
663                last_name: "Cena".into(),
664                extra: "Bar".into(),
665                complex_data: None,
666                dummy: Some("foo".into()),
667            },
668        ];
669
670        assert_eq!(
671            build_list_table(users.iter(), &config),
672            (
673                vec!["Long".into(),],
674                vec![vec!["Foo".into(),], vec!["Bar".into(),],]
675            )
676        );
677    }
678
679    #[test]
680    fn test_list_wide() {
681        let config = OutputConfig {
682            fields: BTreeSet::new(),
683            wide: true,
684            pretty: false,
685        };
686        let users = vec![
687            User {
688                id: 1,
689                first_name: "Scooby".into(),
690                last_name: "Doo".into(),
691                extra: "Foo".into(),
692                complex_data: Some(json!({"a": "b", "c": "d"})),
693                dummy: None,
694            },
695            User {
696                id: 2,
697                first_name: "John".into(),
698                last_name: "Cena".into(),
699                extra: "Bar".into(),
700                complex_data: None,
701                dummy: Some("foo".into()),
702            },
703        ];
704
705        assert_eq!(
706            build_list_table(users.iter(), &config),
707            (
708                vec![
709                    "ID".into(),
710                    "first_name".into(),
711                    "last_name".into(),
712                    "Long".into(),
713                    "complex_data".into(),
714                    "dummy".into()
715                ],
716                vec![
717                    vec![
718                        "1".into(),
719                        "Scooby".into(),
720                        "Doo".into(),
721                        "Foo".into(),
722                        "{\"a\":\"b\",\"c\":\"d\"}".to_string(),
723                        " ".to_string()
724                    ],
725                    vec![
726                        "2".into(),
727                        "John".into(),
728                        "Cena".into(),
729                        "Bar".into(),
730                        " ".to_string(),
731                        "foo".into()
732                    ],
733                ]
734            )
735        );
736    }
737
738    #[test]
739    fn test_deser() {
740        #[derive(Deserialize, Serialize, StructTable)]
741        struct Foo {
742            #[structable(title = "ID")]
743            id: u64,
744            #[structable(optional)]
745            foo: Option<String>,
746            #[structable(optional)]
747            bar: Option<bool>,
748        }
749
750        let foo: Foo = serde_json::from_value(json!({"id": 1})).expect("Foo object");
751
752        assert_eq!(
753            build_table(&foo, &OutputConfig::default()),
754            (
755                vec!["Attribute".into(), "Value".into()],
756                vec![vec!["ID".into(), "1".into()],]
757            )
758        );
759    }
760
761    #[test]
762    fn test_output_config() {
763        let config = OutputConfig {
764            fields: BTreeSet::from(["Foo".into(), "bAr".into(), "BAZ".into(), "a:b-c".into()]),
765            ..Default::default()
766        };
767
768        assert!(config.should_return_field("Foo", false));
769        assert!(config.should_return_field("FOO", false));
770        assert!(config.should_return_field("bar", false));
771        assert!(config.should_return_field("baz", false));
772        assert!(config.should_return_field("a:b-c", false));
773    }
774
775    #[test]
776    fn test_instance_headers() {
777        struct Sot(BTreeMap<String, String>);
778
779        impl StructTable for Sot {
780            fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
781                Some(self.0.keys().map(Into::into).collect())
782            }
783            fn data<O: StructTableOptions>(&self, _config: &O) -> Vec<Option<String>> {
784                Vec::from_iter(self.0.values().map(|x| Some(x.to_string())))
785            }
786        }
787
788        let sot = Sot(BTreeMap::from([
789            ("a".into(), "1".into()),
790            ("b".into(), "2".into()),
791            ("c".into(), "3".into()),
792        ]));
793
794        assert_eq!(
795            build_table(&sot, &OutputConfig::default()),
796            (
797                vec!["Attribute".into(), "Value".into()],
798                vec![
799                    vec!["a".into(), "1".into()],
800                    vec!["b".into(), "2".into()],
801                    vec!["c".into(), "3".into()]
802                ]
803            )
804        );
805    }
806}