ccsds_ndm/lib.rs
1// SPDX-FileCopyrightText: 2025 Jochim Maene <jochim.maene+github@gmail.com>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! # CCSDS NDM
6//!
7//! A high-performance, type-safe library for parsing and generating CCSDS Navigation Data Messages (NDM)
8//! in both KVN (Key-Value Notation) and XML formats.
9//!
10//! This crate is designed for mission-critical space applications where correctness, performance,
11//! and adherence to standards are paramount.
12//!
13//! ## Key Features
14//!
15//! - **Comprehensive Support**: Full support for OPM, OMM, OEM, OCM, CDM, TDM, RDM, AEM, APM, and ACM messages.
16//! - **Format Agnostic**: Seamlessly convert between KVN and XML formats.
17//! - **Type Safety**: Strictly typed units (e.g., `km`, `deg`, `s`) prevent physical unit errors.
18//! - **High Performance Parsing**: Utilizes `winnow` and `quick-xml` for efficient, low-allocation parsing.
19//! - **Ergonomic Construction**: Uses the builder pattern (via the [`bon`](https://docs.rs/bon) crate) for safe and easy message creation.
20//! - **Standard Compliant**: Validates messages against CCSDS 502.0-B-3 and related standards.
21//!
22//! ## Architecture
23//!
24//! The library is organized around a few core concepts:
25//!
26//! - **[`Ndm`](traits::Ndm) Trait**: The unifying interface for all message types. It defines the standard `to_kvn`, `from_kvn`, `to_xml`, and `from_xml` methods.
27//! - **[`MessageType`] Enum**: A container that holds any valid NDM. This is the primary return type when parsing files with unknown contents (auto-detection).
28//! - **Strong Typing**: All physical quantities (Distance, Velocity, Mass, etc.) are wrapped in the [`UnitValue`](types::UnitValue) struct, ensuring that units are always tracked and validated.
29//!
30//! ## Quick Start
31//!
32//! ### 1. Parse any NDM file (auto-detection)
33//!
34//! The library automatically detects whether the input is KVN or XML and what message type it contains.
35//!
36//! ```no_run
37//! use ccsds_ndm::{from_file, MessageType};
38//!
39//! let ndm = from_file("example.opm").unwrap();
40//!
41//! match ndm {
42//! MessageType::Opm(opm) => {
43//! println!("Object: {}", opm.body.segment.metadata.object_name);
44//! }
45//! MessageType::Oem(oem) => {
46//! println!("Ephemeris points: {}", oem.body.segment[0].data.state_vector.len());
47//! }
48//! _ => println!("Other message type"),
49//! }
50//! ```
51//!
52//! ### 2. Parse a specific message type
53//!
54//! If you know the message type in advance, you can parse it directly:
55//!
56//! ```no_run
57//! use ccsds_ndm::messages::opm::Opm;
58//! use ccsds_ndm::traits::Ndm;
59//!
60//! // Parses strict KVN for OPM
61//! let opm = Opm::from_kvn("CCSDS_OPM_VERS = 3.0\n...").unwrap();
62//! ```
63//!
64//! ### 3. Generate a message using the Builder Pattern
65//!
66//! Creating messages from scratch is safe and verbose-free using the `builder()` methods.
67//!
68//! ```no_run
69//! use ccsds_ndm::messages::opm::{Opm, OpmBody, OpmSegment, OpmMetadata, OpmData};
70//! use ccsds_ndm::common::{OdmHeader, StateVector};
71//! use ccsds_ndm::types::{Epoch, Position, Velocity};
72//! use ccsds_ndm::traits::Ndm;
73//!
74//! let opm = Opm::builder()
75//! .version("3.0")
76//! .header(OdmHeader::builder()
77//! .creation_date("2024-01-01T00:00:00".parse().unwrap())
78//! .originator("EXAMPLE")
79//! .build())
80//! .body(OpmBody::builder()
81//! .segment(OpmSegment::builder()
82//! .metadata(OpmMetadata::builder()
83//! .object_name("SATELLITE")
84//! .object_id("2024-001A")
85//! .center_name("EARTH")
86//! .ref_frame("GCRF")
87//! .time_system("UTC")
88//! .build())
89//! .data(OpmData::builder()
90//! .state_vector(StateVector::builder()
91//! .epoch("2024-01-01T12:00:00".parse().unwrap())
92//! .x(Position::new(7000.0, None))
93//! .y(Position::new(0.0, None))
94//! .z(Position::new(0.0, None))
95//! .x_dot(Velocity::new(0.0, None))
96//! .y_dot(Velocity::new(7.5, None))
97//! .z_dot(Velocity::new(0.0, None))
98//! .build())
99//! .build())
100//! .build())
101//! .build())
102//! .build();
103//!
104//! // Convert to KVN string
105//! println!("{}", opm.to_kvn().unwrap());
106//! ```
107//!
108//! ### 4. Serialize to KVN or XML
109//!
110//! ```no_run
111//! use ccsds_ndm::{from_file, MessageType};
112//!
113//! let ndm = from_file("example.opm").unwrap();
114//!
115//! // Serialize to string
116//! let kvn_string = ndm.to_kvn().unwrap();
117//! let xml_string = ndm.to_xml().unwrap();
118//!
119//! // Write to file
120//! ndm.to_xml_file("output.xml").unwrap();
121//! ```
122//!
123//! ## Modules
124//!
125//! - [`messages`]: Supported NDM message types (OPM, OEM, TDM, etc.).
126//! - [`traits`]: Core traits like `Ndm` and `UnitValue` handling.
127//! - [`types`]: Physical types (Distance, Velocity, Epoch, etc.) and CCSDS enumerations.
128//! - [`kvn`] & [`xml`]: Format-specific parsing and serialization logic.
129
130pub mod common;
131pub mod detect;
132pub mod error;
133pub mod kvn;
134pub mod messages;
135pub mod traits;
136pub mod types;
137pub mod utils;
138pub mod validation;
139pub mod versioning;
140pub mod xml;
141
142use error::{CcsdsNdmError, Result};
143use std::fs;
144use std::path::Path;
145pub use validation::{take_warnings as take_validation_warnings, ValidationMode};
146
147/// A generic container for any parsed NDM message.
148///
149/// This enum wraps all supported CCSDS message types, allowing uniform handling
150/// of messages when the type is not known at compile time.
151///
152/// # Example
153///
154/// ```no_run
155/// use ccsds_ndm::{from_str, MessageType};
156///
157/// let ndm = from_str("CCSDS_OPM_VERS = 3.0\n...").unwrap();
158///
159/// match ndm {
160/// MessageType::Opm(opm) => println!("Got OPM"),
161/// MessageType::Oem(oem) => println!("Got OEM"),
162/// _ => println!("Other message type"),
163/// }
164/// ```
165#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
166pub enum MessageType {
167 /// Orbit Ephemeris Message - orbit state time series with optional covariance.
168 #[serde(rename = "oem")]
169 Oem(messages::oem::Oem),
170 /// Conjunction Data Message - collision assessment data between two objects.
171 #[serde(rename = "cdm")]
172 Cdm(messages::cdm::Cdm),
173 /// Orbit Parameter Message - single state vector and orbital parameters.
174 #[serde(rename = "opm")]
175 Opm(messages::opm::Opm),
176 /// Orbit Mean-Elements Message - mean orbital elements (e.g., TLE-like).
177 #[serde(rename = "omm")]
178 Omm(messages::omm::Omm),
179 /// Reentry Data Message - reentry prediction information.
180 #[serde(rename = "rdm")]
181 Rdm(messages::rdm::Rdm),
182 /// Tracking Data Message - ground station tracking measurements.
183 #[serde(rename = "tdm")]
184 Tdm(messages::tdm::Tdm),
185 /// Orbit Comprehensive Message - detailed orbit data with maneuvers.
186 #[serde(rename = "ocm")]
187 Ocm(messages::ocm::Ocm),
188 /// Attitude Comprehensive Message - detailed attitude data with maneuvers.
189 #[serde(rename = "acm")]
190 Acm(messages::acm::Acm),
191 /// Attitude Ephemeris Message - attitude state time series.
192 #[serde(rename = "aem")]
193 Aem(messages::aem::Aem),
194 /// Attitude Parameter Message - attitude state and parameter data.
195 #[serde(rename = "apm")]
196 Apm(messages::apm::Apm),
197 /// Combined Instantiation NDM - container for multiple messages.
198 #[serde(rename = "ndm")]
199 Ndm(messages::ndm::CombinedNdm),
200}
201
202impl MessageType {
203 /// Serialize NDM to a KVN string.
204 pub fn to_kvn(&self) -> Result<String> {
205 match self {
206 MessageType::Oem(msg) => crate::traits::Ndm::to_kvn(msg),
207 MessageType::Cdm(msg) => crate::traits::Ndm::to_kvn(msg),
208 MessageType::Opm(msg) => crate::traits::Ndm::to_kvn(msg),
209 MessageType::Omm(msg) => crate::traits::Ndm::to_kvn(msg),
210 MessageType::Rdm(msg) => crate::traits::Ndm::to_kvn(msg),
211 MessageType::Tdm(msg) => crate::traits::Ndm::to_kvn(msg),
212 MessageType::Ocm(msg) => crate::traits::Ndm::to_kvn(msg),
213 MessageType::Acm(msg) => crate::traits::Ndm::to_kvn(msg),
214 MessageType::Aem(msg) => crate::traits::Ndm::to_kvn(msg),
215 MessageType::Apm(msg) => crate::traits::Ndm::to_kvn(msg),
216 MessageType::Ndm(msg) => crate::traits::Ndm::to_kvn(msg),
217 }
218 }
219
220 /// Serialize NDM to an XML string.
221 pub fn to_xml(&self) -> Result<String> {
222 match self {
223 MessageType::Oem(msg) => crate::traits::Ndm::to_xml(msg),
224 MessageType::Cdm(msg) => crate::traits::Ndm::to_xml(msg),
225 MessageType::Opm(msg) => crate::traits::Ndm::to_xml(msg),
226 MessageType::Omm(msg) => crate::traits::Ndm::to_xml(msg),
227 MessageType::Rdm(msg) => crate::traits::Ndm::to_xml(msg),
228 MessageType::Tdm(msg) => crate::traits::Ndm::to_xml(msg),
229 MessageType::Ocm(msg) => crate::traits::Ndm::to_xml(msg),
230 MessageType::Acm(msg) => crate::traits::Ndm::to_xml(msg),
231 MessageType::Aem(msg) => crate::traits::Ndm::to_xml(msg),
232 MessageType::Apm(msg) => crate::traits::Ndm::to_xml(msg),
233 MessageType::Ndm(msg) => crate::traits::Ndm::to_xml(msg),
234 }
235 }
236
237 /// Write KVN to a file.
238 pub fn to_kvn_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
239 let kvn = self.to_kvn()?;
240 fs::write(path, kvn).map_err(CcsdsNdmError::from)
241 }
242
243 /// Write XML to a file.
244 pub fn to_xml_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
245 let xml = self.to_xml()?;
246 fs::write(path, xml).map_err(CcsdsNdmError::from)
247 }
248}
249
250/// Parse an NDM from a string, auto-detecting the message format (KVN or XML) and type.
251///
252/// This function inspects the input to determine whether it's KVN or XML format,
253/// then parses the appropriate message type based on the version header (KVN) or
254/// root element (XML).
255///
256/// # Arguments
257///
258/// * `s` - The NDM content as a string (KVN or XML format)
259///
260/// # Returns
261///
262/// A [`MessageType`] variant containing the parsed message, or an error if
263/// parsing fails or the message type is not supported.
264///
265/// # Example
266///
267/// ```no_run
268/// use ccsds_ndm::from_str;
269///
270/// let kvn = "CCSDS_OPM_VERS = 3.0\nCREATION_DATE = 2024-01-01\n...";
271/// let ndm = from_str(kvn).unwrap();
272/// ```
273pub fn from_str(s: &str) -> Result<MessageType> {
274 detect::detect_message_type(s)
275}
276
277/// Parse an NDM from a string with explicit validation mode.
278pub fn from_str_with_mode(s: &str, mode: ValidationMode) -> Result<MessageType> {
279 validation::with_validation_mode(mode, || detect::detect_message_type(s))
280}
281
282/// Parse an NDM from a file path, auto-detecting the message format (KVN or XML) and type.
283///
284/// Reads the file contents and delegates to [`from_str`] for parsing.
285///
286/// # Arguments
287///
288/// * `path` - Path to the NDM file
289///
290/// # Returns
291///
292/// A [`MessageType`] variant containing the parsed message, or an error if
293/// the file cannot be read or parsing fails.
294///
295/// # Example
296///
297/// ```no_run
298/// use ccsds_ndm::from_file;
299///
300/// let ndm = from_file("satellite.opm").unwrap();
301/// ```
302pub fn from_file<P: AsRef<Path>>(path: P) -> Result<MessageType> {
303 let content = fs::read_to_string(path).map_err(CcsdsNdmError::from)?;
304 from_str(&content)
305}
306
307/// Parse an NDM from a file with explicit validation mode.
308pub fn from_file_with_mode<P: AsRef<Path>>(path: P, mode: ValidationMode) -> Result<MessageType> {
309 let content = fs::read_to_string(path).map_err(CcsdsNdmError::from)?;
310 from_str_with_mode(&content, mode)
311}