Skip to main content

ferric_fred/
shape_file.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// A GeoFRED / Maps shape file (the `geofred/shapes/file` endpoint) — the region
5/// boundary polygons for a [`ShapeType`](crate::ShapeType), as a GeoJSON
6/// `FeatureCollection`.
7///
8/// This crate **transports** GeoJSON without interpreting it (ADR-0025):
9/// per-feature `properties` vary by shape type and the geometry coordinates are
10/// in GeoFRED's own display projection (integer pixel-like pairs, not lat/lon),
11/// so those parts are carried as [`serde_json::Value`] rather than modelled down
12/// to the polygon. A consumer that needs typed, projected geometry can re-parse
13/// the geometry with a dedicated GeoJSON crate. The `type`/`crs` fields are kept
14/// so the value round-trips as valid GeoJSON.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17pub struct ShapeFile {
18    /// The GeoJSON object type — `"FeatureCollection"`.
19    #[serde(rename = "type")]
20    pub kind: String,
21
22    /// FRED's name for the shape set, e.g. `"state_bea_region"`.
23    pub name: String,
24
25    /// The GeoJSON coordinate reference system, carried verbatim (kept for a
26    /// faithful round-trip; absent for shapes that omit it).
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub crs: Option<Value>,
29
30    /// The region features — one boundary per region.
31    pub features: Vec<Feature>,
32}
33
34/// A single GeoJSON feature in a [`ShapeFile`]: a region's properties and its
35/// boundary geometry.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
38pub struct Feature {
39    /// The GeoJSON object type — `"Feature"`.
40    #[serde(rename = "type")]
41    pub kind: String,
42
43    /// The region's properties, carried untyped. Keys vary by shape type (e.g. a
44    /// `bea` shape carries `bea_region`/`bea_regi_1`), and FRED emits an empty
45    /// **array** `[]` — not `{}` — for a feature with no properties, so this is a
46    /// [`serde_json::Value`] rather than a map.
47    pub properties: Value,
48
49    /// The region's boundary geometry.
50    pub geometry: Geometry,
51}
52
53/// A GeoJSON geometry within a [`Feature`]. The coordinates are carried untyped
54/// (they are deep nested arrays in GeoFRED's display projection); `kind` is the
55/// GeoJSON geometry type, e.g. `"MultiPolygon"`.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58pub struct Geometry {
59    /// The GeoJSON geometry type, e.g. `"MultiPolygon"` or `"Polygon"`.
60    #[serde(rename = "type")]
61    pub kind: String,
62
63    /// The geometry coordinates, verbatim — nested arrays in GeoFRED's display
64    /// projection, not interpreted by this crate.
65    pub coordinates: Value,
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    const SHAPE_FILE: &str = r#"{
73        "type": "FeatureCollection",
74        "name": "state_bea_region",
75        "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}},
76        "features": [
77            {
78                "type": "Feature",
79                "properties": {"bea_region": 8, "bea_regi_1": "Far West"},
80                "geometry": {"type": "MultiPolygon", "coordinates": [[[[1485, 2651], [1482, 2635]]]]}
81            }
82        ]
83    }"#;
84
85    #[test]
86    fn parses_feature_collection() {
87        let shapes: ShapeFile = serde_json::from_str(SHAPE_FILE).expect("shape file parses");
88        assert_eq!(shapes.kind, "FeatureCollection");
89        assert_eq!(shapes.name, "state_bea_region");
90        assert_eq!(shapes.features.len(), 1);
91        let feature = &shapes.features[0];
92        assert_eq!(feature.kind, "Feature");
93        assert_eq!(feature.properties["bea_regi_1"], Value::from("Far West"));
94        assert_eq!(feature.geometry.kind, "MultiPolygon");
95    }
96
97    #[test]
98    fn empty_properties_array_parses() {
99        // FRED emits `[]` (not `{}`) for a feature with no properties, and can
100        // return non-polygon geometries like MultiLineString — both must parse.
101        let feature: Feature = serde_json::from_str(
102            r#"{"type":"Feature","properties":[],
103                "geometry":{"type":"MultiLineString","coordinates":[[[-707,5188],[3651,2950]]]}}"#,
104        )
105        .expect("empty-properties feature parses");
106        assert_eq!(feature.properties, Value::Array(vec![]));
107        assert_eq!(feature.geometry.kind, "MultiLineString");
108    }
109
110    #[test]
111    fn round_trips_geometry_verbatim() {
112        // The untyped geometry must survive a parse -> serialize cycle unchanged.
113        let shapes: ShapeFile = serde_json::from_str(SHAPE_FILE).unwrap();
114        let reserialized = serde_json::to_value(&shapes).unwrap();
115        let original: Value = serde_json::from_str(SHAPE_FILE).unwrap();
116        assert_eq!(reserialized, original);
117    }
118}