1use 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#[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
80pub 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
131impl 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
233impl 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
286impl 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#[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}