From a8a655f388c3acceec3e80e70d2720d929e88b67 Mon Sep 17 00:00:00 2001 From: Paul van Tilburg Date: Thu, 29 Sep 2022 20:51:30 +0200 Subject: [PATCH] Validate latitude/longitude coordinates for WGS-84 Implement this for the parser as well as the builder. Also rename `CrsId` to `CoordRefSystem` and the `GeoUri.crs_id` field to `GeoUri.crs`. --- src/lib.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dc748df..e893a94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,18 +36,51 @@ const URI_SCHEME_NAME: &str = "geo"; /// [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870). #[non_exhaustive] #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum CrsId { +pub enum CoordRefSystem { /// The WGS-84 coordinate reference system. Wgs84, } -impl Default for CrsId { +impl CoordRefSystem { + /// Validates geo location coordinates against the selected coordinate reference system. + /// + /// # Examples + /// + /// ```rust + /// # use geo_uri::{CoordRefSystem, ParseError}; + /// let crs = CoordRefSystem::Wgs84; + /// assert_eq!(crs.validate(52.107, 5.134), Ok(())); + /// assert_eq!( + /// crs.validate(100.0, 5.134), // Latitude not in range `-90.0..=90.0`! + /// Err(ParseError::OutOfRangeLatitudeCoord) + /// ); + /// assert_eq!( + /// crs.validate(51.107, -200.0), // Longitude not in range `-180.0..=180.0`! + /// Err(ParseError::OutOfRangeLongitudeCoord) + /// ); + /// ``` + pub fn validate(&self, latitude: f64, longitude: f64) -> Result<(), ParseError> { + // This holds only for WGS-84, but it is the only one supported right now! + if !(-90.0..=90.0).contains(&latitude) { + return Err(ParseError::OutOfRangeLatitudeCoord); + } + + // This holds only for WGS-84, but it is the only one supported right now! + if !(-180.0..=180.0).contains(&longitude) { + return Err(ParseError::OutOfRangeLongitudeCoord); + } + + Ok(()) + } +} + +impl Default for CoordRefSystem { fn default() -> Self { Self::Wgs84 } } -/// Possible geo URI parse errors. +/// Possible geo URI errors. #[derive(Debug, Error, Eq, PartialEq)] pub enum ParseError { /// The geo URI is missing a proper scheme, i.e. the prefix `geo:`. @@ -71,6 +104,16 @@ pub enum ParseError { /// The geo URI contains an unparsable/invalid (uncertainty) distance. #[error("Invalid distance in geo URI: {0}")] InvalidDistance(ParseIntError), + /// The latitude coordinate is out of range of `-90..=90` degrees. + /// + /// This can only fail for the WGS-84 coordinate reference system. + #[error("Latitude coordinate is out of range")] + OutOfRangeLatitudeCoord, + /// The longitude coordinate is out of range of `-180..=180` degrees + /// + /// This can only fail for the WGS-84 coordinate reference system. + #[error("Longitude coordinate is out of range")] + OutOfRangeLongitudeCoord, } /// A uniform resource identifier for geographic locations (geo URI). @@ -133,13 +176,20 @@ pub enum ParseError { /// /// For the proposed IEEE standard, see [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870). #[derive(Builder, Copy, Clone, Debug, Default)] +#[builder(build_fn(validate = "Self::validate"))] pub struct GeoUri { /// The coordinate reference system used by the coordinates of this URI. #[builder(default)] - pub crs_id: CrsId, + pub crs: CoordRefSystem, /// The latitude coordinate of a location. + /// + /// For the WGS-84 coordinate reference system, this should be in the range of + /// `-90.0` up until including `90.0` degrees. pub latitude: f64, /// The longitude coordinate of a location. + /// + /// For the WGS-84 coordinate reference system, this should be in the range of + /// `-180.0` up until including `180.0` degrees. pub longitude: f64, /// The altitude coordinate of a location, if provided. #[builder(default, setter(strip_option))] @@ -195,7 +245,7 @@ impl GeoUri { // It can be followed by a "u" parameter or that can be the first one. // All other parameters are ignored. let mut param_parts = parts.flat_map(|part| part.split_once('=')); - let (crs_id, uncertainty) = match param_parts.next() { + let (crs, uncertainty) = match param_parts.next() { Some(("crs", value)) => { if value.to_ascii_lowercase() != "wgs84" { return Err(ParseError::InvalidCoordRefSystem); @@ -203,21 +253,23 @@ impl GeoUri { match param_parts.next() { Some(("u", value)) => ( - CrsId::Wgs84, + CoordRefSystem::Wgs84, Some(value.parse().map_err(ParseError::InvalidDistance)?), ), - Some(_) | None => (CrsId::Wgs84, None), + Some(_) | None => (CoordRefSystem::Wgs84, None), } } Some(("u", value)) => ( - CrsId::default(), + CoordRefSystem::default(), Some(value.parse().map_err(ParseError::InvalidDistance)?), ), - Some(_) | None => (CrsId::default(), None), + Some(_) | None => (CoordRefSystem::default(), None), }; + crs.validate(latitude, longitude)?; + Ok(GeoUri { - crs_id, + crs, latitude, longitude, altitude, @@ -267,9 +319,9 @@ impl TryFrom<&str> for GeoUri { impl PartialEq for GeoUri { fn eq(&self, other: &Self) -> bool { // In the WGS-84 CRS the the longitude is ignored for the poles. - let ignore_longitude = self.crs_id == CrsId::Wgs84 && self.latitude.abs() == 90.0; + let ignore_longitude = self.crs == CoordRefSystem::Wgs84 && self.latitude.abs() == 90.0; - self.crs_id == other.crs_id + self.crs == other.crs && self.latitude == other.latitude && (ignore_longitude || self.longitude == other.longitude) && self.altitude == other.altitude @@ -277,6 +329,19 @@ impl PartialEq for GeoUri { } } +impl GeoUriBuilder { + /// Validates the coordinates against the + fn validate(&self) -> Result<(), String> { + self.crs + .unwrap_or_default() + .validate( + self.latitude.unwrap_or_default(), + self.longitude.unwrap_or_default(), + ) + .map_err(|e| format!("{e}")) + } +} + #[cfg(test)] mod tests { use float_eq::assert_float_eq; @@ -284,8 +349,22 @@ mod tests { use super::*; #[test] - fn crs_id_default() { - assert_eq!(CrsId::default(), CrsId::Wgs84); + fn coord_ref_system_default() { + assert_eq!(CoordRefSystem::default(), CoordRefSystem::Wgs84); + } + + #[test] + fn coord_ref_system_validate() { + let crs = CoordRefSystem::Wgs84; + assert_eq!(crs.validate(52.107, 5.134), Ok(())); + assert_eq!( + crs.validate(100.0, 5.134), + Err(ParseError::OutOfRangeLatitudeCoord) + ); + assert_eq!( + crs.validate(51.107, -200.0), + Err(ParseError::OutOfRangeLongitudeCoord) + ); } #[test] @@ -363,7 +442,7 @@ mod tests { assert!(matches!(geo_uri, Err(ParseError::InvalidCoordRefSystem))); let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;crs=wgs84")?; - assert!(matches!(geo_uri.crs_id, CrsId::Wgs84)); + assert!(matches!(geo_uri.crs, CoordRefSystem::Wgs84)); // TODO: Add exmaples from RFC 5870! @@ -373,7 +452,7 @@ mod tests { #[test] fn geo_uri_display() { let mut geo_uri = GeoUri { - crs_id: CrsId::Wgs84, + crs: CoordRefSystem::Wgs84, latitude: 52.107, longitude: 5.134, altitude: None,