Skip to main content

rfham_core/
agencies.rs

1//! Regulatory, standards-setting, and maintaining agencies.
2//!
3//! [`Agency`] describes an organisation with a name, optional abbreviation,
4//! an [`AgencyKind`] classification, and an optional [`Jurisdiction`].
5//!
6//! Several well-known agencies are available as convenience constructors:
7//! [`agency_itu`], [`agency_iaru`], [`agency_arrl`], [`agency_fcc`],
8//! [`agency_ofcom`], [`agency_rsgb`].
9//!
10//! # Examples
11//!
12//! ```rust
13//! use rfham_core::agency::{agency_fcc, agency_itu, AgencyKind};
14//! use rfham_core::country::CountryCode;
15//! use std::str::FromStr;
16//!
17//! let itu = agency_itu();
18//! assert_eq!(itu.abbreviation().map(|s| s.as_str()), Some("ITU"));
19//! assert!(itu.kind().is_standards_setting());
20//!
21//! let fcc = agency_fcc();
22//! assert!(fcc.kind().is_regulatory());
23//! let us = CountryCode::from_str("US").unwrap();
24//! assert_eq!(fcc.within_jurisdiction(&us), Some(true));
25//! let gb = CountryCode::from_str("GB").unwrap();
26//! assert_eq!(fcc.within_jurisdiction(&gb), Some(false));
27//! ```
28//!
29//! `AgencyKind` serialises to its short string form:
30//!
31//! ```rust
32//! use rfham_core::agency::AgencyKind;
33//! use std::str::FromStr;
34//!
35//! assert_eq!(AgencyKind::Regulatory.to_string(), "regulatory");
36//! assert_eq!(AgencyKind::from_str("maintaining").unwrap(), AgencyKind::Maintaining);
37//! ```
38
39use crate::{
40    CountryCode,
41    countries::{country_code_uk, country_code_us},
42    error::CoreError,
43};
44use serde::{Deserialize, Serialize};
45use serde_with::{DeserializeFromStr, SerializeDisplay};
46use std::{fmt::Display, str::FromStr};
47
48// ------------------------------------------------------------------------------------------------
49// Public Macros
50// ------------------------------------------------------------------------------------------------
51
52// ------------------------------------------------------------------------------------------------
53// Public Types
54// ------------------------------------------------------------------------------------------------
55
56#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
57pub struct Agency {
58    name: String,
59    abbreviation: Option<String>,
60    kind: AgencyKind,
61    jurisdiction: Option<Jurisdiction>,
62    url: Option<String>,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
66pub enum AgencyKind {
67    StandardsSetting,
68    Regulatory,
69    Maintaining,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
73#[serde(untagged)]
74pub enum Jurisdiction {
75    International,
76    Just(CountryCode),
77    All(Vec<CountryCode>),
78}
79
80// ------------------------------------------------------------------------------------------------
81// Public Functions
82// ------------------------------------------------------------------------------------------------
83
84pub fn agency_itu() -> Agency {
85    Agency::new(
86        "The International Telecommunication Union",
87        AgencyKind::StandardsSetting,
88    )
89    .with_abbreviation("ITU")
90    .with_jurisdiction(Jurisdiction::International)
91    .with_url("https://www.itu.int")
92}
93
94pub fn agency_iaru() -> Agency {
95    Agency::new(
96        "The International Amateur Radio Union",
97        AgencyKind::Maintaining,
98    )
99    .with_abbreviation("IARU")
100    .with_jurisdiction(Jurisdiction::International)
101    .with_url("https://www.iaru.org")
102}
103
104pub fn agency_arrl() -> Agency {
105    Agency::new("The American Radio Relay League", AgencyKind::Maintaining)
106        .with_abbreviation("ARRL")
107        .with_jurisdiction(Jurisdiction::International)
108        .with_url("http://www.arrl.org")
109}
110
111pub fn agency_fcc() -> Agency {
112    Agency::new("Federal Communications Commission", AgencyKind::Regulatory)
113        .with_abbreviation("FCC")
114        .with_jurisdiction(Jurisdiction::Just(country_code_us()))
115        .with_url("https://www.fcc.gov")
116}
117
118pub fn agency_ofcom() -> Agency {
119    Agency::new("Ofcom", AgencyKind::Regulatory)
120        .with_jurisdiction(Jurisdiction::Just(country_code_uk()))
121        .with_url("https://www.ofcom.org.uk")
122}
123
124pub fn agency_rsgb() -> Agency {
125    Agency::new("Radio Society of Great Britain", AgencyKind::Maintaining)
126        .with_abbreviation("RSGB")
127        .with_jurisdiction(Jurisdiction::Just(country_code_uk()))
128        .with_url("https://www.rsgb.org")
129}
130
131// ------------------------------------------------------------------------------------------------
132// Private Macros
133// ------------------------------------------------------------------------------------------------
134
135// ------------------------------------------------------------------------------------------------
136// Private Types
137// ------------------------------------------------------------------------------------------------
138
139// ------------------------------------------------------------------------------------------------
140// Implementations ❯ Agency
141// ------------------------------------------------------------------------------------------------
142
143impl Display for Agency {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        if f.alternate() {
146            write!(
147                f,
148                "{}{}, {:#}{}",
149                self.name,
150                if let Some(abbreviation) = self.abbreviation.as_ref() {
151                    format!(" ({})", abbreviation)
152                } else {
153                    String::default()
154                },
155                self.kind,
156                match &self.jurisdiction {
157                    Some(Jurisdiction::International) => "international".to_string(),
158                    Some(Jurisdiction::Just(cc)) => cc.to_string(),
159                    Some(Jurisdiction::All(ccs)) => ccs
160                        .iter()
161                        .map(|c| c.as_str())
162                        .collect::<Vec<_>>()
163                        .join(", "),
164                    None => String::default(),
165                }
166            )
167        } else {
168            write!(
169                f,
170                "{}{}",
171                self.name,
172                if let Some(abbreviation) = self.abbreviation.as_ref() {
173                    format!(" ({})", abbreviation)
174                } else {
175                    String::default()
176                }
177            )
178        }
179    }
180}
181
182impl Agency {
183    pub fn new(name: &str, kind: AgencyKind) -> Self {
184        Self {
185            name: name.to_string(),
186            abbreviation: None,
187            kind,
188            jurisdiction: None,
189            url: None,
190        }
191    }
192
193    pub fn with_abbreviation(mut self, abbreviation: &str) -> Self {
194        self.abbreviation = Some(abbreviation.to_string());
195        self
196    }
197
198    pub fn with_jurisdiction(mut self, jurisdiction: Jurisdiction) -> Self {
199        self.jurisdiction = Some(jurisdiction);
200        self
201    }
202
203    pub fn with_url(mut self, url: &str) -> Self {
204        self.url = Some(url.to_string());
205        self
206    }
207
208    pub fn within_jurisdiction(&self, country: &CountryCode) -> Option<bool> {
209        self.jurisdiction.as_ref().map(|v| v.contains(country))
210    }
211
212    pub fn name(&self) -> &String {
213        &self.name
214    }
215
216    pub fn abbreviation(&self) -> Option<&String> {
217        self.abbreviation.as_ref()
218    }
219
220    pub fn kind(&self) -> AgencyKind {
221        self.kind
222    }
223
224    pub fn jurisdiction(&self) -> Option<&Jurisdiction> {
225        self.jurisdiction.as_ref()
226    }
227
228    pub fn url(&self) -> Option<&String> {
229        self.url.as_ref()
230    }
231}
232
233// ------------------------------------------------------------------------------------------------
234// Implementations ❯ AgencyKind
235// ------------------------------------------------------------------------------------------------
236
237impl Display for AgencyKind {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        write!(
240            f,
241            "{}",
242            if f.alternate() {
243                match self {
244                    Self::StandardsSetting => "Standards-Setting Agency",
245                    Self::Regulatory => "Regulatory Agency",
246                    Self::Maintaining => "Maintaining Agency",
247                }
248            } else {
249                match self {
250                    Self::StandardsSetting => "standards",
251                    Self::Regulatory => "regulatory",
252                    Self::Maintaining => "maintaining",
253                }
254            }
255        )
256    }
257}
258
259impl FromStr for AgencyKind {
260    type Err = CoreError;
261
262    fn from_str(s: &str) -> Result<Self, Self::Err> {
263        match s {
264            "standards" => Ok(Self::StandardsSetting),
265            "regulatory" => Ok(Self::Regulatory),
266            "maintaining" => Ok(Self::Maintaining),
267            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "AgencyKind")),
268        }
269    }
270}
271
272impl AgencyKind {
273    pub fn is_standards_setting(&self) -> bool {
274        matches!(self, Self::StandardsSetting)
275    }
276
277    pub fn is_regulatory(&self) -> bool {
278        matches!(self, Self::Regulatory)
279    }
280
281    pub fn is_maintaining(&self) -> bool {
282        matches!(self, Self::Maintaining)
283    }
284}
285
286// ------------------------------------------------------------------------------------------------
287// Implementations ❯ Jurisdiction
288// ------------------------------------------------------------------------------------------------
289
290impl Display for Jurisdiction {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        write!(
293            f,
294            "{}",
295            match self {
296                Self::International => "international".to_string(),
297                Self::Just(cc) => cc.to_string(),
298                Self::All(ccs) => ccs
299                    .iter()
300                    .map(|c| c.as_str())
301                    .collect::<Vec<_>>()
302                    .join(", "),
303            }
304        )
305    }
306}
307
308impl FromStr for Jurisdiction {
309    type Err = CoreError;
310
311    fn from_str(s: &str) -> Result<Self, Self::Err> {
312        if s == "international" {
313            Ok(Self::International)
314        } else if s.contains(',') {
315            let list: Result<Vec<CountryCode>, CoreError> =
316                s.split(',').map(CountryCode::from_str).collect();
317            Ok(Self::All(list?))
318        } else {
319            Ok(Self::Just(CountryCode::from_str(s)?))
320        }
321    }
322}
323
324impl Jurisdiction {
325    pub fn contains(&self, country: &CountryCode) -> bool {
326        match self {
327            Jurisdiction::International => true,
328            Jurisdiction::Just(country_code) => country_code == country,
329            Jurisdiction::All(country_codes) => country_codes.contains(country),
330        }
331    }
332}
333
334// ------------------------------------------------------------------------------------------------
335// Private Functions
336// ------------------------------------------------------------------------------------------------
337
338// ------------------------------------------------------------------------------------------------
339// Sub-Modules
340// ------------------------------------------------------------------------------------------------
341
342// ------------------------------------------------------------------------------------------------
343// Unit Tests
344// ------------------------------------------------------------------------------------------------
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::countries::CountryCode;
350    use pretty_assertions::assert_eq;
351    use std::str::FromStr;
352
353    #[test]
354    fn test_agency_kind_display_roundtrip() {
355        for (s, kind) in [
356            ("standards", AgencyKind::StandardsSetting),
357            ("regulatory", AgencyKind::Regulatory),
358            ("maintaining", AgencyKind::Maintaining),
359        ] {
360            assert_eq!(kind.to_string(), s);
361            assert_eq!(AgencyKind::from_str(s).unwrap(), kind);
362        }
363    }
364
365    #[test]
366    fn test_agency_kind_invalid() {
367        assert!(AgencyKind::from_str("unknown").is_err());
368    }
369
370    #[test]
371    fn test_jurisdiction_international_contains_all() {
372        let j = Jurisdiction::International;
373        assert!(j.contains(&CountryCode::from_str("US").unwrap()));
374        assert!(j.contains(&CountryCode::from_str("JP").unwrap()));
375    }
376
377    #[test]
378    fn test_jurisdiction_just_contains() {
379        let j = Jurisdiction::Just(CountryCode::from_str("US").unwrap());
380        assert!(j.contains(&CountryCode::from_str("US").unwrap()));
381        assert!(!j.contains(&CountryCode::from_str("GB").unwrap()));
382    }
383
384    #[test]
385    fn test_agency_within_jurisdiction() {
386        let fcc = agency_fcc();
387        let us = CountryCode::from_str("US").unwrap();
388        let gb = CountryCode::from_str("GB").unwrap();
389        assert_eq!(fcc.within_jurisdiction(&us), Some(true));
390        assert_eq!(fcc.within_jurisdiction(&gb), Some(false));
391    }
392
393    #[test]
394    fn test_agency_itu_is_international_standards() {
395        let itu = agency_itu();
396        assert_eq!(itu.jurisdiction(), Some(&Jurisdiction::International));
397        assert!(itu.kind().is_standards_setting());
398        assert!(!itu.kind().is_regulatory());
399        assert!(!itu.kind().is_maintaining());
400    }
401
402    #[test]
403    fn test_agency_no_jurisdiction_returns_none() {
404        let a = Agency::new("Test Agency", AgencyKind::Regulatory);
405        assert_eq!(
406            a.within_jurisdiction(&CountryCode::from_str("US").unwrap()),
407            None
408        );
409    }
410}