ddns_a/config/
toml.rs

1//! TOML configuration file parsing.
2//!
3//! Defines the structure of the configuration file with serde.
4
5use std::collections::HashMap;
6use std::path::Path;
7
8use serde::Deserialize;
9
10use super::ConfigError;
11
12/// Root configuration structure from TOML file.
13///
14/// All fields are optional to allow partial configuration
15/// that can be merged with CLI arguments.
16#[derive(Debug, Default, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct TomlConfig {
19    /// Webhook configuration section
20    #[serde(default)]
21    pub webhook: WebhookSection,
22
23    /// Network adapter filter configuration
24    #[serde(default)]
25    pub filter: FilterSection,
26
27    /// Monitoring configuration
28    #[serde(default)]
29    pub monitor: MonitorSection,
30
31    /// Retry policy configuration
32    #[serde(default)]
33    pub retry: RetrySection,
34}
35
36/// Webhook configuration section.
37#[derive(Debug, Default, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct WebhookSection {
40    /// Webhook URL
41    pub url: Option<String>,
42
43    /// IP version to monitor: "ipv4", "ipv6", or "both"
44    pub ip_version: Option<String>,
45
46    /// HTTP method (default: POST)
47    pub method: Option<String>,
48
49    /// HTTP headers as key-value pairs
50    #[serde(default)]
51    pub headers: HashMap<String, String>,
52
53    /// Bearer token for Authorization header
54    pub bearer: Option<String>,
55
56    /// Handlebars body template
57    pub body_template: Option<String>,
58}
59
60/// Adapter filter configuration section.
61#[derive(Debug, Default, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct FilterSection {
64    /// Regex patterns for adapters to include (by name)
65    #[serde(default)]
66    pub include: Vec<String>,
67
68    /// Regex patterns for adapters to exclude (by name)
69    #[serde(default)]
70    pub exclude: Vec<String>,
71
72    /// Adapter kinds to include (e.g., "ethernet", "wireless")
73    #[serde(default)]
74    pub include_kinds: Vec<String>,
75
76    /// Adapter kinds to exclude (e.g., "virtual", "loopback")
77    #[serde(default)]
78    pub exclude_kinds: Vec<String>,
79}
80
81/// Monitoring configuration section.
82#[derive(Debug, Default, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct MonitorSection {
85    /// Polling interval in seconds
86    pub poll_interval: Option<u64>,
87
88    /// Disable API event listening, use polling only
89    #[serde(default)]
90    pub poll_only: bool,
91
92    /// Path to state file for detecting changes across restarts
93    pub state_file: Option<String>,
94
95    /// Filter changes by type: "added", "removed", or "both" (default: "both")
96    pub change_kind: Option<String>,
97}
98
99/// Retry policy configuration section.
100#[derive(Debug, Default, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct RetrySection {
103    /// Maximum number of retry attempts
104    pub max_attempts: Option<u32>,
105
106    /// Initial retry delay in seconds
107    pub initial_delay: Option<u64>,
108
109    /// Maximum retry delay in seconds
110    pub max_delay: Option<u64>,
111
112    /// Backoff multiplier
113    pub multiplier: Option<f64>,
114}
115
116impl TomlConfig {
117    /// Loads configuration from a TOML file.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the file cannot be read or parsed.
122    pub fn load(path: &Path) -> Result<Self, ConfigError> {
123        let content = std::fs::read_to_string(path).map_err(|e| ConfigError::FileRead {
124            path: path.to_path_buf(),
125            source: e,
126        })?;
127
128        Self::parse(&content)
129    }
130
131    /// Parses configuration from a TOML string.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if the TOML is invalid.
136    pub fn parse(content: &str) -> Result<Self, ConfigError> {
137        toml::from_str(content).map_err(ConfigError::from)
138    }
139}
140
141/// Generates a default configuration file with comments.
142#[must_use]
143pub fn default_config_template() -> String {
144    r#"# DDNS-A Configuration File
145# Documentation: https://github.com/doraemonkeys/ddns-a
146
147[webhook]
148# Webhook URL (required)
149# url = "https://api.example.com/ddns"
150
151# IP version to monitor (required)
152# Accepted values: "ipv4"/"v4"/"4", "ipv6"/"v6"/"6", or "both"/"all"/"dual"
153# ip_version = "both"
154
155# HTTP method (default: POST, can be overridden by --method CLI flag)
156# method = "POST"
157
158# HTTP headers
159# [webhook.headers]
160# X-Custom-Header = "value"
161
162# Bearer token for Authorization header
163# bearer = "your-token-here"
164
165# Handlebars body template
166# Available variables: {{adapter}}, {{address}}, {{timestamp}}, {{kind}}
167# body_template = '{"ip": "{{address}}", "adapter": "{{adapter}}"}'
168
169[filter]
170# Adapter kinds to include (empty = all kinds)
171# Valid values: ethernet, wireless, virtual, loopback
172# Note: CLI --include-kind REPLACES these entirely (not merged)
173# include_kinds = ["ethernet", "wireless"]
174
175# Adapter kinds to exclude
176# Note: Loopback is excluded by default unless explicitly included
177# Note: CLI --exclude-kind REPLACES these entirely (not merged)
178# exclude_kinds = ["virtual"]
179
180# Regex patterns for adapters to include by name (empty = all names)
181# Note: CLI --include-adapter REPLACES these entirely (not merged)
182# include = ["^eth", "^Ethernet"]
183
184# Regex patterns for adapters to exclude by name
185# Note: CLI --exclude-adapter REPLACES these entirely (not merged)
186# exclude = ["^Docker", "^vEthernet"]
187
188[monitor]
189# Polling interval in seconds (default: 60)
190poll_interval = 60
191
192# Disable API event listening, use polling only
193# poll_only = false
194
195# Path to state file for detecting changes across restarts
196# If set, the program will compare current IP addresses with the saved state
197# and trigger webhooks for any changes detected during the program restart
198# state_file = "ddns-a-state.json"
199
200# Filter changes by type (default: "both")
201# Accepted values: "added", "removed", "both"
202# change_kind = "both"
203
204[retry]
205# Maximum number of retry attempts (default: 3)
206# max_attempts = 3
207
208# Initial retry delay in seconds (default: 5)
209# initial_delay = 5
210
211# Maximum retry delay in seconds (default: 60)
212# max_delay = 60
213
214# Backoff multiplier (default: 2.0)
215# multiplier = 2.0
216"#
217    .to_string()
218}