Skip to main content

feedparser_rs/namespace/
georss.rs

1//! GeoRSS namespace support for geographic location data
2//!
3//! Supports parsing GeoRSS Simple elements for specifying geographic locations
4//! in RSS and Atom feeds. GeoRSS is commonly used in mapping applications,
5//! location-based services, and geocoded content.
6//!
7//! # Supported Elements
8//!
9//! - `georss:point` - Single latitude/longitude point
10//! - `georss:line` - Line string (multiple points)
11//! - `georss:polygon` - Polygon (closed shape)
12//! - `georss:box` - Bounding box (lower-left + upper-right)
13//!
14//! # Specification
15//!
16//! GeoRSS Simple: <http://www.georss.org/simple>
17
18use crate::limits::ParserLimits;
19use crate::types::{Entry, FeedMeta};
20
21/// `GeoRSS` namespace URI
22pub const GEORSS: &str = "http://www.georss.org/georss";
23
24/// Type of geographic shape
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum GeoType {
27    /// Single point (latitude, longitude)
28    #[default]
29    Point,
30    /// Line connecting multiple points
31    Line,
32    /// Closed polygon shape
33    Polygon,
34    /// Bounding box (lower-left, upper-right corners)
35    Box,
36}
37
38/// Geographic location data from `GeoRSS`
39#[derive(Debug, Clone, Default, PartialEq)]
40pub struct GeoLocation {
41    /// Type of geographic shape
42    pub geo_type: GeoType,
43    /// Coordinate pairs as (latitude, longitude)
44    ///
45    /// - Point: 1 coordinate pair
46    /// - Line: 2+ coordinate pairs
47    /// - Polygon: 3+ coordinate pairs (first == last for closed polygon)
48    /// - Box: 2 coordinate pairs (lower-left, upper-right)
49    pub coordinates: Vec<(f64, f64)>,
50    /// Coordinate reference system (e.g., "EPSG:4326" for WGS84)
51    ///
52    /// Default is WGS84 (latitude/longitude) if not specified
53    pub srs_name: Option<String>,
54    /// Elevation in meters (from `georss:elev`)
55    pub elev: Option<f64>,
56    /// Feature type classification (from `georss:featuretypetag`)
57    pub feature_type_tag: Option<String>,
58    /// Human-readable place name (from `georss:featurename`)
59    pub feature_name: Option<String>,
60    /// Relationship type (from `georss:relationshiptag`)
61    pub relationship_tag: Option<String>,
62}
63
64impl GeoLocation {
65    /// Creates new point location
66    ///
67    /// # Arguments
68    ///
69    /// * `lat` - Latitude in decimal degrees
70    /// * `lon` - Longitude in decimal degrees
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use feedparser_rs::namespace::georss::GeoLocation;
76    ///
77    /// let loc = GeoLocation::point(45.256, -71.92);
78    /// assert_eq!(loc.coordinates.len(), 1);
79    /// ```
80    #[must_use]
81    pub fn point(lat: f64, lon: f64) -> Self {
82        Self {
83            geo_type: GeoType::Point,
84            coordinates: vec![(lat, lon)],
85            ..Default::default()
86        }
87    }
88
89    /// Creates new line location
90    ///
91    /// # Arguments
92    ///
93    /// * `coords` - Vector of (latitude, longitude) pairs
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use feedparser_rs::namespace::georss::GeoLocation;
99    ///
100    /// let coords = vec![(45.256, -71.92), (46.0, -72.0)];
101    /// let loc = GeoLocation::line(coords);
102    /// assert_eq!(loc.coordinates.len(), 2);
103    /// ```
104    #[must_use]
105    pub fn line(coords: Vec<(f64, f64)>) -> Self {
106        Self {
107            geo_type: GeoType::Line,
108            coordinates: coords,
109            ..Default::default()
110        }
111    }
112
113    /// Creates new polygon location
114    ///
115    /// # Arguments
116    ///
117    /// * `coords` - Vector of (latitude, longitude) pairs
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use feedparser_rs::namespace::georss::GeoLocation;
123    ///
124    /// let coords = vec![
125    ///     (45.0, -71.0),
126    ///     (46.0, -71.0),
127    ///     (46.0, -72.0),
128    ///     (45.0, -71.0), // Close the polygon
129    /// ];
130    /// let loc = GeoLocation::polygon(coords);
131    /// ```
132    #[must_use]
133    pub fn polygon(coords: Vec<(f64, f64)>) -> Self {
134        Self {
135            geo_type: GeoType::Polygon,
136            coordinates: coords,
137            ..Default::default()
138        }
139    }
140
141    /// Creates new bounding box location
142    ///
143    /// # Arguments
144    ///
145    /// * `lower_lat` - Lower-left latitude
146    /// * `lower_lon` - Lower-left longitude
147    /// * `upper_lat` - Upper-right latitude
148    /// * `upper_lon` - Upper-right longitude
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use feedparser_rs::namespace::georss::GeoLocation;
154    ///
155    /// let loc = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
156    /// assert_eq!(loc.coordinates.len(), 2);
157    /// ```
158    #[must_use]
159    pub fn bbox(lower_lat: f64, lower_lon: f64, upper_lat: f64, upper_lon: f64) -> Self {
160        Self {
161            geo_type: GeoType::Box,
162            coordinates: vec![(lower_lat, lower_lon), (upper_lat, upper_lon)],
163            ..Default::default()
164        }
165    }
166}
167
168/// Parse W3C Basic Geo element and update entry
169///
170/// Handles `geo:lat` and `geo:long` elements. When both are present,
171/// auto-constructs `entry.r#where` as a point location.
172///
173/// # Arguments
174///
175/// * `tag` - Element local name (e.g., "lat", "long", "lon")
176/// * `text` - Element text content
177/// * `entry` - Entry to update
178///
179/// # Returns
180///
181/// `true` if element was recognized and handled, `false` otherwise
182pub fn handle_entry_geo_element(tag: &[u8], text: &str, entry: &mut Entry) -> bool {
183    match tag {
184        b"lat" => {
185            entry.geo_lat = Some(text.to_string());
186            try_build_entry_where(entry);
187            true
188        }
189        b"long" | b"lon" => {
190            entry.geo_long = Some(text.to_string());
191            try_build_entry_where(entry);
192            true
193        }
194        _ => false,
195    }
196}
197
198/// Parse W3C Basic Geo element and update feed metadata
199///
200/// Handles `geo:lat` and `geo:long` elements. When both are present,
201/// auto-constructs `feed.r#where` as a point location.
202///
203/// # Arguments
204///
205/// * `tag` - Element local name (e.g., "lat", "long", "lon")
206/// * `text` - Element text content
207/// * `feed` - Feed metadata to update
208///
209/// # Returns
210///
211/// `true` if element was recognized and handled, `false` otherwise
212pub fn handle_feed_geo_element(tag: &[u8], text: &str, feed: &mut FeedMeta) -> bool {
213    match tag {
214        b"lat" => {
215            feed.geo_lat = Some(text.to_string());
216            try_build_feed_where(feed);
217            true
218        }
219        b"long" | b"lon" => {
220            feed.geo_long = Some(text.to_string());
221            try_build_feed_where(feed);
222            true
223        }
224        _ => false,
225    }
226}
227
228fn try_build_entry_where(entry: &mut Entry) {
229    if let (Some(lat_str), Some(lon_str)) = (entry.geo_lat.as_deref(), entry.geo_long.as_deref())
230        && let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
231        && (-90.0..=90.0).contains(&lat)
232        && (-180.0..=180.0).contains(&lon)
233    {
234        entry.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
235    }
236}
237
238fn try_build_feed_where(feed: &mut FeedMeta) {
239    if let (Some(lat_str), Some(lon_str)) = (feed.geo_lat.as_deref(), feed.geo_long.as_deref())
240        && let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
241        && (-90.0..=90.0).contains(&lat)
242        && (-180.0..=180.0).contains(&lon)
243    {
244        feed.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
245    }
246}
247
248/// Parse `GeoRSS` element and update entry
249///
250/// # Arguments
251///
252/// * `tag` - Element local name (e.g., "point", "line", "polygon", "box")
253/// * `text` - Element text content
254/// * `entry` - Entry to update
255/// * `_limits` - Parser limits (unused but kept for API consistency)
256///
257/// # Returns
258///
259/// `true` if element was recognized and handled, `false` otherwise
260pub fn handle_entry_element(
261    tag: &[u8],
262    text: &str,
263    entry: &mut Entry,
264    _limits: &ParserLimits,
265) -> bool {
266    match tag {
267        b"point" => {
268            if let Some(loc) = parse_point(text) {
269                let existing = entry
270                    .r#where
271                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
272                existing.geo_type = loc.geo_type;
273                existing.coordinates = loc.coordinates;
274                existing.srs_name = loc.srs_name;
275            }
276            true
277        }
278        b"line" => {
279            if let Some(loc) = parse_line(text) {
280                let existing = entry
281                    .r#where
282                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
283                existing.geo_type = loc.geo_type;
284                existing.coordinates = loc.coordinates;
285                existing.srs_name = loc.srs_name;
286            }
287            true
288        }
289        b"polygon" => {
290            if let Some(loc) = parse_polygon(text) {
291                let existing = entry
292                    .r#where
293                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
294                existing.geo_type = loc.geo_type;
295                existing.coordinates = loc.coordinates;
296                existing.srs_name = loc.srs_name;
297            }
298            true
299        }
300        b"box" => {
301            if let Some(loc) = parse_box(text) {
302                let existing = entry
303                    .r#where
304                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
305                existing.geo_type = loc.geo_type;
306                existing.coordinates = loc.coordinates;
307                existing.srs_name = loc.srs_name;
308            }
309            true
310        }
311        b"elev" => {
312            if let Ok(v) = text.trim().parse::<f64>()
313                && v.is_finite()
314            {
315                entry
316                    .r#where
317                    .get_or_insert_with(|| Box::new(GeoLocation::default()))
318                    .elev = Some(v);
319            }
320            true
321        }
322        b"featuretypetag" => {
323            entry
324                .r#where
325                .get_or_insert_with(|| Box::new(GeoLocation::default()))
326                .feature_type_tag = Some(text.to_string());
327            true
328        }
329        b"featurename" => {
330            entry
331                .r#where
332                .get_or_insert_with(|| Box::new(GeoLocation::default()))
333                .feature_name = Some(text.to_string());
334            true
335        }
336        b"relationshiptag" => {
337            entry
338                .r#where
339                .get_or_insert_with(|| Box::new(GeoLocation::default()))
340                .relationship_tag = Some(text.to_string());
341            true
342        }
343        _ => false,
344    }
345}
346
347/// Parse `GeoRSS` element and update feed metadata
348///
349/// # Arguments
350///
351/// * `tag` - Element local name (e.g., "point", "line", "polygon", "box")
352/// * `text` - Element text content
353/// * `feed` - Feed metadata to update
354/// * `_limits` - Parser limits (unused but kept for API consistency)
355///
356/// # Returns
357///
358/// `true` if element was recognized and handled, `false` otherwise
359pub fn handle_feed_element(
360    tag: &[u8],
361    text: &str,
362    feed: &mut FeedMeta,
363    _limits: &ParserLimits,
364) -> bool {
365    match tag {
366        b"point" => {
367            if let Some(loc) = parse_point(text) {
368                let existing = feed
369                    .r#where
370                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
371                existing.geo_type = loc.geo_type;
372                existing.coordinates = loc.coordinates;
373                existing.srs_name = loc.srs_name;
374            }
375            true
376        }
377        b"line" => {
378            if let Some(loc) = parse_line(text) {
379                let existing = feed
380                    .r#where
381                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
382                existing.geo_type = loc.geo_type;
383                existing.coordinates = loc.coordinates;
384                existing.srs_name = loc.srs_name;
385            }
386            true
387        }
388        b"polygon" => {
389            if let Some(loc) = parse_polygon(text) {
390                let existing = feed
391                    .r#where
392                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
393                existing.geo_type = loc.geo_type;
394                existing.coordinates = loc.coordinates;
395                existing.srs_name = loc.srs_name;
396            }
397            true
398        }
399        b"box" => {
400            if let Some(loc) = parse_box(text) {
401                let existing = feed
402                    .r#where
403                    .get_or_insert_with(|| Box::new(GeoLocation::default()));
404                existing.geo_type = loc.geo_type;
405                existing.coordinates = loc.coordinates;
406                existing.srs_name = loc.srs_name;
407            }
408            true
409        }
410        b"elev" => {
411            if let Ok(v) = text.trim().parse::<f64>()
412                && v.is_finite()
413            {
414                feed.r#where
415                    .get_or_insert_with(|| Box::new(GeoLocation::default()))
416                    .elev = Some(v);
417            }
418            true
419        }
420        b"featuretypetag" => {
421            feed.r#where
422                .get_or_insert_with(|| Box::new(GeoLocation::default()))
423                .feature_type_tag = Some(text.to_string());
424            true
425        }
426        b"featurename" => {
427            feed.r#where
428                .get_or_insert_with(|| Box::new(GeoLocation::default()))
429                .feature_name = Some(text.to_string());
430            true
431        }
432        b"relationshiptag" => {
433            feed.r#where
434                .get_or_insert_with(|| Box::new(GeoLocation::default()))
435                .relationship_tag = Some(text.to_string());
436            true
437        }
438        _ => false,
439    }
440}
441
442/// Parse georss:point element
443///
444/// Format: "lat lon" (space-separated)
445/// Example: "45.256 -71.92"
446fn parse_point(text: &str) -> Option<GeoLocation> {
447    let coords = parse_coordinates(text)?;
448    if coords.len() == 1 {
449        Some(GeoLocation {
450            geo_type: GeoType::Point,
451            coordinates: coords,
452            ..Default::default()
453        })
454    } else {
455        None
456    }
457}
458
459/// Parse georss:line element
460///
461/// Format: "lat1 lon1 lat2 lon2 ..." (space-separated)
462/// Example: "45.256 -71.92 46.0 -72.0"
463fn parse_line(text: &str) -> Option<GeoLocation> {
464    let coords = parse_coordinates(text)?;
465    if coords.len() >= 2 {
466        Some(GeoLocation {
467            geo_type: GeoType::Line,
468            coordinates: coords,
469            ..Default::default()
470        })
471    } else {
472        None
473    }
474}
475
476/// Parse georss:polygon element
477///
478/// Format: "lat1 lon1 lat2 lon2 lat3 lon3 ..." (space-separated)
479/// Example: "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0"
480fn parse_polygon(text: &str) -> Option<GeoLocation> {
481    let coords = parse_coordinates(text)?;
482    if coords.len() >= 3 {
483        Some(GeoLocation {
484            geo_type: GeoType::Polygon,
485            coordinates: coords,
486            ..Default::default()
487        })
488    } else {
489        None
490    }
491}
492
493/// Parse georss:box element
494///
495/// Format: space-separated values (lower-left, upper-right)
496/// Example: "45.0 -72.0 46.0 -71.0"
497fn parse_box(text: &str) -> Option<GeoLocation> {
498    let coords = parse_coordinates(text)?;
499    if coords.len() == 2 {
500        Some(GeoLocation {
501            geo_type: GeoType::Box,
502            coordinates: coords,
503            ..Default::default()
504        })
505    } else {
506        None
507    }
508}
509
510/// Parse space-separated coordinate pairs
511///
512/// Format: "lat1 lon1 lat2 lon2 ..." (pairs of floats)
513fn parse_coordinates(text: &str) -> Option<Vec<(f64, f64)>> {
514    let parts: Vec<&str> = text.split_whitespace().collect();
515
516    // Must have even number of values (lat/lon pairs)
517    if parts.is_empty() || !parts.len().is_multiple_of(2) {
518        return None;
519    }
520
521    let mut coords = Vec::with_capacity(parts.len() / 2);
522
523    for chunk in parts.chunks(2) {
524        let lat = chunk[0].parse::<f64>().ok()?;
525        let lon = chunk[1].parse::<f64>().ok()?;
526
527        // Basic validation: latitude should be -90 to 90, longitude -180 to 180
528        if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
529            return None;
530        }
531
532        coords.push((lat, lon));
533    }
534
535    Some(coords)
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_parse_point() {
544        let loc = parse_point("45.256 -71.92").unwrap();
545        assert_eq!(loc.geo_type, GeoType::Point);
546        assert_eq!(loc.coordinates.len(), 1);
547        assert_eq!(loc.coordinates[0], (45.256, -71.92));
548    }
549
550    #[test]
551    fn test_parse_point_invalid() {
552        assert!(parse_point("45.256").is_none());
553        assert!(parse_point("45.256 -71.92 extra").is_none());
554        assert!(parse_point("not numbers").is_none());
555        assert!(parse_point("").is_none());
556    }
557
558    #[test]
559    fn test_parse_line() {
560        let loc = parse_line("45.256 -71.92 46.0 -72.0").unwrap();
561        assert_eq!(loc.geo_type, GeoType::Line);
562        assert_eq!(loc.coordinates.len(), 2);
563        assert_eq!(loc.coordinates[0], (45.256, -71.92));
564        assert_eq!(loc.coordinates[1], (46.0, -72.0));
565    }
566
567    #[test]
568    fn test_parse_line_single_point() {
569        // Line needs at least 2 points
570        assert!(parse_line("45.256 -71.92").is_none());
571    }
572
573    #[test]
574    fn test_parse_polygon() {
575        let loc = parse_polygon("45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0").unwrap();
576        assert_eq!(loc.geo_type, GeoType::Polygon);
577        assert_eq!(loc.coordinates.len(), 4);
578        assert_eq!(loc.coordinates[0], (45.0, -71.0));
579        assert_eq!(loc.coordinates[3], (45.0, -71.0)); // Closed polygon
580    }
581
582    #[test]
583    fn test_parse_box() {
584        let loc = parse_box("45.0 -72.0 46.0 -71.0").unwrap();
585        assert_eq!(loc.geo_type, GeoType::Box);
586        assert_eq!(loc.coordinates.len(), 2);
587        assert_eq!(loc.coordinates[0], (45.0, -72.0)); // Lower-left
588        assert_eq!(loc.coordinates[1], (46.0, -71.0)); // Upper-right
589    }
590
591    #[test]
592    fn test_parse_box_invalid() {
593        // Box needs exactly 2 points (4 values)
594        assert!(parse_box("45.0 -72.0").is_none());
595        assert!(parse_box("45.0 -72.0 46.0 -71.0 extra values").is_none());
596    }
597
598    #[test]
599    fn test_coordinate_validation() {
600        // Invalid latitude (> 90)
601        assert!(parse_point("91.0 0.0").is_none());
602        // Invalid latitude (< -90)
603        assert!(parse_point("-91.0 0.0").is_none());
604        // Invalid longitude (> 180)
605        assert!(parse_point("0.0 181.0").is_none());
606        // Invalid longitude (< -180)
607        assert!(parse_point("0.0 -181.0").is_none());
608    }
609
610    #[test]
611    fn test_handle_entry_element_point() {
612        let mut entry = Entry::default();
613        let limits = ParserLimits::default();
614
615        let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
616        assert!(handled);
617        assert!(entry.r#where.is_some());
618
619        let geo = entry.r#where.as_ref().unwrap();
620        assert_eq!(geo.geo_type, GeoType::Point);
621        assert_eq!(geo.coordinates[0], (45.256, -71.92));
622    }
623
624    #[test]
625    fn test_handle_entry_element_line() {
626        let mut entry = Entry::default();
627        let limits = ParserLimits::default();
628
629        let handled =
630            handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
631        assert!(handled);
632        assert!(entry.r#where.is_some());
633        assert_eq!(entry.r#where.as_ref().unwrap().geo_type, GeoType::Line);
634    }
635
636    #[test]
637    fn test_handle_entry_element_unknown() {
638        let mut entry = Entry::default();
639        let limits = ParserLimits::default();
640
641        let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
642        assert!(!handled);
643        assert!(entry.r#where.is_none());
644    }
645
646    #[test]
647    fn test_geo_location_constructors() {
648        let point = GeoLocation::point(45.0, -71.0);
649        assert_eq!(point.geo_type, GeoType::Point);
650        assert_eq!(point.coordinates.len(), 1);
651
652        let line = GeoLocation::line(vec![(45.0, -71.0), (46.0, -72.0)]);
653        assert_eq!(line.geo_type, GeoType::Line);
654        assert_eq!(line.coordinates.len(), 2);
655
656        let polygon = GeoLocation::polygon(vec![(45.0, -71.0), (46.0, -71.0), (45.0, -71.0)]);
657        assert_eq!(polygon.geo_type, GeoType::Polygon);
658        assert_eq!(polygon.coordinates.len(), 3);
659
660        let bbox = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
661        assert_eq!(bbox.geo_type, GeoType::Box);
662        assert_eq!(bbox.coordinates.len(), 2);
663    }
664
665    #[test]
666    fn test_whitespace_handling() {
667        let loc = parse_point("  45.256   -71.92  ").unwrap();
668        assert_eq!(loc.coordinates[0], (45.256, -71.92));
669    }
670
671    #[test]
672    fn test_handle_feed_element_point() {
673        let mut feed = FeedMeta::default();
674        let limits = ParserLimits::default();
675
676        let handled = handle_feed_element(b"point", "45.256 -71.92", &mut feed, &limits);
677        assert!(handled);
678        assert!(feed.r#where.is_some());
679
680        let geo = feed.r#where.as_ref().unwrap();
681        assert_eq!(geo.geo_type, GeoType::Point);
682        assert_eq!(geo.coordinates[0], (45.256, -71.92));
683    }
684
685    #[test]
686    fn test_handle_feed_element_line() {
687        let mut feed = FeedMeta::default();
688        let limits = ParserLimits::default();
689
690        let handled = handle_feed_element(b"line", "45.256 -71.92 46.0 -72.0", &mut feed, &limits);
691        assert!(handled);
692        assert!(feed.r#where.is_some());
693        assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Line);
694    }
695
696    #[test]
697    fn test_handle_feed_element_polygon() {
698        let mut feed = FeedMeta::default();
699        let limits = ParserLimits::default();
700
701        let handled = handle_feed_element(
702            b"polygon",
703            "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0",
704            &mut feed,
705            &limits,
706        );
707        assert!(handled);
708        assert!(feed.r#where.is_some());
709        assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Polygon);
710    }
711
712    #[test]
713    fn test_handle_feed_element_box() {
714        let mut feed = FeedMeta::default();
715        let limits = ParserLimits::default();
716
717        let handled = handle_feed_element(b"box", "45.0 -72.0 46.0 -71.0", &mut feed, &limits);
718        assert!(handled);
719        assert!(feed.r#where.is_some());
720        assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Box);
721    }
722
723    #[test]
724    fn test_handle_feed_element_unknown() {
725        let mut feed = FeedMeta::default();
726        let limits = ParserLimits::default();
727
728        let handled = handle_feed_element(b"unknown", "data", &mut feed, &limits);
729        assert!(!handled);
730        assert!(feed.r#where.is_none());
731    }
732
733    #[test]
734    fn test_handle_feed_element_invalid_data() {
735        let mut feed = FeedMeta::default();
736        let limits = ParserLimits::default();
737
738        let handled = handle_feed_element(b"point", "invalid data", &mut feed, &limits);
739        assert!(handled);
740        assert!(feed.r#where.is_none());
741    }
742
743    #[test]
744    fn test_handle_entry_element_elev() {
745        let mut entry = Entry::default();
746        let limits = ParserLimits::default();
747
748        let handled = handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
749        assert!(handled);
750        let geo = entry.r#where.as_ref().unwrap();
751        assert_eq!(geo.elev, Some(1337.5));
752    }
753
754    #[test]
755    fn test_handle_entry_element_feature_name() {
756        let mut entry = Entry::default();
757        let limits = ParserLimits::default();
758
759        let handled = handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
760        assert!(handled);
761        let geo = entry.r#where.as_ref().unwrap();
762        assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
763    }
764
765    #[test]
766    fn test_handle_entry_element_feature_type_tag() {
767        let mut entry = Entry::default();
768        let limits = ParserLimits::default();
769
770        let handled = handle_entry_element(b"featuretypetag", "mountain", &mut entry, &limits);
771        assert!(handled);
772        let geo = entry.r#where.as_ref().unwrap();
773        assert_eq!(geo.feature_type_tag.as_deref(), Some("mountain"));
774    }
775
776    #[test]
777    fn test_handle_entry_element_relationship_tag() {
778        let mut entry = Entry::default();
779        let limits = ParserLimits::default();
780
781        let handled =
782            handle_entry_element(b"relationshiptag", "is-located-at", &mut entry, &limits);
783        assert!(handled);
784        let geo = entry.r#where.as_ref().unwrap();
785        assert_eq!(geo.relationship_tag.as_deref(), Some("is-located-at"));
786    }
787
788    #[test]
789    fn test_extended_attrs_without_geometry() {
790        let mut entry = Entry::default();
791        let limits = ParserLimits::default();
792
793        handle_entry_element(b"featurename", "Unknown Location", &mut entry, &limits);
794        let geo = entry.r#where.as_ref().unwrap();
795        assert_eq!(geo.feature_name.as_deref(), Some("Unknown Location"));
796        assert!(geo.coordinates.is_empty());
797    }
798
799    #[test]
800    fn test_extended_attrs_invalid_elev() {
801        let mut entry = Entry::default();
802        let limits = ParserLimits::default();
803
804        let handled = handle_entry_element(b"elev", "not-a-number", &mut entry, &limits);
805        assert!(handled);
806        // GeoLocation not created because elev parse failed
807        assert!(entry.r#where.is_none());
808    }
809
810    #[test]
811    fn test_extended_attrs_elev_non_finite_ignored() {
812        let limits = ParserLimits::default();
813
814        for value in ["NaN", "Infinity", "-Infinity"] {
815            let mut entry = Entry::default();
816            let handled = handle_entry_element(b"elev", value, &mut entry, &limits);
817            assert!(handled, "element must be recognized for value {value}");
818            assert!(
819                entry.r#where.is_none(),
820                "non-finite elev '{value}' must not create GeoLocation"
821            );
822        }
823    }
824
825    #[test]
826    fn test_extended_attrs_before_geometry() {
827        let mut entry = Entry::default();
828        let limits = ParserLimits::default();
829
830        handle_entry_element(b"featurename", "Reverse Order", &mut entry, &limits);
831        handle_entry_element(b"elev", "500.0", &mut entry, &limits);
832        handle_entry_element(b"point", "40.0 -74.0", &mut entry, &limits);
833
834        let geo = entry.r#where.as_ref().unwrap();
835        assert_eq!(geo.geo_type, GeoType::Point);
836        assert_eq!(geo.coordinates[0], (40.0, -74.0));
837        assert_eq!(geo.feature_name.as_deref(), Some("Reverse Order"));
838        assert_eq!(geo.elev, Some(500.0));
839    }
840
841    #[test]
842    fn test_extended_attrs_after_geometry() {
843        let mut entry = Entry::default();
844        let limits = ParserLimits::default();
845
846        handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
847        handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
848        handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
849
850        let geo = entry.r#where.as_ref().unwrap();
851        assert_eq!(geo.geo_type, GeoType::Point);
852        assert_eq!(geo.coordinates[0], (45.256, -71.92));
853        assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
854        assert_eq!(geo.elev, Some(1337.5));
855    }
856}