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(stac::geoparquet::WriterOptions),
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 let bytes = reqwest::blocking::get(url)?.bytes()?;
78 self.from_bytes(bytes)?
79 }
80 RealizedHref::PathBuf(path) => {
81 let path = path.canonicalize()?;
82 let value = self.from_path(&path)?;
83 href = path.as_path().to_string_lossy().into_owned();
84 value
85 }
86 };
87 value.set_self_href(href);
88 Ok(value)
89 }
90
91 pub fn from_path<T: Readable + SelfHref>(&self, path: impl AsRef<Path>) -> Result<T> {
102 let path = path.as_ref().canonicalize()?;
103 match self {
104 Format::Json(_) => T::from_json_path(&path),
105 Format::NdJson => T::from_ndjson_path(&path),
106 #[cfg(feature = "geoparquet")]
107 Format::Geoparquet(_) => T::from_geoparquet_path(&path),
108 }
109 .map_err(|err| {
110 if let Error::Io(err) = err {
111 Error::FromPath {
112 io: err,
113 path: path.to_string_lossy().into_owned(),
114 }
115 } else {
116 err
117 }
118 })
119 }
120
121 pub fn from_bytes<T: Readable>(&self, bytes: impl Into<Bytes>) -> Result<T> {
135 let value = match self {
136 Format::Json(_) => T::from_json_slice(&bytes.into())?,
137 Format::NdJson => T::from_ndjson_bytes(bytes)?,
138 #[cfg(feature = "geoparquet")]
139 Format::Geoparquet(_) => T::from_geoparquet_bytes(bytes)?,
140 };
141 Ok(value)
142 }
143
144 pub fn write<T: Writeable>(&self, path: impl AsRef<Path>, value: T) -> Result<()> {
155 match self {
156 Format::Json(pretty) => value.to_json_path(path, *pretty),
157 Format::NdJson => value.to_ndjson_path(path),
158 #[cfg(feature = "geoparquet")]
159 Format::Geoparquet(writer_options) => value.into_geoparquet_path(path, *writer_options),
160 }
161 }
162
163 pub fn into_vec<T: Writeable>(&self, value: T) -> Result<Vec<u8>> {
175 let value = match self {
176 Format::Json(pretty) => value.to_json_vec(*pretty)?,
177 Format::NdJson => value.to_ndjson_vec()?,
178 #[cfg(feature = "geoparquet")]
179 Format::Geoparquet(writer_options) => value.into_geoparquet_vec(*writer_options)?,
180 };
181 Ok(value)
182 }
183
184 pub fn json() -> Format {
186 Format::Json(false)
187 }
188
189 pub fn ndjson() -> Format {
191 Format::NdJson
192 }
193
194 #[cfg(feature = "geoparquet")]
196 pub fn geoparquet() -> Format {
197 Format::Geoparquet(stac::geoparquet::WriterOptions::default())
198 }
199}
200
201impl Default for Format {
202 fn default() -> Self {
203 Self::Json(false)
204 }
205}
206
207impl Display for Format {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 match self {
210 Self::Json(pretty) => {
211 if *pretty {
212 f.write_str("json-pretty")
213 } else {
214 f.write_str("json")
215 }
216 }
217 Self::NdJson => f.write_str("ndjson"),
218 #[cfg(feature = "geoparquet")]
219 Self::Geoparquet(writer_options) => {
220 if let Some(compression) = writer_options.compression {
221 write!(f, "geoparquet[{compression}]")
222 } else {
223 f.write_str("geoparquet")
224 }
225 }
226 }
227 }
228}
229
230impl FromStr for Format {
231 type Err = Error;
232
233 #[cfg_attr(not(feature = "geoparquet"), allow(unused_variables))]
234 fn from_str(s: &str) -> Result<Format> {
235 match s.to_ascii_lowercase().as_str() {
236 "json" | "geojson" => Ok(Self::Json(false)),
237 "json-pretty" | "geojson-pretty" => Ok(Self::Json(true)),
238 "ndjson" => Ok(Self::NdJson),
239 _ => {
240 #[cfg(feature = "geoparquet")]
241 {
242 infer_geoparquet_format(s)
243 }
244 #[cfg(not(feature = "geoparquet"))]
245 Err(Error::UnsupportedFormat(s.to_string()))
246 }
247 }
248 }
249}
250
251#[cfg(feature = "geoparquet")]
252fn infer_geoparquet_format(s: &str) -> Result<Format> {
253 if s.starts_with("parquet") || s.starts_with("geoparquet") {
254 if let Some((_, compression_str)) = s.split_once('[') {
255 if let Some(stop) = compression_str.find(']') {
256 let compression: stac::geoparquet::Compression = compression_str[..stop].parse()?;
257 let writer_options =
258 stac::geoparquet::WriterOptions::new().with_compression(compression);
259 Ok(Format::Geoparquet(writer_options))
260 } else {
261 Err(Error::UnsupportedFormat(s.to_string()))
262 }
263 } else {
264 Ok(Format::Geoparquet(
265 stac::geoparquet::WriterOptions::default(),
266 ))
267 }
268 } else {
269 Err(Error::UnsupportedFormat(s.to_string()))
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::Format;
276
277 #[test]
278 #[cfg(not(feature = "geoparquet"))]
279 fn parse_geoparquet() {
280 assert!(matches!(
281 "parquet".parse::<Format>().unwrap_err(),
282 crate::Error::UnsupportedFormat(_),
283 ));
284 }
285
286 #[cfg(feature = "geoparquet")]
287 mod geoparquet {
288 use super::Format;
289 use stac::geoparquet::{Compression, WriterOptions};
290
291 #[test]
292 fn parse_geoparquet_compression() {
293 let format: Format = "geoparquet[snappy]".parse().unwrap();
294 let expected =
295 Format::Geoparquet(WriterOptions::new().with_compression(Compression::SNAPPY));
296 assert_eq!(format, expected);
297 }
298
299 #[test]
300 fn infer_from_href() {
301 let format = Format::infer_from_href("out.parquet").unwrap();
302 let expected = Format::Geoparquet(WriterOptions::default());
303 assert_eq!(format, expected);
304 }
305 }
306}