1use crate::{Error, Readable, RealizedHref, Result, Writeable};
2use bytes::Bytes;
3use stac::SelfHref;
4use std::{fmt::Display, path::Path, str::FromStr};
5
6#[derive(Debug, Copy, Clone, PartialEq)]
8pub enum Format {
9 Json(bool),
13
14 NdJson,
16
17 #[cfg(feature = "geoparquet")]
19 Geoparquet(Option<stac::geoparquet::Compression>),
20}
21
22impl Format {
23 pub fn infer_from_href(href: &str) -> Option<Format> {
33 href.rsplit_once('.').and_then(|(_, ext)| ext.parse().ok())
34 }
35
36 pub fn extension(&self) -> &'static str {
48 match self {
49 Format::Json(_) => "json",
50 Format::NdJson => "ndjson",
51 #[cfg(feature = "geoparquet")]
52 Format::Geoparquet(_) => "parquet",
53 }
54 }
55
56 #[cfg(feature = "geoparquet")]
58 pub fn is_geoparquet_href(href: &str) -> bool {
59 matches!(Format::infer_from_href(href), Some(Format::Geoparquet(_)))
60 }
61
62 #[allow(unused_variables)]
73 pub fn read<T: Readable + SelfHref>(&self, href: impl ToString) -> Result<T> {
74 let mut href = href.to_string();
75 let mut value: T = match href.as_str().into() {
76 RealizedHref::Url(url) => {
77 #[cfg(feature = "reqwest")]
78 {
79 let bytes = reqwest::blocking::get(url)?.bytes()?;
80 self.from_bytes(bytes)?
81 }
82 #[cfg(not(feature = "reqwest"))]
83 {
84 return Err(Error::FeatureNotEnabled("reqwest"));
85 }
86 }
87 RealizedHref::PathBuf(path) => {
88 let path = path.canonicalize()?;
89 let value = self.from_path(&path)?;
90 href = path.as_path().to_string_lossy().into_owned();
91 value
92 }
93 };
94 value.set_self_href(href);
95 Ok(value)
96 }
97
98 pub fn from_path<T: Readable + SelfHref>(&self, path: impl AsRef<Path>) -> Result<T> {
109 let path = path.as_ref().canonicalize()?;
110 match self {
111 Format::Json(_) => T::from_json_path(&path),
112 Format::NdJson => T::from_ndjson_path(&path),
113 #[cfg(feature = "geoparquet")]
114 Format::Geoparquet(_) => T::from_geoparquet_path(&path),
115 }
116 .map_err(|err| {
117 if let Error::Io(err) = err {
118 Error::FromPath {
119 io: err,
120 path: path.to_string_lossy().into_owned(),
121 }
122 } else {
123 err
124 }
125 })
126 }
127
128 pub fn from_bytes<T: Readable>(&self, bytes: impl Into<Bytes>) -> Result<T> {
142 let value = match self {
143 Format::Json(_) => T::from_json_slice(&bytes.into())?,
144 Format::NdJson => T::from_ndjson_bytes(bytes)?,
145 #[cfg(feature = "geoparquet")]
146 Format::Geoparquet(_) => T::from_geoparquet_bytes(bytes)?,
147 };
148 Ok(value)
149 }
150
151 pub fn write<T: Writeable>(&self, path: impl AsRef<Path>, value: T) -> Result<()> {
162 match self {
163 Format::Json(pretty) => value.to_json_path(path, *pretty),
164 Format::NdJson => value.to_ndjson_path(path),
165 #[cfg(feature = "geoparquet")]
166 Format::Geoparquet(compression) => value.into_geoparquet_path(path, *compression),
167 }
168 }
169
170 pub fn into_vec<T: Writeable>(&self, value: T) -> Result<Vec<u8>> {
182 let value = match self {
183 Format::Json(pretty) => value.to_json_vec(*pretty)?,
184 Format::NdJson => value.to_ndjson_vec()?,
185 #[cfg(feature = "geoparquet")]
186 Format::Geoparquet(compression) => value.into_geoparquet_vec(*compression)?,
187 };
188 Ok(value)
189 }
190
191 pub fn json() -> Format {
193 Format::Json(false)
194 }
195
196 pub fn ndjson() -> Format {
198 Format::NdJson
199 }
200
201 #[cfg(feature = "geoparquet")]
203 pub fn geoparquet() -> Format {
204 Format::Geoparquet(Some(stac::geoparquet::DEFAULT_COMPRESSION))
205 }
206}
207
208impl Default for Format {
209 fn default() -> Self {
210 Self::Json(false)
211 }
212}
213
214impl Display for Format {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 match self {
217 Self::Json(pretty) => {
218 if *pretty {
219 f.write_str("json-pretty")
220 } else {
221 f.write_str("json")
222 }
223 }
224 Self::NdJson => f.write_str("ndjson"),
225 #[cfg(feature = "geoparquet")]
226 Self::Geoparquet(compression) => {
227 if let Some(compression) = *compression {
228 write!(f, "geoparquet[{compression}]")
229 } else {
230 f.write_str("geoparquet")
231 }
232 }
233 }
234 }
235}
236
237impl FromStr for Format {
238 type Err = Error;
239
240 #[cfg_attr(not(feature = "geoparquet"), allow(unused_variables))]
241 fn from_str(s: &str) -> Result<Format> {
242 match s.to_ascii_lowercase().as_str() {
243 "json" | "geojson" => Ok(Self::Json(false)),
244 "json-pretty" | "geojson-pretty" => Ok(Self::Json(true)),
245 "ndjson" => Ok(Self::NdJson),
246 _ => {
247 #[cfg(feature = "geoparquet")]
248 {
249 infer_geoparquet_format(s)
250 }
251 #[cfg(not(feature = "geoparquet"))]
252 Err(Error::UnsupportedFormat(s.to_string()))
253 }
254 }
255 }
256}
257
258#[cfg(feature = "geoparquet")]
259fn infer_geoparquet_format(s: &str) -> Result<Format> {
260 if s.starts_with("parquet") || s.starts_with("geoparquet") {
261 if let Some((_, compression)) = s.split_once('[') {
262 if let Some(stop) = compression.find(']') {
263 let format = compression[..stop]
264 .parse()
265 .map(Some)
266 .map(Format::Geoparquet)?;
267 Ok(format)
268 } else {
269 Err(Error::UnsupportedFormat(s.to_string()))
270 }
271 } else {
272 Ok(Format::Geoparquet(Some(
273 stac::geoparquet::DEFAULT_COMPRESSION,
274 )))
275 }
276 } else {
277 Err(Error::UnsupportedFormat(s.to_string()))
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::Format;
284
285 #[test]
286 #[cfg(not(feature = "geoparquet"))]
287 fn parse_geoparquet() {
288 assert!(matches!(
289 "parquet".parse::<Format>().unwrap_err(),
290 crate::Error::UnsupportedFormat(_),
291 ));
292 }
293
294 #[cfg(feature = "geoparquet")]
295 mod geoparquet {
296 use super::Format;
297 use stac::geoparquet::Compression;
298
299 #[test]
300 fn parse_geoparquet_compression() {
301 let format: Format = "geoparquet[snappy]".parse().unwrap();
302 assert_eq!(format, Format::Geoparquet(Some(Compression::SNAPPY)));
303 }
304
305 #[test]
306 fn infer_from_href() {
307 assert_eq!(
308 Format::Geoparquet(Some(Compression::SNAPPY)),
309 Format::infer_from_href("out.parquet").unwrap()
310 );
311 }
312 }
313}