Skip to main content

mlt_core/convert/
geojson.rs

1//! `GeoJSON` -like data to represent decoded MLT data with i32 coordinates
2
3use std::collections::BTreeMap;
4use std::str::FromStr;
5
6use geo_types::Geometry;
7use serde::ser::SerializeMap as _;
8use serde::{Deserialize, Serialize};
9use serde_json::{Number, Value};
10
11use crate::decoder::PropValueRef;
12use crate::{LendingIterator, MltResult, ParsedLayer};
13#[cfg(feature = "unstable-v2")]
14use crate::{
15    ParsedLayer02,
16    tile::{MValue, NestedValue},
17};
18
19/// `GeoJSON` [`FeatureCollection`]
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct FeatureCollection {
22    #[serde(rename = "type")]
23    pub ty: String,
24    pub features: Vec<Feature>,
25}
26
27impl FeatureCollection {
28    /// Convert already-decoded layers to a `GeoJSON` [`FeatureCollection`], consuming them.
29    /// Make sure to call `decode_all` on Layer before calling this (won't compile otherwise)
30    pub fn from_layers<'a>(layers: impl IntoIterator<Item = ParsedLayer<'a>>) -> MltResult<Self> {
31        let mut features = Vec::new();
32        for layer in layers {
33            // Read the v2-only columns first: the rest of the loop needs only the
34            // shared ones, which is all `parsed` keeps.
35            #[cfg(feature = "unstable-v2")]
36            let mut m_values = match &layer {
37                ParsedLayer::Tag01(_) | ParsedLayer::Unknown(_) => Vec::new().into_iter(),
38                ParsedLayer::Tag02(l) => m_value_properties(l)?.into_iter(),
39            };
40            #[cfg(feature = "unstable-v2")]
41            let mut nested = match &layer {
42                ParsedLayer::Tag01(_) | ParsedLayer::Unknown(_) => Vec::new().into_iter(),
43                ParsedLayer::Tag02(l) => nested_properties(l)?.into_iter(),
44            };
45            let parsed = match layer {
46                ParsedLayer::Tag01(l) => l,
47                #[cfg(feature = "unstable-v2")]
48                ParsedLayer::Tag02(l) => l.into_layer(),
49                ParsedLayer::Unknown(_) => continue,
50            };
51            let layer_name = parsed.name();
52            let extent = parsed.extent().get();
53            let mut feat_iter = parsed.iter_features();
54            while let Some(feat) = feat_iter.next() {
55                let feat = feat?;
56                let mut properties = BTreeMap::new();
57                for p in feat.iter_properties() {
58                    properties.insert(p.name().to_string(), p.value().into());
59                }
60                #[cfg(feature = "unstable-v2")]
61                properties.extend(m_values.next().unwrap_or_default());
62                #[cfg(feature = "unstable-v2")]
63                properties.extend(nested.next().unwrap_or_default());
64                properties.insert("_layer".into(), Value::String(layer_name.to_string()));
65                properties.insert("_extent".into(), Value::Number(extent.into()));
66                features.push(Feature {
67                    geometry: feat.geometry().clone(),
68                    id: feat.id(),
69                    properties,
70                    ty: "Feature".into(),
71                });
72            }
73        }
74        Ok(Self {
75            features,
76            ty: "FeatureCollection".into(),
77        })
78    }
79
80    pub fn equals(&self, other: &Self) -> Result<bool, serde_json::Error> {
81        let self_val = normalize_tiny_floats(serde_json::to_value(self)?);
82        let other_val = normalize_tiny_floats(serde_json::to_value(other)?);
83        Ok(json_values_equal(&self_val, &other_val))
84    }
85}
86
87impl FromStr for FeatureCollection {
88    type Err = serde_json::Error;
89
90    fn from_str(s: &str) -> Result<Self, Self::Err> {
91        serde_json::from_str(s)
92    }
93}
94
95/// `GeoJSON` [`Feature`]
96#[derive(Debug, Clone, PartialEq, Deserialize)]
97pub struct Feature {
98    #[serde(with = "geom_serde")]
99    pub geometry: Geometry<i32>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub id: Option<u64>,
102    #[serde(default)]
103    pub properties: BTreeMap<String, Value>,
104    #[serde(rename = "type")]
105    pub ty: String,
106}
107
108struct Geom32Wire<'a>(&'a Geometry<i32>);
109impl Serialize for Geom32Wire<'_> {
110    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
111        geom_serde::serialize(self.0, s)
112    }
113}
114
115/// Serialize with the preferred order of the keys
116impl Serialize for Feature {
117    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
118        let len = 3 + usize::from(self.id.is_some());
119        let mut map = serializer.serialize_map(Some(len))?;
120        map.serialize_entry("type", &self.ty)?;
121        if let Some(id) = self.id {
122            map.serialize_entry("id", &id)?;
123        }
124        map.serialize_entry("properties", &self.properties)?;
125        map.serialize_entry("geometry", &Geom32Wire(&self.geometry))?;
126        map.end()
127    }
128}
129
130/// Serialize/deserialize [`Geometry<i32>`](geo_types::Geometry) in `GeoJSON` wire format:
131/// `{"type":"…","coordinates":…}` with `[x, y]` integer arrays.
132mod geom_serde {
133    use geo_types::{
134        Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon,
135    };
136    use serde::de::Error as _;
137    use serde::ser::{Error, SerializeMap as _};
138    use serde::{Deserialize, Deserializer, Serializer};
139    use serde_json::Value;
140
141    type Arr = [i32; 2];
142
143    fn ls_arr(ls: &LineString<i32>) -> Vec<Arr> {
144        ls.0.iter().copied().map(Into::into).collect()
145    }
146
147    fn poly_arr(poly: &Polygon<i32>) -> Vec<Vec<Arr>> {
148        std::iter::once(poly.exterior())
149            .chain(poly.interiors())
150            .map(ls_arr)
151            .collect()
152    }
153
154    fn arr_ls(v: Vec<Arr>) -> LineString<i32> {
155        LineString::from(v)
156    }
157
158    fn arr_poly(rings: Vec<Vec<Arr>>) -> Polygon<i32> {
159        let mut it = rings.into_iter();
160        let ext = it.next().map_or_else(|| LineString(vec![]), arr_ls);
161        Polygon::new(ext, it.map(arr_ls).collect())
162    }
163
164    pub fn serialize<S: Serializer>(g: &Geometry<i32>, s: S) -> Result<S::Ok, S::Error> {
165        let mut m = s.serialize_map(Some(2))?;
166        let (ty, coords): (&str, Value) = match g {
167            Geometry::Point(p) => ("Point", serde_json::to_value(Arr::from(*p)).unwrap()),
168            Geometry::LineString(ls) => ("LineString", serde_json::to_value(ls_arr(ls)).unwrap()),
169            Geometry::Polygon(poly) => ("Polygon", serde_json::to_value(poly_arr(poly)).unwrap()),
170            Geometry::MultiPoint(mp) => (
171                "MultiPoint",
172                serde_json::to_value(mp.0.iter().copied().map(Arr::from).collect::<Vec<_>>())
173                    .unwrap(),
174            ),
175            Geometry::MultiLineString(mls) => (
176                "MultiLineString",
177                serde_json::to_value(mls.iter().map(ls_arr).collect::<Vec<_>>()).unwrap(),
178            ),
179            Geometry::MultiPolygon(mpoly) => (
180                "MultiPolygon",
181                serde_json::to_value(mpoly.iter().map(poly_arr).collect::<Vec<_>>()).unwrap(),
182            ),
183            Geometry::Line(_)
184            | Geometry::Rect(_)
185            | Geometry::Triangle(_)
186            | Geometry::GeometryCollection(_) => {
187                return Err(Error::custom("unsupported geometry variant"));
188            }
189        };
190        m.serialize_entry("type", ty)?;
191        m.serialize_entry("coordinates", &coords)?;
192        m.end()
193    }
194
195    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Geometry<i32>, D::Error> {
196        fn parse<T: serde::de::DeserializeOwned, E: serde::de::Error>(v: Value) -> Result<T, E> {
197            serde_json::from_value(v).map_err(E::custom)
198        }
199
200        #[derive(Deserialize)]
201        struct Wire {
202            #[serde(rename = "type")]
203            ty: String,
204            coordinates: Value,
205        }
206
207        let Wire { ty, coordinates: c } = Wire::deserialize(d)?;
208        Ok(match ty.as_str() {
209            "Point" => Geometry::Point(Point::from(parse::<Arr, _>(c)?)),
210            "LineString" => Geometry::LineString(arr_ls(parse(c)?)),
211            "Polygon" => Geometry::Polygon(arr_poly(parse(c)?)),
212            "MultiPoint" => {
213                let v: Vec<Arr> = parse(c)?;
214                Geometry::MultiPoint(MultiPoint(v.into_iter().map(Point::from).collect()))
215            }
216            "MultiLineString" => {
217                let v: Vec<Vec<Arr>> = parse(c)?;
218                Geometry::MultiLineString(MultiLineString(v.into_iter().map(arr_ls).collect()))
219            }
220            "MultiPolygon" => {
221                let v: Vec<Vec<Vec<Arr>>> = parse(c)?;
222                Geometry::MultiPolygon(MultiPolygon(v.into_iter().map(arr_poly).collect()))
223            }
224            _ => {
225                return Err(D::Error::unknown_variant(
226                    &ty,
227                    &[
228                        "Point",
229                        "LineString",
230                        "Polygon",
231                        "MultiPoint",
232                        "MultiLineString",
233                        "MultiPolygon",
234                    ],
235                ));
236            }
237        })
238    }
239}
240
241/// Convert f32 to `GeoJSON` value: finite as number, non-finite as string per issue #978.
242#[must_use]
243pub fn f32_to_json(f: f32) -> Value {
244    if f.is_nan() {
245        Value::String("f32::NAN".to_owned())
246    } else if f == f32::INFINITY {
247        Value::String("f32::INFINITY".to_owned())
248    } else if f == f32::NEG_INFINITY {
249        Value::String("f32::NEG_INFINITY".to_owned())
250    } else {
251        Number::from_f64(f64::from(f)).expect("finite f32").into()
252    }
253}
254
255/// Convert f64 to `GeoJSON` value: finite as number, non-finite as string per issue #978.
256#[must_use]
257pub fn f64_to_json(f: f64) -> Value {
258    if f.is_nan() {
259        Value::String("f64::NAN".to_owned())
260    } else if f == f64::INFINITY {
261        Value::String("f64::INFINITY".to_owned())
262    } else if f == f64::NEG_INFINITY {
263        Value::String("f64::NEG_INFINITY".to_owned())
264    } else {
265        Number::from_f64(f).expect("finite f64").into()
266    }
267}
268
269/// Every feature's m-values as `m:<name>` properties, in feature order.
270///
271/// A feature with no values for a column has no property for it, the way a null
272/// property is left out.
273#[cfg(feature = "unstable-v2")]
274fn m_value_properties(layer: &ParsedLayer02<'_>) -> MltResult<Vec<Vec<(String, Value)>>> {
275    let geometry = layer.layer().geometry_values();
276    let mut features = vec![Vec::new(); geometry.feature_count()];
277    for column in layer.m_values() {
278        let key = format!("m:{}", column.name());
279        for (index, span) in column.spans(geometry).enumerate() {
280            let values = column.values().row(column.name(), span?)?;
281            if let Some(value) = m_value_to_json(&values) {
282                features[index].push((key.clone(), value));
283            }
284        }
285    }
286    Ok(features)
287}
288
289/// Every feature's nested columns as JSON, one entry per column under its own name.
290#[cfg(feature = "unstable-v2")]
291fn nested_properties(layer: &ParsedLayer02<'_>) -> MltResult<Vec<Vec<(String, Value)>>> {
292    let mut features = vec![Vec::new(); layer.layer().geometry_values().feature_count()];
293    for column in layer.nested() {
294        for (index, feature) in features.iter_mut().enumerate() {
295            feature.push((
296                column.name().to_string(),
297                nested_to_json(&column.row(index)?),
298            ));
299        }
300    }
301    Ok(features)
302}
303
304/// One nested value as the JSON it stands for: an object, an array, a scalar or null.
305#[cfg(feature = "unstable-v2")]
306fn nested_to_json(value: &NestedValue) -> Value {
307    match value {
308        NestedValue::Leaf(leaf) => prop_to_json(leaf),
309        NestedValue::List(None) | NestedValue::Map(None) => Value::Null,
310        NestedValue::List(Some(items)) => Value::Array(items.iter().map(nested_to_json).collect()),
311        NestedValue::Map(Some(entries)) => Value::Object(
312            entries
313                .iter()
314                .map(|(key, value)| (key.clone(), nested_to_json(value)))
315                .collect(),
316        ),
317    }
318}
319
320/// One scalar as JSON, a null value included.
321#[cfg(feature = "unstable-v2")]
322fn prop_to_json(value: &crate::PropValue) -> Value {
323    use crate::PropValue as P;
324    match value {
325        P::Bool(v) => v.map_or(Value::Null, Value::Bool),
326        P::I8(v) => v.map_or(Value::Null, Value::from),
327        P::U8(v) => v.map_or(Value::Null, Value::from),
328        P::I32(v) => v.map_or(Value::Null, Value::from),
329        P::U32(v) => v.map_or(Value::Null, Value::from),
330        P::I64(v) => v.map_or(Value::Null, Value::from),
331        P::U64(v) => v.map_or(Value::Null, Value::from),
332        P::F32(v) => v.map_or(Value::Null, f32_to_json),
333        P::F64(v) => v.map_or(Value::Null, f64_to_json),
334        P::Str(v) => v.clone().map_or(Value::Null, Value::String),
335    }
336}
337
338/// One feature's m-values as a JSON array, or [`None`] when it carries none.
339#[cfg(feature = "unstable-v2")]
340fn m_value_to_json(values: &MValue) -> Option<Value> {
341    /// A present run, mapped value by value.
342    macro_rules! array {
343        ($values:expr, $to_json:expr) => {
344            Value::Array($values.iter().copied().map($to_json).collect())
345        };
346    }
347    Some(match values {
348        MValue::Bool(v) => array!(v.as_ref()?, Value::Bool),
349        MValue::I8(v) => array!(v.as_ref()?, Value::from),
350        MValue::U8(v) => array!(v.as_ref()?, Value::from),
351        MValue::I32(v) => array!(v.as_ref()?, Value::from),
352        MValue::U32(v) => array!(v.as_ref()?, Value::from),
353        MValue::I64(v) => array!(v.as_ref()?, Value::from),
354        MValue::U64(v) => array!(v.as_ref()?, Value::from),
355        MValue::F32(v) => array!(v.as_ref()?, f32_to_json),
356        MValue::F64(v) => array!(v.as_ref()?, f64_to_json),
357        MValue::Str(v) => Value::Array(
358            v.as_ref()?
359                .iter()
360                .map(|s| Value::String(s.clone()))
361                .collect(),
362        ),
363    })
364}
365
366impl From<PropValueRef<'_>> for Value {
367    fn from(v: PropValueRef<'_>) -> Self {
368        match v {
369            PropValueRef::Bool(v) => Self::Bool(v),
370            PropValueRef::I8(v) => Self::from(v),
371            PropValueRef::U8(v) => Self::from(v),
372            PropValueRef::I32(v) => Self::from(v),
373            PropValueRef::U32(v) => Self::from(v),
374            PropValueRef::I64(v) => Self::from(v),
375            PropValueRef::U64(v) => Self::from(v),
376            PropValueRef::F32(v) => f32_to_json(v),
377            PropValueRef::F64(v) => f64_to_json(v),
378            PropValueRef::Str(s) => Self::String(s.to_string()),
379        }
380    }
381}
382
383/// Replace tiny float values (e.g. `1e-40`) with `0.0` to handle codec precision issues.
384fn normalize_tiny_floats(value: Value) -> Value {
385    match value {
386        Value::Number(ref n) => {
387            let eps = f64::from(f32::EPSILON);
388            if let Some(f) = n.as_f64()
389                && f.is_finite()
390                && f.abs() < eps
391            {
392                Value::from(0.0)
393            } else {
394                value
395            }
396        }
397        Value::Array(arr) => Value::Array(arr.into_iter().map(normalize_tiny_floats).collect()),
398        Value::Object(obj) => Value::Object(
399            obj.into_iter()
400                .map(|(k, v)| (k, normalize_tiny_floats(v)))
401                .collect(),
402        ),
403        Value::Null | Value::Bool(_) | Value::String(_) => value,
404    }
405}
406
407/// Compare two JSON values for equality. Numbers are compared with float tolerance so that
408/// f32 round-trip (e.g. 3.14 vs 3.140000104904175) and Java minimal decimal (e.g. 3.4028235e+38)
409/// match the Rust decoder output.
410fn json_values_equal(a: &Value, b: &Value) -> bool {
411    match (a, b) {
412        (Value::Number(na), Value::Number(nb)) if na.is_f64() && nb.is_f64() => {
413            let na = na.as_f64().expect("f64");
414            let nb = nb.as_f64().expect("f64");
415            assert!(
416                !na.is_nan() && !nb.is_nan(),
417                "unexpected non-finite numbers"
418            );
419            let abs_diff = (na - nb).abs();
420            let max_abs = na.abs().max(nb.abs()).max(1.0);
421            abs_diff <= f64::from(f32::EPSILON) * max_abs * 2.0
422        }
423        (Value::Array(aa), Value::Array(ab)) => {
424            aa.len() == ab.len()
425                && aa
426                    .iter()
427                    .zip(ab.iter())
428                    .all(|(x, y)| json_values_equal(x, y))
429        }
430        (Value::Object(ao), Value::Object(bo)) => {
431            ao.len() == bo.len()
432                && ao
433                    .iter()
434                    .all(|(k, v)| bo.get(k).is_some_and(|w| json_values_equal(v, w)))
435        }
436        _ => a == b,
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use geo_types::{
443        Coord, GeometryCollection, Line, LineString, MultiLineString, MultiPoint, MultiPolygon,
444        Point, Polygon, Rect, Triangle,
445    };
446    use insta::assert_snapshot;
447    use rstest::rstest;
448
449    use super::*;
450
451    fn ring(pts: &[(i32, i32)]) -> LineString<i32> {
452        LineString::from(pts.iter().map(|&(x, y)| Coord { x, y }).collect::<Vec<_>>())
453    }
454
455    fn square_with_hole() -> Polygon<i32> {
456        Polygon::new(
457            ring(&[(0, 0), (8, 0), (8, 8), (0, 8), (0, 0)]),
458            vec![ring(&[(2, 2), (4, 2), (4, 4), (2, 2)])],
459        )
460    }
461
462    fn feature(geometry: Geometry<i32>) -> Feature {
463        Feature {
464            geometry,
465            id: None,
466            properties: BTreeMap::new(),
467            ty: "Feature".into(),
468        }
469    }
470
471    fn supported_geometries() -> Vec<Geometry<i32>> {
472        vec![
473            Geometry::Point(Point::new(1, -2)),
474            Geometry::LineString(ring(&[(0, 0), (1, 1)])),
475            Geometry::LineString(LineString(vec![])),
476            Geometry::Polygon(square_with_hole()),
477            Geometry::Polygon(Polygon::new(LineString(vec![]), vec![])),
478            Geometry::MultiPoint(MultiPoint(vec![Point::new(1, 2), Point::new(3, 4)])),
479            Geometry::MultiPoint(MultiPoint(vec![])),
480            Geometry::MultiLineString(MultiLineString(vec![ring(&[(0, 0), (1, 1)])])),
481            Geometry::MultiPolygon(MultiPolygon(vec![square_with_hole()])),
482        ]
483    }
484
485    #[test]
486    fn supported_geometries_round_trip_through_geojson() {
487        let mut rendered = Vec::new();
488        for geometry in supported_geometries() {
489            let json = serde_json::to_string(&feature(geometry.clone())).expect("serialize");
490            let back: Feature = serde_json::from_str(&json).expect("deserialize");
491            assert_eq!(back.geometry, geometry);
492            rendered.push(json);
493        }
494
495        assert_snapshot!(rendered.join("\n"), @r#"
496        {"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[1,-2]}}
497        {"type":"Feature","properties":{},"geometry":{"type":"LineString","coordinates":[[0,0],[1,1]]}}
498        {"type":"Feature","properties":{},"geometry":{"type":"LineString","coordinates":[]}}
499        {"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[0,0],[8,0],[8,8],[0,8],[0,0]],[[2,2],[4,2],[4,4],[2,2]]]}}
500        {"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[]]}}
501        {"type":"Feature","properties":{},"geometry":{"type":"MultiPoint","coordinates":[[1,2],[3,4]]}}
502        {"type":"Feature","properties":{},"geometry":{"type":"MultiPoint","coordinates":[]}}
503        {"type":"Feature","properties":{},"geometry":{"type":"MultiLineString","coordinates":[[[0,0],[1,1]]]}}
504        {"type":"Feature","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[0,0],[8,0],[8,8],[0,8],[0,0]],[[2,2],[4,2],[4,4],[2,2]]]]}}
505        "#);
506    }
507
508    #[rstest]
509    #[case::line(Geometry::Line(Line::new(Coord { x: 0, y: 0 }, Coord { x: 1, y: 1 })))]
510    #[case::rect(Geometry::Rect(Rect::new(Coord { x: 0, y: 0 }, Coord { x: 1, y: 1 })))]
511    #[case::triangle(Geometry::Triangle(Triangle::new(
512        Coord { x: 0, y: 0 },
513        Coord { x: 1, y: 0 },
514        Coord { x: 0, y: 1 },
515    )))]
516    #[case::collection(Geometry::GeometryCollection(GeometryCollection(vec![])))]
517    fn geometries_outside_geojson_fail_to_serialize(#[case] geometry: Geometry<i32>) {
518        let err = serde_json::to_string(&feature(geometry)).expect_err("must not serialize");
519        assert_eq!(err.to_string(), "unsupported geometry variant");
520    }
521
522    #[test]
523    fn unknown_geometry_type_fails_to_deserialize() {
524        let err = serde_json::from_str::<Feature>(
525            r#"{"type":"Feature","properties":{},"geometry":{"type":"Circle","coordinates":[0,0]}}"#,
526        )
527        .expect_err("must not deserialize");
528        assert_snapshot!(
529            err,
530            @"unknown variant `Circle`, expected one of `Point`, `LineString`, `Polygon`, `MultiPoint`, `MultiLineString`, `MultiPolygon` at line 1 column 83"
531        );
532    }
533
534    #[test]
535    fn a_feature_collection_round_trips_through_its_text_form() {
536        let collection = FeatureCollection {
537            ty: "FeatureCollection".into(),
538            features: vec![Feature {
539                geometry: Geometry::Point(Point::new(7, 9)),
540                id: Some(42),
541                properties: BTreeMap::from([
542                    ("name".into(), Value::String("ß".into())),
543                    ("rank".into(), Value::from(3)),
544                ]),
545                ty: "Feature".into(),
546            }],
547        };
548
549        let text = serde_json::to_string(&collection).expect("serialize");
550        assert_snapshot!(
551            text,
552            @r#"{"type":"FeatureCollection","features":[{"type":"Feature","id":42,"properties":{"name":"ß","rank":3},"geometry":{"type":"Point","coordinates":[7,9]}}]}"#
553        );
554        assert_eq!(
555            FeatureCollection::from_str(&text).expect("parse"),
556            collection
557        );
558    }
559
560    #[test]
561    fn floats_outside_the_json_number_range_become_names() {
562        let values = [
563            f32_to_json(1.5),
564            f32_to_json(f32::NAN),
565            f32_to_json(f32::INFINITY),
566            f32_to_json(f32::NEG_INFINITY),
567            f64_to_json(1.5),
568            f64_to_json(f64::NAN),
569            f64_to_json(f64::INFINITY),
570            f64_to_json(f64::NEG_INFINITY),
571        ];
572        assert_snapshot!(
573            Value::Array(values.to_vec()),
574            @r#"[1.5,"f32::NAN","f32::INFINITY","f32::NEG_INFINITY",1.5,"f64::NAN","f64::INFINITY","f64::NEG_INFINITY"]"#
575        );
576    }
577
578    #[test]
579    fn every_property_kind_becomes_json() {
580        let values = [
581            PropValueRef::Bool(true),
582            PropValueRef::I8(-8),
583            PropValueRef::U8(8),
584            PropValueRef::I32(i32::MIN),
585            PropValueRef::U32(u32::MAX),
586            PropValueRef::I64(i64::MIN),
587            PropValueRef::U64(u64::MAX),
588            PropValueRef::F32(0.5),
589            PropValueRef::F64(f64::NAN),
590            PropValueRef::Str("text"),
591        ];
592        let json = Value::Array(values.into_iter().map(Value::from).collect());
593        assert_snapshot!(
594            json,
595            @r#"[true,-8,8,-2147483648,4294967295,-9223372036854775808,18446744073709551615,0.5,"f64::NAN","text"]"#
596        );
597    }
598
599    fn collection_with(value: &str) -> FeatureCollection {
600        FeatureCollection::from_str(&format!(
601            r#"{{"type":"FeatureCollection","features":[{{"type":"Feature","properties":{{"a":{value}}},"geometry":{{"type":"Point","coordinates":[0,0]}}}}]}}"#
602        ))
603        .expect("parse")
604    }
605
606    #[rstest]
607    #[case::identical("3.14", "3.14", true)]
608    #[case::f32_round_trip("3.14", "3.140000104904175", true)]
609    #[case::denormal_against_zero("1e-41", "0.0", true)]
610    #[case::denormal_against_integer_zero("1e-41", "0", true)]
611    #[case::largest_f32("3.4028235e38", "3.4028234663852886e38", true)]
612    #[case::beyond_f32_tolerance("1.0", "1.001", false)]
613    #[case::different_integers("1", "2", false)]
614    #[case::nested_arrays("[1.0, [2.0]]", "[1.0, [2.0]]", true)]
615    #[case::shorter_array("[1.0, 2.0]", "[1.0]", false)]
616    #[case::nan_name(r#""f64::NAN""#, r#""f64::NAN""#, true)]
617    #[case::number_against_string("1.0", r#""1.0""#, false)]
618    fn equals_compares_with_float_tolerance(
619        #[case] left: &str,
620        #[case] right: &str,
621        #[case] expected: bool,
622    ) {
623        let (left, right) = (collection_with(left), collection_with(right));
624        assert_eq!(left.equals(&right).expect("compare"), expected);
625        assert_eq!(right.equals(&left).expect("compare"), expected);
626    }
627
628    #[test]
629    fn equals_is_false_for_a_missing_property() {
630        let with_extra = FeatureCollection::from_str(
631            r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"a":1.0,"b":2.0},"geometry":{"type":"Point","coordinates":[0,0]}}]}"#,
632        )
633        .expect("parse");
634        assert!(!collection_with("1.0").equals(&with_extra).expect("compare"));
635        assert!(!with_extra.equals(&collection_with("1.0")).expect("compare"));
636    }
637}