yozefu_app/configuration/
global_config.rs

1//! module defining the configuration structure of the application
2
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use indexmap::IndexMap;
9use itertools::Itertools;
10use lib::Error;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    APPLICATION_NAME,
15    configuration::{ClusterConfig, ConsumerConfig},
16};
17
18use super::cluster_config::SchemaRegistryConfig;
19
20const EXAMPLE_PROMPTS: &[&str] = &[
21    r#"timestamp between "2 hours ago" and "1 hour ago" limit 100 from beginning"#,
22    r#"offset > 100000 and value contains "music" limit 10"#,
23    r#"key == "ABC" and timestamp >= "2 days ago""#,
24];
25
26/// Configuration of the application
27#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
28#[cfg_attr(test, derive(schemars::JsonSchema))]
29pub struct GlobalConfig {
30    /// Path of this config
31    #[serde(skip)]
32    pub path: PathBuf,
33    /// A placeholder url that will be used when you want to open a kafka record in the browser
34    #[serde(default = "default_url_template")]
35    pub default_url_template: String,
36    /// The initial search query when you start the UI
37    pub initial_query: String,
38    /// The theme to use in the TUI
39    #[serde(default = "default_theme")]
40    pub theme: String,
41    /// The theme to use for syntax highlighting
42    pub highlighter_theme: Option<String>,
43    /// The kafka properties for each cluster
44    pub clusters: IndexMap<String, ClusterConfig>,
45    #[serde(default)]
46    /// The default configuration for the yozefu kafka consumer
47    pub consumer: ConsumerConfig,
48    /// The default kafka properties inherited for every cluster
49    pub default_kafka_config: IndexMap<String, String>,
50    /// History of past search queries
51    pub history: Vec<String>,
52    /// Show shortcuts
53    #[serde(default = "default_show_shortcuts")]
54    pub show_shortcuts: bool,
55    #[serde(default = "default_export_directory")]
56    pub export_directory: PathBuf,
57    /// The file to write logs to
58    pub log_file: Option<PathBuf>,
59}
60
61fn default_url_template() -> String {
62    "http://localhost/cluster/{topic}/{partition}/{offset}".to_string()
63}
64
65fn default_export_directory() -> PathBuf {
66    PathBuf::from(format!("./{APPLICATION_NAME}-exports"))
67}
68
69fn default_theme() -> String {
70    if cfg!(target_os = "windows") {
71        "dark".to_string()
72    } else {
73        "light".to_string()
74    }
75}
76
77fn default_show_shortcuts() -> bool {
78    true
79}
80
81impl GlobalConfig {
82    pub fn new(path: &Path) -> Self {
83        Self {
84            path: path.to_path_buf(),
85            default_url_template: default_url_template(),
86            history: EXAMPLE_PROMPTS
87                .iter()
88                .map(|e| (*e).to_string())
89                .collect_vec(),
90            initial_query: "from end - 10".to_string(),
91            clusters: IndexMap::default(),
92            default_kafka_config: IndexMap::default(),
93            theme: default_theme(),
94            highlighter_theme: None,
95            show_shortcuts: true,
96            export_directory: default_export_directory(),
97            consumer: ConsumerConfig::default(),
98            log_file: None,
99        }
100    }
101
102    /// Reads a configuration file.
103    pub fn read(file: &Path) -> Result<Self, Error> {
104        let content = fs::read_to_string(file);
105        if let Err(e) = &content {
106            return Err(Error::Error(format!(
107                "Failed to read the configuration file {:?}: {}",
108                file.display(),
109                e
110            )));
111        }
112
113        let content = content.unwrap();
114        let mut config: Self = serde_json::from_str(&content).map_err(|e| {
115            Error::Error(format!(
116                "Failed to parse the configuration file {:?}: {}",
117                file.display(),
118                e
119            ))
120        })?;
121        config.path = file.to_path_buf();
122        Ok(config)
123    }
124
125    /// Returns the name of the logs file
126    pub fn log_file(&self) -> Option<PathBuf> {
127        self.log_file.clone()
128    }
129
130    /// web URL template for a given cluster
131    pub fn url_template_of(&self, cluster: &str) -> String {
132        self.clusters
133            .get(cluster)
134            .and_then(|e| e.url_template.clone())
135            .unwrap_or(self.default_url_template.clone())
136    }
137
138    /// Consumer config of a given cluster
139    pub(crate) fn consumer_config_of(&self, cluster: &str) -> ConsumerConfig {
140        self.clusters
141            .get(cluster)
142            .and_then(|e| e.consumer.clone())
143            .unwrap_or(self.consumer.clone())
144    }
145
146    /// Returns the schema registry configuration for the given cluster.
147    pub fn schema_registry_config_of(&self, cluster: &str) -> Option<SchemaRegistryConfig> {
148        self.clusters
149            .get(cluster.trim())
150            .and_then(|config| config.schema_registry.clone())
151    }
152}
153
154#[test]
155fn generate_json_schema_for_global_config() {
156    use schemars::schema_for;
157    let mut schema = schema_for!(GlobalConfig);
158    schema.insert("$id".into(), "https://raw.githubusercontent.com/MAIF/yozefu/refs/heads/main/docs/json-schemas/global-config.json".into());
159    fs::write(
160        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
161            .parent()
162            .unwrap()
163            .parent()
164            .unwrap()
165            .join("docs")
166            .join("json-schemas")
167            .join("global-config.json"),
168        serde_json::to_string_pretty(&schema).unwrap(),
169    )
170    .unwrap();
171}