Repository: stac-utils/stac-rs (language: python)

Issue:
Allow reads from future versions
v1.1.0 was released and we can't read it. We should be able to handle future version upgrades as gracefully as possible (maybe with a "unknown" `Version` variant).


Discussion / hints:

Fix patch:
diff --git a/cli/src/args/migrate.rs b/cli/src/args/migrate.rs
index 2e03b0f6..87fa0575 100644
--- a/cli/src/args/migrate.rs
+++ b/cli/src/args/migrate.rs
@@ -21,7 +21,7 @@ impl Run for Args {
     async fn run(self, input: Input, _: Option<Sender<Value>>) -> Result<Option<Value>> {
         let value = input.get().await?;
         value
-            .migrate(self.version)
+            .migrate(&self.version)
             .map(Value::from)
             .map(Some)
             .map_err(Error::from)
diff --git a/cli/src/args/mod.rs b/cli/src/args/mod.rs
index bc627e6c..242beecc 100644
--- a/cli/src/args/mod.rs
+++ b/cli/src/args/mod.rs
@@ -133,7 +133,7 @@ impl Args {
         )?;
         let mut output = Output::new(
             self.subcommand.take_outfile(),
-            self.output_format.or_else(|| {
+            self.output_format.or({
                 if self.stream {
                     Some(crate::output::Format::NdJson)
                 } else {
diff --git a/cli/src/input.rs b/cli/src/input.rs
index e97797f3..be639c83 100644
--- a/cli/src/input.rs
+++ b/cli/src/input.rs
@@ -52,7 +52,7 @@ impl Input {
 
     /// Creates a new input with the given href.
     pub(crate) fn with_href(&self, href: &str) -> Result<Input> {
-        let (object_store, path) = parse_href_opts(&href, self.config.iter())?;
+        let (object_store, path) = parse_href_opts(href, self.config.iter())?;
         let reader = Reader::ObjectStore { object_store, path };
         Ok(Input {
             format: self.format,
@@ -66,7 +66,7 @@ impl Input {
         tracing::debug!("getting {}", self.format);
         match &self.reader {
             Reader::ObjectStore { object_store, path } => {
-                let bytes = object_store.get(&path).await?.bytes().await?;
+                let bytes = object_store.get(path).await?.bytes().await?;
                 match self.format {
                     Format::Json => serde_json::from_slice(&bytes).map_err(Error::from),
                     Format::NdJson => bytes
@@ -110,7 +110,7 @@ where
     K: AsRef<str>,
     V: Into<String>,
 {
-    if let Some(url) = Url::parse(href).ok() {
+    if let Ok(url) = Url::parse(href) {
         object_store::parse_url_opts(&url, options).map_err(Error::from)
     } else {
         let path = Path::from_filesystem_path(href)?;
diff --git a/cli/src/output.rs b/cli/src/output.rs
index febdf0d7..f275b4ae 100644
--- a/cli/src/output.rs
+++ b/cli/src/output.rs
@@ -103,7 +103,7 @@ impl Output {
         let bytes = value.into_bytes(self.format)?;
         if let Some((object_store, path)) = &self.writer.object_store {
             object_store
-                .put(path, PutPayload::from_bytes(bytes.into()))
+                .put(path, PutPayload::from_bytes(bytes))
                 .await
                 .map(Some)
                 .map_err(Error::from)
diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md
index f8c1a8d1..769995c6 100644
--- a/core/CHANGELOG.md
+++ b/core/CHANGELOG.md
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - Deref `ItemCollection` ([#363](https://github.com/stac-utils/stac-rs/pull/363))
 - `object_store` and `ndjson` ([#369](https://github.com/stac-utils/stac-rs/pull/369))
 - `Format` ([#372](https://github.com/stac-utils/stac-rs/pull/371))
+- Read unknown versions ([#378](https://github.com/stac-utils/stac-rs/pull/378))
 
 ### Changed
 
diff --git a/core/src/item_collection.rs b/core/src/item_collection.rs
index acdc30e7..5ff39fd6 100644
--- a/core/src/item_collection.rs
+++ b/core/src/item_collection.rs
@@ -109,7 +109,7 @@ where
 }
 
 impl Migrate for ItemCollection {
-    fn migrate(mut self, version: crate::Version) -> crate::Result<Self> {
+    fn migrate(mut self, version: &crate::Version) -> crate::Result<Self> {
         let mut items = Vec::with_capacity(self.items.len());
         for item in self.items {
             items.push(item.migrate(version)?);
diff --git a/core/src/migrate.rs b/core/src/migrate.rs
index a1becfea..3e2dc38b 100644
--- a/core/src/migrate.rs
+++ b/core/src/migrate.rs
@@ -1,11 +1,10 @@
-use std::collections::HashMap;
-
 use crate::{Error, Result, Version};
 use serde::{de::DeserializeOwned, Serialize};
 use serde_json::{Map, Value};
+use std::collections::HashMap;
 
 /// Migrates a STAC object from one version to another.
-pub trait Migrate: Sized + Serialize + DeserializeOwned {
+pub trait Migrate: Sized + Serialize + DeserializeOwned + std::fmt::Debug {
     /// Migrates this object to another version.
     ///
     /// # Examples
@@ -14,17 +13,17 @@ pub trait Migrate: Sized + Serialize + DeserializeOwned {
     /// use stac::{Item, Migrate, Version};
     ///
     /// let mut item: Item = stac::read("../spec-examples/v1.0.0/simple-item.json").unwrap();
-    /// let item = item.migrate(Version::v1_1_0_beta_1).unwrap();
+    /// let item = item.migrate(&Version::v1_1_0_beta_1).unwrap();
     /// assert_eq!(item.version, Version::v1_1_0_beta_1);
     /// ```
-    fn migrate(self, to: Version) -> Result<Self> {
+    fn migrate(self, to: &Version) -> Result<Self> {
         let mut value = serde_json::to_value(self)?;
         if let Some(version) = value
             .as_object()
             .and_then(|object| object.get("stac_version"))
             .and_then(|version| version.as_str())
         {
-            let from: Version = version.parse()?;
+            let from: Version = version.parse().unwrap(); // infallible
             let steps = from.steps(to)?;
             for step in steps {
                 value = step.migrate(value)?;
@@ -46,16 +45,18 @@ enum Step {
 }
 
 impl Version {
-    fn steps(self, to: Version) -> Result<Vec<Step>> {
+    fn steps(self, to: &Version) -> Result<Vec<Step>> {
         match self {
             Version::v1_0_0 => match to {
                 Version::v1_0_0 => Ok(Vec::new()),
                 Version::v1_1_0_beta_1 => Ok(vec![Step::v1_0_0_to_v1_1_0_beta_1]),
+                _ => Err(Error::UnsupportedMigration(self, to.clone())),
             },
             Version::v1_1_0_beta_1 => match to {
-                Version::v1_0_0 => Err(Error::UnsupportedMigration(self, to)),
                 Version::v1_1_0_beta_1 => Ok(Vec::new()),
+                _ => Err(Error::UnsupportedMigration(self, to.clone())),
             },
+            Version::Unknown(_) => Err(Error::UnsupportedMigration(self, to.clone())),
         }
     }
 }
@@ -215,7 +216,7 @@ mod tests {
     #[test]
     fn migrate_v1_0_0_to_v1_1_0_beta_1() {
         let item: Item = crate::read("data/bands-v1.0.0.json").unwrap();
-        let item = item.migrate(Version::v1_1_0_beta_1).unwrap();
+        let item = item.migrate(&Version::v1_1_0_beta_1).unwrap();
         let asset = &item.assets["example"];
         assert_eq!(asset.data_type.as_ref().unwrap(), &DataType::UInt16);
         assert_eq!(asset.bands[0].name.as_ref().unwrap(), "r");
@@ -229,12 +230,12 @@ mod tests {
         assert_json_eq!(expected, serde_json::to_value(item).unwrap());
 
         let collection = Collection::new("an-id", "a description");
-        let collection = collection.migrate(Version::v1_1_0_beta_1).unwrap();
+        let collection = collection.migrate(&Version::v1_1_0_beta_1).unwrap();
         assert_eq!(collection.license, "other");
 
         let mut item = Item::new("an-id");
         item.set_link(Link::self_("/an/absolute/href"));
-        let item = item.migrate(Version::v1_1_0_beta_1).unwrap();
+        let item = item.migrate(&Version::v1_1_0_beta_1).unwrap();
         assert_eq!(item.link("self").unwrap().href, "file:///an/absolute/href");
     }
 
@@ -242,7 +243,7 @@ mod tests {
     fn remove_empty_bands() {
         // https://github.com/stac-utils/stac-rs/issues/350
         let item: Item = crate::read("data/20201211_223832_CS2.json").unwrap();
-        let item = item.migrate(Version::v1_1_0_beta_1).unwrap();
+        let item = item.migrate(&Version::v1_1_0_beta_1).unwrap();
         let asset = &item.assets["data"];
         assert!(asset.bands.is_empty());
     }
diff --git a/core/src/value.rs b/core/src/value.rs
index 2b802456..941bb2ab 100644
--- a/core/src/value.rs
+++ b/core/src/value.rs
@@ -274,7 +274,7 @@ impl From<ItemCollection> for Value {
 }
 
 impl Migrate for Value {
-    fn migrate(self, version: Version) -> Result<Value> {
+    fn migrate(self, version: &Version) -> Result<Value> {
         match self {
             Value::Item(item) => item.migrate(version).map(Value::Item),
             Value::Catalog(catalog) => catalog.migrate(version).map(Value::Catalog),
diff --git a/core/src/version.rs b/core/src/version.rs
index ae3951be..d2a02da4 100644
--- a/core/src/version.rs
+++ b/core/src/version.rs
@@ -1,10 +1,10 @@
-use crate::Error;
 use serde::{Deserialize, Serialize};
-use std::{fmt::Display, str::FromStr};
+use std::{convert::Infallible, fmt::Display, str::FromStr};
 
 /// A version of the STAC specification.
-#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq, Hash, PartialOrd)]
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, PartialOrd)]
 #[allow(non_camel_case_types)]
+#[non_exhaustive]
 pub enum Version {
     /// [v1.0.0](https://github.com/radiantearth/stac-spec/releases/tag/v1.0.0)
     #[default]
@@ -14,16 +14,20 @@ pub enum Version {
     /// [v1.1.0-beta.1](https://github.com/radiantearth/stac-spec/releases/tag/v1.1.0-beta.1)
     #[serde(rename = "1.1.0-beta.1")]
     v1_1_0_beta_1,
+
+    /// An unknown STAC version.
+    #[serde(untagged)]
+    Unknown(String),
 }
 
 impl FromStr for Version {
-    type Err = Error;
+    type Err = Infallible;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
             "1.0.0" => Ok(Version::v1_0_0),
             "1.1.0-beta.1" => Ok(Version::v1_1_0_beta_1),
-            _ => Err(Error::UnsupportedVersion(s.to_string())),
+            _ => Ok(Version::Unknown(s.to_string())),
         }
     }
 }
@@ -36,6 +40,7 @@ impl Display for Version {
             match self {
                 Version::v1_0_0 => "1.0.0",
                 Version::v1_1_0_beta_1 => "1.1.0-beta.1",
+                Version::Unknown(v) => v,
             }
         )
     }
diff --git a/python/src/lib.rs b/python/src/lib.rs
index af2b0775..8aa06084 100644
--- a/python/src/lib.rs
+++ b/python/src/lib.rs
@@ -36,12 +36,10 @@ fn migrate<'py>(value: &Bound<'py, PyDict>, version: Option<&str>) -> PyResult<B
     let py = value.py();
     let value: Value = pythonize::depythonize(value)?;
     let version = version
-        .map(|version| version.parse())
-        .transpose()
-        .map_err(|err: stac::Error| StacrsError::new_err(err.to_string()))?
+        .map(|version| version.parse().unwrap())
         .unwrap_or_default();
     let value = value
-        .migrate(version)
+        .migrate(&version)
         .map_err(|err| StacrsError::new_err(err.to_string()))?;
     let value = pythonize::pythonize(py, &value)?;
     value.downcast_into().map_err(PyErr::from)
@@ -75,12 +73,10 @@ fn migrate_href<'py>(
 ) -> PyResult<Bound<'py, PyDict>> {
     let value: Value = stac::read(href).map_err(|err| StacrsError::new_err(err.to_string()))?;
     let version = version
-        .map(|version| version.parse())
-        .transpose()
-        .map_err(|err: stac::Error| StacrsError::new_err(err.to_string()))?
+        .map(|version| version.parse().unwrap())
         .unwrap_or_default();
     let value = value
-        .migrate(version)
+        .migrate(&version)
         .map_err(|err| StacrsError::new_err(err.to_string()))?;
     let value = pythonize::pythonize(py, &value)?;
     value.downcast_into().map_err(PyErr::from)
diff --git a/validate/src/validator.rs b/validate/src/validator.rs
index 77666095..18f32709 100644
--- a/validate/src/validator.rs
+++ b/validate/src/validator.rs
@@ -103,7 +103,7 @@ impl Validator {
                 "https://schemas.stacspec.org/v{}/item-spec/json-schema/item.json",
                 version
             ))?;
-            let _ = self.item_schemas.insert(version, schema);
+            let _ = self.item_schemas.insert(version.clone(), schema);
         }
         Ok(self
             .item_schemas
@@ -124,7 +124,7 @@ impl Validator {
                 "https://schemas.stacspec.org/v{}/catalog-spec/json-schema/catalog.json",
                 version
             ))?;
-            let _ = self.catalog_schemas.insert(version, schema);
+            let _ = self.catalog_schemas.insert(version.clone(), schema);
         }
         Ok(self
             .catalog_schemas
@@ -145,7 +145,7 @@ impl Validator {
                 "https://schemas.stacspec.org/v{}/collection-spec/json-schema/collection.json",
                 version
             ))?;
-            let _ = self.collection_schemas.insert(version, schema);
+            let _ = self.collection_schemas.insert(version.clone(), schema);
         }
         Ok(self
             .collection_schemas
@@ -347,6 +347,7 @@ fn version(value: &Value) -> Result<Version> {
         .and_then(|object| object.get("stac_version"))
         .and_then(|value| value.as_str())
         .map(|version| version.parse())
-        .transpose()?
+        .transpose()
+        .unwrap()
         .ok_or(Error::MissingStacVersion)
 }


Test patch:
diff --git a/core/tests/migrate.rs b/core/tests/migrate.rs
index caf4809b..26abeca4 100644
--- a/core/tests/migrate.rs
+++ b/core/tests/migrate.rs
@@ -5,5 +5,5 @@ use std::path::PathBuf;
 #[rstest]
 fn v1_0_0_to_v1_1_0_beta_1(#[files("../spec-examples/v1.0.0/**/*.json")] path: PathBuf) {
     let value: Value = stac::read(path.to_str().unwrap()).unwrap();
-    let _ = value.migrate(Version::v1_1_0_beta_1).unwrap();
+    let _ = value.migrate(&Version::v1_1_0_beta_1).unwrap();
 }
diff --git a/validate/tests/migrate.rs b/validate/tests/migrate.rs
index 0cf2b397..ab282266 100644
--- a/validate/tests/migrate.rs
+++ b/validate/tests/migrate.rs
@@ -6,6 +6,6 @@ use std::path::PathBuf;
 #[rstest]
 fn v1_0_0_to_v1_1_0_beta_1(#[files("../spec-examples/v1.0.0/**/*.json")] path: PathBuf) {
     let value: Value = stac::read(path.to_str().unwrap()).unwrap();
-    let value = value.migrate(Version::v1_1_0_beta_1).unwrap();
+    let value = value.migrate(&Version::v1_1_0_beta_1).unwrap();
     value.validate().unwrap();
 }