1use arrow_schema::{ArrowError, Field};
2use serde::{Deserialize, Serialize};
3
4use crate::Edges;
5use crate::crs::Crs;
6
7#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
16pub struct Metadata {
17 #[serde(flatten)]
20 crs: Crs,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
25 edges: Option<Edges>,
26}
27
28impl Metadata {
29 pub fn new(crs: Crs, edges: Option<Edges>) -> Self {
31 Self { crs, edges }
32 }
33
34 pub fn crs(&self) -> &Crs {
36 &self.crs
37 }
38
39 pub fn edges(&self) -> Option<Edges> {
41 self.edges
42 }
43
44 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 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 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}