aviso_validators/
point.rs1use crate::coordinate::{Coordinate, coordinate_to_json, parse_coordinate, validate_coordinate};
10use anyhow::{Result, bail};
11use serde_json::Value;
12use tracing::debug;
13
14pub struct PointHandler;
18
19impl PointHandler {
20 pub fn validate_and_canonicalize(value: &str, field_name: &str) -> Result<String> {
21 let (lat, lon) = Self::parse_point_coordinates(value)?;
22 debug!(
23 field = field_name,
24 lat = lat,
25 lon = lon,
26 "Point validated successfully"
27 );
28 Ok(format!("{lat},{lon}"))
29 }
30
31 pub fn validate_json_and_canonicalize(value: &Value, field_name: &str) -> Result<Value> {
33 let coordinate = Self::parse_point_value(value).map_err(|error| {
34 anyhow::anyhow!("field '{}' must be a valid point: {}", field_name, error)
35 })?;
36 coordinate_to_json(coordinate).map_err(Into::into)
37 }
38
39 pub fn parse_point_value(value: &Value) -> Result<Coordinate> {
41 match value {
42 Value::Array(_) => parse_coordinate(value).map_err(Into::into),
43 Value::String(value) => Self::parse_point_coordinates(value),
44 _ => bail!("point must be a two-element array [lat,lon] or a compatible string"),
45 }
46 }
47
48 pub fn parse_point_coordinates(value: &str) -> Result<(f64, f64)> {
49 let trimmed = value.trim();
50 if trimmed.is_empty() {
51 bail!("Point coordinate string cannot be empty");
52 }
53
54 if trimmed.starts_with('[') {
55 let value: Value = serde_json::from_str(trimmed)
56 .map_err(|error| anyhow::anyhow!("invalid point JSON array: {}", error))?;
57 return Self::parse_point_value(&value);
58 }
59
60 let inner = trimmed
61 .strip_prefix('(')
62 .and_then(|s| s.strip_suffix(')'))
63 .unwrap_or(trimmed)
64 .trim();
65
66 let mut parts = inner.split(',').map(str::trim);
67 let lat_str = parts
68 .next()
69 .ok_or_else(|| anyhow::anyhow!("Point must be in 'lat,lon' format"))?;
70 let lon_str = parts
71 .next()
72 .ok_or_else(|| anyhow::anyhow!("Point must be in 'lat,lon' format"))?;
73
74 if parts.next().is_some() {
75 bail!("Point must contain exactly two values: 'lat,lon'");
76 }
77
78 let lat: f64 = lat_str
79 .parse()
80 .map_err(|_| anyhow::anyhow!("Invalid latitude value: {}", lat_str))?;
81 let lon: f64 = lon_str
82 .parse()
83 .map_err(|_| anyhow::anyhow!("Invalid longitude value: {}", lon_str))?;
84
85 validate_coordinate(lat, lon).map_err(Into::into)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::PointHandler;
92
93 #[test]
94 fn parse_point_coordinates_accepts_basic_format() {
95 let (lat, lon) = PointHandler::parse_point_coordinates("52.55,13.5").unwrap();
96 assert_eq!(lat, 52.55);
97 assert_eq!(lon, 13.5);
98 }
99
100 #[test]
101 fn parse_point_coordinates_accepts_parenthesized_format() {
102 let (lat, lon) = PointHandler::parse_point_coordinates("(52.55,13.5)").unwrap();
103 assert_eq!(lat, 52.55);
104 assert_eq!(lon, 13.5);
105 }
106
107 #[test]
108 fn parse_point_coordinates_accepts_canonical_json_string() {
109 let point = PointHandler::parse_point_coordinates("[52.55,13.5]").unwrap();
110 assert_eq!(point, (52.55, 13.5));
111 }
112
113 #[test]
114 fn parse_point_coordinates_rejects_bad_format() {
115 assert!(PointHandler::parse_point_coordinates("52.55").is_err());
116 assert!(PointHandler::parse_point_coordinates("52.55,13.5,1.0").is_err());
117 }
118
119 #[test]
120 fn parse_point_coordinates_rejects_out_of_range() {
121 assert!(PointHandler::parse_point_coordinates("91,13.5").is_err());
122 assert!(PointHandler::parse_point_coordinates("52.5,181").is_err());
123 }
124
125 #[test]
126 fn validate_and_canonicalize_returns_canonical_form() {
127 let canonical = PointHandler::validate_and_canonicalize("(52.5500,13.5000)", "p").unwrap();
128 assert_eq!(canonical, "52.55,13.5");
129 }
130
131 #[test]
132 fn json_array_is_canonical() {
133 let canonical = PointHandler::validate_json_and_canonicalize(
134 &serde_json::json!([52.5500, 13.5000]),
135 "point",
136 )
137 .unwrap();
138 assert_eq!(canonical, serde_json::json!([52.55, 13.5]));
139 }
140}