use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiVersion {
Current,
Specific(String),
Legacy,
}
impl fmt::Display for ApiVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiVersion::Current => write!(f, "current"),
ApiVersion::Specific(version) => write!(f, "{}", version),
ApiVersion::Legacy => write!(f, ""),
}
}
}
impl ApiVersion {
pub fn version(version: impl Into<String>) -> Self {
Self::Specific(version.into())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename = "QRZDatabase")]
pub struct QrzXmlResponse {
#[serde(rename = "@version")]
pub version: Option<String>,
#[serde(rename = "@xmlns")]
pub xmlns: Option<String>,
#[serde(rename = "Session")]
pub session: SessionInfo,
#[serde(rename = "Callsign")]
pub callsign: Option<CallsignInfo>,
#[serde(rename = "DXCC")]
pub dxcc: Option<DxccInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SessionInfo {
#[serde(rename = "Key")]
pub key: Option<String>,
#[serde(rename = "Count")]
pub count: Option<u32>,
#[serde(rename = "SubExp")]
pub sub_exp: Option<String>,
#[serde(rename = "GMTime")]
pub gm_time: Option<String>,
#[serde(rename = "Message")]
pub message: Option<String>,
#[serde(rename = "Error")]
pub error: Option<String>,
}
impl SessionInfo {
pub fn has_valid_session(&self) -> bool {
self.key.is_some()
}
pub fn has_error(&self) -> bool {
self.error.is_some()
}
pub fn error_message(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn info_message(&self) -> Option<&str> {
self.message.as_deref()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CallsignInfo {
#[serde(rename = "call")]
pub call: String,
#[serde(rename = "xref")]
pub xref: Option<String>,
#[serde(rename = "aliases")]
pub aliases: Option<String>,
#[serde(rename = "dxcc")]
pub dxcc: Option<u32>,
#[serde(rename = "fname")]
pub fname: Option<String>,
#[serde(rename = "name")]
pub name: Option<String>,
#[serde(rename = "addr1")]
pub addr1: Option<String>,
#[serde(rename = "addr2")]
pub addr2: Option<String>,
#[serde(rename = "state")]
pub state: Option<String>,
#[serde(rename = "zip")]
pub zip: Option<String>,
#[serde(rename = "country")]
pub country: Option<String>,
#[serde(rename = "ccode")]
pub ccode: Option<u32>,
#[serde(rename = "lat")]
pub lat: Option<f64>,
#[serde(rename = "lon")]
pub lon: Option<f64>,
#[serde(rename = "grid")]
pub grid: Option<String>,
#[serde(rename = "county")]
pub county: Option<String>,
#[serde(rename = "fips")]
pub fips: Option<String>,
#[serde(rename = "land")]
pub land: Option<String>,
#[serde(rename = "efdate")]
pub efdate: Option<String>,
#[serde(rename = "expdate")]
pub expdate: Option<String>,
#[serde(rename = "p_call")]
pub p_call: Option<String>,
#[serde(rename = "class")]
pub class: Option<String>,
#[serde(rename = "codes")]
pub codes: Option<String>,
#[serde(rename = "qslmgr")]
pub qslmgr: Option<String>,
#[serde(rename = "email")]
pub email: Option<String>,
#[serde(rename = "url")]
pub url: Option<String>,
#[serde(rename = "u_views")]
pub u_views: Option<u32>,
#[serde(rename = "bio")]
pub bio: Option<String>,
#[serde(rename = "biodate")]
pub biodate: Option<String>,
#[serde(rename = "image")]
pub image: Option<String>,
#[serde(rename = "imageinfo")]
pub imageinfo: Option<String>,
#[serde(rename = "serial")]
pub serial: Option<u32>,
#[serde(rename = "moddate")]
pub moddate: Option<String>,
#[serde(rename = "MSA")]
pub msa: Option<String>,
#[serde(rename = "AreaCode")]
pub area_code: Option<String>,
#[serde(rename = "TimeZone")]
pub time_zone: Option<String>,
#[serde(rename = "GMTOffset")]
pub gmt_offset: Option<String>,
#[serde(rename = "DST")]
pub dst: Option<String>,
#[serde(rename = "eqsl")]
pub eqsl: Option<String>,
#[serde(rename = "mqsl")]
pub mqsl: Option<String>,
#[serde(rename = "cqzone")]
pub cqzone: Option<u32>,
#[serde(rename = "ituzone")]
pub ituzone: Option<u32>,
#[serde(rename = "born")]
pub born: Option<u32>,
#[serde(rename = "user")]
pub user: Option<String>,
#[serde(rename = "lotw")]
pub lotw: Option<String>,
#[serde(rename = "iota")]
pub iota: Option<String>,
#[serde(rename = "geoloc")]
pub geoloc: Option<String>,
#[serde(rename = "attn")]
pub attn: Option<String>,
#[serde(rename = "nickname")]
pub nickname: Option<String>,
#[serde(rename = "name_fmt")]
pub name_fmt: Option<String>,
}
impl CallsignInfo {
pub fn full_name(&self) -> Option<String> {
match (&self.fname, &self.name) {
(Some(first), Some(last)) => Some(format!("{} {}", first, last)),
(Some(first), None) => Some(first.clone()),
(None, Some(last)) => Some(last.clone()),
(None, None) => None,
}
}
pub fn coordinates(&self) -> Option<(f64, f64)> {
match (self.lat, self.lon) {
(Some(lat), Some(lon)) => Some((lat, lon)),
_ => None,
}
}
pub fn accepts_eqsl(&self) -> Option<bool> {
self.eqsl.as_ref().map(|s| s.eq_ignore_ascii_case("y"))
}
pub fn returns_paper_qsl(&self) -> Option<bool> {
self.mqsl.as_ref().map(|s| s.eq_ignore_ascii_case("y"))
}
pub fn accepts_lotw(&self) -> Option<bool> {
self.lotw.as_ref().map(|s| s.eq_ignore_ascii_case("y"))
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DxccInfo {
#[serde(rename = "dxcc")]
pub dxcc: u32,
#[serde(rename = "cc")]
pub cc: Option<String>,
#[serde(rename = "ccc")]
pub ccc: Option<String>,
#[serde(rename = "name")]
pub name: String,
#[serde(rename = "continent")]
pub continent: Option<String>,
#[serde(rename = "ituzone")]
pub ituzone: Option<u32>,
#[serde(rename = "cqzone")]
pub cqzone: Option<u32>,
#[serde(rename = "timezone")]
pub timezone: Option<String>,
#[serde(rename = "lat")]
pub lat: Option<f64>,
#[serde(rename = "lon")]
pub lon: Option<f64>,
#[serde(rename = "notes")]
pub notes: Option<String>,
}
impl DxccInfo {
pub fn coordinates(&self) -> Option<(f64, f64)> {
match (self.lat, self.lon) {
(Some(lat), Some(lon)) => Some((lat, lon)),
_ => None,
}
}
pub fn timezone_hours(&self) -> Option<f32> {
self.timezone.as_ref().and_then(|tz| {
let tz = tz.trim_start_matches('+');
if tz.len() >= 3 {
if let (Ok(hours), Ok(minutes)) = (
tz[..tz.len() - 2].parse::<i32>(),
tz[tz.len() - 2..].parse::<i32>(),
) {
return Some(hours as f32 + minutes as f32 / 60.0);
}
}
tz.parse::<f32>().ok()
})
}
}
#[derive(Debug, Clone)]
pub struct BiographyData {
pub callsign: String,
pub html_content: String,
}
impl BiographyData {
pub fn new(callsign: impl Into<String>, html_content: impl Into<String>) -> Self {
Self {
callsign: callsign.into(),
html_content: html_content.into(),
}
}
pub fn html(&self) -> &str {
&self.html_content
}
pub fn is_empty(&self) -> bool {
self.html_content.trim().is_empty()
}
}
#[allow(clippy::derivable_impls)]
impl Default for CallsignInfo {
fn default() -> Self {
Self {
call: String::new(),
xref: None,
aliases: None,
dxcc: None,
fname: None,
name: None,
addr1: None,
addr2: None,
state: None,
zip: None,
country: None,
ccode: None,
lat: None,
lon: None,
grid: None,
county: None,
fips: None,
land: None,
efdate: None,
expdate: None,
p_call: None,
class: None,
codes: None,
qslmgr: None,
email: None,
url: None,
u_views: None,
bio: None,
biodate: None,
image: None,
imageinfo: None,
serial: None,
moddate: None,
msa: None,
area_code: None,
time_zone: None,
gmt_offset: None,
dst: None,
eqsl: None,
mqsl: None,
cqzone: None,
ituzone: None,
born: None,
user: None,
lotw: None,
iota: None,
geoloc: None,
attn: None,
nickname: None,
name_fmt: None,
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for DxccInfo {
fn default() -> Self {
Self {
dxcc: 0,
cc: None,
ccc: None,
name: String::new(),
continent: None,
ituzone: None,
cqzone: None,
timezone: None,
lat: None,
lon: None,
notes: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_version_display() {
assert_eq!(ApiVersion::Current.to_string(), "current");
assert_eq!(ApiVersion::version("1.34").to_string(), "1.34");
assert_eq!(ApiVersion::Legacy.to_string(), "");
}
#[test]
fn test_callsign_full_name() {
let mut info = CallsignInfo {
call: "TEST".to_string(),
fname: Some("John".to_string()),
name: Some("Doe".to_string()),
..Default::default()
};
assert_eq!(info.full_name(), Some("John Doe".to_string()));
info.name = None;
assert_eq!(info.full_name(), Some("John".to_string()));
}
#[test]
fn test_coordinates() {
let info = CallsignInfo {
call: "TEST".to_string(),
lat: Some(40.7128),
lon: Some(-74.0060),
..Default::default()
};
assert_eq!(info.coordinates(), Some((40.7128, -74.0060)));
}
#[test]
fn test_qsl_flags() {
let info = CallsignInfo {
call: "TEST".to_string(),
eqsl: Some("Y".to_string()),
mqsl: Some("N".to_string()),
lotw: Some("y".to_string()),
..Default::default()
};
assert_eq!(info.accepts_eqsl(), Some(true));
assert_eq!(info.returns_paper_qsl(), Some(false));
assert_eq!(info.accepts_lotw(), Some(true));
}
#[test]
fn test_dxcc_timezone_parsing() {
let mut dxcc = DxccInfo {
dxcc: 291,
name: "Test".to_string(),
timezone: Some("-5".to_string()),
..Default::default()
};
assert_eq!(dxcc.timezone_hours(), Some(-5.0));
dxcc.timezone = Some("545".to_string());
assert_eq!(dxcc.timezone_hours(), Some(5.75)); }
}