Skip to content

Commit 27c076d

Browse files
committed
feat: add TOML configuration file support with JSON backward compatibility
- Add toml dependency for dual format support - Implement automatic format detection based on file extension - Add smart config file discovery (TOML preferred, JSON fallback) - Maintain full backward compatibility with existing JSON configs - Default new installations to TOML format for better readability - Update config path handling to support both extensions - Add comprehensive tests for both TOML and JSON formats - Update documentation with dual format examples and migration info - Mark issue #17 as complete in ROADMAP All acceptance criteria fulfilled: ✅ Creates default config file if not exists (TOML format) ✅ Loads printer settings from config (both formats) ✅ CLI arguments override config values (preserved functionality) ✅ Validates configuration format (format-specific error messages) ✅ Clear error messages for invalid config (JSON/TOML specific) Resolves #17
1 parent afcdfce commit 27c076d

6 files changed

Lines changed: 329 additions & 16 deletions

File tree

Cargo.lock

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rumqttc = "0.24"
99
tokio = { version = "1.0", features = ["full"] }
1010
serde = { version = "1.0", features = ["derive"] }
1111
serde_json = "1.0"
12+
toml = "0.8"
1213
tokio-native-tls = "0.3"
1314
rustls = "0.22"
1415
thiserror = "1.0"

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,18 +281,50 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
281281

282282
## Configuration System
283283

284-
PulsePrint-CLI now includes a comprehensive configuration system for managing multiple printers.
284+
PulsePrint-CLI includes a comprehensive configuration system for managing multiple printers with support for both TOML and JSON formats.
285+
286+
### Configuration File Formats
287+
288+
PulsePrint-CLI supports both **TOML** (preferred) and **JSON** configuration formats:
289+
290+
- **New installations**: Default to TOML format (`config.toml`)
291+
- **Existing installations**: Continue to support JSON format (`config.json`)
292+
- **Format detection**: Automatic based on file extension
293+
- **Backward compatibility**: Existing JSON configs work seamlessly
285294

286295
### Configuration File Location
287296

288-
The configuration file is automatically stored in the appropriate location for your operating system:
297+
Configuration files are automatically stored in the appropriate location for your operating system:
289298

290-
- **Linux**: `~/.config/pulseprint-cli/config.json`
291-
- **macOS**: `~/Library/Application Support/pulseprint-cli/config.json`
292-
- **Windows**: `%APPDATA%\pulseprint-cli\config.json`
299+
- **Linux**: `~/.config/pulseprint-cli/config.toml` (or `config.json`)
300+
- **macOS**: `~/Library/Application Support/pulseprint-cli/config.toml` (or `config.json`)
301+
- **Windows**: `%APPDATA%\pulseprint-cli\config.toml` (or `config.json`)
293302

294303
### Configuration Structure
295304

