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