1use std::{
4 fmt::{self, Display},
5 hash::{Hash, Hasher},
6 str::FromStr,
7};
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use serde_with::{serde_as, DeserializeFromStr, NoneAsEmptyString, SerializeDisplay};
12use time::OffsetDateTime;
13
14use crate::{
15 callsign::Callsign,
16 summit::{HasSummit, SummitCode},
17 ParseError,
18};
19
20use super::{Mode, Notice};
21
22#[allow(clippy::upper_case_acronyms, missing_docs)]
23#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
24pub enum SpotType {
25 Normal,
26 QRT,
27 Test,
28}
29
30impl FromStr for SpotType {
31 type Err = fmt::Error;
32
33 fn from_str(s: &str) -> Result<Self, Self::Err> {
34 match s.to_lowercase().as_ref() {
35 "normal" => Ok(SpotType::Normal),
36 "qrt" => Ok(SpotType::QRT),
37 "test" => Ok(SpotType::Test),
38 _ => Err(fmt::Error),
39 }
40 }
41}
42
43impl Display for SpotType {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{:?}", self)
46 }
47}
48
49#[serde_as]
51#[derive(Debug, Clone, Deserialize, Serialize, Eq)]
52#[serde(rename_all = "camelCase")]
53pub struct Spot {
54 pub id: usize,
56 #[serde(with = "time::serde::rfc3339")]
58 pub time_stamp: OffsetDateTime,
59 pub activator_callsign: Callsign,
61 pub activator_name: String,
63 pub callsign: Callsign,
67 #[serde_as(as = "NoneAsEmptyString")]
69 pub comments: Option<String>,
70 #[serde(with = "frequency")]
72 pub frequency: Option<String>,
73 pub mode: Mode,
75 pub summit_code: String,
77 pub summit_name: String,
79 #[serde(rename = "AltFt")]
81 pub alt_ft: usize,
82 #[serde(rename = "AltM")]
84 pub alt_m: usize,
85 pub points: usize,
87 pub r#type: Option<SpotType>,
89 pub epoch: String,
91}
92
93impl Notice for Spot {
94 #[allow(clippy::misnamed_getters)]
95 fn callsign(&self) -> &Callsign {
96 &self.activator_callsign
97 }
98
99 fn epoch(&self) -> &str {
100 &self.epoch
101 }
102}
103
104impl HasSummit for Spot {
105 fn summit_code(&self) -> Result<SummitCode, ParseError> {
106 self.summit_code.parse()
107 }
108}
109
110impl Hash for Spot {
113 fn hash<H: Hasher>(&self, state: &mut H) {
114 self.activator_callsign.hash(state);
115 self.summit_code.hash(state);
116 self.frequency.hash(state);
117 self.mode.hash(state);
118 self.time_stamp.hash(state);
119 }
120}
121
122impl PartialEq for Spot {
123 fn eq(&self, other: &Self) -> bool {
124 self.activator_callsign == other.activator_callsign
125 && self.summit_code == other.summit_code
126 && self.frequency == other.frequency
127 && self.mode == other.mode
128 && self.time_stamp == other.time_stamp
129 }
130}
131mod frequency {
132 use super::*;
135 use serde::{de, ser, Deserializer, Serializer};
136
137 pub(super) fn serialize<S: Serializer>(
138 freq: &Option<String>,
139 serializer: S,
140 ) -> Result<S::Ok, S::Error> {
141 match freq {
142 Some(freq) => f64::from_str(freq)
143 .map_err(ser::Error::custom)?
144 .serialize(serializer),
145 None => serializer.serialize_none(),
146 }
147 }
148
149 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
150 where
151 D: Deserializer<'de>,
152 {
153 Ok(match Value::deserialize(deserializer)? {
154 Value::Number(num) => Some(num.to_string()),
155 Value::Null => None,
156 _ => return Err(de::Error::custom("Expected number")),
157 })
158 }
159}