305+
#### TOML Format (Preferred)
306+
307+
```toml
308+
default_printer = "my_printer"
309+
310+
[printers.my_printer]
311+
name = "my_printer"
312+
ip = "192.168.1.100"
313+
device_id = "01S00A000000000"
314+
access_code = "12345678"
315+
port = 8883
316+
use_tls = true
317+
318+
[mqtt_settings]
319+
keep_alive_secs = 30
320+
connection_timeout_secs = 10
321+
retry_attempts = 5
322+
retry_delay_secs = 5
323+
queue_size = 10
324+
```
325+
326+
#### JSON Format (Legacy Support)
327+
296328
```json
297329
{
298330
"printers": {
@@ -326,7 +358,7 @@ The configuration file is automatically stored in the appropriate location for y
326358
- ✅ Basic printer monitoring
327359
- ✅ CLI interface with help system
328360
- ✅ Error handling and retry logic
329-
- ✅ Configuration management system
361+
- ✅ Configuration management system with TOML and JSON support (issue #17)
330362
- ✅ Multiple printer support with named configurations
331363
- ✅ Printer management CLI commands (add, remove, list, set-default)
332364
- ✅ Message parsing for MQTT JSON messages (issue #15)

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ PulsePrint-CLI aims to be the premier command-line tool for monitoring and manag
1313
- [x] MQTT client implementation (issue #14)
1414
- [x] Basic printer connection via local network
1515
- [x] Simple status polling (`monitor` command)
16-
- [x] Configuration file support (JSON)
16+
- [x] Configuration file support with TOML and JSON formats (issue #17)
1717
- [x] Single printer monitoring
1818

1919
#### v0.2.0 - Core Monitoring (Target: September 2025)

src/config/mod.rs

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
use serde::{Deserialize, Serialize};
22
use std::collections::HashMap;
33
use std::fs;
4-
use std::path::PathBuf;
4+
use std::path::{Path, PathBuf};
55

66
#[cfg(test)]
77
mod tests;
88

9+
#[derive(Debug, Clone)]
10+
pub enum ConfigFormat {
11+
Json,
12+
Toml,
13+
}
14+
915
#[derive(Debug, Clone, Serialize, Deserialize)]
1016
pub struct PrinterConfig {
1117
pub name: String,
@@ -77,16 +83,57 @@ impl Default for MqttSettings {
7783
}
7884

7985
impl AppConfig {
86+
pub fn detect_format(path: &Path) -> ConfigFormat {
87+
match path.extension().and_then(|ext| ext.to_str()) {
88+
Some("toml") => ConfigFormat::Toml,
89+
Some("json") => ConfigFormat::Json,
90+
_ => ConfigFormat::Toml, // Default to TOML for new configs
91+
}
92+
}
93+
8094
pub fn load_from_file(path: &PathBuf) -> Result<Self, ConfigError> {
95+
// Try both formats if the specified file doesn't exist
8196
if !path.exists() {
97+
// Try to find existing config in either format
98+
if let Some(existing_path) = Self::find_existing_config_file(path) {
99+
return Self::load_from_existing_file(&existing_path);
100+
}
82101
return Ok(Self::default());
83102
}
84103

104+
Self::load_from_existing_file(path)
105+
}
106+
107+
fn find_existing_config_file(preferred_path: &Path) -> Option<PathBuf> {
108+
let base_dir = preferred_path.parent()?;
109+
110+
// Try TOML first, then JSON
111+
let toml_path = base_dir.join("config.toml");
112+
let json_path = base_dir.join("config.json");
113+
114+
if toml_path.exists() {
115+
Some(toml_path)
116+
} else if json_path.exists() {
117+
Some(json_path)
118+
} else {
119+
None
120+
}
121+
}
122+
123+
fn load_from_existing_file(path: &PathBuf) -> Result<Self, ConfigError> {
85124
let contents = fs::read_to_string(path)
86125
.map_err(|e| ConfigError::IoError(format!("Failed to read config file: {e}")))?;
87126

88-
let config: AppConfig = serde_json::from_str(&contents)
89-
.map_err(|e| ConfigError::ParseError(format!("Failed to parse config: {e}")))?;
127+
let format = Self::detect_format(path);
128+
129+
let config: AppConfig = match format {
130+
ConfigFormat::Json => serde_json::from_str(&contents).map_err(|e| {
131+
ConfigError::ParseError(format!("Failed to parse JSON config: {e}"))
132+
})?,
133+
ConfigFormat::Toml => toml::from_str(&contents).map_err(|e| {
134+
ConfigError::ParseError(format!("Failed to parse TOML config: {e}"))
135+
})?,
136+
};
90137

91138
Ok(config)
92139
}
@@ -98,8 +145,16 @@ impl AppConfig {
98145
})?;
99146
}
100147

101-
let contents = serde_json::to_string_pretty(self)
102-
.map_err(|e| ConfigError::SerializeError(format!("Failed to serialize config: {e}")))?;
148+
let format = Self::detect_format(path);
149+
150+
let contents = match format {
151+
ConfigFormat::Json => serde_json::to_string_pretty(self).map_err(|e| {
152+
ConfigError::SerializeError(format!("Failed to serialize JSON config: {e}"))
153+
})?,
154+
ConfigFormat::Toml => toml::to_string_pretty(self).map_err(|e| {
155+
ConfigError::SerializeError(format!("Failed to serialize TOML config: {e}"))
156+
})?,
157+
};
103158

104159
fs::write(path, contents)
105160
.map_err(|e| ConfigError::IoError(format!("Failed to write config file: {e}")))?;
@@ -166,15 +221,26 @@ impl AppConfig {
166221
}
167222

168223
pub fn get_config_path() -> PathBuf {
224+
Self::get_config_path_with_format(ConfigFormat::Toml)
225+
}
226+
227+
pub fn get_config_path_with_format(format: ConfigFormat) -> PathBuf {
228+
let extension = match format {
229+
ConfigFormat::Json => "json",
230+
ConfigFormat::Toml => "toml",
231+
};
232+
169233
// Check for test environment override
170234
if let Ok(test_config_dir) = std::env::var("PULSEPRINT_TEST_CONFIG_DIR") {
171-
return PathBuf::from(test_config_dir).join("config.json");
235+
return PathBuf::from(test_config_dir).join(format!("config.{extension}"));
172236
}
173237

174238
if let Some(config_dir) = dirs::config_dir() {
175-
config_dir.join("pulseprint-cli").join("config.json")
239+
config_dir
240+
.join("pulseprint-cli")
241+
.join(format!("config.{extension}"))
176242
} else {
177-
PathBuf::from(".pulseprint-config.json")
243+
PathBuf::from(format!(".pulseprint-config.{extension}"))
178244
}
179245
}
180246
}

0 commit comments

Comments
 (0)