yozefu_command/
cli.rs

1//! The command line argument Parser struct
2use crate::cluster::Cluster;
3use crate::command::{Command, MainCommand, UtilityCommands};
4use crate::theme::init_themes_file;
5use app::configuration::{ClusterConfig, GlobalConfig, SchemaRegistryConfig, YozefuConfig};
6use app::APPLICATION_NAME;
7use clap::command;
8use lib::Error;
9use reqwest::Url;
10use std::fs;
11use std::path::PathBuf;
12use tui::error::TuiError;
13
14pub use clap::Parser;
15use indexmap::IndexMap;
16
17// https://github.com/clap-rs/clap/issues/975
18/// CLI parser
19#[derive(Parser)]
20#[command(author, version, about = "A terminal user interface to navigate Kafka topics and search for Kafka records.", name = APPLICATION_NAME, bin_name = APPLICATION_NAME, display_name = APPLICATION_NAME, long_about = None, propagate_version = true, args_conflicts_with_subcommands = true)]
21pub struct Cli<T>
22where
23    T: Cluster,
24{
25    #[command(subcommand)]
26    subcommands: Option<UtilityCommands>,
27    #[command(flatten)]
28    default_command: MainCommand<T>,
29    #[clap(skip)]
30    logs_file: Option<PathBuf>,
31}
32
33impl<T> Cli<T>
34where
35    T: Cluster,
36{
37    /// Executes the CLI.
38    /// The config will be loaded from the default config file.
39    pub async fn execute(&self) -> Result<(), TuiError> {
40        self.run(None).await
41    }
42
43    /// Executes the CLI with a specified kafka config client
44    pub async fn execute_with(&self, yozefu_config: YozefuConfig) -> Result<(), TuiError> {
45        self.run(Some(yozefu_config)).await
46    }
47
48    /// This function returns `Some(T)` when the user starts the TUI.
49    /// Otherwise, for subcommands commands that such `config` or `new-filter`, it returns `None`.
50    pub fn cluster(&self) -> Option<T> {
51        match self.subcommands.is_some() {
52            true => None,
53            false => Some(self.default_command.cluster()),
54        }
55    }
56
57    pub fn is_main_command(&self) -> bool {
58        self.cluster().is_some()
59    }
60
61    /// Changes the default logs file path
62    pub fn logs_file(&mut self, logs: PathBuf) -> &mut Self {
63        self.logs_file = Some(logs);
64        self
65    }
66
67    async fn run(&self, yozefu_config: Option<YozefuConfig>) -> Result<(), TuiError> {
68        init_files().await?;
69        match &self.subcommands {
70            Some(c) => c.execute().await.map_err(|e| e.into()),
71            None => {
72                // Load the config from the yozefu config file
73                let yozefu_config = match yozefu_config {
74                    None => self.default_command.yozefu_config()?,
75                    Some(c) => c,
76                };
77                let mut command = self.default_command.clone();
78                command.logs_file = self.logs_file.clone();
79                command.execute(yozefu_config).await
80            }
81        }
82    }
83}
84
85/// Initializes a default configuration file if it does not exist.
86/// The default cluster is `localhost`.
87async fn init_files() -> Result<(), Error> {
88    init_config_file()?;
89    init_themes_file().await?;
90    Ok(())
91}
92
93/// Initializes a default configuration file if it does not exist.
94/// The default cluster is `localhost`.
95fn init_config_file() -> Result<PathBuf, Error> {
96    let path = GlobalConfig::path()?;
97    if fs::metadata(&path).is_ok() {
98        return Ok(path);
99    }
100    let mut config = GlobalConfig::try_from(&path)?;
101    let mut localhost_config = IndexMap::new();
102    localhost_config.insert(
103        "bootstrap.servers".to_string(),
104        "localhost:9092".to_string(),
105    );
106    localhost_config.insert("security.protocol".to_string(), "plaintext".to_string());
107    localhost_config.insert("broker.address.family".to_string(), "v4".to_string());
108    config
109        .default_kafka_config
110        .insert("fetch.min.bytes".to_string(), "10000".to_string());
111
112    config.clusters.insert(
113        "localhost".into(),
114        ClusterConfig {
115            kafka: localhost_config,
116            schema_registry: Some(SchemaRegistryConfig {
117                url: Url::parse("http://localhost:8081").unwrap(),
118                headers: Default::default(),
119            }),
120            ..Default::default()
121        },
122    );
123
124    fs::create_dir_all(config.filters_dir())?;
125    fs::write(&path, serde_json::to_string_pretty(&config).unwrap()).unwrap();
126    #[cfg(unix)]
127    {
128        use std::os::unix::fs::PermissionsExt;
129        let mut perms: fs::Permissions = fs::metadata(&path)?.permissions();
130        perms.set_mode(0o600);
131        fs::set_permissions(&path, perms)?;
132    }
133
134    Ok(path)
135}
136
137#[test]
138pub fn test_conflicts() {
139    use clap::CommandFactory;
140    Cli::<String>::command().debug_assert();
141}
142#[test]
143fn test_valid_themes() {
144    use std::collections::HashMap;
145    use tui::Theme;
146
147    let content = include_str!("../themes.json");
148    let themes: HashMap<String, Theme> = serde_json::from_str(content).unwrap();
149    assert!(themes.keys().len() >= 3)
150}