ddns_a/config/
cli.rs

1//! CLI argument parsing using clap.
2//!
3//! Defines the command-line interface with all options and subcommands.
4
5use std::path::PathBuf;
6
7use clap::{Parser, Subcommand, ValueEnum};
8
9use crate::network::AdapterKind;
10
11/// DDNS-A: Dynamic DNS Address Monitor
12///
13/// Monitors IP address changes on network adapters and notifies
14/// external services via webhooks.
15#[derive(Debug, Parser)]
16#[command(name = "ddns-a")]
17#[command(version, about, long_about = None)]
18#[allow(clippy::struct_excessive_bools)] // CLI flags are naturally boolean
19pub struct Cli {
20    /// Subcommand to run
21    #[command(subcommand)]
22    pub command: Option<Command>,
23
24    /// Webhook URL (required for run mode)
25    #[arg(long, global = true)]
26    pub url: Option<String>,
27
28    /// IP version to monitor (required for run mode)
29    #[arg(long = "ip-version", value_enum, global = true)]
30    pub ip_version: Option<IpVersionArg>,
31
32    /// Filter changes by type: added, removed, or both (default: both)
33    #[arg(long = "change-kind", value_enum, global = true)]
34    pub change_kind: Option<ChangeKindArg>,
35
36    /// HTTP method for webhook requests
37    #[arg(long)]
38    pub method: Option<String>,
39
40    /// HTTP headers in 'Key=Value' or 'Key: Value' format (can be specified multiple times)
41    #[arg(long = "header", value_name = "K=V")]
42    pub headers: Vec<String>,
43
44    /// Bearer token for Authorization header
45    #[arg(long)]
46    pub bearer: Option<String>,
47
48    /// Handlebars body template for webhook requests
49    #[arg(long = "body-template")]
50    pub body_template: Option<String>,
51
52    /// Regex pattern for adapters to include (can be specified multiple times)
53    #[arg(long = "include-adapter", value_name = "PATTERN")]
54    pub include_adapters: Vec<String>,
55
56    /// Regex pattern for adapters to exclude (can be specified multiple times)
57    #[arg(long = "exclude-adapter", value_name = "PATTERN")]
58    pub exclude_adapters: Vec<String>,
59
60    /// Adapter kinds to include (can be specified multiple times or comma-separated)
61    #[arg(long = "include-kind", value_name = "KIND", value_delimiter = ',')]
62    pub include_kinds: Vec<AdapterKindArg>,
63
64    /// Adapter kinds to exclude (can be specified multiple times or comma-separated)
65    #[arg(long = "exclude-kind", value_name = "KIND", value_delimiter = ',')]
66    pub exclude_kinds: Vec<AdapterKindArg>,
67
68    /// Polling interval in seconds
69    #[arg(long = "poll-interval")]
70    pub poll_interval: Option<u64>,
71
72    /// Disable API event listening, use polling only
73    #[arg(long = "poll-only")]
74    pub poll_only: bool,
75
76    /// Maximum number of retry attempts
77    #[arg(long = "retry-max")]
78    pub retry_max: Option<u32>,
79
80    /// Initial retry delay in seconds
81    #[arg(long = "retry-delay")]
82    pub retry_delay: Option<u64>,
83
84    /// Path to configuration file
85    #[arg(long, short)]
86    pub config: Option<PathBuf>,
87
88    /// Path to state file for detecting changes across restarts
89    #[arg(long = "state-file")]
90    pub state_file: Option<PathBuf>,
91
92    /// Test mode - log changes without sending webhooks
93    #[arg(long)]
94    pub dry_run: bool,
95
96    /// Enable verbose logging
97    #[arg(long, short)]
98    pub verbose: bool,
99}
100
101/// Subcommands for ddns-a
102#[derive(Debug, Subcommand)]
103pub enum Command {
104    /// Generate a default configuration file
105    Init {
106        /// Output path for the configuration file
107        #[arg(long, short, default_value = "ddns-a.toml")]
108        output: PathBuf,
109    },
110}
111
112/// IP version argument for CLI parsing
113#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
114pub enum IpVersionArg {
115    /// Monitor IPv4 addresses only
116    #[value(name = "ipv4")]
117    V4,
118    /// Monitor IPv6 addresses only
119    #[value(name = "ipv6")]
120    V6,
121    /// Monitor both IPv4 and IPv6 addresses
122    #[value(name = "both")]
123    Both,
124}
125
126/// Change kind argument for CLI parsing.
127///
128/// Filters which IP changes to report based on the change type.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
130pub enum ChangeKindArg {
131    /// Report only IP addresses that were added
132    #[value(name = "added")]
133    Added,
134    /// Report only IP addresses that were removed
135    #[value(name = "removed")]
136    Removed,
137    /// Report both added and removed IP addresses (default)
138    #[value(name = "both")]
139    Both,
140}
141
142impl From<IpVersionArg> for crate::network::IpVersion {
143    fn from(arg: IpVersionArg) -> Self {
144        match arg {
145            IpVersionArg::V4 => Self::V4,
146            IpVersionArg::V6 => Self::V6,
147            IpVersionArg::Both => Self::Both,
148        }
149    }
150}
151
152impl From<ChangeKindArg> for crate::monitor::ChangeKind {
153    fn from(arg: ChangeKindArg) -> Self {
154        match arg {
155            ChangeKindArg::Added => Self::Added,
156            ChangeKindArg::Removed => Self::Removed,
157            ChangeKindArg::Both => Self::Both,
158        }
159    }
160}
161
162/// Adapter kind argument for CLI parsing.
163///
164/// Maps to [`AdapterKind`] for filtering adapters by type.
165/// Only the four known adapter kinds are exposed; use name regex filters
166/// for `Other(u32)` variants.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
168#[value(rename_all = "lowercase")]
169pub enum AdapterKindArg {
170    /// Physical Ethernet adapter
171    Ethernet,
172    /// Wireless (Wi-Fi) adapter
173    Wireless,
174    /// Virtual adapter (`VMware`, `VirtualBox`, `Hyper-V`, WSL, etc.)
175    Virtual,
176    /// Loopback adapter (localhost)
177    Loopback,
178}
179
180impl From<AdapterKindArg> for AdapterKind {
181    fn from(arg: AdapterKindArg) -> Self {
182        match arg {
183            AdapterKindArg::Ethernet => Self::Ethernet,
184            AdapterKindArg::Wireless => Self::Wireless,
185            AdapterKindArg::Virtual => Self::Virtual,
186            AdapterKindArg::Loopback => Self::Loopback,
187        }
188    }
189}
190
191impl Cli {
192    /// Parses CLI arguments from the command line.
193    #[must_use]
194    pub fn parse_args() -> Self {
195        Self::parse()
196    }
197
198    /// Parses CLI arguments from an iterator (useful for testing).
199    pub fn parse_from_iter<I, T>(iter: I) -> Self
200    where
201        I: IntoIterator<Item = T>,
202        T: Into<std::ffi::OsString> + Clone,
203    {
204        Self::parse_from(iter)
205    }
206
207    /// Returns true if this is the init command.
208    #[must_use]
209    pub const fn is_init(&self) -> bool {
210        matches!(self.command, Some(Command::Init { .. }))
211    }
212}