Skip to main content

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}