1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ApiVersion {
9 Current,
11 Specific(String),
13 Legacy,
15}
16
17impl fmt::Display for ApiVersion {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 ApiVersion::Current => write!(f, "current"),
21 ApiVersion::Specific(version) => write!(f, "{}", version),
22 ApiVersion::Legacy => write!(f, ""),
23 }
24 }
25}
26
27impl ApiVersion {
28 pub fn version(version: impl Into<String>) -> Self {
30 Self::Specific(version.into())
31 }
32}
33
34#[derive(Debug, Clone, Deserialize, Serialize)]
36#[serde(rename = "QRZDatabase")]
37pub struct QrzXmlResponse {
38 #[serde(rename = "@version")]
40 pub version: Option<String>,
41
42 #[serde(rename = "@xmlns")]
44 pub xmlns: Option<String>,
45
46 #[serde(rename = "Session")]
48 pub session: SessionInfo,
49
50 #[serde(rename = "Callsign")]
52 pub callsign: Option<CallsignInfo>,
53
54 #[serde(rename = "DXCC")]
56 pub dxcc: Option<DxccInfo>,
57}
58
59#[derive(Debug, Clone, Deserialize, Serialize)]
61pub struct SessionInfo {
62 #[serde(rename = "Key")]
64 pub key: Option<String>,
65
66 #[serde(rename = "Count")]
68 pub count: Option<u32>,
69
70 #[serde(rename = "SubExp")]
72 pub sub_exp: Option<String>,
73
74 #[serde(rename = "GMTime")]
76 pub gm_time: Option<String>,
77
78 #[serde(rename = "Message")]
80 pub message: Option<String>,
81
82 #[serde(rename = "Error")]
84 pub error: Option<String>,
85}
86
87impl SessionInfo {
88 pub fn has_valid_session(&self) -> bool {
90 self.key.is_some()
91 }
92
93 pub fn has_error(&self) -> bool {
95 self.error.is_some()
96 }
97
98 pub fn error_message(&self) -> Option<&str> {
100 self.error.as_deref()
101 }
102
103 pub fn info_message(&self) -> Option<&str> {
105 self.message.as_deref()
106 }
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct CallsignInfo {
112 #[serde(rename = "call")]
114 pub call: String,
115
116 #[serde(rename = "xref")]
118 pub xref: Option<String>,
119
120 #[serde(rename = "aliases")]
122 pub aliases: Option<String>,
123
124 #[serde(rename = "dxcc")]
126 pub dxcc: Option<u32>,
127
128 #[serde(rename = "fname")]
130 pub fname: Option<String>,
131
132 #[serde(rename = "name")]
134 pub name: Option<String>,
135
136 #[serde(rename = "addr1")]
138 pub addr1: Option<String>,
139
140 #[serde(rename = "addr2")]
142 pub addr2: Option<String>,
143
144 #[serde(rename = "state")]
146 pub state: Option<String>,
147
148 #[serde(rename = "zip")]
150 pub zip: Option<String>,
151
152 #[serde(rename = "country")]
154 pub country: Option<String>,
155
156 #[serde(rename = "ccode")]
158 pub ccode: Option<u32>,
159
160 #[serde(rename = "lat")]
162 pub lat: Option<f64>,
163
164 #[serde(rename = "lon")]
166 pub lon: Option<f64>,
167
168 #[serde(rename = "grid")]
170 pub grid: Option<String>,
171
172 #[serde(rename = "county")]
174 pub county: Option<String>,
175
176 #[serde(rename = "fips")]
178 pub fips: Option<String>,
179
180 #[serde(rename = "land")]
182 pub land: Option<String>,
183
184 #[serde(rename = "efdate")]
186 pub efdate: Option<String>,
187
188 #[serde(rename = "expdate")]
190 pub expdate: Option<String>,
191
192 #[serde(rename = "p_call")]
194 pub p_call: Option<String>,
195
196 #[serde(rename = "class")]
198 pub class: Option<String>,
199
200 #[serde(rename = "codes")]
202 pub codes: Option<String>,
203
204 #[serde(rename = "qslmgr")]
206 pub qslmgr: Option<String>,
207
208 #[serde(rename = "email")]
210 pub email: Option<String>,
211
212 #[serde(rename = "url")]
214 pub url: Option<String>,
215
216 #[serde(rename = "u_views")]
218 pub u_views: Option<u32>,
219
220 #[serde(rename = "bio")]
222 pub bio: Option<String>,
223
224 #[serde(rename = "biodate")]
226 pub biodate: Option<String>,
227
228 #[serde(rename = "image")]
230 pub image: Option<String>,
231
232 #[serde(rename = "imageinfo")]
234 pub imageinfo: Option<String>,
235
236 #[serde(rename = "serial")]
238 pub serial: Option<u32>,
239
240 #[serde(rename = "moddate")]
242 pub moddate: Option<String>,
243
244 #[serde(rename = "MSA")]
246 pub msa: Option<String>,
247
248 #[serde(rename = "AreaCode")]
250 pub area_code: Option<String>,
251
252 #[serde(rename = "TimeZone")]
254 pub time_zone: Option<String>,
255
256 #[serde(rename = "GMTOffset")]
258 pub gmt_offset: Option<String>,
259
260 #[serde(rename = "DST")]
262 pub dst: Option<String>,
263
264 #[serde(rename = "eqsl")]
266 pub eqsl: Option<String>,
267
268 #[serde(rename = "mqsl")]
270 pub mqsl: Option<String>,
271
272 #[serde(rename = "cqzone")]
274 pub cqzone: Option<u32>,
275
276 #[serde(rename = "ituzone")]
278 pub ituzone: Option<u32>,
279
280 #[serde(rename = "born")]
282 pub born: Option<u32>,
283
284 #[serde(rename = "user")]
286 pub user: Option<String>,
287
288 #[serde(rename = "lotw")]
290 pub lotw: Option<String>,
291
292 #[serde(rename = "iota")]
294 pub iota: Option<String>,
295
296 #[serde(rename = "geoloc")]
298 pub geoloc: Option<String>,
299
300 #[serde(rename = "attn")]
302 pub attn: Option<String>,
303
304 #[serde(rename = "nickname")]
306 pub nickname: Option<String>,
307
308 #[serde(rename = "name_fmt")]
310 pub name_fmt: Option<String>,
311}
312
313impl CallsignInfo {
314 pub fn full_name(&self) -> Option<String> {
316 match (&self.fname, &self.name) {
317 (Some(first), Some(last)) => Some(format!("{} {}", first, last)),
318 (Some(first), None) => Some(first.clone()),
319 (None, Some(last)) => Some(last.clone()),
320 (None, None) => None,
321 }
322 }
323
324 pub fn coordinates(&self) -> Option<(f64, f64)> {
326 match (self.lat, self.lon) {
327 (Some(lat), Some(lon)) => Some((lat, lon)),
328 _ => None,
329 }
330 }
331
332 pub fn accepts_eqsl(&self) -> Option<bool> {
334 self.eqsl.as_ref().map(|s| s.eq_ignore_ascii_case("y"))
335 }
336
337 pub fn returns_paper_qsl(&self) -> Option<bool> {
339 self.mqsl.as_ref().map(|s| s.eq_ignore_ascii_case("y"))
340 }
341
342 pub fn accepts_lotw(&self) -> Option<bool> {
344 self.lotw.as_ref().map(|s| s.eq_ignore_ascii_case("y"))
345 }
346}
347
348#[derive(Debug, Clone, Deserialize, Serialize)]
350pub struct DxccInfo {
351 #[serde(rename = "dxcc")]
353 pub dxcc: u32,
354
355 #[serde(rename = "cc")]
357 pub cc: Option<String>,
358
359 #[serde(rename = "ccc")]
361 pub ccc: Option<String>,
362
363 #[serde(rename = "name")]
365 pub name: String,
366
367 #[serde(rename = "continent")]
369 pub continent: Option<String>,
370
371 #[serde(rename = "ituzone")]
373 pub ituzone: Option<u32>,
374
375 #[serde(rename = "cqzone")]
377 pub cqzone: Option<u32>,
378
379 #[serde(rename = "timezone")]
381 pub timezone: Option<String>,
382
383 #[serde(rename = "lat")]
385 pub lat: Option<f64>,
386
387 #[serde(rename = "lon")]
389 pub lon: Option<f64>,
390
391 #[serde(rename = "notes")]
393 pub notes: Option<String>,
394}
395
396impl DxccInfo {
397 pub fn coordinates(&self) -> Option<(f64, f64)> {
399 match (self.lat, self.lon) {
400 (Some(lat), Some(lon)) => Some((lat, lon)),
401 _ => None,
402 }
403 }
404
405 pub fn timezone_hours(&self) -> Option<f32> {
407 self.timezone.as_ref().and_then(|tz| {
408 let tz = tz.trim_start_matches('+');
410 if tz.len() >= 3 {
411 if let (Ok(hours), Ok(minutes)) = (
413 tz[..tz.len() - 2].parse::<i32>(),
414 tz[tz.len() - 2..].parse::<i32>(),
415 ) {
416 return Some(hours as f32 + minutes as f32 / 60.0);
417 }
418 }
419 tz.parse::<f32>().ok()
420 })
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct BiographyData {
427 pub callsign: String,
429 pub html_content: String,
431}
432
433impl BiographyData {
434 pub fn new(callsign: impl Into<String>, html_content: impl Into<String>) -> Self {
436 Self {
437 callsign: callsign.into(),
438 html_content: html_content.into(),
439 }
440 }
441
442 pub fn html(&self) -> &str {
444 &self.html_content
445 }
446
447 pub fn is_empty(&self) -> bool {
449 self.html_content.trim().is_empty()
450 }
451}
452
453#[allow(clippy::derivable_impls)]
455impl Default for CallsignInfo {
456 fn default() -> Self {
457 Self {
458 call: String::new(),
459 xref: None,
460 aliases: None,
461 dxcc: None,
462 fname: None,
463 name: None,
464 addr1: None,
465 addr2: None,
466 state: None,
467 zip: None,
468 country: None,
469 ccode: None,
470 lat: None,
471 lon: None,
472 grid: None,
473 county: None,
474 fips: None,
475 land: None,
476 efdate: None,
477 expdate: None,
478 p_call: None,
479 class: None,
480 codes: None,
481 qslmgr: None,
482 email: None,
483 url: None,
484 u_views: None,
485 bio: None,
486 biodate: None,
487 image: None,
488 imageinfo: None,
489 serial: None,
490 moddate: None,
491 msa: None,
492 area_code: None,
493 time_zone: None,
494 gmt_offset: None,
495 dst: None,
496 eqsl: None,
497 mqsl: None,
498 cqzone: None,
499 ituzone: None,
500 born: None,
501 user: None,
502 lotw: None,
503 iota: None,
504 geoloc: None,
505 attn: None,
506 nickname: None,
507 name_fmt: None,
508 }
509 }
510}
511
512#[allow(clippy::derivable_impls)]
513impl Default for DxccInfo {
514 fn default() -> Self {
515 Self {
516 dxcc: 0,
517 cc: None,
518 ccc: None,
519 name: String::new(),
520 continent: None,
521 ituzone: None,
522 cqzone: None,
523 timezone: None,
524 lat: None,
525 lon: None,
526 notes: None,
527 }
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_api_version_display() {
537 assert_eq!(ApiVersion::Current.to_string(), "current");
538 assert_eq!(ApiVersion::version("1.34").to_string(), "1.34");
539 assert_eq!(ApiVersion::Legacy.to_string(), "");
540 }
541
542 #[test]
543 fn test_callsign_full_name() {
544 let mut info = CallsignInfo {
545 call: "TEST".to_string(),
546 fname: Some("John".to_string()),
547 name: Some("Doe".to_string()),
548 ..Default::default()
549 };
550
551 assert_eq!(info.full_name(), Some("John Doe".to_string()));
552
553 info.name = None;
554 assert_eq!(info.full_name(), Some("John".to_string()));
555 }
556
557 #[test]
558 fn test_coordinates() {
559 let info = CallsignInfo {
560 call: "TEST".to_string(),
561 lat: Some(40.7128),
562 lon: Some(-74.0060),
563 ..Default::default()
564 };
565
566 assert_eq!(info.coordinates(), Some((40.7128, -74.0060)));
567 }
568
569 #[test]
570 fn test_qsl_flags() {
571 let info = CallsignInfo {
572 call: "TEST".to_string(),
573 eqsl: Some("Y".to_string()),
574 mqsl: Some("N".to_string()),
575 lotw: Some("y".to_string()),
576 ..Default::default()
577 };
578
579 assert_eq!(info.accepts_eqsl(), Some(true));
580 assert_eq!(info.returns_paper_qsl(), Some(false));
581 assert_eq!(info.accepts_lotw(), Some(true));
582 }
583
584 #[test]
585 fn test_dxcc_timezone_parsing() {
586 let mut dxcc = DxccInfo {
587 dxcc: 291,
588 name: "Test".to_string(),
589 timezone: Some("-5".to_string()),
590 ..Default::default()
591 };
592
593 assert_eq!(dxcc.timezone_hours(), Some(-5.0));
594
595 dxcc.timezone = Some("545".to_string());
596 assert_eq!(dxcc.timezone_hours(), Some(5.75)); }
598}