From 8ccca3b59865f51b1ad659ac67b21385fd5ac5f5 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Sun, 21 Apr 2024 07:33:09 -0600 Subject: [PATCH] feat: rearchitecture cli --- stac-cli/Cargo.toml | 3 + stac-cli/src/args.rs | 295 ++++++++++++++++++++- stac-cli/src/command.rs | 223 ---------------- stac-cli/src/commands/item.rs | 26 -- stac-cli/src/commands/mod.rs | 6 - stac-cli/src/commands/search.rs | 51 ---- stac-cli/src/commands/sort.rs | 17 -- stac-cli/src/commands/validate.rs | 74 ------ stac-cli/src/error.rs | 12 +- stac-cli/src/lib.rs | 5 +- stac-cli/src/main.rs | 8 +- stac-cli/src/subcommand.rs | 127 +++++++++ stac-cli/tests/help.rs | 7 + stac-cli/tests/item.rs | 22 ++ stac-cli/tests/sort.rs | 13 + stac-cli/tests/{command.rs => validate.rs} | 17 -- stac-validate/src/validate.rs | 2 +- 17 files changed, 473 insertions(+), 435 deletions(-) delete mode 100644 stac-cli/src/command.rs delete mode 100644 stac-cli/src/commands/item.rs delete mode 100644 stac-cli/src/commands/mod.rs delete mode 100644 stac-cli/src/commands/search.rs delete mode 100644 stac-cli/src/commands/sort.rs delete mode 100644 stac-cli/src/commands/validate.rs create mode 100644 stac-cli/src/subcommand.rs create mode 100644 stac-cli/tests/help.rs create mode 100644 stac-cli/tests/item.rs create mode 100644 stac-cli/tests/sort.rs rename stac-cli/tests/{command.rs => validate.rs} (56%) diff --git a/stac-cli/Cargo.toml b/stac-cli/Cargo.toml index 6c308ee1..13fb5184 100644 --- a/stac-cli/Cargo.toml +++ b/stac-cli/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["geospatial", "stac", "metadata", "geo", "raster"] categories = ["science", "data-structures"] [features] +default = ["gdal"] gdal = ["stac/gdal"] [dependencies] @@ -27,6 +28,8 @@ stac-validate = { version = "0.1", path = "../stac-validate" } thiserror = "1" tokio = { version = "1.23", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1" +tracing = "0.1" +tracing-subscriber = "0.3" url = "2" [dev-dependencies] diff --git a/stac-cli/src/args.rs b/stac-cli/src/args.rs index 883f2221..9289d3d6 100644 --- a/stac-cli/src/args.rs +++ b/stac-cli/src/args.rs @@ -1,10 +1,301 @@ -use crate::Command; +use crate::{Error, Result, Subcommand}; use clap::Parser; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::json; +use stac::{item::Builder, Asset, Value}; +use stac_api::{GetSearch, Item, ItemCollection}; +use stac_async::ApiClient; +use stac_validate::Validate; +use std::path::Path; +use tokio_stream::StreamExt; +use url::Url; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { + /// Use a compact representation of the output, if possible. + #[arg(short, long)] + compact: bool, + /// The subcommand to run. #[command(subcommand)] - pub command: Command, + pub subcommand: Subcommand, +} + +impl Args { + pub async fn execute(self) -> i32 { + use Subcommand::*; + let result = match &self.subcommand { + Item { + id_or_href, + id, + key, + role, + allow_relative_paths, + disable_gdal, + collect, + } => self.item( + id_or_href, + id.as_deref(), + key, + role, + *allow_relative_paths, + *disable_gdal, + *collect, + ), + Search { + href, + max_items, + limit, + bbox, + datetime, + intersects, + ids, + collections, + fields, + sortby, + filter_crs, + filter_lang, + filter, + stream, + } => { + self.api_search( + href, + *max_items, + limit, + bbox, + datetime, + intersects, + ids, + collections, + fields, + sortby, + filter_crs, + filter_lang, + filter, + *stream, + ) + .await + } + Sort { href } => self.sort(href.as_deref()).await, + Validate { href } => self.validate(href.as_deref()).await, + }; + match result { + Ok(()) => 0, + Err(err) => { + eprintln!("ERROR: {}", err); + err.code() + } + } + } + + fn item( + &self, + href_or_id: &str, + id: Option<&str>, + key: &str, + roles: &[String], + allow_relative_paths: bool, + mut disable_gdal: bool, + collect: bool, + ) -> Result<()> { + if !cfg!(feature = "gdal") { + tracing::info!(disable_gdal = true, "gdal feature not enabled"); + disable_gdal = true; + } + let mut href = None; + let id = if let Ok(url) = Url::parse(href_or_id) { + href = Some(href_or_id); + id.map(|id| id.to_string()).unwrap_or_else(|| { + 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_or_id.to_string()) + }) + } else { + let path = Path::new(href_or_id); + if path.exists() { + href = Some(href_or_id); + id.map(|id| id.to_string()).unwrap_or_else(|| { + path.file_stem() + .map(|file_stem| file_stem.to_string_lossy().into_owned()) + .unwrap_or_else(|| href_or_id.to_string()) + }) + } else { + href_or_id.to_string() + } + }; + let mut builder = Builder::new(id) + .enable_gdal(!disable_gdal) + .canonicalize_paths(!allow_relative_paths); + if let Some(href) = href { + let mut asset = Asset::new(href); + asset.roles = roles.to_vec(); + builder = builder.asset(key, asset); + } + let item = builder.into_item()?; + if collect { + let value = serde_json::from_reader(std::io::stdin())?; + match value { + Value::Item(stdin_item) => { + self.println(stac::ItemCollection::from(vec![stdin_item, item])) + } + Value::ItemCollection(mut item_collection) => { + item_collection.items.push(item); + self.println(item_collection) + } + Value::Catalog(_) | Value::Collection(_) => Err(Error::Custom(format!( + "unexpected STAC object type on standard input: {}", + value.type_name() + ))), + } + } else { + self.println(item) + } + } + + async fn api_search( + &self, + href: &str, + max_items: Option, + limit: &Option, + bbox: &Option, + datetime: &Option, + intersects: &Option, + ids: &Option>, + collections: &Option>, + fields: &Option, + sortby: &Option, + filter_crs: &Option, + filter_lang: &Option, + filter: &Option, + stream: bool, + ) -> Result<()> { + let get_search = GetSearch { + limit: limit.clone(), + bbox: bbox.clone(), + datetime: datetime.clone(), + intersects: intersects.clone(), + ids: ids.clone(), + collections: collections.clone(), + fields: fields.clone(), + sortby: sortby.clone(), + filter_crs: filter_crs.clone(), + filter_lang: filter_lang.clone(), + filter: filter.clone(), + additional_fields: Default::default(), + }; + let search = get_search.try_into()?; + let client = ApiClient::new(href)?; + let item_stream = client.search(search).await?; + tokio::pin!(item_stream); + let mut num_items = 0; + if stream { + while let Some(result) = item_stream.next().await { + let item: Item = result?; + num_items += 1; + self.println_compact(item)?; + if max_items + .map(|max_items| num_items >= max_items) + .unwrap_or(false) + { + break; + } + } + } else { + let mut items = Vec::new(); + while let Some(result) = item_stream.next().await { + num_items += 1; + items.push(result?); + if max_items + .map(|max_items| num_items >= max_items) + .unwrap_or(false) + { + break; + } + } + let item_collection = ItemCollection::new(items)?; + self.println(item_collection)?; + } + Ok(()) + } + + async fn sort(&self, href: Option<&str>) -> Result<()> { + let value: Value = self.read_href(href).await?; + self.println(value) + } + + async fn validate(&self, href: Option<&str>) -> Result<()> { + let value: serde_json::Value = self.read_href(href).await?; + let mut errors: Vec = Vec::new(); + let mut update_errors = |result: std::result::Result<(), stac_validate::Error>| match result + { + Ok(()) => {} + Err(err) => match err { + stac_validate::Error::Validation(ref e) => { + errors.extend(e.iter().map(|error| { + json!({ + "type": "validation", + "instance_path": error.instance_path, + "schema_path": error.schema_path, + "description": error.to_string(), + }) + })); + } + _ => errors.push(json!({ + "type": "other", + "message": err.to_string(), + })), + }, + }; + if let Some(collections) = value + .as_object() + .and_then(|object| object.get("collections")) + { + if let Some(collections) = collections.as_array() { + for collection in collections.iter() { + let collection = collection.clone(); + let result = tokio::task::spawn_blocking(move || collection.validate()).await?; + update_errors(result); + } + } else { + return Err(Error::Custom( + "expected the 'collections' key to be an array".to_string(), + )); + } + } else { + let result = tokio::task::spawn_blocking(move || value.validate()).await?; + update_errors(result); + } + if errors.is_empty() { + Ok(()) + } else { + self.println(errors)?; + Err(Error::Custom(format!( + "one or more errors during validation" + ))) + } + } + + async fn read_href(&self, href: Option<&str>) -> Result { + if let Some(href) = href { + stac_async::read_json(href).await.map_err(Error::from) + } else { + serde_json::from_reader(std::io::stdin()).map_err(Error::from) + } + } + + fn println_compact(&self, s: S) -> Result<()> { + Ok(println!("{}", serde_json::to_string(&s)?)) + } + + fn println(&self, s: S) -> Result<()> { + let output = if self.compact { + serde_json::to_string(&s)? + } else { + serde_json::to_string_pretty(&s)? + }; + Ok(println!("{}", output)) + } } diff --git a/stac-cli/src/command.rs b/stac-cli/src/command.rs deleted file mode 100644 index ab522990..00000000 --- a/stac-cli/src/command.rs +++ /dev/null @@ -1,223 +0,0 @@ -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, - - /// The asset key. - #[arg(short, long, default_value = "data")] - key: String, - - /// The asset roles. - /// - /// Can be provided multiple times. - #[arg(short, long)] - role: Vec, - - /// 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, - - /// Don't use GDAL for item creation. - /// - /// Automatically set to true if this crate is compiled without GDAL. - #[arg(long)] - disable_gdal: bool, - }, - - /// Searches a STAC API. - Search { - /// The href of the STAC API. - href: String, - - /// The maximum number of items to print. - #[arg(short, long)] - max_items: Option, - - /// The maximum number of results to return (page size). - #[arg(short, long)] - limit: Option, - - /// Requested bounding box. - #[arg(short, long)] - bbox: Option, - - /// Requested bounding box. - /// Use double dots `..` for open date ranges. - #[arg(short, long)] - datetime: Option, - - /// Searches items by performing intersection between their geometry and provided GeoJSON geometry. - /// - /// All GeoJSON geometry types must be supported. - #[arg(long)] - intersects: Option, - - /// Array of Item ids to return. - #[arg(short, long)] - ids: Option>, - - /// Array of one or more Collection IDs that each matching Item must be in. - #[arg(short, long)] - collections: Option>, - - /// Include/exclude fields from item collections. - #[arg(long)] - fields: Option, - - /// Fields by which to sort results. - #[arg(short, long)] - sortby: Option, - - /// Recommended to not be passed, but server must only accept - /// as a valid value, may - /// reject any others - #[arg(long)] - filter_crs: Option, - - /// CQL2 filter expression. - #[arg(long)] - filter_lang: Option, - - /// CQL2 filter expression. - #[arg(short, long)] - filter: Option, - - /// Stream the items to standard output as ndjson. - #[arg(long)] - stream: bool, - - /// Do not pretty print the output. - /// - /// Only used if stream is false. - #[arg(long)] - compact: bool, - }, - - /// Sorts the fields of STAC object. - Sort { - /// The href of the STAC object. - /// - /// If this is not provided, will read from standard input. - href: Option, - - /// If true, don't pretty-print the output - #[arg(short, long)] - compact: bool, - }, - - /// Validates a STAC object or API endpoint using json-schema validation. - Validate { - /// The href of the STAC object or endpoint. - /// - /// The validator will make some decisions depending on what type of - /// data is returned from the href. If it's a STAC Catalog, Collection, - /// or Item, that object will be validated. If its a collections - /// endpoint from a STAC API, all collections will be validated. - /// Additional behavior TBD. - /// - /// If this is not provided, will read from standard input. - href: Option, - }, -} - -impl Command { - pub async fn execute(self) -> Result<()> { - use Command::*; - match self { - Item { - id, - href, - key, - role, - allow_relative_paths, - compact, - mut disable_gdal, - } => { - let id = id.unwrap_or_else(|| infer_id(&href)); - if !cfg!(feature = "gdal") { - disable_gdal = true; - } - crate::commands::item( - id, - href, - key, - role, - allow_relative_paths, - compact, - disable_gdal, - ) - } - Search { - href, - max_items, - limit, - bbox, - datetime, - intersects, - ids, - collections, - fields, - sortby, - filter_crs, - filter_lang, - filter, - stream, - compact, - } => { - let get_search = GetSearch { - limit, - bbox, - datetime, - intersects, - ids, - collections, - fields, - sortby, - filter_crs, - filter_lang, - filter, - additional_fields: Default::default(), - }; - let search = get_search.try_into()?; - crate::commands::search(&href, search, max_items, stream, !(compact | stream)).await - } - Sort { href, compact } => crate::commands::sort(href.as_deref(), compact).await, - Validate { href } => crate::commands::validate(href.as_deref()).await, - } - } -} - -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()) - } -} diff --git a/stac-cli/src/commands/item.rs b/stac-cli/src/commands/item.rs deleted file mode 100644 index aa263aa3..00000000 --- a/stac-cli/src/commands/item.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::Result; -use stac::{item::Builder, Asset}; - -pub fn item( - id: String, - href: String, - key: String, - roles: Vec, - allow_relative_paths: bool, - compact: bool, - disable_gdal: bool, -) -> Result<()> { - let mut asset = Asset::new(href); - asset.roles = roles; - let item = Builder::new(id) - .asset(key, asset) - .canonicalize_paths(!allow_relative_paths) - .enable_gdal(!disable_gdal) - .into_item()?; - if compact { - println!("{}", serde_json::to_string(&item)?); - } else { - println!("{}", serde_json::to_string_pretty(&item)?); - } - Ok(()) -} diff --git a/stac-cli/src/commands/mod.rs b/stac-cli/src/commands/mod.rs deleted file mode 100644 index dc2145a6..00000000 --- a/stac-cli/src/commands/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod item; -mod search; -mod sort; -mod validate; - -pub use {item::item, search::search, sort::sort, validate::validate}; diff --git a/stac-cli/src/commands/search.rs b/stac-cli/src/commands/search.rs deleted file mode 100644 index 817fea53..00000000 --- a/stac-cli/src/commands/search.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::Result; -use stac_api::{Item, ItemCollection, Search}; -use stac_async::ApiClient; -use tokio_stream::StreamExt; - -pub async fn search( - href: &str, - search: Search, - max_items: Option, - stream: bool, - pretty: bool, -) -> Result<()> { - let client = ApiClient::new(href)?; - let item_stream = client.search(search).await?; - tokio::pin!(item_stream); - let mut num_items = 0; - if stream { - assert!(!pretty); - while let Some(result) = item_stream.next().await { - let item: Item = result?; - num_items += 1; - println!("{}", serde_json::to_string(&item)?); - if max_items - .map(|max_items| num_items >= max_items) - .unwrap_or(false) - { - break; - } - } - } else { - let mut items = Vec::new(); - while let Some(result) = item_stream.next().await { - num_items += 1; - items.push(result?); - if max_items - .map(|max_items| num_items >= max_items) - .unwrap_or(false) - { - break; - } - } - let item_collection = ItemCollection::new(items)?; - let output = if pretty { - serde_json::to_string_pretty(&item_collection)? - } else { - serde_json::to_string(&item_collection)? - }; - println!("{}", output); - } - Ok(()) -} diff --git a/stac-cli/src/commands/sort.rs b/stac-cli/src/commands/sort.rs deleted file mode 100644 index e6806bc0..00000000 --- a/stac-cli/src/commands/sort.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::Result; -use stac::Value; - -pub async fn sort(href: Option<&str>, compact: bool) -> Result<()> { - let value: Value = if let Some(href) = href { - stac_async::read_json(href).await? - } else { - serde_json::from_reader(std::io::stdin())? - }; - let output = if compact { - serde_json::to_string(&value).unwrap() - } else { - serde_json::to_string_pretty(&value).unwrap() - }; - println!("{}", output); - Ok(()) -} diff --git a/stac-cli/src/commands/validate.rs b/stac-cli/src/commands/validate.rs deleted file mode 100644 index c7b49ecb..00000000 --- a/stac-cli/src/commands/validate.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::{Error, Result}; -use serde_json::Value; -use stac_validate::{Validate, Validator}; - -pub async fn validate(href: Option<&str>) -> Result<()> { - let value: Value = if let Some(href) = href { - stac_async::read_json(href).await? - } else { - serde_json::from_reader(std::io::stdin())? - }; - if let Some(map) = value.as_object() { - if map.contains_key("type") { - let value = value.clone(); - let result = tokio::task::spawn_blocking(move || value.validate()).await?; - print_result(result).map_err(Error::from) - } else if let Some(collections) = map - .get("collections") - .and_then(|collections| collections.as_array()) - { - let collections = collections.clone(); - let result = tokio::task::spawn_blocking(move || { - let mut errors = Vec::new(); - let mut validator = Validator::new(); - let num_collections = collections.len(); - let mut valid_collections = 0; - for collection in collections { - if let Some(id) = collection.get("id").and_then(|id| id.as_str()) { - println!("== Validating {}", id); - } - let result = validator.validate(collection); - match print_result(result) { - Ok(()) => valid_collections += 1, - Err(err) => errors.push(err), - } - println!("") - } - println!( - "{}/{} collections are valid", - valid_collections, num_collections - ); - if errors.is_empty() { - Ok(()) - } else { - Err(Error::ValidationGroup(errors)) - } - }) - .await?; - result - } else { - todo!() - } - } else { - todo!() - } -} - -pub fn print_result(result: stac_validate::Result<()>) -> stac_validate::Result<()> { - match result { - Ok(()) => { - println!("OK!"); - Ok(()) - } - Err(stac_validate::Error::Validation(errors)) => { - for err in &errors { - println!("Validation error at {}: {}", err.instance_path, err) - } - Err(stac_validate::Error::Validation(errors)) - } - Err(err) => { - println!("Error while validating: {}", err); - Err(err) - } - } -} diff --git a/stac-cli/src/error.rs b/stac-cli/src/error.rs index b35691a5..fee7041e 100644 --- a/stac-cli/src/error.rs +++ b/stac-cli/src/error.rs @@ -1,15 +1,14 @@ -use stac::Value; use thiserror::Error; #[derive(Error, Debug)] #[non_exhaustive] pub enum Error { + #[error("{0}")] + Custom(String), + #[error(transparent)] Io(#[from] std::io::Error), - #[error("invalid STAC")] - InvalidValue(Value), - #[error(transparent)] SerdeJson(#[from] serde_json::Error), @@ -27,13 +26,10 @@ pub enum Error { #[error(transparent)] TokioJoinError(#[from] tokio::task::JoinError), - - #[error("many validation errors")] - ValidationGroup(Vec), } impl Error { - pub fn return_code(&self) -> i32 { + pub fn code(&self) -> i32 { // TODO make these codes more meaningful 1 } diff --git a/stac-cli/src/lib.rs b/stac-cli/src/lib.rs index 7b838d75..081d0469 100644 --- a/stac-cli/src/lib.rs +++ b/stac-cli/src/lib.rs @@ -1,8 +1,7 @@ mod args; -mod command; -mod commands; mod error; +mod subcommand; -pub use {args::Args, command::Command, error::Error}; +pub use {args::Args, error::Error, subcommand::Subcommand}; pub type Result = std::result::Result; diff --git a/stac-cli/src/main.rs b/stac-cli/src/main.rs index f2e558e5..2bf1e796 100644 --- a/stac-cli/src/main.rs +++ b/stac-cli/src/main.rs @@ -4,11 +4,5 @@ use stac_cli::Args; #[tokio::main] async fn main() { let args = Args::parse(); - match args.command.execute().await { - Ok(()) => return, - Err(err) => { - eprintln!("ERROR: {}", err); - std::process::exit(err.return_code()) - } - } + std::process::exit(args.execute().await) } diff --git a/stac-cli/src/subcommand.rs b/stac-cli/src/subcommand.rs new file mode 100644 index 00000000..f987dcdc --- /dev/null +++ b/stac-cli/src/subcommand.rs @@ -0,0 +1,127 @@ +#[derive(Debug, clap::Subcommand)] +pub enum Subcommand { + /// Creates a STAC Item. + Item { + /// The item id or asset href. + id_or_href: String, + + /// The item id, if the positional argument is an href. + /// + /// If not provided, will be inferred from the filename in the href. + #[arg(short, long)] + id: Option, + + /// The asset key, if the positional argument is an href. + #[arg(short, long, default_value = "data")] + key: String, + + /// The asset roles, if the positional argument is an href. + /// + /// Can be provided multiple times. + #[arg(short, long)] + role: Vec, + + /// Allow relative paths. + /// + /// If false, all path will be canonicalized, which requires that the + /// files actually exist on the filesystem. + #[arg(long)] + allow_relative_paths: bool, + + /// Don't use GDAL for item creation, if the positional argument is an href. + /// + /// Automatically set to true if this crate is compiled without GDAL. + #[arg(long)] + disable_gdal: bool, + + /// Collect an item or item collection from standard input, and add the + /// newly created to it into a new item collection. + #[arg(short, long)] + collect: bool, + }, + + /// Searches a STAC API. + Search { + /// The href of the STAC API. + href: String, + + /// The maximum number of items to print. + #[arg(short, long)] + max_items: Option, + + /// The maximum number of results to return (page size). + #[arg(short, long)] + limit: Option, + + /// Requested bounding box. + #[arg(short, long)] + bbox: Option, + + /// Requested bounding box. + /// Use double dots `..` for open date ranges. + #[arg(short, long)] + datetime: Option, + + /// Searches items by performing intersection between their geometry and provided GeoJSON geometry. + /// + /// All GeoJSON geometry types must be supported. + #[arg(long)] + intersects: Option, + + /// Array of Item ids to return. + #[arg(short, long)] + ids: Option>, + + /// Array of one or more Collection IDs that each matching Item must be in. + #[arg(short, long)] + collections: Option>, + + /// Include/exclude fields from item collections. + #[arg(long)] + fields: Option, + + /// Fields by which to sort results. + #[arg(short, long)] + sortby: Option, + + /// Recommended to not be passed, but server must only accept + /// as a valid value, may + /// reject any others + #[arg(long)] + filter_crs: Option, + + /// CQL2 filter expression. + #[arg(long)] + filter_lang: Option, + + /// CQL2 filter expression. + #[arg(short, long)] + filter: Option, + + /// Stream the items to standard output as ndjson. + #[arg(long)] + stream: bool, + }, + + /// Sorts the fields of STAC object. + Sort { + /// The href of the STAC object. + /// + /// If this is not provided, will read from standard input. + href: Option, + }, + + /// Validates a STAC object or API endpoint using json-schema validation. + Validate { + /// The href of the STAC object or endpoint. + /// + /// The validator will make some decisions depending on what type of + /// data is returned from the href. If it's a STAC Catalog, Collection, + /// or Item, that object will be validated. If its a collections + /// endpoint from a STAC API, all collections will be validated. + /// Additional behavior TBD. + /// + /// If this is not provided, will read from standard input. + href: Option, + }, +} diff --git a/stac-cli/tests/help.rs b/stac-cli/tests/help.rs new file mode 100644 index 00000000..02f9098d --- /dev/null +++ b/stac-cli/tests/help.rs @@ -0,0 +1,7 @@ +use assert_cmd::Command; + +#[test] +fn help() { + let mut command = Command::cargo_bin("stac").unwrap(); + command.arg("help").assert().success(); +} diff --git a/stac-cli/tests/item.rs b/stac-cli/tests/item.rs new file mode 100644 index 00000000..7e762505 --- /dev/null +++ b/stac-cli/tests/item.rs @@ -0,0 +1,22 @@ +use assert_cmd::Command; +use stac::{Item, ItemCollection}; + +#[test] +fn item() { + let mut command = Command::cargo_bin("stac").unwrap(); + command.arg("item").arg("an-id").assert().success(); +} + +#[test] +fn item_collection() { + let mut command = Command::cargo_bin("stac").unwrap(); + let item_a = serde_json::to_string(&Item::new("item-a")).unwrap(); + let output = command + .arg("item") + .arg("item-b") + .arg("-c") + .write_stdin(item_a) + .unwrap(); + let item_collection: ItemCollection = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(item_collection.items.len(), 2); +} diff --git a/stac-cli/tests/sort.rs b/stac-cli/tests/sort.rs new file mode 100644 index 00000000..0215cbee --- /dev/null +++ b/stac-cli/tests/sort.rs @@ -0,0 +1,13 @@ +use assert_cmd::Command; +use std::{fs::File, io::Read}; + +#[test] +fn sort_stdin() { + let mut command = Command::cargo_bin("stac").unwrap(); + let mut item = String::new(); + File::open("data/simple-item.json") + .unwrap() + .read_to_string(&mut item) + .unwrap(); + command.arg("sort").write_stdin(item).assert().success(); +} diff --git a/stac-cli/tests/command.rs b/stac-cli/tests/validate.rs similarity index 56% rename from stac-cli/tests/command.rs rename to stac-cli/tests/validate.rs index cdfa4369..c6c14af4 100644 --- a/stac-cli/tests/command.rs +++ b/stac-cli/tests/validate.rs @@ -1,12 +1,6 @@ use assert_cmd::Command; use std::{fs::File, io::Read}; -#[test] -fn help() { - let mut command = Command::cargo_bin("stac").unwrap(); - command.arg("help").assert().success(); -} - #[test] fn validate() { let mut command = Command::cargo_bin("stac").unwrap(); @@ -27,14 +21,3 @@ fn validate_stdin() { .unwrap(); command.arg("validate").write_stdin(item).assert().success(); } - -#[test] -fn sort_stdin() { - let mut command = Command::cargo_bin("stac").unwrap(); - let mut item = String::new(); - File::open("data/simple-item.json") - .unwrap() - .read_to_string(&mut item) - .unwrap(); - command.arg("sort").write_stdin(item).assert().success(); -} diff --git a/stac-validate/src/validate.rs b/stac-validate/src/validate.rs index 9b63e65b..ed08eac4 100644 --- a/stac-validate/src/validate.rs +++ b/stac-validate/src/validate.rs @@ -59,7 +59,7 @@ pub trait Validate: ValidateCore { /// If your STAC object is the same version as [stac::STAC_VERSION], this will /// be a quick, cheap operation, since the schemas are stored in the library. pub trait ValidateCore: Serialize { - /// Validate a [serde_json::Value] agasint a specific STAC jsonschema. + /// Validate a [serde_json::Value] against a specific STAC jsonschema. /// /// # Examples ///