diff --git a/stac/CHANGELOG.md b/stac/CHANGELOG.md index 73e74030..e1715eab 100644 --- a/stac/CHANGELOG.md +++ b/stac/CHANGELOG.md @@ -9,10 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - The projection and raster extensions, the `Extension` trait, and the `Fields` trait ([#234](https://github.com/stac-utils/stac-rs/pull/234)) +- `stac::item::Builder` ([#237](https://github.com/stac-utils/stac-rs/pull/237)) ### Changed - The `extensions` attribute of catalogs, collections, and items is now non-optional ([#234](https://github.com/stac-utils/stac-rs/pull/234)) +- The `roles` attribute of assets is now non-optional ([#237](https://github.com/stac-utils/stac-rs/pull/237)) ## [0.5.3] - 2024-04-07 diff --git a/stac/assets/dataset.tif b/stac/assets/dataset.tif new file mode 100644 index 00000000..fcc73e58 Binary files /dev/null and b/stac/assets/dataset.tif differ diff --git a/stac/src/asset.rs b/stac/src/asset.rs index 74acd90c..abff44c0 100644 --- a/stac/src/asset.rs +++ b/stac/src/asset.rs @@ -29,8 +29,9 @@ pub struct Asset { pub r#type: Option, /// The semantic roles of the asset, similar to the use of rel in [Links](crate::Link). - #[serde(skip_serializing_if = "Option::is_none")] - pub roles: Option>, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub roles: Vec, /// Creation date and time of the corresponding data, in UTC. /// @@ -107,13 +108,30 @@ impl Asset { title: None, description: None, r#type: None, - roles: None, + roles: Vec::new(), created: None, updated: None, additional_fields: Map::new(), extensions: Vec::new(), } } + + /// Adds a role to this asset, returning the modified asset. + /// + /// Useful for builder patterns. + /// + /// # Examples + /// + /// ``` + /// use stac::Asset; + /// let asset = Asset::new("asset/dataset.tif").role("data"); + /// assert_eq!(asset.roles, vec!["data"]); + /// ``` + pub fn role(mut self, role: impl ToString) -> Asset { + self.roles.push(role.to_string()); + self.roles.dedup(); + self + } } impl Fields for Asset { @@ -134,6 +152,18 @@ impl Extensions for Asset { } } +impl From for Asset { + fn from(value: String) -> Self { + Asset::new(value) + } +} + +impl<'a> From<&'a str> for Asset { + fn from(value: &'a str) -> Self { + Asset::new(value) + } +} + #[cfg(test)] mod tests { use super::Asset; @@ -145,7 +175,7 @@ mod tests { assert!(asset.title.is_none()); assert!(asset.description.is_none()); assert!(asset.r#type.is_none()); - assert!(asset.roles.is_none()); + assert!(asset.roles.is_empty()); } #[test] diff --git a/stac/src/item.rs b/stac/src/item.rs index 11f9a797..9515d23f 100644 --- a/stac/src/item.rs +++ b/stac/src/item.rs @@ -1,10 +1,13 @@ +//! STAC Items. + use crate::{ Asset, Assets, Error, Extensions, Fields, Geometry, Href, Link, Links, Result, STAC_VERSION, }; use chrono::{DateTime, FixedOffset, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::collections::HashMap; +use std::{collections::HashMap, path::Path}; +use url::Url; /// The type field for [Items](Item). pub const ITEM_TYPE: &str = "Feature"; @@ -156,6 +159,84 @@ pub struct Properties { pub additional_fields: Map, } +/// Builder for a STAC Item. +#[derive(Debug)] +pub struct Builder { + id: String, + canonicalize_paths: bool, + assets: HashMap, +} + +impl Builder { + /// Creates a new builder. + /// + /// # Examples + /// + /// ``` + /// use stac::item::Builder; + /// let builder = Builder::new("an-id"); + /// ``` + pub fn new(id: impl ToString) -> Builder { + Builder { + id: id.to_string(), + canonicalize_paths: true, + assets: HashMap::new(), + } + } + + /// Set to false to not canonicalize paths. + /// + /// Useful if you want relative paths, or the files don't actually exist. + /// + /// # Examples + /// + /// ``` + /// use stac::item::Builder; + /// let builder = Builder::new("an-id").canonicalize_paths(false); + /// ``` + pub fn canonicalize_paths(mut self, canonicalize_paths: bool) -> Builder { + self.canonicalize_paths = canonicalize_paths; + self + } + + /// Adds an asset by href to this builder. + /// + /// # Examples + /// + /// ``` + /// use stac::item::Builder; + /// let builder = Builder::new("an-id").asset("data", "assets/dataset.tif"); + /// ``` + pub fn asset(mut self, key: impl ToString, href: impl Into) -> Builder { + let _ = self.assets.insert(key.to_string(), href.into()); + self + } + + /// Creates an [Item] by consuming this builder. + /// + /// # Examples + /// + /// ``` + /// use stac::item::Builder; + /// let builder = Builder::new("an-id").asset("data", "assets/dataset.tif"); + /// let item = builder.into_item().unwrap(); + /// assert_eq!(item.assets.len(), 1); + /// ``` + pub fn into_item(self) -> Result { + let mut item = Item::new(self.id); + for (key, mut asset) in self.assets { + if Url::parse(&asset.href).is_err() && self.canonicalize_paths { + asset.href = Path::new(&asset.href) + .canonicalize()? + .to_string_lossy() + .into_owned(); + } + let _ = item.assets.insert(key, asset); + } + Ok(item) + } +} + impl Default for Properties { fn default() -> Properties { Properties { @@ -473,8 +554,8 @@ where #[cfg(test)] mod tests { - use super::Item; - use crate::STAC_VERSION; + use super::{Builder, Item}; + use crate::{Asset, STAC_VERSION}; use serde_json::Value; #[test] @@ -596,4 +677,33 @@ mod tests { Item ); } + + #[test] + fn builder() { + let builder = Builder::new("an-id").asset("data", "assets/dataset.tif"); + let item = builder.into_item().unwrap(); + assert_eq!(item.assets.len(), 1); + let asset = item.assets.get("data").unwrap(); + assert!(asset.href.ends_with("assets/dataset.tif")); + } + + #[test] + fn builder_relative_paths() { + let builder = Builder::new("an-id") + .canonicalize_paths(false) + .asset("data", "assets/dataset.tif"); + let item = builder.into_item().unwrap(); + let asset = item.assets.get("data").unwrap(); + assert_eq!(asset.href, "assets/dataset.tif"); + } + + #[test] + fn builder_asset_roles() { + let item = Builder::new("an-id") + .asset("data", Asset::new("assets/dataset.tif").role("data")) + .into_item() + .unwrap(); + let asset = item.assets.get("data").unwrap(); + assert_eq!(asset.roles, vec!["data"]); + } } diff --git a/stac/src/lib.rs b/stac/src/lib.rs index 23368012..cb554706 100644 --- a/stac/src/lib.rs +++ b/stac/src/lib.rs @@ -127,7 +127,7 @@ pub mod geo; mod geometry; mod href; mod io; -mod item; +pub mod item; mod item_collection; pub mod link; pub mod media_type;