diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 5cc13b87..7592a189 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - `stac::geoparquet::Compression`, even if geoparquet is not enabled ([#396](https://github.com/stac-utils/stac-rs/pull/396)) - `Type` ([#397](https://github.com/stac-utils/stac-rs/pull/397)) - `Collection::item_assets` and `ItemAsset` ([#404](https://github.com/stac-utils/stac-rs/pull/404)) +- A few extension methods on `Fields` ([#405](https://github.com/stac-utils/stac-rs/pull/405)) ### Changed @@ -26,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Removed - `Error::ReqwestNotEnabled` and `Error::GdalNotEnabled` ([#396](https://github.com/stac-utils/stac-rs/pull/382)) +- `Asset::extensions` ([#405](https://github.com/stac-utils/stac-rs/pull/405)) ## [0.9.0] - 2024-09-05 diff --git a/core/src/asset.rs b/core/src/asset.rs index e16d5378..b78ebb3f 100644 --- a/core/src/asset.rs +++ b/core/src/asset.rs @@ -1,4 +1,4 @@ -use crate::{Band, DataType, Extensions, Fields, Statistics}; +use crate::{Band, DataType, Fields, Statistics}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashMap; @@ -80,9 +80,6 @@ pub struct Asset { /// Additional fields on the asset. #[serde(flatten)] pub additional_fields: Map, - - #[serde(skip)] - extensions: Vec, } /// Trait implemented by anything that has assets. @@ -141,7 +138,6 @@ impl Asset { statistics: None, unit: None, additional_fields: Map::new(), - extensions: Vec::new(), } } @@ -172,15 +168,6 @@ impl Fields for Asset { } } -impl Extensions for Asset { - fn extensions(&self) -> &Vec { - &self.extensions - } - fn extensions_mut(&mut self) -> &mut Vec { - &mut self.extensions - } -} - impl From for Asset { fn from(value: String) -> Self { Asset::new(value) diff --git a/core/src/extensions/authentication.rs b/core/src/extensions/authentication.rs index 62442917..8ed4d350 100644 --- a/core/src/extensions/authentication.rs +++ b/core/src/extensions/authentication.rs @@ -167,25 +167,21 @@ impl Extension for Authentication { #[cfg(test)] mod tests { use super::{Authentication, In, Scheme}; - use crate::{Collection, Extensions, Item}; + use crate::{Collection, Fields, Item}; use serde_json::json; #[test] fn collection() { let collection: Collection = crate::read("data/auth/collection.json").unwrap(); - let authentication: Authentication = collection.extension().unwrap().unwrap(); + let authentication: Authentication = collection.extension().unwrap(); let oauth = authentication.schemes.get("oauth").unwrap(); let _ = oauth.flows.get("authorizationCode").unwrap(); - // FIXME: assets should be able to have extensions from their parent item - // let asset = collection.assets.get("example").unwrap(); - // let authentication: Authentication = asset.extension().unwrap().unwrap(); - // assert_eq!(authentication.refs, vec!["signed_url_auth".to_string()]); } #[test] fn item() { let collection: Item = crate::read("data/auth/item.json").unwrap(); - let authentication: Authentication = collection.extension().unwrap().unwrap(); + let authentication: Authentication = collection.extension().unwrap(); let _ = authentication.schemes.get("none").unwrap(); } diff --git a/core/src/extensions/electro_optical.rs b/core/src/extensions/electro_optical.rs index 89029aa2..10918713 100644 --- a/core/src/extensions/electro_optical.rs +++ b/core/src/extensions/electro_optical.rs @@ -72,11 +72,11 @@ impl Extension for ElectroOptical { #[cfg(test)] mod tests { use super::ElectroOptical; - use crate::{Extensions, Item}; + use crate::{Fields, Item}; #[test] fn item() { let item: Item = crate::read("data/eo/item.json").unwrap(); - let _: ElectroOptical = item.extension().unwrap().unwrap(); + let _: ElectroOptical = item.extension().unwrap(); } } diff --git a/core/src/extensions/mod.rs b/core/src/extensions/mod.rs index bdeb1127..608493eb 100644 --- a/core/src/extensions/mod.rs +++ b/core/src/extensions/mod.rs @@ -22,24 +22,24 @@ //! ## Usage //! //! [Item](crate::Item), [Collection](crate::Collection), -//! [Catalog](crate::Catalog), and [Asset](crate::Asset) all implement the -//! [Extensions] trait, which provides methods to get, set, and remove extension information: +//! [Catalog](crate::Catalog) all implement the [Extensions] trait, which +//! provides methods to get, set, and remove extension information: //! //! ``` -//! use stac::{Item, Extensions, extensions::{Projection, projection::Centroid}}; +//! use stac::{Item, Extensions, Fields, extensions::{Projection, projection::Centroid}}; //! let mut item: Item = stac::read("examples/extensions-collection/proj-example/proj-example.json").unwrap(); //! assert!(item.has_extension::()); //! //! // Get extension information -//! let mut projection: Projection = item.extension().unwrap().unwrap(); +//! let mut projection: Projection = item.extension().unwrap(); //! println!("code: {}", projection.code.as_ref().unwrap()); //! //! // Set extension information //! projection.centroid = Some(Centroid { lat: 34.595302, lon: -101.344483 }); -//! item.set_extension(projection).unwrap(); +//! Extensions::set_extension(&mut item, projection).unwrap(); //! //! // Remove an extension -//! item.remove_extension::(); +//! Extensions::remove_extension::(&mut item); //! assert!(!item.has_extension::()); //! ``` @@ -122,26 +122,6 @@ pub trait Extensions: Fields { .any(|extension| extension.starts_with(E::identifier_prefix())) } - /// Gets an extension's data. - /// - /// Returns `Ok(None)` if the object doesn't have the given extension. - /// - /// # Examples - /// - /// ``` - /// use stac::{Item, extensions::{Projection, Extensions}}; - /// let item: Item = stac::read("examples/extensions-collection/proj-example/proj-example.json").unwrap(); - /// let projection: Projection = item.extension().unwrap().unwrap(); - /// assert_eq!(projection.code.unwrap(), "EPSG:32614"); - /// ``` - fn extension(&self) -> Result> { - if self.has_extension::() { - self.fields_with_prefix(E::PREFIX).map(|v| Some(v)) - } else { - Ok(None) - } - } - /// Adds an extension's identifier to this object. /// /// # Examples @@ -169,10 +149,9 @@ pub trait Extensions: Fields { /// item.set_extension(projection).unwrap(); /// ``` fn set_extension(&mut self, extension: E) -> Result<()> { - self.remove_extension::(); self.extensions_mut().push(E::IDENTIFIER.to_string()); self.extensions_mut().dedup(); - self.set_fields_with_prefix(E::PREFIX, extension) + Fields::set_extension(self, extension) } /// Removes this extension and all of its fields from this object. @@ -187,8 +166,7 @@ pub trait Extensions: Fields { /// assert!(!item.has_extension::()); /// ``` fn remove_extension(&mut self) { - // TODO how do we handle removing from assets when this is done on an item? - self.remove_fields_with_prefix(E::PREFIX); + Fields::remove_extension::(self); self.extensions_mut() .retain(|extension| !extension.starts_with(E::identifier_prefix())) } @@ -220,13 +198,13 @@ mod tests { #[test] fn set_extension_on_asset() { + use crate::Fields; + let mut asset = Asset::new("a/href.tif"); - assert!(!asset.has_extension::()); let mut band = Band::default(); band.unit = Some("parsecs".to_string()); let raster = Raster { bands: vec![band] }; asset.set_extension(raster).unwrap(); - assert!(asset.has_extension::()); let mut item = Item::new("an-id"); let _ = item.assets.insert("data".to_string(), asset); } diff --git a/core/src/extensions/projection.rs b/core/src/extensions/projection.rs index e45075b5..23026bfb 100644 --- a/core/src/extensions/projection.rs +++ b/core/src/extensions/projection.rs @@ -123,6 +123,22 @@ impl Projection { Ok(None) } } + + /// Returns true if this projection structure is empty. + /// + /// # Examples + /// + /// ``` + /// use stac::extensions::Projection; + /// + /// let projection = Projection::default(); + /// assert!(projection.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + serde_json::to_value(self) + .map(|v| v == Value::Object(Default::default())) + .unwrap_or(true) + } } impl Extension for Projection { @@ -134,7 +150,7 @@ impl Extension for Projection { #[cfg(test)] mod tests { use super::Projection; - use crate::{Extensions, Item}; + use crate::{Fields, Item}; #[cfg(feature = "gdal")] #[test] @@ -162,7 +178,7 @@ mod tests { fn example() { let item: Item = crate::read("examples/extensions-collection/proj-example/proj-example.json").unwrap(); - let projection = item.extension::().unwrap().unwrap(); + let projection = item.extension::().unwrap(); assert_eq!(projection.code.unwrap(), "EPSG:32614"); } } diff --git a/core/src/extensions/raster.rs b/core/src/extensions/raster.rs index fa8d6753..e6cf6907 100644 --- a/core/src/extensions/raster.rs +++ b/core/src/extensions/raster.rs @@ -119,6 +119,22 @@ impl Extension for Raster { const PREFIX: &'static str = "raster"; } +impl Raster { + /// Returns true if this raster structure is empty. + /// + /// # Examples + /// + /// ``` + /// use stac::extensions::Raster; + /// + /// let projection = Raster::default(); + /// assert!(projection.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.bands.is_empty() + } +} + #[cfg(feature = "gdal")] impl From for DataType { fn from(value: gdal::raster::GdalDataType) -> Self { diff --git a/core/src/fields.rs b/core/src/fields.rs index 7cc1a713..1b521ab8 100644 --- a/core/src/fields.rs +++ b/core/src/fields.rs @@ -1,4 +1,4 @@ -use crate::{Error, Result}; +use crate::{Error, Extension, Result}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::{json, Map, Value}; @@ -124,11 +124,59 @@ pub trait Fields { /// use stac::{Fields, Item, extensions::Projection}; /// let projection = Projection { code: Some("EPSG:4326".to_string()), ..Default::default() }; /// let mut item = Item::new("an-id"); - /// item.remove_fields_with_prefix("proj"); // Prefer `Extensions::remove_extension` + /// item.remove_fields_with_prefix("proj"); // Prefer `Fields::remove_extension` /// ``` fn remove_fields_with_prefix(&mut self, prefix: &str) { let prefix = format!("{}:", prefix); self.fields_mut() .retain(|key, _| !(key.starts_with(&prefix) && key.len() > prefix.len())); } + + /// Gets an extension's data. + /// + /// Returns `Ok(None)` if the object doesn't have the given extension. + /// + /// # Examples + /// + /// ``` + /// use stac::{Item, Fields, extensions::Projection}; + /// let item: Item = stac::read("examples/extensions-collection/proj-example/proj-example.json").unwrap(); + /// let projection: Projection = item.extension().unwrap(); + /// assert_eq!(projection.code.unwrap(), "EPSG:32614"); + /// ``` + fn extension(&self) -> Result { + self.fields_with_prefix(E::PREFIX) + } + + /// Sets an extension's data into this object. + /// + /// This will remove any previous fields from this extension + /// + /// # Examples + /// + /// ``` + /// use stac::{Item, Fields, extensions::Projection}; + /// let mut item = Item::new("an-id"); + /// let projection = Projection { code: Some("EPSG:4326".to_string()), ..Default::default() }; + /// item.set_extension(projection).unwrap(); + /// ``` + fn set_extension(&mut self, extension: E) -> Result<()> { + self.remove_extension::(); + self.set_fields_with_prefix(E::PREFIX, extension) + } + + /// Removes all of the extension's fields from this object. + /// + /// # Examples + /// + /// ``` + /// use stac::{Item, extensions::{Projection, Extensions}}; + /// let mut item: Item = stac::read("examples/extensions-collection/proj-example/proj-example.json").unwrap(); + /// assert!(item.has_extension::()); + /// item.remove_extension::(); + /// assert!(!item.has_extension::()); + /// ``` + fn remove_extension(&mut self) { + self.remove_fields_with_prefix(E::PREFIX); + } } diff --git a/core/src/gdal.rs b/core/src/gdal.rs index 373012f8..d116a72c 100644 --- a/core/src/gdal.rs +++ b/core/src/gdal.rs @@ -6,7 +6,7 @@ use crate::{ raster::{Band, Raster, Statistics}, Projection, }, - Asset, Bbox, Extensions, Item, Result, + Asset, Bbox, Extensions, Fields, Item, Result, }; use gdal::{ spatial_ref::{CoordTransform, SpatialRef}, @@ -44,14 +44,15 @@ pub fn update_item( let mut bbox = Bbox::new(180., 90., -180., 90.); // Intentionally invalid bbox so the first update always takes for asset in item.assets.values_mut() { update_asset(asset, force_statistics, is_approx_statistics_ok)?; - if let Some(projection) = asset.extension::()? { + let projection = asset.extension::()?; + if !projection.is_empty() { has_projection = true; if let Some(asset_bounds) = projection.wgs84_bounds()? { bbox.update(asset_bounds); } projections.push(projection); } - if !has_raster && asset.has_extension::() { + if !has_raster && !asset.extension::()?.is_empty() { has_raster = true; } } @@ -65,7 +66,7 @@ pub fn update_item( .iter() .all(|projection| *projection == projections[0]) { - item.set_extension(projections[0].clone())?; + Extensions::set_extension(item, projections[0].clone())?; for asset in item.assets.values_mut() { asset.remove_extension::(); } @@ -174,7 +175,7 @@ mod tests { use crate::{ extensions::{projection::Centroid, raster::DataType, Projection, Raster}, item::Builder, - Extensions, + Extensions, Fields, }; #[test] @@ -185,13 +186,7 @@ mod tests { .unwrap(); super::update_item(&mut item, false, true).unwrap(); assert!(item.has_extension::()); - let raster: Raster = item - .assets - .get("data") - .unwrap() - .extension() - .unwrap() - .unwrap(); + let raster: Raster = item.assets.get("data").unwrap().extension().unwrap(); assert_eq!( *raster.bands[0].data_type.as_ref().unwrap(), DataType::UInt16 @@ -205,13 +200,7 @@ mod tests { .build() .unwrap(); super::update_item(&mut item, false, true).unwrap(); - let raster: Raster = item - .assets - .get("data") - .unwrap() - .extension() - .unwrap() - .unwrap(); + let raster: Raster = item.assets.get("data").unwrap().extension().unwrap(); assert_eq!( raster.bands[0].spatial_resolution.unwrap(), 100.01126757344893 @@ -225,7 +214,7 @@ mod tests { .build() .unwrap(); super::update_item(&mut item, false, true).unwrap(); - let projection: Projection = item.extension().unwrap().unwrap(); + let projection: Projection = item.extension().unwrap(); assert_eq!(projection.code.unwrap(), "EPSG:32621"); assert_eq!( projection.bbox.unwrap(),