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 71fa4db
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 9 deletions.
8 changes: 8 additions & 0 deletions stac-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ Use the cli `--help` flag to see all available options:
stac --help
```

### Item

Create a STAC Item from an href:

```shell
stac item https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif
```

### Search

Search a STAC API:
Expand Down
60 changes: 60 additions & 0 deletions stac-cli/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
use crate::Result;
use clap::Subcommand;
use stac_api::GetSearch;
use std::path::Path;
use url::Url;

#[derive(Debug, Subcommand)]
pub enum Command {
/// Creates a STAC Item from an asset href.
Item {
/// The asset href.
href: String,

/// The item id.
///
/// If not provided, will be inferred from the filename in the href.
#[arg(short, long)]
id: Option<String>,

/// The asset key.
#[arg(short, long, default_value = "data")]
key: String,

/// The asset roles.
///
/// Can be provided multiple times.
#[arg(short, long)]
role: Vec<String>,

/// Allow relative paths.
///
/// If false, paths will be canonicalized, which requires that the files actually exist on the filesystem.
#[arg(long)]
allow_relative_paths: bool,

/// Use compact representation for the output.
#[arg(short, long)]
compact: bool,
},

/// Searches a STAC API.
Search {
/// The href of the STAC API.
Expand Down Expand Up @@ -102,6 +136,17 @@ impl Command {
pub async fn execute(self) -> Result<()> {
use Command::*;
match self {
Item {
id,
href,
key,
role,
allow_relative_paths,
compact,
} => {
let id = id.unwrap_or_else(|| infer_id(&href));
crate::commands::item(id, href, key, role, allow_relative_paths, compact)
}
Search {
href,
max_items,
Expand Down Expand Up @@ -141,3 +186,18 @@ impl Command {
}
}
}

fn infer_id(href: &str) -> String {
if let Ok(url) = Url::parse(href) {
url.path_segments()
.and_then(|path_segments| path_segments.last())
.and_then(|path_segment| Path::new(path_segment).file_stem())
.map(|file_stem| file_stem.to_string_lossy().into_owned())
.unwrap_or_else(|| href.to_string())
} else {
Path::new(href)
.file_stem()
.map(|file_stem| file_stem.to_string_lossy().into_owned())
.unwrap_or_else(|| href.to_string())
}
}
24 changes: 24 additions & 0 deletions stac-cli/src/commands/item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::Result;
use stac::{item::Builder, Asset};

pub fn item(
id: String,
href: String,
key: String,
roles: Vec<String>,
allow_relative_paths: bool,
compact: bool,
) -> Result<()> {
let mut asset = Asset::new(href);
asset.roles = roles;
let item = Builder::new(id)
.asset(key, asset)
.canonicalize_paths(!allow_relative_paths)
.into_item()?;
if compact {
println!("{}", serde_json::to_string(&item)?);
} else {
println!("{}", serde_json::to_string_pretty(&item)?);
}
Ok(())
}
3 changes: 2 additions & 1 deletion stac-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod item;
mod search;
mod sort;
mod validate;

pub use {search::search, sort::sort, validate::validate};
pub use {item::item, search::search, sort::sort, validate::validate};
3 changes: 3 additions & 0 deletions stac-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub enum Error {
#[error(transparent)]
StacApi(#[from] stac_api::Error),

#[error(transparent)]
Stac(#[from] stac::Error),

#[error(transparent)]
StacAsync(#[from] stac_async::Error),

Expand Down
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
118 changes: 115 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, asset: impl Into<Asset>) -> Builder {
let _ = self.assets.insert(key.to_string(), asset.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,35 @@ 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(&format!("assets{}dataset.tif", std::path::MAIN_SEPARATOR)));
}

#[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 71fa4db

Please sign in to comment.