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(Option<stac::geoparquet::Compression>),
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                #[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    /// Reads a local file in the given format.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use stac::Item;
104    /// use stac_io::Format;
105    ///
106    /// let item: Item = Format::json().from_path("examples/simple-item.json").unwrap();
107    /// ```
108    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    /// Reads a STAC object from some bytes.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use stac::Item;
134    /// use stac_io::Format;
135    /// use std::{io::Read, fs::File};
136    ///
137    /// let mut buf = Vec::new();
138    /// File::open("examples/simple-item.json").unwrap().read_to_end(&mut buf).unwrap();
139    /// let item: Item = Format::json().from_bytes(buf).unwrap();
140    /// ```
141    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    /// Writes a STAC value to the provided path.
152    ///
153    /// # Examples
154    ///
155    /// ```no_run
156    /// use stac::Item;
157    /// use stac_io::Format;
158    ///
159    /// Format::json().write("an-id.json", Item::new("an-id")).unwrap();
160    /// ```
161    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    /// Converts a STAC object into some bytes.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use stac::Item;
176    /// use stac_io::Format;
177    ///
178    /// let item = Item::new("an-id");
179    /// let bytes = Format::json().into_vec(item).unwrap();
180    /// ```
181    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    /// Returns the default JSON format (compact).
192    pub fn json() -> Format {
193        Format::Json(false)
194    }
195
196    /// Returns the newline-delimited JSON format.
197    pub fn ndjson() -> Format {
198        Format::NdJson
199    }
200
201    /// Returns the default geoparquet format (snappy compression if compression is enabled).
202    #[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}