Skip to main content

tpt_geo_geojson/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use serde::ser::SerializeMap;
5use serde::Serialize;
6use serde_json::Value;
7use std::collections::HashMap;
8use std::fmt;
9use std::io::Read;
10
11// ---- Error types ----
12
13/// The kind of validation or parse error.
14#[derive(Debug)]
15pub enum GeoErrorKind {
16    /// The GeoJSON `type` field has an unexpected value.
17    InvalidType(String),
18    /// A coordinate array has the wrong length or structure.
19    MalformedCoordinates(String),
20    /// A polygon ring is not closed or has fewer than 4 positions.
21    InvalidRing(String),
22    /// An I/O error reading the input.
23    Io(std::io::Error),
24    /// A JSON deserialization error.
25    Json(serde_json::Error),
26}
27
28impl fmt::Display for GeoErrorKind {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::InvalidType(s) => write!(f, "invalid type: {}", s),
32            Self::MalformedCoordinates(s) => write!(f, "malformed coordinates: {}", s),
33            Self::InvalidRing(s) => write!(f, "invalid ring: {}", s),
34            Self::Io(e) => write!(f, "I/O error: {}", e),
35            Self::Json(e) => write!(f, "JSON error: {}", e),
36        }
37    }
38}
39
40/// A GeoJSON parse or validation error with a path into the structure.
41///
42/// The `path` field uses dot/bracket notation to locate the error, e.g.
43/// `"features[2].geometry.coordinates[0]"`.
44#[derive(Debug)]
45pub struct GeoError {
46    /// The kind of error.
47    pub kind: GeoErrorKind,
48    /// A dot/bracket path into the GeoJSON structure where the error occurred.
49    pub path: String,
50}
51
52impl fmt::Display for GeoError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "geojson error at {}: {}", self.path, self.kind)
55    }
56}
57
58impl std::error::Error for GeoError {}
59
60// ---- Coordinate types ----
61
62/// A GeoJSON position: `[longitude, latitude]` or `[longitude, latitude, altitude]`.
63///
64/// GeoJSON (RFC 7946) requires at least two elements; a third optional element is altitude.
65/// Construct via [`Position::new`] which validates the length, so [`Position::longitude`]
66/// and [`Position::latitude`] can never panic on a too-short vector.
67#[derive(Debug, Clone, PartialEq, Serialize)]
68pub struct Position(Vec<f64>);
69
70impl Position {
71    /// Construct a position, validating that `coords` has 2 or 3 elements.
72    ///
73    /// Returns a [`GeoErrorKind::MalformedCoordinates`] error otherwise.
74    pub fn new(coords: Vec<f64>) -> Result<Position, GeoError> {
75        if coords.len() < 2 || coords.len() > 3 {
76            return Err(GeoError {
77                kind: GeoErrorKind::MalformedCoordinates(format!(
78                    "position must have 2 or 3 elements, got {}",
79                    coords.len()
80                )),
81                path: String::new(),
82            });
83        }
84        Ok(Position(coords))
85    }
86
87    /// Longitude (first element).
88    pub fn longitude(&self) -> f64 {
89        self.0[0]
90    }
91    /// Latitude (second element).
92    pub fn latitude(&self) -> f64 {
93        self.0[1]
94    }
95    /// Altitude, if present (third element).
96    pub fn altitude(&self) -> Option<f64> {
97        self.0.get(2).copied()
98    }
99}
100
101// ---- Geometry types ----
102
103/// A GeoJSON geometry object.
104#[derive(Debug, Clone, PartialEq, Serialize)]
105#[serde(tag = "type")]
106pub enum Geometry {
107    /// A single point.
108    Point {
109        /// The point's position.
110        coordinates: Position,
111    },
112    /// Multiple points.
113    MultiPoint {
114        /// The positions of each point.
115        coordinates: Vec<Position>,
116    },
117    /// A line string.
118    LineString {
119        /// The ordered sequence of positions forming the line.
120        coordinates: Vec<Position>,
121    },
122    /// Multiple line strings.
123    MultiLineString {
124        /// The ordered sequences of positions for each line.
125        coordinates: Vec<Vec<Position>>,
126    },
127    /// A polygon (first ring is exterior, remaining rings are holes).
128    Polygon {
129        /// Rings: first is the exterior boundary, rest are holes.
130        coordinates: Vec<Vec<Position>>,
131    },
132    /// Multiple polygons.
133    MultiPolygon {
134        /// Each element is a polygon's ring array.
135        coordinates: Vec<Vec<Vec<Position>>>,
136    },
137    /// A collection of heterogeneous geometries.
138    GeometryCollection {
139        /// The geometries in this collection.
140        geometries: Vec<Geometry>,
141    },
142}
143
144// ---- Feature types ----
145
146/// A GeoJSON Feature.
147#[derive(Debug, Clone, PartialEq)]
148pub struct Feature {
149    /// The feature's geometry, if any.
150    pub geometry: Option<Geometry>,
151    /// Arbitrary properties associated with the feature.
152    pub properties: Option<Value>,
153    /// An optional feature identifier.
154    pub id: Option<Value>,
155    /// An optional bounding box `[west, south, east, north]` (plus optional altitude pairs).
156    pub bbox: Option<Vec<f64>>,
157    /// Non-standard members present on the feature, preserved across a serialize/parse round-trip.
158    pub foreign_members: HashMap<String, Value>,
159}
160
161impl Serialize for Feature {
162    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163        let mut state = serializer.serialize_map(None)?;
164        state.serialize_entry("type", "Feature")?;
165        state.serialize_entry("geometry", &self.geometry)?;
166        state.serialize_entry("properties", &self.properties)?;
167        if let Some(id) = &self.id {
168            state.serialize_entry("id", id)?;
169        }
170        if let Some(bbox) = &self.bbox {
171            state.serialize_entry("bbox", bbox)?;
172        }
173        for (k, v) in &self.foreign_members {
174            state.serialize_entry(k, v)?;
175        }
176        state.end()
177    }
178}
179
180/// A GeoJSON FeatureCollection.
181#[derive(Debug, Clone, PartialEq)]
182pub struct FeatureCollection {
183    /// The features in this collection.
184    pub features: Vec<Feature>,
185    /// An optional bounding box `[west, south, east, north]` (plus optional altitude pairs).
186    pub bbox: Option<Vec<f64>>,
187    /// Non-standard members present on the collection, preserved across a serialize/parse round-trip.
188    pub foreign_members: HashMap<String, Value>,
189}
190
191impl Serialize for FeatureCollection {
192    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
193        let mut state = serializer.serialize_map(None)?;
194        state.serialize_entry("type", "FeatureCollection")?;
195        state.serialize_entry("features", &self.features)?;
196        if let Some(bbox) = &self.bbox {
197            state.serialize_entry("bbox", bbox)?;
198        }
199        for (k, v) in &self.foreign_members {
200            state.serialize_entry(k, v)?;
201        }
202        state.end()
203    }
204}
205
206/// The top-level GeoJSON object.
207#[derive(Debug, Clone, PartialEq)]
208pub enum GeoJson {
209    /// A single Feature.
210    Feature(Feature),
211    /// A collection of Features.
212    FeatureCollection(FeatureCollection),
213    /// A bare Geometry.
214    Geometry(Geometry),
215}
216
217impl Serialize for GeoJson {
218    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
219        match self {
220            Self::Feature(f) => f.serialize(serializer),
221            Self::FeatureCollection(fc) => fc.serialize(serializer),
222            Self::Geometry(g) => g.serialize(serializer),
223        }
224    }
225}
226
227// ---- Public API ----
228
229/// Parse a GeoJSON string.
230///
231/// Performs a full validation pass after deserialization:
232/// - Coordinate arrays must have 2 or 3 elements.
233/// - Polygon rings must be closed (first == last) and have ≥ 4 positions.
234///
235/// # Example
236///
237/// ```
238/// use tpt_geo_geojson::parse;
239///
240/// let geojson = r#"{"type":"Point","coordinates":[125.6,10.1]}"#;
241/// let result = parse(geojson).unwrap();
242/// ```
243pub fn parse(input: &str) -> Result<GeoJson, GeoError> {
244    let raw: Value = serde_json::from_str(input).map_err(|e| GeoError {
245        kind: GeoErrorKind::Json(e),
246        path: String::new(),
247    })?;
248    parse_value(&raw, "")
249}
250
251/// Parse GeoJSON from any [`Read`] source.
252pub fn parse_reader<R: Read>(mut reader: R) -> Result<GeoJson, GeoError> {
253    let raw: Value = serde_json::from_reader(&mut reader).map_err(|e| GeoError {
254        kind: GeoErrorKind::Json(e),
255        path: String::new(),
256    })?;
257    parse_value(&raw, "")
258}
259
260/// Serialize this GeoJSON value back to a compact JSON string.
261///
262/// The output is valid GeoJSON: [`Feature`] and [`FeatureCollection`] include
263/// their `"type"` member, and any captured `bbox`/foreign members are preserved.
264///
265/// # Example
266///
267/// ```
268/// use tpt_geo_geojson::parse;
269///
270/// let geo = parse(r#"{"type":"Point","coordinates":[1.0,2.0]}"#).unwrap();
271/// let json = tpt_geo_geojson::to_json(&geo).unwrap();
272/// assert!(json.contains("\"type\":\"Point\""));
273/// ```
274pub fn to_json(value: &GeoJson) -> Result<String, GeoError> {
275    serde_json::to_string(value).map_err(|e| GeoError {
276        kind: GeoErrorKind::Json(e),
277        path: String::new(),
278    })
279}
280
281// ---- Internal parsing ----
282
283/// Extract the optional `bbox` array and any non-standard members from `v`,
284/// excluding the `known` field names. Used to preserve `bbox` and foreign
285/// members across a parse/serialize round-trip.
286fn collect_extra(v: &Value, known: &[&str]) -> (Option<Vec<f64>>, HashMap<String, Value>) {
287    let bbox = v
288        .get("bbox")
289        .and_then(Value::as_array)
290        .map(|arr| arr.iter().filter_map(Value::as_f64).collect::<Vec<f64>>());
291    let foreign = v
292        .as_object()
293        .map(|obj| {
294            obj.iter()
295                .filter(|(k, _)| !known.contains(&k.as_str()))
296                .map(|(k, val)| (k.clone(), val.clone()))
297                .collect::<HashMap<String, Value>>()
298        })
299        .unwrap_or_default();
300    (bbox, foreign)
301}
302
303fn parse_value(v: &Value, path: &str) -> Result<GeoJson, GeoError> {
304    let type_str = v
305        .get("type")
306        .and_then(Value::as_str)
307        .ok_or_else(|| GeoError {
308            kind: GeoErrorKind::InvalidType("missing or non-string 'type' field".into()),
309            path: format!("{}.type", path),
310        })?;
311
312    match type_str {
313        "FeatureCollection" => {
314            let features_val =
315                v.get("features")
316                    .and_then(Value::as_array)
317                    .ok_or_else(|| GeoError {
318                        kind: GeoErrorKind::InvalidType(
319                            "FeatureCollection missing 'features' array".into(),
320                        ),
321                        path: format!("{}.features", path),
322                    })?;
323            let mut features = Vec::with_capacity(features_val.len());
324            for (i, fv) in features_val.iter().enumerate() {
325                let prefix = if path.is_empty() {
326                    String::new()
327                } else {
328                    format!("{}.", path)
329                };
330                let fp = format!("{}features[{}]", prefix, i);
331                features.push(parse_feature(fv, &fp)?);
332            }
333            let (bbox, foreign_members) = collect_extra(v, &["type", "features", "bbox"]);
334            Ok(GeoJson::FeatureCollection(FeatureCollection {
335                features,
336                bbox,
337                foreign_members,
338            }))
339        }
340        "Feature" => {
341            let fp = if path.is_empty() {
342                String::new()
343            } else {
344                path.to_owned()
345            };
346            Ok(GeoJson::Feature(parse_feature(v, &fp)?))
347        }
348        _ => {
349            let geom = parse_geometry(v, path)?;
350            Ok(GeoJson::Geometry(geom))
351        }
352    }
353}
354
355fn parse_feature(v: &Value, path: &str) -> Result<Feature, GeoError> {
356    if v.get("type").and_then(Value::as_str) != Some("Feature") {
357        return Err(GeoError {
358            kind: GeoErrorKind::InvalidType(format!("expected 'Feature', got {:?}", v.get("type"))),
359            path: format!("{}.type", path),
360        });
361    }
362
363    let geometry = match v.get("geometry") {
364        None | Some(Value::Null) => None,
365        Some(geom_val) => {
366            let gp = format!("{}.geometry", path);
367            Some(parse_geometry(geom_val, &gp)?)
368        }
369    };
370
371    let properties = v.get("properties").cloned();
372    let id = v.get("id").cloned();
373    let (bbox, foreign_members) =
374        collect_extra(v, &["type", "geometry", "properties", "id", "bbox"]);
375
376    Ok(Feature {
377        geometry,
378        properties,
379        id,
380        bbox,
381        foreign_members,
382    })
383}
384
385fn parse_geometry(v: &Value, path: &str) -> Result<Geometry, GeoError> {
386    let type_str = v
387        .get("type")
388        .and_then(Value::as_str)
389        .ok_or_else(|| GeoError {
390            kind: GeoErrorKind::InvalidType("geometry missing 'type' field".into()),
391            path: format!("{}.type", path),
392        })?;
393
394    match type_str {
395        "Point" => {
396            let coords_path = format!("{}.coordinates", path);
397            let raw = coords_raw(v, &coords_path)?;
398            let pos = parse_position(raw, &coords_path)?;
399            Ok(Geometry::Point { coordinates: pos })
400        }
401        "MultiPoint" => {
402            let coords_path = format!("{}.coordinates", path);
403            let arr = coords_raw(v, &coords_path)?;
404            let positions = parse_position_array(arr, &coords_path)?;
405            Ok(Geometry::MultiPoint {
406                coordinates: positions,
407            })
408        }
409        "LineString" => {
410            let coords_path = format!("{}.coordinates", path);
411            let arr = coords_raw(v, &coords_path)?;
412            let positions = parse_position_array(arr, &coords_path)?;
413            Ok(Geometry::LineString {
414                coordinates: positions,
415            })
416        }
417        "MultiLineString" => {
418            let coords_path = format!("{}.coordinates", path);
419            let arr = coords_raw(v, &coords_path)?
420                .as_array()
421                .ok_or_else(|| GeoError {
422                    kind: GeoErrorKind::MalformedCoordinates(
423                        "expected array of line strings".into(),
424                    ),
425                    path: coords_path.clone(),
426                })?;
427            let mut lines = Vec::with_capacity(arr.len());
428            for (i, line_val) in arr.iter().enumerate() {
429                let lp = format!("{}[{}]", coords_path, i);
430                let line_arr = line_val.as_array().ok_or_else(|| GeoError {
431                    kind: GeoErrorKind::MalformedCoordinates("expected array".into()),
432                    path: lp.clone(),
433                })?;
434                lines.push(parse_position_array(line_val, &lp)?);
435                let _ = line_arr;
436            }
437            Ok(Geometry::MultiLineString { coordinates: lines })
438        }
439        "Polygon" => {
440            let coords_path = format!("{}.coordinates", path);
441            let rings = parse_rings(v, &coords_path)?;
442            Ok(Geometry::Polygon { coordinates: rings })
443        }
444        "MultiPolygon" => {
445            let coords_path = format!("{}.coordinates", path);
446            let arr = coords_raw(v, &coords_path)?
447                .as_array()
448                .ok_or_else(|| GeoError {
449                    kind: GeoErrorKind::MalformedCoordinates("expected array of polygons".into()),
450                    path: coords_path.clone(),
451                })?;
452            let mut polys = Vec::with_capacity(arr.len());
453            for (i, poly_val) in arr.iter().enumerate() {
454                let pp = format!("{}[{}]", coords_path, i);
455                let rings = parse_rings_value(poly_val, &pp)?;
456                polys.push(rings);
457            }
458            Ok(Geometry::MultiPolygon { coordinates: polys })
459        }
460        "GeometryCollection" => {
461            let geoms_val = v
462                .get("geometries")
463                .and_then(Value::as_array)
464                .ok_or_else(|| GeoError {
465                    kind: GeoErrorKind::InvalidType(
466                        "GeometryCollection missing 'geometries' array".into(),
467                    ),
468                    path: format!("{}.geometries", path),
469                })?;
470            let mut geoms = Vec::with_capacity(geoms_val.len());
471            for (i, gv) in geoms_val.iter().enumerate() {
472                let gp = format!("{}.geometries[{}]", path, i);
473                geoms.push(parse_geometry(gv, &gp)?);
474            }
475            Ok(Geometry::GeometryCollection { geometries: geoms })
476        }
477        other => Err(GeoError {
478            kind: GeoErrorKind::InvalidType(format!("unknown geometry type '{}'", other)),
479            path: format!("{}.type", path),
480        }),
481    }
482}
483
484fn coords_raw<'a>(v: &'a Value, path: &str) -> Result<&'a Value, GeoError> {
485    v.get("coordinates").ok_or_else(|| GeoError {
486        kind: GeoErrorKind::MalformedCoordinates("missing 'coordinates' field".into()),
487        path: path.to_owned(),
488    })
489}
490
491fn parse_position(v: &Value, path: &str) -> Result<Position, GeoError> {
492    let arr = v.as_array().ok_or_else(|| GeoError {
493        kind: GeoErrorKind::MalformedCoordinates("position must be an array".into()),
494        path: path.to_owned(),
495    })?;
496    if arr.len() < 2 || arr.len() > 3 {
497        return Err(GeoError {
498            kind: GeoErrorKind::MalformedCoordinates(format!(
499                "position must have 2 or 3 elements, got {}",
500                arr.len()
501            )),
502            path: path.to_owned(),
503        });
504    }
505    let coords: Result<Vec<f64>, _> = arr
506        .iter()
507        .map(|n| {
508            n.as_f64().ok_or_else(|| GeoError {
509                kind: GeoErrorKind::MalformedCoordinates("coordinate must be a number".into()),
510                path: path.to_owned(),
511            })
512        })
513        .collect();
514    Position::new(coords?)
515}
516
517fn parse_position_array(v: &Value, path: &str) -> Result<Vec<Position>, GeoError> {
518    let arr = v.as_array().ok_or_else(|| GeoError {
519        kind: GeoErrorKind::MalformedCoordinates("expected array of positions".into()),
520        path: path.to_owned(),
521    })?;
522    let mut positions = Vec::with_capacity(arr.len());
523    for (i, pv) in arr.iter().enumerate() {
524        let pp = format!("{}[{}]", path, i);
525        positions.push(parse_position(pv, &pp)?);
526    }
527    Ok(positions)
528}
529
530fn parse_rings(v: &Value, path: &str) -> Result<Vec<Vec<Position>>, GeoError> {
531    let arr = coords_raw(v, path)?.as_array().ok_or_else(|| GeoError {
532        kind: GeoErrorKind::MalformedCoordinates(
533            "polygon coordinates must be an array of rings".into(),
534        ),
535        path: path.to_owned(),
536    })?;
537    parse_ring_array(arr, path)
538}
539
540fn parse_rings_value(v: &Value, path: &str) -> Result<Vec<Vec<Position>>, GeoError> {
541    let arr = v.as_array().ok_or_else(|| GeoError {
542        kind: GeoErrorKind::MalformedCoordinates("polygon must be an array of rings".into()),
543        path: path.to_owned(),
544    })?;
545    parse_ring_array(arr, path)
546}
547
548fn parse_ring_array(arr: &[Value], path: &str) -> Result<Vec<Vec<Position>>, GeoError> {
549    let mut rings = Vec::with_capacity(arr.len());
550    for (i, ring_val) in arr.iter().enumerate() {
551        let rp = format!("{}[{}]", path, i);
552        let positions = parse_position_array(ring_val, &rp)?;
553        // RFC 7946: ring must have ≥ 4 positions and be closed
554        if positions.len() < 4 {
555            return Err(GeoError {
556                kind: GeoErrorKind::InvalidRing(format!(
557                    "ring must have at least 4 positions, got {}",
558                    positions.len()
559                )),
560                path: rp,
561            });
562        }
563        let first = &positions[0];
564        let last = &positions[positions.len() - 1];
565        if (first.longitude() - last.longitude()).abs() > f64::EPSILON
566            || (first.latitude() - last.latitude()).abs() > f64::EPSILON
567        {
568            return Err(GeoError {
569                kind: GeoErrorKind::InvalidRing(
570                    "polygon ring is not closed (first position != last position)".into(),
571                ),
572                path: rp,
573            });
574        }
575        rings.push(positions);
576    }
577    Ok(rings)
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn parse_point() {
586        let s = r#"{"type":"Point","coordinates":[125.6,10.1]}"#;
587        let geo = parse(s).unwrap();
588        assert!(matches!(geo, GeoJson::Geometry(Geometry::Point { .. })));
589    }
590
591    #[test]
592    fn parse_point_with_altitude() {
593        let s = r#"{"type":"Point","coordinates":[0.0,0.0,100.0]}"#;
594        let geo = parse(s).unwrap();
595        if let GeoJson::Geometry(Geometry::Point { coordinates: p }) = geo {
596            assert_eq!(p.altitude(), Some(100.0));
597        } else {
598            panic!("expected point");
599        }
600    }
601
602    #[test]
603    fn parse_feature() {
604        let s = r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"name":"test"}}"#;
605        let geo = parse(s).unwrap();
606        assert!(matches!(geo, GeoJson::Feature(_)));
607    }
608
609    #[test]
610    fn parse_feature_collection() {
611        let s = r#"{
612            "type":"FeatureCollection",
613            "features":[
614                {"type":"Feature","geometry":{"type":"Point","coordinates":[1,2]},"properties":null}
615            ]
616        }"#;
617        let geo = parse(s).unwrap();
618        if let GeoJson::FeatureCollection(fc) = geo {
619            assert_eq!(fc.features.len(), 1);
620        } else {
621            panic!();
622        }
623    }
624
625    #[test]
626    fn parse_valid_polygon() {
627        let s = r#"{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}"#;
628        let geo = parse(s).unwrap();
629        assert!(matches!(geo, GeoJson::Geometry(Geometry::Polygon { .. })));
630    }
631
632    #[test]
633    fn error_on_wrong_coord_length() {
634        let s = r#"{"type":"Point","coordinates":[1,2,3,4]}"#;
635        let err = parse(s).unwrap_err();
636        assert!(matches!(err.kind, GeoErrorKind::MalformedCoordinates(_)));
637        assert!(err.path.contains("coordinates"));
638    }
639
640    #[test]
641    fn error_on_unclosed_polygon() {
642        let s = r#"{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1]]]}"#;
643        let err = parse(s).unwrap_err();
644        assert!(matches!(err.kind, GeoErrorKind::InvalidRing(_)));
645    }
646
647    #[test]
648    fn error_on_short_ring() {
649        let s = r#"{"type":"Polygon","coordinates":[[[0,0],[1,0],[0,0]]]}"#;
650        let err = parse(s).unwrap_err();
651        assert!(matches!(err.kind, GeoErrorKind::InvalidRing(_)));
652    }
653
654    #[test]
655    fn error_on_missing_type() {
656        let s = r#"{"coordinates":[0,0]}"#;
657        let err = parse(s).unwrap_err();
658        assert!(matches!(err.kind, GeoErrorKind::InvalidType(_)));
659    }
660
661    #[test]
662    fn error_has_nonempty_path_for_nested() {
663        let s = r#"{
664            "type":"FeatureCollection",
665            "features":[
666                {"type":"Feature","geometry":{"type":"Point","coordinates":[1,2,3,4]},"properties":null}
667            ]
668        }"#;
669        let err = parse(s).unwrap_err();
670        assert!(
671            !err.path.is_empty(),
672            "path should be non-empty: {:?}",
673            err.path
674        );
675    }
676
677    #[test]
678    fn parse_linestring() {
679        let s = r#"{"type":"LineString","coordinates":[[0,0],[1,1],[2,2]]}"#;
680        assert!(matches!(
681            parse(s).unwrap(),
682            GeoJson::Geometry(Geometry::LineString { .. })
683        ));
684    }
685
686    #[test]
687    fn parse_geometry_collection() {
688        let s =
689            r#"{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[0,0]}]}"#;
690        assert!(matches!(
691            parse(s).unwrap(),
692            GeoJson::Geometry(Geometry::GeometryCollection { .. })
693        ));
694    }
695
696    #[test]
697    fn parse_reader_api() {
698        let data = br#"{"type":"Point","coordinates":[1.0,2.0]}"#;
699        let geo = parse_reader(data.as_slice()).unwrap();
700        assert!(matches!(geo, GeoJson::Geometry(Geometry::Point { .. })));
701    }
702
703    #[test]
704    fn position_constructor_rejects_short_vector() {
705        assert!(Position::new(vec![1.0]).is_err());
706        let p = Position::new(vec![1.0, 2.0]).unwrap();
707        assert_eq!(p.longitude(), 1.0);
708        assert_eq!(p.latitude(), 2.0);
709        assert_eq!(p.altitude(), None);
710    }
711
712    #[test]
713    fn feature_serializes_type_member() {
714        let s = r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":null}"#;
715        let geo = parse(s).unwrap();
716        let json = to_json(&geo).unwrap();
717        assert!(json.contains(r#""type":"Feature""#));
718    }
719
720    #[test]
721    fn feature_collection_round_trips() {
722        let s = r#"{
723            "type":"FeatureCollection",
724            "features":[
725                {"type":"Feature","geometry":{"type":"Point","coordinates":[1,2]},"properties":{"n":1}},
726                {"type":"Feature","geometry":null,"properties":null}
727            ]
728        }"#;
729        let geo = parse(s).unwrap();
730        let json = to_json(&geo).unwrap();
731        let again = parse(&json).unwrap();
732        assert_eq!(geo, again);
733    }
734
735    #[test]
736    fn bbox_is_preserved() {
737        let s = r#"{"type":"FeatureCollection","bbox":[0,0,10,10],"features":[]}"#;
738        let geo = parse(s).unwrap();
739        if let GeoJson::FeatureCollection(fc) = &geo {
740            assert_eq!(fc.bbox, Some(vec![0.0, 0.0, 10.0, 10.0]));
741        } else {
742            panic!("expected FeatureCollection");
743        }
744        let json = to_json(&geo).unwrap();
745        assert!(json.contains(r#""bbox":[0.0,0.0,10.0,10.0]"#));
746        assert_eq!(parse(&json).unwrap(), geo);
747    }
748
749    #[test]
750    fn foreign_members_are_preserved() {
751        let s = r#"{"type":"Feature","properties":null,"title":"hello","extra":42}"#;
752        let geo = parse(s).unwrap();
753        if let GeoJson::Feature(f) = &geo {
754            assert_eq!(
755                f.foreign_members.get("title"),
756                Some(&serde_json::json!("hello"))
757            );
758            assert_eq!(f.foreign_members.get("extra"), Some(&serde_json::json!(42)));
759        } else {
760            panic!("expected Feature");
761        }
762        let json = to_json(&geo).unwrap();
763        assert!(json.contains(r#""title":"hello""#));
764        assert!(json.contains(r#""extra":42"#));
765    }
766}