Skip to main content

openstack_cli_core/
output.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//! Output processing module
16
17use std::collections::BTreeSet;
18use std::io::{self, Write};
19
20use comfy_table::{
21    Cell, Color, ColumnConstraint, ContentArrangement, Table, Width, presets::UTF8_FULL_CONDENSED,
22};
23use itertools::Itertools;
24use openstack_sdk_core::types::EntryStatus;
25use owo_colors::{OwoColorize, Stream::Stderr};
26use rand::prelude::*;
27use serde::de::DeserializeOwned;
28
29use structable::{OutputConfig, StructTable, StructTableOptions};
30
31use crate::cli::{CliArgs, OutputFormat, TableArrangement};
32use crate::config::ViewConfig;
33use crate::error::OpenStackCliError;
34
35/// Output target (human or machine) enum
36#[derive(Default, Clone)]
37pub enum OutputFor {
38    #[default]
39    Human,
40    Machine,
41}
42
43impl From<TableArrangement> for ContentArrangement {
44    fn from(value: TableArrangement) -> Self {
45        match value {
46            TableArrangement::Dynamic => Self::Dynamic,
47            TableArrangement::DynamicFullWidth => Self::DynamicFullWidth,
48            TableArrangement::Disabled => Self::Disabled,
49        }
50    }
51}
52
53/// Output Processor.
54#[derive(Default, Clone)]
55pub struct OutputProcessor {
56    /// Resource output configuration
57    pub config: Option<ViewConfig>,
58    /// Whether output is for human or for machine
59    pub target: OutputFor,
60    /// Table arrangement
61    pub table_arrangement: TableArrangement,
62    /// Fields requested
63    pub fields: BTreeSet<String>,
64    /// Wide mode
65    pub wide: bool,
66    /// Pretty mode
67    pub pretty: bool,
68    /// Command hints
69    hints: Option<Vec<String>>,
70}
71
72impl StructTableOptions for OutputProcessor {
73    fn wide_mode(&self) -> bool {
74        self.wide
75            || self
76                .config
77                .as_ref()
78                .is_some_and(|cfg| cfg.wide.is_some_and(|w| w))
79    }
80
81    fn pretty_mode(&self) -> bool {
82        self.pretty
83    }
84
85    fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool {
86        let is_requested = self
87            .fields
88            .iter()
89            .any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
90            || (self.fields.is_empty()
91                && self
92                    .config
93                    .as_ref()
94                    .map(|cfg| {
95                        cfg.default_fields
96                            .iter()
97                            .any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
98                    })
99                    .is_some_and(|x| x));
100
101        if !is_wide_field {
102            // Return non wide field when no field filters passed or explicitly requested the field
103            is_requested
104                || (self.fields.is_empty()
105                    && self
106                        .config
107                        .as_ref()
108                        .is_none_or(|cfg| cfg.default_fields.is_empty()))
109        } else {
110            // The wide field is returned in wide mode when no filters passed or explicitly
111            // requested the field
112            (self.fields.is_empty() && self.wide_mode()) || is_requested
113        }
114    }
115
116    fn field_data_json_pointer<S: AsRef<str>>(&self, field: S) -> Option<String> {
117        if !self.wide_mode() {
118            self.config.as_ref().and_then(|config| {
119                config
120                    .fields
121                    .iter()
122                    .find(|x| x.name.to_lowercase() == field.as_ref().to_lowercase())
123                    .and_then(|field_config| field_config.json_pointer.clone())
124            })
125        } else {
126            None
127        }
128    }
129
130    fn field_data_json_pointer_fallback<S: AsRef<str>>(&self, field: S) -> Option<String> {
131        if !self.wide_mode() {
132            self.config.as_ref().and_then(|config| {
133                config
134                    .fields
135                    .iter()
136                    .find(|x| x.name.to_lowercase() == field.as_ref().to_lowercase())
137                    .and_then(|field_config| field_config.json_pointer_fallback.clone())
138            })
139        } else {
140            None
141        }
142    }
143}
144
145impl OutputProcessor {
146    /// Get OutputConfig from passed arguments
147    pub fn from_args<C: CliArgs, R: AsRef<str>, A: AsRef<str>>(
148        args: &C,
149        resource_key: Option<R>,
150        action: Option<A>,
151    ) -> Self {
152        let target = match args.global_opts().output.output {
153            None => OutputFor::Human,
154            Some(OutputFormat::Wide) => OutputFor::Human,
155            _ => OutputFor::Machine,
156        };
157        let mut hints: Vec<String> = args.config().hints.clone();
158
159        if let (Some(resource_key), Some(action)) = (&resource_key, &action) {
160            args.config()
161                .command_hints
162                .get(resource_key.as_ref())
163                .and_then(|cmd_hints| {
164                    cmd_hints.get(action.as_ref()).map(|val| {
165                        hints.extend(val.clone());
166                    })
167                });
168        }
169
170        Self {
171            config: resource_key
172                .as_ref()
173                .and_then(|val| args.config().views.get(val.as_ref()).cloned()),
174            target,
175            table_arrangement: args.global_opts().output.table_arrangement,
176            fields: BTreeSet::from_iter(args.global_opts().output.fields.iter().cloned()),
177            wide: matches!(args.global_opts().output.output, Some(OutputFormat::Wide)),
178            pretty: args.global_opts().output.pretty,
179            hints: Some(hints),
180        }
181    }
182
183    /// Validate command arguments with respect to the output producing
184    pub fn validate_args<C: CliArgs>(&self, _args: &C) -> Result<(), OpenStackCliError> {
185        Ok(())
186    }
187
188    /// Re-sort table according to the configuration and determine column constraints
189    fn prepare_table(
190        &self,
191        headers: Vec<String>,
192        data: Vec<Vec<String>>,
193    ) -> (Vec<String>, Vec<Vec<String>>, Vec<Option<ColumnConstraint>>) {
194        let mut headers = headers;
195        let mut rows = data;
196        let mut column_constrains: Vec<Option<ColumnConstraint>> = vec![None; headers.len()];
197
198        if let Some(cfg) = &self.config {
199            // Offset from the current iteration pointer
200            if headers.len() > 1 {
201                let mut idx_offset: usize = 0;
202                for (default_idx, field) in cfg.default_fields.iter().unique().enumerate() {
203                    if let Some(curr_idx) = headers
204                        .iter()
205                        .position(|x| x.to_lowercase() == field.to_lowercase())
206                    {
207                        // Swap headers between current and should pos
208                        if default_idx - idx_offset < headers.len() {
209                            headers.swap(default_idx - idx_offset, curr_idx);
210                            for row in rows.iter_mut() {
211                                // Swap also data columns
212                                row.swap(default_idx - idx_offset, curr_idx);
213                            }
214                        }
215                    } else {
216                        // This column is not found in the data. Perhars structable returned some
217                        // other name. Move the column to the very end
218                        if default_idx - idx_offset < headers.len() {
219                            let curr_hdr = headers.remove(default_idx - idx_offset);
220                            headers.push(curr_hdr);
221                            for row in rows.iter_mut() {
222                                let curr_cell = row.remove(default_idx - idx_offset);
223                                row.push(curr_cell);
224                            }
225                            // Some unmatched field moved to the end. Our "current" index should respect
226                            // the offset
227                            idx_offset += 1;
228                        }
229                    }
230                }
231            }
232            // Find field configuration
233            for (idx, field) in headers.iter().enumerate() {
234                if let Some(field_config) = cfg
235                    .fields
236                    .iter()
237                    .find(|x| x.name.to_lowercase() == field.to_lowercase())
238                {
239                    let constraint = match (
240                        field_config.width,
241                        field_config.min_width,
242                        field_config.max_width,
243                    ) {
244                        (Some(fixed), _, _) => {
245                            Some(ColumnConstraint::Absolute(Width::Fixed(fixed as u16)))
246                        }
247                        (None, Some(lower), Some(upper)) => Some(ColumnConstraint::Boundaries {
248                            lower: Width::Fixed(lower as u16),
249                            upper: Width::Fixed(upper as u16),
250                        }),
251                        (None, Some(lower), None) => {
252                            Some(ColumnConstraint::LowerBoundary(Width::Fixed(lower as u16)))
253                        }
254                        (None, None, Some(upper)) => {
255                            Some(ColumnConstraint::UpperBoundary(Width::Fixed(upper as u16)))
256                        }
257                        _ => None,
258                    };
259                    column_constrains[idx] = constraint;
260                }
261            }
262        }
263        (headers, rows, column_constrains)
264    }
265
266    /// Output List of resources
267    pub fn output_list<T>(&self, data: Vec<serde_json::Value>) -> Result<(), OpenStackCliError>
268    where
269        T: StructTable,
270        T: DeserializeOwned,
271        for<'a> &'a T: StructTable,
272    {
273        match self.target {
274            OutputFor::Human => {
275                let table: Vec<T> = serde_json::from_value(serde_json::Value::Array(data.clone()))
276                    .map_err(|err| {
277                        OpenStackCliError::deserialize(
278                            err,
279                            serde_json::to_string(&serde_json::Value::Array(
280                                data.into_iter()
281                                    .filter(|item| {
282                                        serde_json::from_value::<T>(item.clone()).is_err()
283                                    })
284                                    .collect(),
285                            ))
286                            .unwrap_or_else(|v| format!("{v:?}")),
287                        )
288                    })?;
289
290                let data = structable::build_list_table(table.iter(), self);
291                let (headers, table_rows, table_constraints) = self.prepare_table(data.0, data.1);
292                let mut statuses: Vec<Option<String>> =
293                    table.iter().map(|item| item.status()).collect();
294
295                // Ensure we have as many statuses as rows to zip them properly
296                statuses.resize_with(table_rows.len(), Default::default);
297
298                let rows = table_rows
299                    .iter()
300                    .zip(statuses.iter())
301                    .map(|(data, status)| {
302                        let color = match EntryStatus::from(status.as_ref()) {
303                            EntryStatus::Error => Some(Color::Red),
304                            EntryStatus::Pending => Some(Color::Yellow),
305                            EntryStatus::Inactive => Some(Color::Cyan),
306                            _ => None,
307                        };
308                        data.iter().map(move |cell| {
309                            if let Some(color) = color {
310                                Cell::new(cell).fg(color)
311                            } else {
312                                Cell::new(cell)
313                            }
314                        })
315                    });
316                let mut table = Table::new();
317                table
318                    .load_style(UTF8_FULL_CONDENSED)
319                    .set_content_arrangement(ContentArrangement::from(self.table_arrangement))
320                    .set_header(headers)
321                    .add_rows(rows);
322
323                for (idx, constraint) in table_constraints.iter().enumerate() {
324                    if let Some(constraint) = constraint
325                        && let Some(col) = table.column_mut(idx)
326                    {
327                        col.set_constraint(*constraint);
328                    }
329                }
330
331                println!("{table}");
332                Ok(())
333            }
334            _ => self.output_machine(serde_json::from_value(serde_json::Value::Array(data))?),
335        }
336    }
337
338    /// Output List of resources
339    pub fn output_single<T>(&self, data: serde_json::Value) -> Result<(), OpenStackCliError>
340    where
341        T: StructTable,
342        T: DeserializeOwned,
343    {
344        match self.target {
345            OutputFor::Human => {
346                let table: T = serde_json::from_value(data.clone()).map_err(|err| {
347                    OpenStackCliError::deserialize(
348                        err,
349                        serde_json::to_string(&data.clone()).unwrap_or_default(),
350                    )
351                })?;
352
353                self.output_human(&table)
354            }
355            _ => self.output_machine(serde_json::from_value(data)?),
356        }
357    }
358
359    /// Produce output for humans (table) for a single resource
360    pub fn output_human<T: StructTable>(&self, data: &T) -> Result<(), OpenStackCliError> {
361        let (headers, table_rows) = structable::build_table(data, &OutputConfig::default());
362
363        let mut table = Table::new();
364        table
365            .load_style(UTF8_FULL_CONDENSED)
366            .set_content_arrangement(ContentArrangement::from(self.table_arrangement))
367            .set_header(headers)
368            .add_rows(table_rows);
369        println!("{table}");
370        Ok(())
371    }
372
373    /// Produce output for machine
374    /// Return machine readable output with the API side names
375    pub fn output_machine(&self, data: serde_json::Value) -> Result<(), OpenStackCliError> {
376        if self.pretty {
377            serde_json::to_writer_pretty(io::stdout(), &data)?;
378        } else {
379            serde_json::to_writer(io::stdout(), &data)?;
380        }
381        io::stdout().write_all(b"\n")?;
382        Ok(())
383    }
384
385    /// Show hints
386    pub fn show_command_hint(&self) -> Result<(), OpenStackCliError> {
387        if rand::random_bool(1.0 / 2.0) {
388            self.hints.as_ref().and_then(|hints| {
389                hints.choose(&mut rand::rng()).map(|hint| {
390                    eprintln!(
391                        "\n{} {}",
392                        "Hint:".if_supports_color(Stderr, |text| text.green()),
393                        hint.if_supports_color(Stderr, |text| text.blue())
394                    );
395                })
396            });
397        }
398        Ok(())
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use clap::Parser;
406    use std::io::Write;
407    use tempfile::Builder;
408
409    use crate::{cli::*, config::*};
410
411    #[test]
412    fn test_wide_mode() {
413        assert!(!OutputProcessor::default().wide_mode());
414        assert!(
415            OutputProcessor {
416                wide: true,
417                ..Default::default()
418            }
419            .wide_mode()
420        );
421        assert!(
422            OutputProcessor {
423                config: Some(ViewConfig {
424                    wide: Some(true),
425                    ..Default::default()
426                }),
427                ..Default::default()
428            }
429            .wide_mode()
430        );
431    }
432
433    #[test]
434    fn test_field_returned_no_selection() {
435        let out = OutputProcessor::default();
436
437        assert!(
438            out.should_return_field("dummy", false),
439            "default field returned in non-wide mode with empty fields selector"
440        );
441        assert!(
442            !out.should_return_field("dummy", true),
443            "wide field not returned in non-wide mode with empty fields selector"
444        );
445
446        let out = OutputProcessor {
447            wide: true,
448            ..Default::default()
449        };
450
451        assert!(
452            out.should_return_field("dummy", false),
453            "default field returned in wide mode with empty fields selector"
454        );
455        assert!(
456            out.should_return_field("dummy", true),
457            "wide field returned in wide mode with empty fields selector"
458        );
459    }
460
461    #[test]
462    fn test_field_returned_selection_no_config() {
463        let out = OutputProcessor {
464            fields: BTreeSet::from(["foo".to_string()]),
465            ..Default::default()
466        };
467
468        assert!(
469            !out.should_return_field("dummy", false),
470            "default field not returned in non-wide mode with mismatching fields selector"
471        );
472        assert!(
473            !out.should_return_field("dummy", true),
474            "wide field not returned in non-wide mode with mismatching fields selector"
475        );
476        assert!(
477            out.should_return_field("foo", false),
478            "default field returned in non-wide mode with matching fields selector"
479        );
480        assert!(
481            out.should_return_field("foo", true),
482            "wide field returned in non-wide mode with matching fields selector"
483        );
484
485        let out = OutputProcessor {
486            fields: BTreeSet::from(["foo".to_string()]),
487            wide: true,
488            ..Default::default()
489        };
490
491        assert!(
492            !out.should_return_field("dummy", false),
493            "default field not returned in wide mode with mismatching fields selector"
494        );
495        assert!(
496            !out.should_return_field("dummy", true),
497            "wide field not returned in wide mode with mismatching fields selector"
498        );
499    }
500
501    #[test]
502    fn test_field_returned_selection_empty_config() {
503        let out = OutputProcessor {
504            config: Some(ViewConfig::default()),
505            target: OutputFor::Human,
506            table_arrangement: TableArrangement::Disabled,
507            fields: BTreeSet::new(),
508            wide: false,
509            pretty: false,
510            ..Default::default()
511        };
512
513        assert!(
514            out.should_return_field("dummy", false),
515            "default field returned in non-wide mode with mismatching fields selector and empty config"
516        );
517        assert!(
518            !out.should_return_field("dummy", true),
519            "wide field not returned in non-wide mode with mismatching fields selector and empty config"
520        );
521    }
522
523    #[test]
524    fn test_field_returned_selection_with_config_with_filters() {
525        let out = OutputProcessor {
526            config: Some(ViewConfig {
527                default_fields: vec!["foo".to_string()],
528                ..Default::default()
529            }),
530            fields: BTreeSet::from(["bar".to_string()]),
531            ..Default::default()
532        };
533
534        assert!(
535            !out.should_return_field("dummy", false),
536            "default field not returned in non-wide mode with mismatching fields selector"
537        );
538        assert!(
539            !out.should_return_field("dummy", true),
540            "wide field not returned in non-wide mode with mismatching fields selector"
541        );
542        assert!(
543            !out.should_return_field("foo", false),
544            "default field not returned in non-wide mode with mismatching fields selector"
545        );
546        assert!(
547            !out.should_return_field("foo", true),
548            "wide field not returned in non-wide mode with mismatching fields selector"
549        );
550        assert!(
551            out.should_return_field("bar", false),
552            "default field returned in non-wide mode with matching fields selector"
553        );
554        assert!(
555            out.should_return_field("bar", true),
556            "wide field returned in non-wide mode with matching fields selector"
557        );
558
559        let out = OutputProcessor {
560            config: Some(ViewConfig {
561                default_fields: vec!["foo".to_string()],
562                ..Default::default()
563            }),
564            fields: BTreeSet::from(["bar".to_string()]),
565            wide: true,
566            ..Default::default()
567        };
568
569        assert!(
570            !out.should_return_field("dummy", false),
571            "default field not returned in wide mode with mismatching fields selector"
572        );
573        assert!(
574            !out.should_return_field("dummy", true),
575            "wide field not returned in wide mode with mismatching fields selector"
576        );
577        assert!(
578            !out.should_return_field("foo", false),
579            "config field not returned in wide mode with mismatching fields selector"
580        );
581        assert!(
582            !out.should_return_field("foo", true),
583            "wide config field not returned in wide mode with mismatching fields selector"
584        );
585        assert!(
586            out.should_return_field("bar", false),
587            "default field returned in wide mode with matching fields selector"
588        );
589        assert!(
590            out.should_return_field("bar", true),
591            "wide field returned in wide mode with matching fields selector"
592        );
593    }
594
595    #[test]
596    fn test_field_returned_selection_with_config_no_filters() {
597        let out = OutputProcessor {
598            config: Some(ViewConfig {
599                default_fields: vec!["foo".to_string()],
600                ..Default::default()
601            }),
602            ..Default::default()
603        };
604
605        assert!(
606            !out.should_return_field("dummy", false),
607            "default field not returned in non-wide mode with empty fields selector and not in config"
608        );
609        assert!(
610            out.should_return_field("foo", false),
611            "default field not returned in non-wide mode with empty fields selector, but in config"
612        );
613        assert!(
614            !out.should_return_field("dummy", true),
615            "wide field not returned in non-wide mode with empty fields selector and not in config"
616        );
617        assert!(
618            out.should_return_field("foo", true),
619            "wide field returned in non-wide mode with empty fields selector, but in config"
620        );
621
622        let out = OutputProcessor {
623            config: Some(ViewConfig {
624                default_fields: vec!["foo".to_string()],
625                ..Default::default()
626            }),
627            wide: true,
628            ..Default::default()
629        };
630
631        assert!(
632            !out.should_return_field("dummy", false),
633            "default field not returned in wide mode with empty fields selector and not in config"
634        );
635        assert!(
636            out.should_return_field("foo", false),
637            "default field returned in wide mode with empty fields selector, but in config"
638        );
639        assert!(
640            out.should_return_field("dummy", true),
641            "wide field returned in wide mode with empty fields selector and not in config"
642        );
643        assert!(
644            out.should_return_field("foo", true),
645            "wide field returned in wide mode with empty fields selector, but in config"
646        );
647    }
648
649    #[test]
650    fn test_prepare_table() {
651        let out = OutputProcessor {
652            config: Some(ViewConfig {
653                default_fields: vec![
654                    "foo".to_string(),
655                    "bar".to_string(),
656                    "baz".to_string(),
657                    "dummy".to_string(),
658                ],
659                fields: vec![FieldConfig {
660                    name: "bar".to_string(),
661                    min_width: Some(15),
662                    ..Default::default()
663                }],
664                ..Default::default()
665            }),
666            ..Default::default()
667        };
668        let (hdr, rows, constraints) = out.prepare_table(
669            vec![
670                "dummy".to_string(),
671                "bar".to_string(),
672                "foo".to_string(),
673                "baz".to_string(),
674            ],
675            vec![
676                vec![
677                    "11".to_string(),
678                    "12".to_string(),
679                    "13".to_string(),
680                    "14".to_string(),
681                ],
682                vec![
683                    "21".to_string(),
684                    "22".to_string(),
685                    "23".to_string(),
686                    "24".to_string(),
687                ],
688            ],
689        );
690        assert_eq!(
691            vec![
692                "foo".to_string(),
693                "bar".to_string(),
694                "baz".to_string(),
695                "dummy".to_string()
696            ],
697            hdr,
698            "headers in the correct sort order"
699        );
700        assert_eq!(
701            vec![
702                vec![
703                    "13".to_string(),
704                    "12".to_string(),
705                    "14".to_string(),
706                    "11".to_string(),
707                ],
708                vec![
709                    "23".to_string(),
710                    "22".to_string(),
711                    "24".to_string(),
712                    "21".to_string(),
713                ],
714            ],
715            rows,
716            "row columns sorted properly"
717        );
718        assert_eq![
719            vec![
720                None,
721                Some(ColumnConstraint::LowerBoundary(Width::Fixed(15))),
722                None,
723                None
724            ],
725            constraints
726        ];
727
728        let (hdr, rows, _constraints) = out.prepare_table(
729            vec![
730                "dummy".to_string(),
731                "bar2".to_string(),
732                "foo".to_string(),
733                "baz2".to_string(),
734            ],
735            vec![
736                vec![
737                    "11".to_string(),
738                    "12".to_string(),
739                    "13".to_string(),
740                    "14".to_string(),
741                ],
742                vec![
743                    "21".to_string(),
744                    "22".to_string(),
745                    "23".to_string(),
746                    "24".to_string(),
747                ],
748            ],
749        );
750        assert_eq!(
751            vec![
752                "foo".to_string(),
753                "dummy".to_string(),
754                "bar2".to_string(),
755                "baz2".to_string(),
756            ],
757            hdr,
758            "headers with unknown fields in the correct sort order"
759        );
760        assert_eq!(
761            vec![
762                vec![
763                    "13".to_string(),
764                    "11".to_string(),
765                    "12".to_string(),
766                    "14".to_string(),
767                ],
768                vec![
769                    "23".to_string(),
770                    "21".to_string(),
771                    "22".to_string(),
772                    "24".to_string(),
773                ],
774            ],
775            rows,
776            "row columns sorted properly"
777        );
778    }
779
780    #[test]
781    fn test_prepare_table_duplicated_values() {
782        let out = OutputProcessor {
783            config: Some(ViewConfig {
784                default_fields: vec![
785                    "foo".to_string(),
786                    "bar".to_string(),
787                    "foo".to_string(),
788                    "baz".to_string(),
789                ],
790                ..Default::default()
791            }),
792            ..Default::default()
793        };
794        let (hdr, rows, _constraints) = out.prepare_table(
795            vec!["bar".to_string(), "foo".to_string(), "baz".to_string()],
796            vec![
797                vec!["11".to_string(), "12".to_string(), "13".to_string()],
798                vec!["21".to_string(), "22".to_string(), "23".to_string()],
799            ],
800        );
801        assert_eq!(
802            vec!["foo".to_string(), "bar".to_string(), "baz".to_string(),],
803            hdr,
804            "headers in the correct sort order"
805        );
806        assert_eq!(
807            vec![
808                vec!["12".to_string(), "11".to_string(), "13".to_string(),],
809                vec!["22".to_string(), "21".to_string(), "23".to_string(),],
810            ],
811            rows,
812            "row columns sorted properly"
813        );
814    }
815
816    #[test]
817    fn test_prepare_table_missing_default_fields() {
818        let out = OutputProcessor {
819            config: Some(ViewConfig {
820                default_fields: vec![
821                    "foo".to_string(),
822                    "bar1".to_string(),
823                    "foo1".to_string(),
824                    "baz1".to_string(),
825                ],
826                ..Default::default()
827            }),
828            ..Default::default()
829        };
830        let (hdr, rows, _constraints) = out.prepare_table(
831            vec!["bar".to_string(), "foo".to_string(), "baz".to_string()],
832            vec![
833                vec!["11".to_string(), "12".to_string(), "13".to_string()],
834                vec!["21".to_string(), "22".to_string(), "23".to_string()],
835            ],
836        );
837        assert_eq!(
838            vec!["foo".to_string(), "baz".to_string(), "bar".to_string(),],
839            hdr,
840            "headers in the correct sort order"
841        );
842        assert_eq!(
843            vec![
844                vec!["12".to_string(), "13".to_string(), "11".to_string(),],
845                vec!["22".to_string(), "23".to_string(), "21".to_string(),],
846            ],
847            rows,
848            "row columns sorted properly"
849        );
850    }
851
852    #[test]
853    fn test_output_processor_from_args_hints() {
854        let mut config_file = Builder::new().suffix(".yaml").tempfile().unwrap();
855
856        const CONFIG_DATA: &str = r#"
857            views:
858              foo:
859                default_fields: ["a", "b", "c"]
860              bar:
861                fields:
862                  - name: "b"
863                    min_width: 1
864            command_hints:
865              res:
866                cmd:
867                  - cmd_hint1
868                  - cmd_hint2
869                cmd2: [cmd2_hint1]
870              res2:
871                cmd: []
872            hints:
873              - hint1
874              - hint2
875            enable_hints: true
876        "#;
877
878        write!(config_file, "{CONFIG_DATA}").unwrap();
879
880        #[derive(Parser)]
881        struct Cli {
882            #[command(flatten)]
883            global_opts: GlobalOpts,
884            #[arg(long("cli-config"), value_parser = parse_config, default_value_t = Config::new().unwrap())]
885            config: Config,
886        }
887
888        impl CliArgs for Cli {
889            fn global_opts(&self) -> &GlobalOpts {
890                &self.global_opts
891            }
892
893            fn config(&self) -> &Config {
894                &self.config
895            }
896        }
897
898        let op = OutputProcessor::from_args(
899            &Cli::parse_from([
900                "osc",
901                "--cli-config",
902                &config_file.path().as_os_str().to_string_lossy(),
903            ]),
904            Some("res"),
905            Some("cmd"),
906        );
907        assert_eq!(
908            Some(vec![
909                "hint1".to_string(),
910                "hint2".to_string(),
911                "cmd_hint1".to_string(),
912                "cmd_hint2".to_string()
913            ]),
914            op.hints
915        );
916    }
917}