Skip to content

Commit

Permalink
feat!: add item builder
Browse files Browse the repository at this point in the history
Includes a breaking change to the Asset interface (non-optional roles).
  • Loading branch information
gadomski committed Apr 10, 2024
1 parent a5d2a5c commit 737a29a
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 8 deletions.
2 changes: 2 additions & 0 deletions stac/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file added stac/assets/dataset.tif
Binary file not shown.
38 changes: 34 additions & 4 deletions stac/src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ pub struct Asset {
pub r#type: Option<String>,

/// 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<Vec<String>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub roles: Vec<String>,

/// Creation date and time of the corresponding data, in UTC.
///
Expand Down Expand Up @@ -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 {
Expand All @@ -134,6 +152,18 @@ impl Extensions for Asset {
}
}

impl From<String> 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;
Expand All @@ -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]
Expand Down
116 changes: 113 additions & 3 deletions stac/src/item.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -156,6 +159,84 @@ pub struct Properties {
pub additional_fields: Map<String, Value>,
}

/// Builder for a STAC Item.
#[derive(Debug)]
pub struct Builder {
id: String,
canonicalize_paths: bool,
assets: HashMap<String, Asset>,
}

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<Asset>) -> 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<Item> {
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 {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"]);
}
}
2 changes: 1 addition & 1 deletion stac/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 737a29a

Please sign in to comment.