geoarrow_schema/
metadata.rs

1use arrow_schema::{ArrowError, Field};
2use serde::{Deserialize, Serialize};
3
4use crate::Edges;
5use crate::crs::Crs;
6
7/// GeoArrow extension metadata.
8///
9/// This follows the extension metadata [defined by the GeoArrow
10/// specification](https://geoarrow.org/extension-types).
11///
12/// This struct is contained within all GeoArrow geometry type definitions, such as
13/// [`PointType`][crate::PointType], [`GeometryType`][crate::GeometryType], or
14/// [`WkbType`][crate::WkbType].
15#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
16pub struct Metadata {
17    // Raise the underlying crs fields to this level.
18    // https://serde.rs/attr-flatten.html
19    #[serde(flatten)]
20    crs: Crs,
21
22    /// If present, instructs consumers that edges follow a spherical path rather than a planar
23    /// one. If this value is omitted, edges will be interpreted as planar.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    edges: Option<Edges>,
26}
27
28impl Metadata {
29    /// Creates a new [`Metadata`] object.
30    pub fn new(crs: Crs, edges: Option<Edges>) -> Self {
31        Self { crs, edges }
32    }
33
34    /// Expose the underlying Coordinate Reference System information.
35    pub fn crs(&self) -> &Crs {
36        &self.crs
37    }
38
39    /// Expose the underlying edge interpolation
40    pub fn edges(&self) -> Option<Edges> {
41        self.edges
42    }
43
44    /// Serialize this metadata to a string.
45    ///
46    /// If `None`, no extension metadata should be written.
47    pub(crate) fn serialize(&self) -> Option<String> {
48        if self.crs.should_serialize() || self.edges.is_some() {
49            Some(serde_json::to_string(&self).unwrap())
50        } else {
51            None
52        }
53    }
54
55    /// Deserialize metadata from a string.
56    pub(crate) fn deserialize<S: AsRef<str>>(metadata: Option<S>) -> Result<Self, ArrowError> {
57        if let Some(ext_meta) = metadata {
58            Ok(serde_json::from_str(ext_meta.as_ref())
59                .map_err(|err| ArrowError::ExternalError(Box::new(err)))?)
60        } else {
61            Ok(Default::default())
62        }
63    }
64}
65
66impl TryFrom<&Field> for Metadata {
67    type Error = ArrowError;
68
69    fn try_from(value: &Field) -> Result<Self, Self::Error> {
70        Self::deserialize(value.extension_type_metadata())
71    }
72}
73
74#[cfg(test)]
75mod test {
76    use std::collections::HashMap;
77    use std::str::FromStr;
78
79    use arrow_schema::DataType;
80    use serde_json::{Value, json};
81
82    use super::*;
83
84    const EPSG_4326_WKT: &str = r#"GEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["World."],BBOX[-90,-180,90,180]],ID["EPSG",4326]]"#;
85
86    const EPSG_4326_PROJJSON: &str = r#"{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","type":"GeographicCRS","name":"WGS 84","datum_ensemble":{"name":"World Geodetic System 1984 ensemble","members":[{"name":"World Geodetic System 1984 (Transit)","id":{"authority":"EPSG","code":1166}},{"name":"World Geodetic System 1984 (G730)","id":{"authority":"EPSG","code":1152}},{"name":"World Geodetic System 1984 (G873)","id":{"authority":"EPSG","code":1153}},{"name":"World Geodetic System 1984 (G1150)","id":{"authority":"EPSG","code":1154}},{"name":"World Geodetic System 1984 (G1674)","id":{"authority":"EPSG","code":1155}},{"name":"World Geodetic System 1984 (G1762)","id":{"authority":"EPSG","code":1156}},{"name":"World Geodetic System 1984 (G2139)","id":{"authority":"EPSG","code":1309}}],"ellipsoid":{"name":"WGS 84","semi_major_axis":6378137,"inverse_flattening":298.257223563},"accuracy":"2.0","id":{"authority":"EPSG","code":6326}},"coordinate_system":{"subtype":"ellipsoidal","axis":[{"name":"Geodetic latitude","abbreviation":"Lat","direction":"north","unit":"degree"},{"name":"Geodetic longitude","abbreviation":"Lon","direction":"east","unit":"degree"}]},"scope":"Horizontal component of 3D system.","area":"World.","bbox":{"south_latitude":-90,"west_longitude":-180,"north_latitude":90,"east_longitude":180},"id":{"authority":"EPSG","code":4326}}"#;
87
88    #[test]
89    fn test_crs_authority_code() {
90        let crs = Crs::from_authority_code("EPSG:4326".to_string());
91        let metadata = Metadata::new(crs, Some(Edges::Spherical));
92
93        let expected = r#"{"crs":"EPSG:4326","crs_type":"authority_code","edges":"spherical"}"#;
94        let serialized = metadata.serialize();
95        assert_eq!(serialized.as_deref(), Some(expected));
96
97        assert_eq!(
98            metadata,
99            Metadata::deserialize(serialized.as_deref()).unwrap()
100        );
101    }
102
103    #[test]
104    fn test_crs_authority_code_no_edges() {
105        let crs = Crs::from_authority_code("EPSG:4326".to_string());
106        let metadata = Metadata::new(crs, None);
107
108        let expected = r#"{"crs":"EPSG:4326","crs_type":"authority_code"}"#;
109
110        let serialized = metadata.serialize();
111        assert_eq!(serialized.as_deref(), Some(expected));
112
113        assert_eq!(
114            metadata,
115            Metadata::deserialize(serialized.as_deref()).unwrap()
116        );
117    }
118
119    #[test]
120    fn test_crs_wkt() {
121        let crs = Crs::from_wkt2_2019(EPSG_4326_WKT.to_string());
122        let metadata = Metadata::new(crs, None);
123
124        let expected = r#"{"crs":"GEOGCRS[\"WGS 84\",ENSEMBLE[\"World Geodetic System 1984 ensemble\",MEMBER[\"World Geodetic System 1984 (Transit)\"],MEMBER[\"World Geodetic System 1984 (G730)\"],MEMBER[\"World Geodetic System 1984 (G873)\"],MEMBER[\"World Geodetic System 1984 (G1150)\"],MEMBER[\"World Geodetic System 1984 (G1674)\"],MEMBER[\"World Geodetic System 1984 (G1762)\"],MEMBER[\"World Geodetic System 1984 (G2139)\"],ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ENSEMBLEACCURACY[2.0]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS[ellipsoidal,2],AXIS[\"geodetic latitude (Lat)\",north,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS[\"geodetic longitude (Lon)\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],USAGE[SCOPE[\"Horizontal component of 3D system.\"],AREA[\"World.\"],BBOX[-90,-180,90,180]],ID[\"EPSG\",4326]]","crs_type":"wkt2:2019"}"#;
125
126        let serialized = metadata.serialize();
127        assert_eq!(serialized.as_deref(), Some(expected));
128
129        assert_eq!(
130            metadata,
131            Metadata::deserialize(serialized.as_deref()).unwrap()
132        );
133    }
134
135    #[test]
136    fn test_projjson() {
137        let crs = Crs::from_projjson(Value::from_str(EPSG_4326_PROJJSON).unwrap());
138        let metadata = Metadata::new(crs, None);
139
140        let expected = r#"{"crs":{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","type":"GeographicCRS","name":"WGS 84","datum_ensemble":{"name":"World Geodetic System 1984 ensemble","members":[{"name":"World Geodetic System 1984 (Transit)","id":{"authority":"EPSG","code":1166}},{"name":"World Geodetic System 1984 (G730)","id":{"authority":"EPSG","code":1152}},{"name":"World Geodetic System 1984 (G873)","id":{"authority":"EPSG","code":1153}},{"name":"World Geodetic System 1984 (G1150)","id":{"authority":"EPSG","code":1154}},{"name":"World Geodetic System 1984 (G1674)","id":{"authority":"EPSG","code":1155}},{"name":"World Geodetic System 1984 (G1762)","id":{"authority":"EPSG","code":1156}},{"name":"World Geodetic System 1984 (G2139)","id":{"authority":"EPSG","code":1309}}],"ellipsoid":{"name":"WGS 84","semi_major_axis":6378137,"inverse_flattening":298.257223563},"accuracy":"2.0","id":{"authority":"EPSG","code":6326}},"coordinate_system":{"subtype":"ellipsoidal","axis":[{"name":"Geodetic latitude","abbreviation":"Lat","direction":"north","unit":"degree"},{"name":"Geodetic longitude","abbreviation":"Lon","direction":"east","unit":"degree"}]},"scope":"Horizontal component of 3D system.","area":"World.","bbox":{"south_latitude":-90,"west_longitude":-180,"north_latitude":90,"east_longitude":180},"id":{"authority":"EPSG","code":4326}},"crs_type":"projjson"}"#;
141
142        let serialized = metadata.serialize();
143
144        // We use Value for equality checking because JSON string formatting is different
145        assert_eq!(
146            Value::from_str(serialized.as_deref().unwrap()).unwrap(),
147            Value::from_str(expected).unwrap()
148        );
149
150        assert_eq!(
151            metadata,
152            Metadata::deserialize(serialized.as_deref()).unwrap()
153        );
154    }
155
156    #[test]
157    fn test_unknown_crs() {
158        let crs = Crs::from_unknown_crs_type("CRS".to_string());
159        let metadata = Metadata::new(crs, None);
160
161        let expected = r#"{"crs":"CRS"}"#;
162
163        let serialized = metadata.serialize();
164        assert_eq!(serialized.as_deref(), Some(expected));
165
166        assert_eq!(
167            metadata,
168            Metadata::deserialize(serialized.as_deref()).unwrap()
169        );
170    }
171
172    #[test]
173    fn test_empty_metadata() {
174        let metadata = Metadata::default();
175        let serialized = metadata.serialize();
176        assert_eq!(serialized.as_deref(), None);
177
178        assert_eq!(
179            metadata,
180            Metadata::deserialize(serialized.as_deref()).unwrap()
181        );
182    }
183
184    #[test]
185    fn from_field() {
186        let field = Field::new("", DataType::Null, false).with_metadata(HashMap::from([(
187            "ARROW:extension:metadata".to_string(),
188            r#"{"crs": {}, "crs_type": "projjson", "edges": "spherical"}"#.to_string(),
189        )]));
190
191        let metadata = Metadata::try_from(&field).unwrap();
192        assert_eq!(metadata.crs(), &Crs::from_projjson(json!({})));
193        assert_eq!(metadata.edges(), Some(Edges::Spherical));
194
195        let bad_field = Field::new("", DataType::Null, false).with_metadata(HashMap::from([(
196            "ARROW:extension:metadata".to_string(),
197            "not valid json".to_string(),
198        )]));
199        assert_eq!(
200            Metadata::try_from(&bad_field).unwrap_err().to_string(),
201            "External error: expected ident at line 1 column 2"
202        );
203    }
204}