stac_io/
format.rs

1use crate::{Error, Readable, RealizedHref, Result, Writeable};
2use bytes::Bytes;
3use stac::SelfHref;
4use std::{fmt::Display, path::Path, str::FromStr};
5
6/// The format of STAC data.
7#[derive(Debug, Copy, Clone, PartialEq)]
8pub enum Format {
9    /// JSON data (the default).
10    ///
11    /// If `true`, the data will be pretty-printed on write.
12    Json(bool),
13
14    /// Newline-delimited JSON.
15    NdJson,
16
17    /// [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet)
18    #[cfg(feature = "geoparquet")]
19    Geoparquet(stac::geoparquet::WriterOptions),
20}
21
22impl Format {
23    /// Infer the format from a file extension.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use stac_io::Format;
29    ///
30    /// assert_eq!(Format::Json(false), Format::infer_from_href("item.json").unwrap());
31    /// ```
32    pub fn infer_from_href(href: &str) -> Option<Format> {
33        href.rsplit_once('.').and_then(|(_, ext)| ext.parse().ok())
34    }
35
36    /// Returns this format's file extension.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use stac_io::Format;
42    /// assert_eq!(Format::json().extension(), "json");
43    /// assert_eq!(Format::ndjson().extension(), "ndjson");
44    /// #[cfg(feature = "geoparquet")]
45    /// assert_eq!(Format::geoparquet().extension(), "parquet");
46    /// ```
47    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    /// Returns true if this is a geoparquet href.
57    #[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    /// Reads a STAC object from an href in this format.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use stac::Item;
68    /// use stac_io::Format;
69    ///
70    /// let item: Item = Format::json().read("examples/simple-item.json").unwrap();
71    /// ```
72    #[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    /// Reads a local file in the given format.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use stac::Item;
97    /// use stac_io::Format;
98    ///
99    /// let item: Item = Format::json().from_path("examples/simple-item.json").unwrap();
100    /// ```
101    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    /// Reads a STAC object from some bytes.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use stac::Item;
127    /// use stac_io::Format;
128    /// use std::{io::Read, fs::File};
129    ///
130    /// let mut buf = Vec::new();
131    /// File::open("examples/simple-item.json").unwrap().read_to_end(&mut buf).unwrap();
132    /// let item: Item = Format::json().from_bytes(buf).unwrap();
133    /// ```
134    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    /// Writes a STAC value to the provided path.
145    ///
146    /// # Examples
147    ///
148    /// ```no_run
149    /// use stac::Item;
150    /// use stac_io::Format;
151    ///
152    /// Format::json().write("an-id.json", Item::new("an-id")).unwrap();
153    /// ```
154    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    /// Converts a STAC object into some bytes.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use stac::Item;
169    /// use stac_io::Format;
170    ///
171    /// let item = Item::new("an-id");
172    /// let bytes = Format::json().into_vec(item).unwrap();
173    /// ```
174    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    /// Returns the default JSON format (compact).
185    pub fn json() -> Format {
186        Format::Json(false)
187    }
188
189    /// Returns the newline-delimited JSON format.
190    pub fn ndjson() -> Format {
191        Format::NdJson
192    }
193
194    /// Returns the default geoparquet format.
195    #[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}