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}