use std::{
fmt::{self, Display},
hash::{Hash, Hasher},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{serde_as, DeserializeFromStr, NoneAsEmptyString, SerializeDisplay};
use time::OffsetDateTime;
use crate::{
callsign::Callsign,
summit::{HasSummit, SummitCode},
ParseError,
};
use super::{Mode, Notice};
#[allow(clippy::upper_case_acronyms, missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub enum SpotType {
Normal,
QRT,
Test,
}
impl FromStr for SpotType {
type Err = fmt::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"normal" => Ok(SpotType::Normal),
"qrt" => Ok(SpotType::QRT),
"test" => Ok(SpotType::Test),
_ => Err(fmt::Error),
}
}
}
impl Display for SpotType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[serde_as]
#[derive(Debug, Clone, Deserialize, Serialize, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Spot {
pub id: usize,
#[serde(with = "time::serde::rfc3339")]
pub time_stamp: OffsetDateTime,
pub activator_callsign: Callsign,
pub activator_name: String,
pub callsign: Callsign,
#[serde_as(as = "NoneAsEmptyString")]
pub comments: Option<String>,
#[serde(with = "frequency")]
pub frequency: Option<String>,
pub mode: Mode,
pub summit_code: String,
pub summit_name: String,
#[serde(rename = "AltFt")]
pub alt_ft: usize,
#[serde(rename = "AltM")]
pub alt_m: usize,
pub points: usize,
pub r#type: Option<SpotType>,
pub epoch: String,
}
impl Notice for Spot {
#[allow(clippy::misnamed_getters)]
fn callsign(&self) -> &Callsign {
&self.activator_callsign
}
fn epoch(&self) -> &str {
&self.epoch
}
}
impl HasSummit for Spot {
fn summit_code(&self) -> Result<SummitCode, ParseError> {
self.summit_code.parse()
}
}
impl Hash for Spot {
fn hash<H: Hasher>(&self, state: &mut H) {
self.activator_callsign.hash(state);
self.summit_code.hash(state);
self.frequency.hash(state);
self.mode.hash(state);
self.time_stamp.hash(state);
}
}
impl PartialEq for Spot {
fn eq(&self, other: &Self) -> bool {
self.activator_callsign == other.activator_callsign
&& self.summit_code == other.summit_code
&& self.frequency == other.frequency
&& self.mode == other.mode
&& self.time_stamp == other.time_stamp
}
}
mod frequency {
use super::*;
use serde::{de, ser, Deserializer, Serializer};
pub(super) fn serialize<S: Serializer>(
freq: &Option<String>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match freq {
Some(freq) => f64::from_str(freq)
.map_err(ser::Error::custom)?
.serialize(serializer),
None => serializer.serialize_none(),
}
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
Ok(match Value::deserialize(deserializer)? {
Value::Number(num) => Some(num.to_string()),
Value::Null => None,
_ => return Err(de::Error::custom("Expected number")),
})
}
}