Skip to main content

rfham_config/
lib.rs

1//!
2//! One-line description.
3//!
4//! More detailed description.
5//!
6//! # Examples
7//!
8//! ```rust
9//! ```
10//!
11//! # Features
12//!
13//! - **feature-name**; Feature description
14//!
15
16use crate::error::{ConfigError, ConfigResult};
17use rfham_core::{CountryCode, Name, Power, callsign::CallSign, error::CoreError};
18use rfham_geo::grid::maidenhead::MaidenheadLocator;
19use rfham_itu::{bands::FrequencyBand, regions::Region};
20use serde::{Deserialize, Serialize};
21use serde_with::{DeserializeFromStr, SerializeDisplay};
22use std::{
23    fmt::Display,
24    fs::{self, File, create_dir_all},
25    io::Write,
26    path::{Path, PathBuf},
27    str::FromStr,
28};
29use tracing::trace;
30
31// ────────────────────────────────────────────────────────────────────────────────────────────────
32// Public Types
33// ────────────────────────────────────────────────────────────────────────────────────────────────
34
35#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
36#[serde(rename_all = "kebab-case")]
37pub struct Configuration {
38    #[serde(skip)]
39    path: Option<PathBuf>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    station: Option<Station>,
42    #[serde(skip_serializing_if = "Vec::is_empty")]
43    equipment: Vec<Equipment>,
44}
45
46#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
47#[serde(rename_all = "kebab-case")]
48pub struct Station {
49    callsign: CallSign,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    location: Option<Location>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    operator_name: Option<String>,
54}
55
56#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
57#[serde(rename_all = "kebab-case")]
58pub struct Location {
59    kind: LocationKind,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    label: Option<String>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    grid_locator: Option<MaidenheadLocator>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    itu_region: Option<Region>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    country: Option<CountryCode>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    mailing_address: Option<String>,
70}
71
72#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
73#[serde(rename_all = "kebab-case")]
74pub struct Equipment {
75    brand: Name,
76    model: Name,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    label: Option<String>,
79    #[serde(skip_serializing_if = "Vec::is_empty")]
80    usage: Vec<Usage>,
81    #[serde(skip_serializing_if = "Vec::is_empty")]
82    modes: Vec<Mode>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    mobility: Option<Mobility>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    max_power: Option<Power>,
87    #[serde(skip_serializing_if = "Vec::is_empty")]
88    bands: Vec<FrequencyBand>,
89    #[serde(skip_serializing_if = "Vec::is_empty")]
90    using: Vec<Equipment>,
91}
92
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
94pub enum LocationKind {
95    #[default]
96    Home,
97    Alternate,
98    Remote,
99    Club,
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
103pub enum Mobility {
104    StationFixed,
105    Portable,
106    Mobile,
107    Handheld,
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
111pub enum Usage {
112    Local,
113    Qrp,
114    Dx,
115    EmComm,
116    Activation,
117    Satellite,
118    Scanning,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
122pub enum Mode {
123    Am,
124    Fm,
125    Ssb,
126    Rtty,
127    Digital,
128    Image,
129}
130
131pub const CONFIG_DIR_NAME: &str = "rfham";
132pub const CONFIG_FILE_NAME: &str = "rfham-config.toml";
133
134pub trait Dump {
135    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()>;
136}
137
138// ────────────────────────────────────────────────────────────────────────────────────────────────
139// Implementations
140// ────────────────────────────────────────────────────────────────────────────────────────────────
141
142impl Dump for Configuration {
143    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
144        writeln!(writer, "Configuration:")?;
145        if let Some(path) = &self.path {
146            writeln!(writer, "{prefix}└── path to file: {path:?}")?;
147        }
148        if let Some(station) = &self.station {
149            station.dump(writer, "    ")?;
150        }
151        if !self.equipment.is_empty() {
152            writeln!(writer, "{prefix}└── Equipment:")?;
153            for equipment in &self.equipment {
154                equipment.dump(writer, "    ")?;
155            }
156        }
157        Ok(())
158    }
159}
160
161impl Configuration {
162    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
163    // Constructors
164    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
165    pub fn with_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
166        self.path = path.map(|p| p.into());
167        self
168    }
169
170    pub fn with_station(mut self, station: Option<Station>) -> Self {
171        self.station = station;
172        self
173    }
174
175    pub fn with_equipment(mut self, equipment: Vec<Equipment>) -> Self {
176        self.equipment = equipment;
177        self
178    }
179
180    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
181    // Field accessors
182    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
183
184    pub fn path(&self) -> Option<&PathBuf> {
185        self.path.as_ref()
186    }
187
188    pub fn set_path<P: Into<PathBuf>>(&mut self, path: P) {
189        self.path = Some(path.into())
190    }
191
192    pub fn unset_path(&mut self) {
193        self.path = None
194    }
195
196    pub fn station(&self) -> Option<&Station> {
197        self.station.as_ref()
198    }
199
200    pub fn set_station(&mut self, station: Station) {
201        self.station = Some(station)
202    }
203
204    pub fn unset_station(&mut self) {
205        self.station = None
206    }
207
208    pub fn exists(&self) -> bool {
209        Self::default_file_path()
210            .map(|path| path.exists())
211            .unwrap_or_default()
212    }
213
214    pub fn default_file_path() -> ConfigResult<PathBuf> {
215        Ok(xdirs::config_dir_for(CONFIG_DIR_NAME)
216            .ok_or(ConfigError::ConfigDir)?
217            .join(CONFIG_FILE_NAME))
218    }
219
220    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
221    // File I/O
222    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
223
224    pub fn load() -> ConfigResult<Self> {
225        Self::load_from(Self::default_file_path()?)
226    }
227
228    pub fn load_from<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
229        let content = fs::read_to_string(path.as_ref())?;
230        let mut config: Configuration = toml::from_str(&content)?;
231        config.set_path(PathBuf::from(path.as_ref()));
232        Ok(config)
233    }
234
235    pub fn save_to<P: AsRef<Path>>(&mut self, path: P, overwrite: bool) -> ConfigResult<()> {
236        self.path = Some(PathBuf::from(path.as_ref()));
237        let parent_dir = self.path.as_ref().map(|p| p.parent()).unwrap_or_default();
238        if let Some(actual_parent_dir) = parent_dir
239            && !actual_parent_dir.is_dir()
240        {
241            trace!("creating parent directory for config file");
242            create_dir_all(actual_parent_dir)?;
243        }
244        let mut file = if overwrite {
245            File::create(path)
246        } else {
247            File::create_new(path)
248        }?;
249        let content = toml::to_string_pretty(self)?;
250        file.write_all(content.as_bytes())?;
251
252        Ok(())
253    }
254
255    pub fn save(&mut self, overwrite: bool) -> ConfigResult<()> {
256        let path = if let Some(path) = &self.path {
257            path.clone()
258        } else {
259            Self::default_file_path()?
260        };
261        self.save_to(path, overwrite)
262    }
263}
264
265// ════════════════════════════════════════════════════════════════════════════════════════════════
266
267impl Dump for Station {
268    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
269        writeln!(writer, "{prefix}Station:")?;
270        writeln!(writer, "{prefix}└── call sign: {}", self.callsign)?;
271        if let Some(operator_name) = &self.operator_name {
272            writeln!(writer, "{prefix}└── operator name: {operator_name}")?;
273        }
274        if let Some(location) = &self.location {
275            location.dump(writer, &format!("{prefix}    "))?;
276        }
277        Ok(())
278    }
279}
280
281impl Station {
282    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
283    // Constructors
284    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
285
286    pub const fn new(callsign: CallSign) -> Self {
287        Self {
288            callsign,
289            location: None,
290            operator_name: None,
291        }
292    }
293
294    pub fn with_location(mut self, location: Option<Location>) -> Self {
295        self.location = location;
296        self
297    }
298
299    pub fn with_operator_name<S: Into<String>>(mut self, operator_name: Option<S>) -> Self {
300        self.operator_name = operator_name.map(|s| s.into());
301        self
302    }
303
304    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
305    // Field Accessors
306    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
307
308    pub const fn callsign(&self) -> &CallSign {
309        &self.callsign
310    }
311
312    pub fn set_callsign(&mut self, callsign: CallSign) {
313        self.callsign = callsign
314    }
315
316    pub const fn location(&self) -> Option<&Location> {
317        self.location.as_ref()
318    }
319
320    pub fn set_location(&mut self, location: Location) {
321        self.location = Some(location);
322    }
323
324    pub fn unset_location(&mut self) {
325        self.location = None
326    }
327
328    pub const fn operator_name(&self) -> Option<&String> {
329        self.operator_name.as_ref()
330    }
331
332    pub fn set_operator_name<S: Into<String>>(&mut self, operator_name: S) {
333        self.operator_name = Some(operator_name.into())
334    }
335
336    pub fn unset_operator_name(&mut self) {
337        self.operator_name = None
338    }
339}
340
341// ════════════════════════════════════════════════════════════════════════════════════════════════
342
343impl Dump for Location {
344    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
345        writeln!(writer, "{prefix}Location:")?;
346        if let Some(grid_locator) = &self.grid_locator {
347            writeln!(writer, "{prefix}└── grid locator: {grid_locator}")?;
348        }
349        if let Some(region) = &self.itu_region {
350            writeln!(writer, "{prefix}└── ITU region: {region}")?;
351        }
352        if let Some(country) = &self.country {
353            writeln!(writer, "{prefix}└── country: {country}")?;
354        }
355        if let Some(mailing_address) = &self.mailing_address {
356            writeln!(writer, "{prefix}└── mailing address: {mailing_address}")?;
357        }
358        Ok(())
359    }
360}
361
362impl Location {
363    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
364    // Constructors
365    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
366
367    pub fn new(kind: LocationKind) -> Self {
368        Self {
369            kind,
370            label: None,
371            grid_locator: None,
372            itu_region: None,
373            country: None,
374            mailing_address: None,
375        }
376    }
377
378    pub fn with_label<S: Into<String>>(mut self, label: Option<S>) -> Self {
379        self.label = label.map(|s| s.into());
380        self
381    }
382
383    pub fn with_grid_locator(mut self, grid_locator: Option<MaidenheadLocator>) -> Self {
384        self.grid_locator = grid_locator;
385        self
386    }
387
388    pub fn with_itu_region(mut self, itu_region: Option<Region>) -> Self {
389        self.itu_region = itu_region;
390        self
391    }
392
393    pub fn with_country(mut self, country: Option<CountryCode>) -> Self {
394        self.country = country;
395        self
396    }
397
398    pub fn with_mailing_address<S: Into<String>>(mut self, mailing_address: Option<S>) -> Self {
399        self.mailing_address = mailing_address.map(|s| s.into());
400        self
401    }
402
403    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
404    // Field Accessors
405    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
406
407    pub const fn kind(&self) -> LocationKind {
408        self.kind
409    }
410
411    pub fn set_kind(&mut self, kind: LocationKind) {
412        self.kind = kind;
413    }
414
415    pub const fn grid_locator(&self) -> Option<&MaidenheadLocator> {
416        self.grid_locator.as_ref()
417    }
418
419    pub fn set_grid_locator(&mut self, grid_locator: MaidenheadLocator) {
420        self.grid_locator = Some(grid_locator)
421    }
422
423    pub fn unset_grid_locator(&mut self) {
424        self.grid_locator = None;
425    }
426
427    pub const fn itu_region(&self) -> Option<&Region> {
428        self.itu_region.as_ref()
429    }
430
431    pub fn set_itu_region(&mut self, itu_region: Region) {
432        self.itu_region = Some(itu_region)
433    }
434
435    pub fn unset_itu_region(&mut self) {
436        self.itu_region = None;
437    }
438
439    pub const fn country(&self) -> Option<&CountryCode> {
440        self.country.as_ref()
441    }
442
443    pub fn set_country(&mut self, country: CountryCode) {
444        self.country = Some(country)
445    }
446
447    pub fn unset_country(&mut self) {
448        self.country = None
449    }
450
451    pub const fn mailing_address(&self) -> Option<&String> {
452        self.mailing_address.as_ref()
453    }
454
455    pub fn set_mailing_address<S: Into<String>>(&mut self, mailing_address: S) {
456        self.mailing_address = Some(mailing_address.into())
457    }
458
459    pub fn unset_mailing_address(&mut self) {
460        self.mailing_address = None
461    }
462}
463
464// ════════════════════════════════════════════════════════════════════════════════════════════════
465
466impl Dump for Equipment {
467    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
468        if let Some(label) = &self.label {
469            writeln!(writer, "{prefix}{label}:")?;
470            writeln!(writer, "{prefix}└── brand: {}", self.brand)?;
471            writeln!(writer, "{prefix}└── model: {}", self.model)?;
472        } else {
473            writeln!(writer, "{prefix}{} {}:", self.brand, self.model)?;
474        }
475        if let Some(max_power) = &self.max_power {
476            writeln!(writer, "{prefix}└── max power: {}", max_power)?;
477        }
478        if !self.bands.is_empty() {
479            writeln!(
480                writer,
481                "{prefix}└── operating bands: {}",
482                self.bands
483                    .iter()
484                    .map(|b| b.to_string())
485                    .collect::<Vec<_>>()
486                    .join(", ")
487            )?;
488        }
489        if !self.using.is_empty() {
490            writeln!(writer, "{prefix}└── Using:")?;
491            for equipment in &self.using {
492                equipment.dump(writer, &format!("{prefix}    "))?;
493            }
494        }
495        Ok(())
496    }
497}
498
499impl Equipment {
500    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
501    // Constructors
502    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
503
504    pub fn new<S: Into<String>>(brand: Name, model: Name) -> Self {
505        Self {
506            brand,
507            model,
508            label: None,
509            usage: Vec::default(),
510            modes: Vec::default(),
511            mobility: None,
512            max_power: None,
513            bands: Vec::default(),
514            using: Vec::default(),
515        }
516    }
517
518    pub fn with_label<S: Into<String>>(mut self, label: Option<S>) -> Self {
519        self.label = label.map(|s| s.into());
520        self
521    }
522
523    pub fn with_mobility(mut self, mobility: Option<Mobility>) -> Self {
524        self.mobility = mobility;
525        self
526    }
527
528    pub fn with_max_power(mut self, max_power: Option<Power>) -> Self {
529        self.max_power = max_power;
530        self
531    }
532
533    pub fn with_usage(mut self, usage: Vec<Usage>) -> Self {
534        self.usage = usage;
535        self
536    }
537
538    pub fn with_modes(mut self, modes: Vec<Mode>) -> Self {
539        self.modes = modes;
540        self
541    }
542
543    pub fn with_bands(mut self, bands: Vec<FrequencyBand>) -> Self {
544        self.bands = bands;
545        self
546    }
547
548    pub fn with_using(mut self, using: Vec<Equipment>) -> Self {
549        self.using = using;
550        self
551    }
552    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
553    // Field Accessors
554    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
555}
556
557// ════════════════════════════════════════════════════════════════════════════════════════════════
558
559impl Display for LocationKind {
560    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561        write!(
562            f,
563            "{}",
564            if f.alternate() {
565                match self {
566                    Self::Home => "Home or Primary licensed station",
567                    Self::Alternate => "Alternate licensed station",
568                    Self::Remote => "Remote operating location",
569                    Self::Club => "Club location",
570                }
571            } else {
572                match self {
573                    Self::Home => "home",
574                    Self::Alternate => "alternate",
575                    Self::Remote => "remote",
576                    Self::Club => "club",
577                }
578            }
579        )
580    }
581}
582
583impl FromStr for LocationKind {
584    type Err = CoreError;
585
586    fn from_str(s: &str) -> Result<Self, Self::Err> {
587        match s {
588            "home" => Ok(Self::Home),
589            "alternate" => Ok(Self::Alternate),
590            "remote" => Ok(Self::Alternate),
591            "club" => Ok(Self::Club),
592            _ => Err(CoreError::InvalidValueFromStr(
593                s.to_string(),
594                "LocationKind",
595            )),
596        }
597    }
598}
599
600// ════════════════════════════════════════════════════════════════════════════════════════════════
601
602impl Display for Usage {
603    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604        write!(
605            f,
606            "{}",
607            if f.alternate() {
608                match self {
609                    Self::Local => "Local area, including repeaters",
610                    Self::Qrp => "QRP, low power, operation",
611                    Self::Dx => "DX, distance, operation",
612                    Self::EmComm => "Emergency Communications, including ARES/RACES",
613                    Self::Activation => "POTA, SOTA, IOTA, etc. activation",
614                    Self::Satellite => "Satellite opertions",
615                    Self::Scanning => "Scanning",
616                }
617            } else {
618                match self {
619                    Self::Local => "local",
620                    Self::Qrp => "qrp",
621                    Self::Dx => "dx",
622                    Self::EmComm => "emcomm",
623                    Self::Activation => "activation",
624                    Self::Satellite => "satellite",
625                    Self::Scanning => "scanning",
626                }
627            }
628        )
629    }
630}
631
632impl FromStr for Usage {
633    type Err = CoreError;
634
635    fn from_str(s: &str) -> Result<Self, Self::Err> {
636        match s {
637            "local" => Ok(Self::Local),
638            "qrp" => Ok(Self::Qrp),
639            "dx" => Ok(Self::Dx),
640            "emcomm" => Ok(Self::EmComm),
641            "activation" => Ok(Self::Activation),
642            "satellite" => Ok(Self::Satellite),
643            "scanning" => Ok(Self::Scanning),
644            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "Usage")),
645        }
646    }
647}
648
649// ════════════════════════════════════════════════════════════════════════════════════════════════
650
651impl Display for Mode {
652    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653        write!(
654            f,
655            "{}",
656            if f.alternate() {
657                match self {
658                    Self::Am => "AM",
659                    Self::Fm => "FM",
660                    Self::Ssb => "SSB",
661                    Self::Rtty => "RTTY",
662                    Self::Digital => "Digital modes",
663                    Self::Image => "Images and SSTV",
664                }
665            } else {
666                match self {
667                    Self::Am => "am",
668                    Self::Fm => "fm",
669                    Self::Ssb => "ssb",
670                    Self::Rtty => "rtty",
671                    Self::Digital => "digital",
672                    Self::Image => "image",
673                }
674            }
675        )
676    }
677}
678
679impl FromStr for Mode {
680    type Err = CoreError;
681
682    fn from_str(s: &str) -> Result<Self, Self::Err> {
683        match s {
684            "am" => Ok(Self::Am),
685            "fm" => Ok(Self::Fm),
686            "ssb" => Ok(Self::Ssb),
687            "rtty" => Ok(Self::Rtty),
688            "digital" => Ok(Self::Digital),
689            "image" => Ok(Self::Image),
690            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "Mode")),
691        }
692    }
693}
694
695// ════════════════════════════════════════════════════════════════════════════════════════════════
696
697impl Display for Mobility {
698    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699        write!(
700            f,
701            "{}",
702            if f.alternate() {
703                match self {
704                    Self::StationFixed => "Station only",
705                    Self::Portable => "Portable from station",
706                    Self::Mobile => "Mobile, vehicle/vessel mounted",
707                    Self::Handheld => "Handheld",
708                }
709            } else {
710                match self {
711                    Self::StationFixed => "station-fixed",
712                    Self::Portable => "portable",
713                    Self::Mobile => "mobile",
714                    Self::Handheld => "handheld",
715                }
716            }
717        )
718    }
719}
720
721impl FromStr for Mobility {
722    type Err = CoreError;
723
724    fn from_str(s: &str) -> Result<Self, Self::Err> {
725        match s {
726            "station-fixed" => Ok(Self::StationFixed),
727            "portable" => Ok(Self::Portable),
728            "mobile" => Ok(Self::Mobile),
729            "handheld" => Ok(Self::Handheld),
730            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "Mobility")),
731        }
732    }
733}
734
735// ────────────────────────────────────────────────────────────────────────────────────────────────
736// Sub-modules
737// ────────────────────────────────────────────────────────────────────────────────────────────────
738
739pub mod error;
740pub mod paths;