cot_proto/
base.rs

1use chrono::{DateTime, SecondsFormat, Utc};
2use quick_xml::Reader;
3use serde::de::Error as DeError;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::Error;
7
8/// Base schema structure and ser/de.
9
10// See References section in README.md
11pub const COT_BASE_EXAMPLE: &str = r#"
12<?xml version='1.0' standalone='yes'?>
13<event version="2.0"
14 uid="J-01334"
15 type="a-h-A-M-F-U-M"
16 time="2005-04-05T11:43:38.07Z"
17 start="2005-04-05T11:43:38.07Z"
18 stale="2005-04-05T11:45:38.07Z" >
19 <detail>
20 </detail>
21 <point lat="30.0090027" lon="-85.9578735" ce="45.3"
22 hae="-42.6" le="99.5" />
23</event>
24"#;
25
26#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
27#[serde(rename = "event")]
28pub struct Cot<D> {
29    #[serde(rename = "@version")]
30    pub version: String,
31    #[serde(rename = "@uid")]
32    pub uid: String,
33    #[serde(rename = "@type")]
34    pub cot_type: String,
35    #[serde(
36        rename = "@time",
37        serialize_with = "serialize_date",
38        deserialize_with = "deserialize_date"
39    )]
40    pub time: DateTime<Utc>,
41    #[serde(
42        rename = "@start",
43        serialize_with = "serialize_date",
44        deserialize_with = "deserialize_date"
45    )]
46    pub start: DateTime<Utc>,
47    #[serde(
48        rename = "@stale",
49        serialize_with = "serialize_date",
50        deserialize_with = "deserialize_date"
51    )]
52    pub stale: DateTime<Utc>,
53    #[serde(rename = "@how", skip_serializing_if = "Option::is_none")]
54    pub how: Option<String>,
55    #[serde(rename = "detail")]
56    pub detail: D,
57    #[serde(rename = "point")]
58    pub point: Point,
59}
60
61/// Parse `type` attribute from a CoT message XML string.
62pub fn parse_cot_msg_type(text: &str) -> Result<String, Error> {
63    match xml_first_element_w_attr(text, "event", "type") {
64        Ok(Some(val)) => Ok(val),
65        _ => Err(Error::BadField("No element 'event' with attribute 'type'")),
66    }
67}
68
69/// XML parsing convenience
70pub fn xml_first_element_w_attr(
71    text: &str,
72    elt_name: &str,
73    attr_name: &str,
74) -> Result<Option<String>, Error> {
75    let mut reader = Reader::from_str(text);
76    reader.config_mut().trim_text(true);
77    loop {
78        match reader.read_event()? {
79            // Parse attribute `type` in the `event` element.
80            quick_xml::events::Event::Start(ref e) => {
81                if e.name().into_inner() == elt_name.as_bytes() {
82                    for attr in e.attributes() {
83                        let attr = attr?;
84                        if attr.key.into_inner() == attr_name.as_bytes() {
85                            return Ok(Some(String::from_utf8_lossy(&attr.value).to_string()));
86                        }
87                    }
88                }
89            }
90            quick_xml::events::Event::Eof => break,
91            _ => {}
92        }
93    }
94    Ok(None)
95}
96
97pub(crate) fn serialize_date<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
98where
99    S: Serializer,
100{
101    let s = date.to_rfc3339_opts(SecondsFormat::Millis, true /* use Z for +00:00 */);
102    serializer.serialize_str(&s)
103}
104
105pub(crate) fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
106where
107    D: Deserializer<'de>,
108{
109    let s = String::deserialize(deserializer)?;
110    DateTime::parse_from_rfc3339(&s)
111        .map_err(DeError::custom)
112        .map(|dt| dt.with_timezone(&Utc))
113}
114
115pub type CotBase = Cot<NoDetail>;
116
117#[derive(Serialize, Deserialize, Debug, PartialEq)]
118pub struct NoDetail {}
119
120#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
121pub struct Point {
122    #[serde(rename = "@lat")]
123    pub lat: f64,
124    #[serde(rename = "@lon")]
125    pub lon: f64,
126    #[serde(rename = "@ce")]
127    pub ce: f32,
128    #[serde(rename = "@hae")]
129    pub hae: f32,
130    #[serde(rename = "@le")]
131    pub le: f32,
132}
133
134impl Point {
135    pub fn north_pole() -> Self {
136        Self {
137            lat: 90.0,
138            lon: 0.0,
139            ce: 0.0,
140            hae: 0.0,
141            le: 0.0,
142        }
143    }
144}
145
146#[cfg(test)]
147mod test {
148    use super::*;
149    #[test]
150    fn test_serde_roundtrip() {
151        // Create two Cot objects, one from example string and another from a round trip from that
152        // to a string and back again. Validate values match.
153        let cot0: CotBase = quick_xml::de::from_str(COT_BASE_EXAMPLE).unwrap();
154        let cot_str = quick_xml::se::to_string(&cot0).unwrap();
155        let cot1: CotBase = quick_xml::de::from_str(&cot_str).unwrap();
156        assert_eq!(cot0, cot1);
157    }
158}