aviso_validators/
coordinate.rs1use serde_json::{Number, Value};
12use thiserror::Error;
13
14pub type Coordinate = (f64, f64);
16
17#[derive(Debug, Error, PartialEq)]
19#[non_exhaustive]
20pub enum CoordinateError {
21 #[error("coordinate must be a two-element array [lat,lon]")]
23 InvalidShape,
24 #[error("{component} must be a number")]
26 NotNumber { component: &'static str },
27 #[error("{component} must be finite")]
29 NotFinite { component: &'static str },
30 #[error("latitude {value} is outside the valid range [-90, 90]")]
32 LatitudeOutOfRange { value: f64 },
33 #[error("longitude {value} is outside the valid range [-180, 180]")]
35 LongitudeOutOfRange { value: f64 },
36}
37
38pub fn parse_coordinate(value: &Value) -> Result<Coordinate, CoordinateError> {
42 let pair = value.as_array().ok_or(CoordinateError::InvalidShape)?;
43 if pair.len() != 2 {
44 return Err(CoordinateError::InvalidShape);
45 }
46
47 let lat = parse_component(&pair[0], "latitude")?;
48 let lon = parse_component(&pair[1], "longitude")?;
49 validate_coordinate(lat, lon)
50}
51
52pub fn validate_coordinate(lat: f64, lon: f64) -> Result<Coordinate, CoordinateError> {
54 if !lat.is_finite() {
55 return Err(CoordinateError::NotFinite {
56 component: "latitude",
57 });
58 }
59 if !lon.is_finite() {
60 return Err(CoordinateError::NotFinite {
61 component: "longitude",
62 });
63 }
64 if !(-90.0..=90.0).contains(&lat) {
65 return Err(CoordinateError::LatitudeOutOfRange { value: lat });
66 }
67 if !(-180.0..=180.0).contains(&lon) {
68 return Err(CoordinateError::LongitudeOutOfRange { value: lon });
69 }
70 Ok((lat, lon))
71}
72
73pub fn coordinates_to_json(coordinates: &[Coordinate]) -> Result<Value, CoordinateError> {
75 coordinates
76 .iter()
77 .map(|&(lat, lon)| coordinate_to_json(validate_coordinate(lat, lon)?))
78 .collect::<Result<Vec<_>, _>>()
79 .map(Value::Array)
80}
81
82pub fn coordinate_to_json(coordinate: Coordinate) -> Result<Value, CoordinateError> {
84 let (lat, lon) = validate_coordinate(coordinate.0, coordinate.1)?;
85 let latitude = Number::from_f64(lat).ok_or(CoordinateError::NotFinite {
86 component: "latitude",
87 })?;
88 let longitude = Number::from_f64(lon).ok_or(CoordinateError::NotFinite {
89 component: "longitude",
90 })?;
91 Ok(Value::Array(vec![
92 Value::Number(latitude),
93 Value::Number(longitude),
94 ]))
95}
96
97fn parse_component(value: &Value, component: &'static str) -> Result<f64, CoordinateError> {
98 let parsed = value
99 .as_f64()
100 .ok_or(CoordinateError::NotNumber { component })?;
101 if !parsed.is_finite() {
102 return Err(CoordinateError::NotFinite { component });
103 }
104 Ok(parsed)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::{CoordinateError, coordinates_to_json, parse_coordinate};
110 use serde_json::json;
111
112 #[test]
113 fn parses_and_canonicalizes_coordinate() {
114 let coordinate = parse_coordinate(&json!([52.5500, 13.5000])).unwrap();
115 assert_eq!(coordinate, (52.55, 13.5));
116 assert_eq!(
117 coordinates_to_json(&[coordinate]).unwrap(),
118 json!([[52.55, 13.5]])
119 );
120 }
121
122 #[test]
123 fn rejects_malformed_and_out_of_range_coordinates() {
124 assert_eq!(
125 parse_coordinate(&json!([1.0])),
126 Err(CoordinateError::InvalidShape)
127 );
128 assert!(matches!(
129 parse_coordinate(&json!([91.0, 0.0])),
130 Err(CoordinateError::LatitudeOutOfRange { .. })
131 ));
132 assert!(matches!(
133 parse_coordinate(&json!([0.0, 181.0])),
134 Err(CoordinateError::LongitudeOutOfRange { .. })
135 ));
136 }
137}