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