Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Towards a dot on the map #4

Merged
merged 5 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 59 additions & 34 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ rust-version = "1.74"
authors = ["Aaron Fabbri"]

[features]
default = []
default = ["tak"]
# test all features: use this in CI
test-default = ["tak"]

tak = []

[dependencies]
chrono = { version = "0.4.38", default-features = false, features = ["std", "now"] }
quick-xml = { version = "0.37.0", features = ["serialize"] }
serde = { version = "1.0.214", features = ["derive"] }
thiserror = "1.0.68"
uuid = { version = "1.11.0", features = ["v4"] }

[dev-dependencies]
regex = { version = "1.11.1" }
serde_json = { version = "1.0.132" }
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ This library aims to provide a simple way to produce (serialize) and consume
- Basic Rust structs for CoT messages, with serde support.

### TODOs
- [ ] better types for timestamps/dates. Currently just strings.
- [ ] Add more typed schemas for common detail contents (sub-schemas)

## References
Expand Down
99 changes: 91 additions & 8 deletions src/base.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use quick_xml::Reader;
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::Error;

/// Base schema structure and ser/de.

Expand Down Expand Up @@ -27,18 +32,84 @@ pub struct Cot<D> {
pub uid: String,
#[serde(rename = "@type")]
pub cot_type: String,
#[serde(rename = "@time")]
pub time: String,
#[serde(rename = "@start")]
pub start: String,
#[serde(rename = "@stale")]
pub stale: String,
#[serde(
rename = "@time",
serialize_with = "serialize_date",
deserialize_with = "deserialize_date"
)]
pub time: DateTime<Utc>,
#[serde(
rename = "@start",
serialize_with = "serialize_date",
deserialize_with = "deserialize_date"
)]
pub start: DateTime<Utc>,
#[serde(
rename = "@stale",
serialize_with = "serialize_date",
deserialize_with = "deserialize_date"
)]
pub stale: DateTime<Utc>,
#[serde(rename = "detail")]
pub detail: D,
#[serde(rename = "point")]
pub point: Point,
}

/// Parse `type` attribute from a CoT message XML string.
pub fn parse_cot_msg_type(text: &str) -> Result<String, Error> {
match xml_first_element_w_attr(text, "event", "type") {
Ok(Some(val)) => Ok(val),
_ => Err(Error::BadField("No element 'event' with attribute 'type'")),
}
}

/// XML parsing convenience
pub fn xml_first_element_w_attr(
text: &str,
elt_name: &str,
attr_name: &str,
) -> Result<Option<String>, Error> {
let mut reader = Reader::from_str(text);
reader.config_mut().trim_text(true);
loop {
match reader.read_event()? {
// Parse attribute `type` in the `event` element.
quick_xml::events::Event::Start(ref e) => {
if e.name().into_inner() == elt_name.as_bytes() {
for attr in e.attributes() {
let attr = attr?;
if attr.key.into_inner() == attr_name.as_bytes() {
return Ok(Some(String::from_utf8_lossy(&attr.value).to_string()));
}
}
}
}
quick_xml::events::Event::Eof => break,
_ => {}
}
}
Ok(None)
}

pub(crate) fn serialize_date<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = date.to_rfc3339();
serializer.serialize_str(&s)
}

pub(crate) fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
DateTime::parse_from_rfc3339(&s)
.map_err(DeError::custom)
.map(|dt| dt.with_timezone(&Utc))
}

pub type CotBase = Cot<NoDetail>;

#[derive(Serialize, Deserialize, Debug, PartialEq)]
Expand All @@ -58,11 +129,23 @@ pub struct Point {
pub le: f32,
}

impl Point {
pub fn north_pole() -> Self {
Self {
lat: 90.0,
lon: 0.0,
ce: 0.0,
hae: 0.0,
le: 0.0,
}
}
}

#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_serde() {
fn test_serde_roundtrip() {
// Create two Cot objects, one from example string and another from a round trip from that
// to a string and back again. Validate values match.
let cot0: CotBase = quick_xml::de::from_str(COT_BASE_EXAMPLE).unwrap();
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ pub mod tak;

#[derive(Debug, Error)]
pub enum Error {
#[error("message field error: {0}")]
BadField(&'static str),

#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
XmlAttr(#[from] quick_xml::events::attributes::AttrError),
#[error(transparent)]
Xml(#[from] quick_xml::errors::Error),
#[error(transparent)]
De(#[from] quick_xml::de::DeError),
Expand Down
35 changes: 35 additions & 0 deletions src/tak/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use chrono::Utc;

use crate::base::{Cot, Point};

use super::detail::TakMarkerDetail;

/// Support for creating TAK CoT messages with reasonable defaults for quickly getting integration
/// working.
///
/// Instead of providing builder APIs, we implement [`Default`] on different CoT variants: You'll
/// want to modify key fields like `point` with your real coordinates.

/// Default CoT type for marker messages.
pub const DEFAULT_COT_TYPE_MARKER: &str = "a-o-G";

/// TAK CoT Marker
impl Default for Cot<TakMarkerDetail> {
fn default() -> Self {
let now = Utc::now();
let detail = TakMarkerDetail {
..Default::default()
};
Self {
version: "2.0".to_string(),
uid: uuid::Uuid::new_v4().to_string(),
cot_type: DEFAULT_COT_TYPE_MARKER.to_string(),
time: now,
start: now,
// now plus 1 day
stale: now + chrono::Duration::days(1),
detail,
point: Point::north_pole(),
}
}
}
Loading