nmea0183_parser/
lib.rs

1//! # A Flexible NMEA Framing Parser for Rust
2//!
3//! [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE-MIT)
4//! [![Apache License 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE-APACHE)
5//! [![docs.rs](https://docs.rs/nmea0183-parser/badge.svg)](https://docs.rs/nmea0183-parser)
6//! [![Crates.io Version](https://img.shields.io/crates/v/nmea0183-parser.svg)](https://crates.io/crates/nmea0183-parser)
7//!
8//! **A zero-allocation NMEA 0183 parser that separates message framing from
9//! content parsing, giving you full control over data handling.**
10//!
11//! This Rust crate provides a generic and configurable parser for NMEA 0183-style
12//! messages, with the typical format:
13//!
14//! ```text
15//! $HHH,D1,D2,...,Dn*CC\r\n
16//! ```
17//!
18//! It focuses on parsing and validating the framing of NMEA 0183-style sentences
19//! (start character, optional checksum, and optional CRLF), allowing you to plug
20//! in your own domain-specific content parsers — or use built-in ones for common
21//! NMEA sentence types.
22//!
23//! ---
24//!
25//! ## ✨ Why Use This Crate?
26//!
27//! Unlike traditional NMEA crates that tightly couple format and content parsing,
28//! `nmea0183_parser` lets you:
29//!
30//! - ✅ Choose your compliance level (strict vs lenient)
31//! - ✅ Plug in your own payload parser (GNSS, marine, custom protocols)
32//! - ✅ Support both `&str` and `&[u8]` inputs
33//! - ✅ Parse without allocations, built on top of [`nom`](https://github.com/Geal/nom),
34//!   a parser combinator library in Rust.
35//!
36//! Perfect for:
37//!
38//! - GNSS/GPS receiver integration
39//! - Marine electronics parsing
40//! - IoT devices consuming NMEA 0183-style protocols
41//! - Debugging or testing tools for embedded equipment
42//! - Legacy formats that resemble NMEA but don’t strictly comply
43//!
44//! ## 📦 Key Features
45//!
46//! - ✅ ASCII-only validation
47//! - ✅ Required or optional checksum validation
48//! - ✅ Required or forbidden CRLF ending enforcement
49//! - ✅ Zero-allocation parsing
50//! - ✅ Built on `nom` combinators
51//! - ✅ Fully pluggable content parser (you bring the domain logic)
52//! - ✅ Optional built-in support for common NMEA sentences
53//!
54//! ---
55//!
56//! ## ⚡ Quick Start
57//!
58//! Here's a minimal example to get you started with parsing NMEA 0183-style sentences:
59//!
60//! ```rust
61//! use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
62//! use nom::Parser;
63//!
64//! // Simple content parser that splits fields by comma
65//! fn parse_fields(input: &str) -> IResult<&str, Vec<&str>> {
66//!     Ok(("", input.split(',').collect()))
67//! }
68//!
69//! // Create parser with strict validation (checksum + CRLF required)
70//! let parser_factory = Nmea0183ParserBuilder::new()
71//!     .checksum_mode(ChecksumMode::Required)
72//!     .line_ending_mode(LineEndingMode::Required);
73//!
74//! let mut parser = parser_factory.build(parse_fields);
75//!
76//! // Parse a GPS sentence
77//! let result =
78//!     parser.parse("$GPGGA,123456.00,4916.29,N,12311.76,W,1,08,0.9,545.4,M,46.9,M,,*73\r\n");
79//!
80//! match result {
81//!     Ok((_remaining, fields)) => {
82//!         println!("Success! Parsed {} fields", fields.len()); // 15 fields
83//!         println!("Sentence type: {}", fields[0]); // "GPGGA"
84//!     }
85//!     Err(e) => println!("Parse error: {:?}", e),
86//! }
87//! ```
88//!
89//! For custom parsing logic, you can define your own content parser. The `Nmea0183ParserBuilder`
90//! creates a parser factory that you then call with your content parser:
91//!
92//! ```rust
93//! use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
94//! use nom::Parser;
95//!
96//! // Your custom logic for the inner data portion (after "$" and before "*CC").
97//! // The `parse_content` function should return an IResult<Input, Output> type
98//! // so it can be used with the framing parser.
99//! // The `Output` type can be any Rust type you define, such as a struct or an enum.
100//! fn parse_content(input: &str) -> IResult<&str, Vec<&str>> {
101//!     // You can decode fields here. In this example, we split the input by commas.
102//!     Ok(("", input.split(',').collect()))
103//! }
104//!
105//! // The `Nmea0183ParserBuilder` creates a parser factory that you then call
106//! // with your content parser.
107//! let parser_factory = Nmea0183ParserBuilder::new()
108//!     .checksum_mode(ChecksumMode::Required)
109//!     .line_ending_mode(LineEndingMode::Required);
110//!
111//! let mut parser = parser_factory.build(parse_content);
112//!
113//! // Or combine into one line:
114//! // let mut parser = Nmea0183ParserBuilder::new()
115//! //     .checksum_mode(ChecksumMode::Required)
116//! //     .line_ending_mode(LineEndingMode::Required)
117//! //     .build(parse_content);
118//!
119//! // Now you can use the parser to parse your NMEA sentences.
120//! match parser.parse("$Header,field1,field2*3C\r\n") {
121//!     Ok((remaining, fields)) => {
122//!         assert_eq!(remaining, "");
123//!         assert_eq!(fields, vec!["Header", "field1", "field2"]);
124//!     }
125//!     Err(e) => println!("Parse error: {:?}", e),
126//! }
127//! ```
128//!
129//! ## 🧐 How It Works
130//!
131//! 1. **Framing parser** handles the outer structure:
132//!
133//!    - ASCII-only validation
134//!    - Start delimiter (`$`)
135//!    - Optional checksum validation (`*CC`)
136//!    - Optional CRLF endings (`\r\n`)
137//!
138//! 2. **Your content parser**, or built-in ones, handle the inner data (`D1,D2,...,Dn`):
139//!
140//!    - Field parsing and validation
141//!    - Type conversion
142//!    - Domain-specific logic
143//!
144//! You have full control over sentence content interpretation.
145//!
146//! In the above example, `parse_content` is your custom logic that processes the inner
147//! data of the sentence. The `Nmea0183ParserBuilder` creates a parser that handles the
148//! framing, while you focus on the content.
149//!
150//! ---
151//!
152//! ## 🔧 Configuration Options
153//!
154//! You can configure the parser's behavior using `ChecksumMode` and `LineEndingMode`:
155//!
156//! ```rust
157//! use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
158//! use nom::Parser;
159//!
160//! fn content_parser(input: &str) -> IResult<&str, bool> {
161//!     Ok((input, true))
162//! }
163//!
164//! // Strict: checksum and CRLF both required
165//! let mut strict_parser = Nmea0183ParserBuilder::new()
166//!     .checksum_mode(ChecksumMode::Required)
167//!     .line_ending_mode(LineEndingMode::Required)
168//!     .build(content_parser);
169//!
170//! assert!(strict_parser.parse("$GPGGA,data*6A\r\n").is_ok());
171//! assert!(strict_parser.parse("$GPGGA,data*6A").is_err()); // (missing CRLF)
172//! assert!(strict_parser.parse("$GPGGA,data\r\n").is_err()); // (missing checksum)
173//!
174//! // Checksum required, no CRLF allowed
175//! let mut no_crlf_parser = Nmea0183ParserBuilder::new()
176//!     .checksum_mode(ChecksumMode::Required)
177//!     .line_ending_mode(LineEndingMode::Forbidden)
178//!     .build(content_parser);
179//!
180//! assert!(no_crlf_parser.parse("$GPGGA,data*6A").is_ok());
181//! assert!(no_crlf_parser.parse("$GPGGA,data*6A\r\n").is_err()); // (CRLF present)
182//! assert!(no_crlf_parser.parse("$GPGGA,data").is_err()); // (missing checksum)
183//!
184//! // Checksum optional, CRLF required
185//! let mut optional_checksum_parser = Nmea0183ParserBuilder::new()
186//!     .checksum_mode(ChecksumMode::Optional)
187//!     .line_ending_mode(LineEndingMode::Required)
188//!     .build(content_parser);
189//!
190//! assert!(optional_checksum_parser.parse("$GPGGA,data*6A\r\n").is_ok()); // (with valid checksum)
191//! assert!(optional_checksum_parser.parse("$GPGGA,data\r\n").is_ok()); // (without checksum)
192//! assert!(optional_checksum_parser.parse("$GPGGA,data*99\r\n").is_err()); // (invalid checksum)
193//! assert!(optional_checksum_parser.parse("$GPGGA,data*6A").is_err()); // (missing CRLF)
194//!
195//! // Lenient: checksum optional, CRLF forbidden
196//! let mut lenient_parser = Nmea0183ParserBuilder::new()
197//!     .checksum_mode(ChecksumMode::Optional)
198//!     .line_ending_mode(LineEndingMode::Forbidden)
199//!     .build(content_parser);
200//!
201//! assert!(lenient_parser.parse("$GPGGA,data*6A").is_ok()); // (with valid checksum)
202//! assert!(lenient_parser.parse("$GPGGA,data").is_ok()); // (without checksum)
203//! assert!(lenient_parser.parse("$GPGGA,data*99").is_err()); // (invalid checksum)
204//! assert!(lenient_parser.parse("$GPGGA,data\r\n").is_err()); // (CRLF present)
205//! ```
206//!
207//! ---
208//!
209//! ## 🔍 Parsing Both String and Byte Inputs
210//!
211//! The parser can handle both `&str` and `&[u8]` inputs. You can define your content
212//! parser for either type; the factory will adapt accordingly.
213//!
214//! ```rust
215//! use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
216//! use nom::Parser;
217//!
218//! fn parse_content_str(input: &str) -> IResult<&str, Vec<&str>> {
219//!     Ok(("", input.split(',').collect()))
220//! }
221//!
222//! let mut parser_str = Nmea0183ParserBuilder::new()
223//!     .checksum_mode(ChecksumMode::Required)
224//!     .line_ending_mode(LineEndingMode::Required)
225//!     .build(parse_content_str);
226//!
227//! // Parse from string
228//! let string_input = "$Header,field1,field2*3C\r\n";
229//! let result = parser_str.parse(string_input);
230//!
231//! assert!(result.is_ok());
232//! assert_eq!(result.unwrap().1, vec!["Header", "field1", "field2"]);
233//!
234//! fn parse_content_bytes(input: &[u8]) -> IResult<&[u8], u8> {
235//!     let (input, first_byte) = nom::number::complete::u8(input)?;
236//!     Ok((input, first_byte))
237//! }
238//!
239//! let mut parser_bytes = Nmea0183ParserBuilder::new()
240//!     .checksum_mode(ChecksumMode::Required)
241//!     .line_ending_mode(LineEndingMode::Required)
242//!     .build(parse_content_bytes);
243//!
244//! // Parse from bytes
245//! let byte_input = b"$Header,field1,field2*3C\r\n";
246//! let result_bytes = parser_bytes.parse(byte_input);
247//!
248//! assert!(result_bytes.is_ok());
249//! assert_eq!(result_bytes.unwrap().1, 72); // 'H' is the first byte of the content
250//! ```
251//!
252//! ---
253//!
254//! ## 🧩 `NmeaParse` trait and `#[derive(NmeaParse)]` Macro
255//!
256//! The `NmeaParse` trait provides a generic interface for parsing values from NMEA 0183-style
257//! content, supporting both primitive and composite types. Implementations are provided for
258//! primitive types, `Option<T>`, `Vec<T>`, and more types, and you can implement this trait
259//! for your own types to enable custom parsing logic.
260//!
261//! ### Implementing the `NmeaParse` Trait
262//!
263//! To implement the `NmeaParse` trait for your type, you need to provide a `parse` method that
264//! takes an input and returns an `IResult` with the remaining input and the parsed value.
265//!
266//! NMEA 0183 fields are typically comma-separated. When parsing composite types (like structs),
267//! you usually want to consume the separator before parsing each subsequent field. However, for
268//! optional fields (`Option<T>`) or repeated fields (`Vec<T>`), always consuming the separator
269//! can cause issues if the field is missing.
270//!
271//! To address this, the trait provides a `parse_preceded(separator)` method. This method ensures
272//! the separator is only consumed if the field is present. By default, `parse_preceded` is
273//! implemented as a simple wrapper around `preceded(separator, Self::parse)`, but you can override
274//! it for custom behavior—such as the implementations for `Option<T>` and `Vec<T>`.
275//!
276//! This design gives you fine-grained control over field parsing and separator handling, making
277//! it easy to implement robust NMEA content parsers for your own types.
278//!
279//! ### Deriving the `NmeaParse` Trait
280//!
281//! Based on [`nom-derive`](https://crates.io/crates/nom-derive) and with a lot of similarities, `NmeaParse` is a custom derive
282//! attribute to derive content parsers for your NMEA 0183-style data structures.
283//!
284//! The `NmeaParse` derive macro automatically generates an implementation of the `NmeaParse` trait
285//! for your structs and enums using `nom` parsers when possible. This allows you to define your
286//! data structures and derive parsing logic without writing boilerplate code.
287//!
288//! For example, you can define a struct and derive the `NmeaParse` trait like this:
289//!
290//! ```rust
291//! use nmea0183_parser::NmeaParse;
292//!
293//! #[derive(NmeaParse)]
294//! struct Data {
295//!     pub id: u8,
296//!     pub value: f64,
297//!     pub timestamp: u64,
298//! }
299//! ```
300//!
301//! This will generate an implementation of the `NmeaParse` trait for the `Data` struct,
302//! allowing you to parse NMEA 0183-style input into instances of `Data`.
303//! The generated code will look something like this (simplified):
304//!
305//! ```rust,ignore
306//! impl NmeaParse for Data {
307//!     fn parse(i: &'a str) -> nmea0183_parser::IResult<&'a str, Self, Error> {
308//!         let (i, id) = <u8>::parse(i)?;
309//!         let (i, value) = <f64>::parse_preceded(nom::character::complete::char(',')).parse(i)?;
310//!         let (i, timestamp) = <u64>::parse_preceded(nom::character::complete::char(',')).parse(i)?;
311//!
312//!         Ok((i, Data { id, value, timestamp }))
313//!     }
314//! }
315//! ```
316//!
317//! You can now parse an input containing NMEA 0183-style content into a `Data` struct:
318//!
319//! ```rust
320//! # use nmea0183_parser::{IResult, NmeaParse};
321//! # use nom::error::ParseError;
322//! #
323//! # #[derive(NmeaParse)]
324//! # struct Data {
325//! #     pub id: u8,
326//! #     pub value: f64,
327//! #     pub timestamp: u64,
328//! # }
329//! let input = "123,45.67,1622547800";
330//! let result: IResult<_, _> = Data::parse(input);
331//! let (remaining, data) = result.unwrap();
332//! assert!(remaining.is_empty());
333//! assert_eq!(data.id, 123);
334//! assert_eq!(data.value, 45.67);
335//! assert_eq!(data.timestamp, 1622547800);
336//! ```
337//!
338//! The macro also supports enums, which require a `selector` attribute to determine which
339//! variant to parse:
340//!
341//! ```rust
342//! use nmea0183_parser::{Error, IResult, NmeaParse};
343//!
344//! # #[derive(Debug)]
345//! #[derive(NmeaParse)]
346//! #[nmea(selector(u8::parse))]
347//! enum Data {
348//!     #[nmea(selector(0))]
349//!     TypeA { id: u8, value: u16 },
350//!     #[nmea(selector(1))]
351//!     TypeB { values: [u8; 4] },
352//! }
353//!
354//! let input = "0,42,100";
355//! let result: IResult<_, _> = Data::parse(input);
356//! assert!(matches!(result, Ok((_, Data::TypeA { id: 42, value: 100 }))));
357//!
358//! let input = "1,2,3,4,5";
359//! let result: IResult<_, _> = Data::parse(input);
360//! assert!(matches!(result, Ok((_, Data::TypeB { values: [2, 3, 4, 5] }))));
361//!
362//! let input = "2,42";
363//! // Expecting an error because no variant matches selector 2
364//! let error = Data::parse(input).unwrap_err();
365//! assert!(matches!(error,
366//!     nom::Err::Error(Error::ParsingError(nom::error::Error {
367//!         code: nom::error::ErrorKind::Switch,
368//!         ..
369//!     }))
370//! ));
371//! ```
372//!
373//! You can use the `#[derive(NmeaParse)]` attribute on your structs and enums to automatically
374//! generate parsing logic based on the field types. The macro will try to infer parsers for
375//! known types (implementors of the `NmeaParse` trait), but you can also customize the parsing
376//! behavior using attributes.
377//!
378//! For more details on how to use the `NmeaParse` derive macro and customize parsing behavior,
379//! refer to the [documentation](https://docs.rs/nmea0183-parser/latest/nmea0183_parser/derive.NmeaParse.html).
380//!
381//! ---
382//!
383//! ## 🧱 Built-in NMEA Sentence Content Parser
384//!
385//! Alongside the flexible framing parser, this crate can provide a built-in `NmeaSentence`
386//! content parser for common NMEA 0183 sentence types. To use it, enable the `nmea-content`
387//! feature in your `Cargo.toml`.
388//!
389//! This parser uses the `NmeaParse` trait to provide content-only parsing. It does not handle
390//! framing — such as the initial `$`, optional checksum (`*CC`), or optional CRLF (`\r\n`).
391//! That responsibility belongs to the framing parser, which wraps around the content parser.
392//!
393//! To parse a complete NMEA sentence, you can use the `Nmea0183ParserBuilder` with the built-in
394//! content parser:
395//!
396//! ```rust
397//! use nmea0183_parser::{
398//!     IResult, Nmea0183ParserBuilder, NmeaParse,
399//!     nmea_content::{GGA, Location, NmeaSentence, Quality},
400//! };
401//! use nom::Parser;
402//!
403//! // Defaults to strict parsing with both checksum and CRLF required
404//! let mut nmea_parser = Nmea0183ParserBuilder::new().build(NmeaSentence::parse);
405//!
406//! let result: IResult<_, _> =
407//!     nmea_parser.parse("$GPGGA,123456.00,4916.29,N,12311.76,W,1,08,0.9,545.4,M,46.9,M,,*73\r\n");
408//!
409//! assert!(
410//!     result.is_ok(),
411//!     "Failed to parse NMEA sentence: {:?}",
412//!     result.unwrap_err()
413//! );
414//!
415//! let (_, sentence) = result.unwrap();
416//! assert!(matches!(
417//!     sentence,
418//!     NmeaSentence::GGA(GGA {
419//!         location: Some(Location {
420//!             latitude: 49.2715,
421//!             longitude: -123.196,
422//!         }),
423//!         fix_quality: Quality::GPSFix,
424//!         satellite_count: Some(8),
425//!         hdop: Some(0.9),
426//!         ..
427//!     })
428//! ));
429//! ```
430//!
431//! > **Note:** While the `Nmea0183ParserBuilder` framing parser can accept both `&str` and `&[u8]`
432//! > inputs, the built-in content parser only accepts `&str`, as it is designed specifically for
433//! > text-based NMEA sentences.
434//!
435//! ### Supported NMEA Sentences
436//!
437//! - [`DBT`](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) - Depth Below Transducer
438//! - [`DPT`](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) - Depth of Water
439//! - [`GGA`](https://gpsd.gitlab.io/gpsd/NMEA.html#_gga_global_positioning_system_fix_data) - Global Positioning System Fix Data
440//! - [`GLL`](https://gpsd.gitlab.io/gpsd/NMEA.html#_gll_geographic_position_latitudelongitude) - Geographic Position: Latitude/Longitude
441//! - [`GSA`](https://gpsd.gitlab.io/gpsd/NMEA.html#_gsa_gps_dop_and_active_satellites) - GPS DOP and Active Satellites
442//! - [`GSV`](https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view) - Satellites in View
443//! - [`RMC`](https://gpsd.gitlab.io/gpsd/NMEA.html#_rmc_recommended_minimum_navigation_information) - Recommended Minimum Navigation Information
444//! - [`VTG`](https://gpsd.gitlab.io/gpsd/NMEA.html#_vtg_track_made_good_and_ground_speed) - Track made good and Ground speed
445//! - [`ZDA`](https://gpsd.gitlab.io/gpsd/NMEA.html#_zda_time_date_utc_day_month_year_and_local_time_zone) - Time & Date: UTC, day, month, year and local time zone
446//!
447//! ### NMEA Version Support
448//!
449//! Different NMEA versions may include additional fields in certain sentence types.
450//! You can choose the version that matches your equipment by enabling the appropriate feature flags.
451//!
452//! | Feature Flag   | NMEA Version | When to Use                |
453//! | -------------- | ------------ | -------------------------- |
454//! | `nmea-content` | Pre-2.3      | Standard NMEA parsing      |
455//! | `nmea-v2-3`    | NMEA 2.3     | Older GPS/marine equipment |
456//! | `nmea-v3-0`    | NMEA 3.0     | Mid-range equipment        |
457//! | `nmea-v4-11`   | NMEA 4.11    | Modern equipment           |
458//!
459//! For specific field differences between versions, please refer to the
460//! [NMEA 0183 standard documentation](https://gpsd.gitlab.io/gpsd/NMEA.html).
461
462#![cfg_attr(docsrs, feature(doc_cfg))]
463
464mod error;
465mod nmea0183;
466#[cfg(feature = "nmea-content")]
467#[cfg_attr(docsrs, doc(cfg(feature = "nmea-content")))]
468pub mod nmea_content;
469mod parse;
470
471pub use error::{Error, IResult};
472pub use nmea0183::{ChecksumMode, LineEndingMode, Nmea0183ParserBuilder};
473#[cfg(feature = "derive")]
474#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
475pub use nmea0183_derive::NmeaParse;
476pub use parse::NmeaParse;