Skip to main content

aviso_validators/
point.rs

1// (C) Copyright 2024- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9use anyhow::{Result, bail};
10use tracing::debug;
11
12/// Point coordinate validator.
13///
14/// Accepts `lat,lon` and `(lat,lon)` input.
15pub struct PointHandler;
16
17impl PointHandler {
18    pub fn validate_and_canonicalize(value: &str, field_name: &str) -> Result<String> {
19        let (lat, lon) = Self::parse_point_coordinates(value)?;
20        debug!(
21            field = field_name,
22            lat = lat,
23            lon = lon,
24            "Point validated successfully"
25        );
26        Ok(format!("{},{}", lat, lon))
27    }
28
29    pub fn parse_point_coordinates(value: &str) -> Result<(f64, f64)> {
30        let trimmed = value.trim();
31        if trimmed.is_empty() {
32            bail!("Point coordinate string cannot be empty");
33        }
34
35        let inner = trimmed
36            .strip_prefix('(')
37            .and_then(|s| s.strip_suffix(')'))
38            .unwrap_or(trimmed)
39            .trim();
40
41        let mut parts = inner.split(',').map(str::trim);
42        let lat_str = parts
43            .next()
44            .ok_or_else(|| anyhow::anyhow!("Point must be in 'lat,lon' format"))?;
45        let lon_str = parts
46            .next()
47            .ok_or_else(|| anyhow::anyhow!("Point must be in 'lat,lon' format"))?;
48
49        if parts.next().is_some() {
50            bail!("Point must contain exactly two values: 'lat,lon'");
51        }
52
53        let lat: f64 = lat_str
54            .parse()
55            .map_err(|_| anyhow::anyhow!("Invalid latitude value: {}", lat_str))?;
56        let lon: f64 = lon_str
57            .parse()
58            .map_err(|_| anyhow::anyhow!("Invalid longitude value: {}", lon_str))?;
59
60        if !(-90.0..=90.0).contains(&lat) {
61            bail!("Latitude out of range [-90, 90]: {}", lat);
62        }
63        if !(-180.0..=180.0).contains(&lon) {
64            bail!("Longitude out of range [-180, 180]: {}", lon);
65        }
66
67        Ok((lat, lon))
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::PointHandler;
74
75    #[test]
76    fn parse_point_coordinates_accepts_basic_format() {
77        let (lat, lon) = PointHandler::parse_point_coordinates("52.55,13.5").unwrap();
78        assert_eq!(lat, 52.55);
79        assert_eq!(lon, 13.5);
80    }
81
82    #[test]
83    fn parse_point_coordinates_accepts_parenthesized_format() {
84        let (lat, lon) = PointHandler::parse_point_coordinates("(52.55,13.5)").unwrap();
85        assert_eq!(lat, 52.55);
86        assert_eq!(lon, 13.5);
87    }
88
89    #[test]
90    fn parse_point_coordinates_rejects_bad_format() {
91        assert!(PointHandler::parse_point_coordinates("52.55").is_err());
92        assert!(PointHandler::parse_point_coordinates("52.55,13.5,1.0").is_err());
93    }
94
95    #[test]
96    fn parse_point_coordinates_rejects_out_of_range() {
97        assert!(PointHandler::parse_point_coordinates("91,13.5").is_err());
98        assert!(PointHandler::parse_point_coordinates("52.5,181").is_err());
99    }
100
101    #[test]
102    fn validate_and_canonicalize_returns_canonical_form() {
103        let canonical = PointHandler::validate_and_canonicalize("(52.5500,13.5000)", "p").unwrap();
104        assert_eq!(canonical, "52.55,13.5");
105    }
106}