Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add item builder #237

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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