use crate::{Error, Readable, RealizedHref, Result, Writeable};
use bytes::Bytes;
use stac::SelfHref;
use std::{fmt::Display, path::Path, str::FromStr};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Format {
Json(bool),
NdJson,
#[cfg(feature = "geoparquet")]
Geoparquet(stac::geoparquet::WriterOptions),
}
impl Format {
pub fn infer_from_href(href: &str) -> Option<Format> {
href.rsplit_once('.').and_then(|(_, ext)| ext.parse().ok())
}
pub fn extension(&self) -> &'static str {
match self {
Format::Json(_) => "json",
Format::NdJson => "ndjson",
#[cfg(feature = "geoparquet")]
Format::Geoparquet(_) => "parquet",
}
}
#[cfg(feature = "geoparquet")]
pub fn is_geoparquet_href(href: &str) -> bool {
matches!(Format::infer_from_href(href), Some(Format::Geoparquet(_)))
}
#[allow(unused_variables)]
pub fn read<T: Readable + SelfHref>(&self, href: impl ToString) -> Result<T> {
let mut href = href.to_string();
let mut value: T = match href.as_str().into() {
RealizedHref::Url(url) => {
let bytes = reqwest::blocking::get(url)?.bytes()?;
self.from_bytes(bytes)?
}
RealizedHref::PathBuf(path) => {
let path = path.canonicalize()?;
let value = self.from_path(&path)?;
href = path.as_path().to_string_lossy().into_owned();
value
}
};
value.set_self_href(href);
Ok(value)
}
pub fn from_path<T: Readable + SelfHref>(&self, path: impl AsRef<Path>) -> Result<T> {
let path = path.as_ref().canonicalize()?;
match self {
Format::Json(_) => T::from_json_path(&path),
Format::NdJson => T::from_ndjson_path(&path),
#[cfg(feature = "geoparquet")]
Format::Geoparquet(_) => T::from_geoparquet_path(&path),
}
.map_err(|err| {
if let Error::Io(err) = err {
Error::FromPath {
io: err,
path: path.to_string_lossy().into_owned(),
}
} else {
err
}
})
}
pub fn from_bytes<T: Readable>(&self, bytes: impl Into<Bytes>) -> Result<T> {
let value = match self {
Format::Json(_) => T::from_json_slice(&bytes.into())?,
Format::NdJson => T::from_ndjson_bytes(bytes)?,
#[cfg(feature = "geoparquet")]
Format::Geoparquet(_) => T::from_geoparquet_bytes(bytes)?,
};
Ok(value)
}
pub fn write<T: Writeable>(&self, path: impl AsRef<Path>, value: T) -> Result<()> {
match self {
Format::Json(pretty) => value.to_json_path(path, *pretty),
Format::NdJson => value.to_ndjson_path(path),
#[cfg(feature = "geoparquet")]
Format::Geoparquet(writer_options) => value.into_geoparquet_path(path, *writer_options),
}
}
pub fn into_vec<T: Writeable>(&self, value: T) -> Result<Vec<u8>> {
let value = match self {
Format::Json(pretty) => value.to_json_vec(*pretty)?,
Format::NdJson => value.to_ndjson_vec()?,
#[cfg(feature = "geoparquet")]
Format::Geoparquet(writer_options) => value.into_geoparquet_vec(*writer_options)?,
};
Ok(value)
}
pub fn json() -> Format {
Format::Json(false)
}
pub fn ndjson() -> Format {
Format::NdJson
}
#[cfg(feature = "geoparquet")]
pub fn geoparquet() -> Format {
Format::Geoparquet(stac::geoparquet::WriterOptions::default())
}
}
impl Default for Format {
fn default() -> Self {
Self::Json(false)
}
}
impl Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json(pretty) => {
if *pretty {
f.write_str("json-pretty")
} else {
f.write_str("json")
}
}
Self::NdJson => f.write_str("ndjson"),
#[cfg(feature = "geoparquet")]
Self::Geoparquet(writer_options) => {
if let Some(compression) = writer_options.compression {
write!(f, "geoparquet[{compression}]")
} else {
f.write_str("geoparquet")
}
}
}
}
}
impl FromStr for Format {
type Err = Error;
#[cfg_attr(not(feature = "geoparquet"), allow(unused_variables))]
fn from_str(s: &str) -> Result<Format> {
match s.to_ascii_lowercase().as_str() {
"json" | "geojson" => Ok(Self::Json(false)),
"json-pretty" | "geojson-pretty" => Ok(Self::Json(true)),
"ndjson" => Ok(Self::NdJson),
_ => {
#[cfg(feature = "geoparquet")]
{
infer_geoparquet_format(s)
}
#[cfg(not(feature = "geoparquet"))]
Err(Error::UnsupportedFormat(s.to_string()))
}
}
}
}
#[cfg(feature = "geoparquet")]
fn infer_geoparquet_format(s: &str) -> Result<Format> {
if s.starts_with("parquet") || s.starts_with("geoparquet") {
if let Some((_, compression_str)) = s.split_once('[') {
if let Some(stop) = compression_str.find(']') {
let compression: stac::geoparquet::Compression = compression_str[..stop].parse()?;
let writer_options =
stac::geoparquet::WriterOptions::new().with_compression(compression);
Ok(Format::Geoparquet(writer_options))
} else {
Err(Error::UnsupportedFormat(s.to_string()))
}
} else {
Ok(Format::Geoparquet(
stac::geoparquet::WriterOptions::default(),
))
}
} else {
Err(Error::UnsupportedFormat(s.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::Format;
#[test]
#[cfg(not(feature = "geoparquet"))]
fn parse_geoparquet() {
assert!(matches!(
"parquet".parse::<Format>().unwrap_err(),
crate::Error::UnsupportedFormat(_),
));
}
#[cfg(feature = "geoparquet")]
mod geoparquet {
use super::Format;
use stac::geoparquet::{Compression, WriterOptions};
#[test]
fn parse_geoparquet_compression() {
let format: Format = "geoparquet[snappy]".parse().unwrap();
let expected =
Format::Geoparquet(WriterOptions::new().with_compression(Compression::SNAPPY));
assert_eq!(format, expected);
}
#[test]
fn infer_from_href() {
let format = Format::infer_from_href("out.parquet").unwrap();
let expected = Format::Geoparquet(WriterOptions::default());
assert_eq!(format, expected);
}
}
}