Skip to main content

typed_geojson/
geometry.rs

1//! GeoJSON geometry types (RFC 7946 §3.1), shaped to match `@types/geojson`
2//! exactly so they round-trip through spec GeoJSON **and** export (via the
3//! `specta` feature) to TypeScript that is mutually assignable with native
4//! `GeoJSON.Point`, `GeoJSON.LineString`, … and the `GeoJSON.Geometry` union.
5//!
6//! Each geometry is its own named type (so you can pin one, like
7//! `Feature<Point, P>`); [`Geometry`] is their union. The `"type"` member is a
8//! single-variant enum (e.g. [`PointType`]) so it exports as the **string
9//! literal** `"Point"`, not `string`, which is what makes the native types
10//! assignable.
11
12use serde::{Deserialize, Serialize};
13
14use crate::Bbox;
15
16/// A GeoJSON position (RFC 7946 §3.1.1): longitude, latitude, and an optional
17/// third element (elevation). Modeled as `Vec<f64>` to match `@types/geojson`'s
18/// `Position = number[]` (a fixed `[f64; N]` would export to a TS tuple, which
19/// is not mutually assignable with `number[]`).
20pub type Position = Vec<f64>;
21
22/// Defines one coordinate-based geometry: a single-variant `type` tag enum that
23/// exports as the string literal, and the struct itself with an optional bbox.
24/// (Docs are static string literals because `specta`'s derive parses `#[doc]`
25/// and rejects a `concat!`.)
26macro_rules! coord_geometry {
27    ($name:ident, $tag:ident, $lit:literal, $coords:ty, $ts_coords:ty) => {
28        /// The `"type"` member of a single geometry kind: a string literal,
29        /// which is what makes the type assignable to the native geometry.
30        #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
31        #[cfg_attr(feature = "specta", derive(specta::Type))]
32        pub enum $tag {
33            #[serde(rename = $lit)]
34            $name,
35        }
36
37        /// A GeoJSON geometry object (RFC 7946 §3.1), matching the native
38        /// `@types/geojson` interface of the same name.
39        // `Serialize` is hand-written (below) so an absent `bbox` is *omitted*,
40        // not emitted as `null`. We can't use `#[serde(skip_serializing_if)]`
41        // because specta's unified mode rejects it on a `#[derive(Type)]` field.
42        #[derive(Clone, Debug, PartialEq, Deserialize)]
43        #[cfg_attr(feature = "specta", derive(specta::Type))]
44        pub struct $name {
45            // `specta` reads `#[serde(rename)]`, so the wire/TS name is `type`.
46            #[serde(rename = "type")]
47            pub r#type: $tag,
48            // serde uses the real `f64` coordinates; specta exports them via the
49            // `number`-rendering override (see [`crate::TsNumber`]).
50            #[cfg_attr(feature = "specta", specta(type = $ts_coords))]
51            pub coordinates: $coords,
52            // `type = Bbox` (not `Option<Bbox>`) makes the TS field `bbox?: Bbox`.
53            #[cfg_attr(feature = "specta", specta(type = Bbox, optional))]
54            pub bbox: Option<Bbox>,
55        }
56
57        impl $name {
58            /// Construct from coordinates (no `bbox`).
59            pub fn new(coordinates: $coords) -> Self {
60                Self {
61                    r#type: $tag::$name,
62                    coordinates,
63                    bbox: None,
64                }
65            }
66        }
67
68        impl Serialize for $name {
69            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
70                use serde::ser::SerializeMap;
71                let mut map = serializer.serialize_map(Some(2 + self.bbox.is_some() as usize))?;
72                map.serialize_entry("type", $lit)?;
73                map.serialize_entry("coordinates", &self.coordinates)?;
74                if let Some(bbox) = &self.bbox {
75                    map.serialize_entry("bbox", bbox)?;
76                }
77                map.end()
78            }
79        }
80    };
81}
82
83coord_geometry!(Point, PointType, "Point", Position, Vec<crate::TsNumber>);
84coord_geometry!(
85    MultiPoint,
86    MultiPointType,
87    "MultiPoint",
88    Vec<Position>,
89    Vec<Vec<crate::TsNumber>>
90);
91coord_geometry!(
92    LineString,
93    LineStringType,
94    "LineString",
95    Vec<Position>,
96    Vec<Vec<crate::TsNumber>>
97);
98coord_geometry!(
99    MultiLineString,
100    MultiLineStringType,
101    "MultiLineString",
102    Vec<Vec<Position>>,
103    Vec<Vec<Vec<crate::TsNumber>>>
104);
105coord_geometry!(
106    Polygon,
107    PolygonType,
108    "Polygon",
109    Vec<Vec<Position>>,
110    Vec<Vec<Vec<crate::TsNumber>>>
111);
112coord_geometry!(
113    MultiPolygon,
114    MultiPolygonType,
115    "MultiPolygon",
116    Vec<Vec<Vec<Position>>>,
117    Vec<Vec<Vec<Vec<crate::TsNumber>>>>
118);
119
120/// The `"GeometryCollection"` value of a [`GeometryCollection`]'s `type` member.
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
122#[cfg_attr(feature = "specta", derive(specta::Type))]
123pub enum GeometryCollectionType {
124    GeometryCollection,
125}
126
127/// A GeoJSON GeometryCollection (RFC 7946 §3.1.8): a list of geometries.
128// `Serialize` is hand-written (below) to omit an absent `bbox`; see the note
129// on the coordinate geometries above.
130#[derive(Clone, Debug, PartialEq, Deserialize)]
131#[cfg_attr(feature = "specta", derive(specta::Type))]
132pub struct GeometryCollection {
133    #[serde(rename = "type")]
134    pub r#type: GeometryCollectionType,
135    pub geometries: Vec<Geometry>,
136    #[cfg_attr(feature = "specta", specta(type = Bbox, optional))]
137    pub bbox: Option<Bbox>,
138}
139
140impl GeometryCollection {
141    /// A `GeometryCollection` from its member geometries (no bbox).
142    pub fn new(geometries: Vec<Geometry>) -> Self {
143        Self {
144            r#type: GeometryCollectionType::GeometryCollection,
145            geometries,
146            bbox: None,
147        }
148    }
149}
150
151impl Serialize for GeometryCollection {
152    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
153        use serde::ser::SerializeMap;
154        let mut map = serializer.serialize_map(Some(2 + self.bbox.is_some() as usize))?;
155        map.serialize_entry("type", "GeometryCollection")?;
156        map.serialize_entry("geometries", &self.geometries)?;
157        if let Some(bbox) = &self.bbox {
158            map.serialize_entry("bbox", bbox)?;
159        }
160        map.end()
161    }
162}
163
164/// The GeoJSON geometry union (RFC 7946 §3.1): mirrors `@types/geojson`'s
165/// `Geometry`. Untagged at the serde layer; each member's own `"type"` literal
166/// disambiguates, so it round-trips losslessly and exports to the TS union.
167#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168#[cfg_attr(feature = "specta", derive(specta::Type))]
169#[serde(untagged)]
170pub enum Geometry {
171    Point(Point),
172    MultiPoint(MultiPoint),
173    LineString(LineString),
174    MultiLineString(MultiLineString),
175    Polygon(Polygon),
176    MultiPolygon(MultiPolygon),
177    GeometryCollection(GeometryCollection),
178}
179
180// --- interop with the untyped `geojson` crate's geometry ----------------------
181//
182// `geojson` models coordinates with its own `Position` newtype (a `TinyVec`)
183// and a struct-variant `GeometryValue`, so a field-by-field conversion would be
184// verbose and easy to get wrong. Both sides are valid RFC 7946 geometry, so we
185// bridge through a serde round-trip: lossless for the geometry types we share,
186// and fallible only if the untyped value is out-of-spec (e.g. a bbox that is
187// not 4 or 6 numbers).
188
189impl TryFrom<Geometry> for geojson::Geometry {
190    type Error = serde_json::Error;
191
192    fn try_from(g: Geometry) -> Result<Self, Self::Error> {
193        serde_json::from_value(serde_json::to_value(g)?)
194    }
195}
196
197impl TryFrom<geojson::Geometry> for Geometry {
198    type Error = serde_json::Error;
199
200    fn try_from(g: geojson::Geometry) -> Result<Self, Self::Error> {
201        serde_json::from_value(serde_json::to_value(g)?)
202    }
203}