Skip to content
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ $ stacrs search items.parquet

# Server
$ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822

# Validate
$ stacrs validate item.json
```

## Python
Expand Down
4 changes: 4 additions & 0 deletions crates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ $ stac search items.parquet

# Server
$ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822

# Validate
$ stacrs validate item.json
```

## Usage
Expand All @@ -52,6 +55,7 @@ $ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822
- `stacrs search`: searches STAC APIs and geoparquet files
- `stacrs serve`: serves a STAC API
- `stacrs translate`: converts STAC from one format to another
- `stacrs validate`: validates a STAC value

Use the `--help` flag to see all available options for the CLI and the subcommands:

Expand Down
81 changes: 81 additions & 0 deletions crates/cli/data/invalid-item.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"stac_version": "1.1.0",
"stac_extensions": [],
"type": "Feature",
"id": "",
"bbox": [
172.91173669923782,
1.3438851951615003,
172.95469614953714,
1.3690476620161975
],
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
172.91173669923782,
1.3438851951615003
],
[
172.95469614953714,
1.3438851951615003
],
[
172.95469614953714,
1.3690476620161975
],
[
172.91173669923782,
1.3690476620161975
],
[
172.91173669923782,
1.3438851951615003
]
]
]
},
"properties": {
"datetime": "2020-12-11T22:38:32.125000Z"
},
"collection": "simple-collection",
"links": [
{
"rel": "collection",
"href": "./collection.json",
"type": "application/json",
"title": "Simple Example Collection"
},
{
"rel": "root",
"href": "./collection.json",
"type": "application/json",
"title": "Simple Example Collection"
},
{
"rel": "parent",
"href": "./collection.json",
"type": "application/json",
"title": "Simple Example Collection"
}
],
"assets": {
"visual": {
"href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif",
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
"title": "3-Band Visual",
"roles": [
"visual"
]
},
"thumbnail": {
"href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg",
"title": "Thumbnail",
"type": "image/jpeg",
"roles": [
"thumbnail"
]
}
}
}
63 changes: 61 additions & 2 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use anyhow::{anyhow, Error, Result};
use clap::{Parser, Subcommand};
use stac::{geoparquet::Compression, Collection, Format, Item, Links, Migrate};
use stac::{geoparquet::Compression, Collection, Format, Item, Links, Migrate, Validate};
use stac_api::{GetItems, GetSearch, Search};
use stac_server::Backend;
use std::{collections::HashMap, io::Write, str::FromStr};
use tokio::{io::AsyncReadExt, net::TcpListener};
use tokio::{io::AsyncReadExt, net::TcpListener, runtime::Handle};

/// stacrs: A command-line interface for the SpatioTemporal Asset Catalog (STAC)
#[derive(Debug, Parser)]
Expand Down Expand Up @@ -193,6 +193,17 @@ pub enum Command {
#[arg(long, default_value_t = true)]
create_collections: bool,
},

/// Validates a STAC value.
///
/// The default output format is plain text — use `--output-format=json` to
/// get structured output.
Validate {
/// The input file.
///
/// To read from standard input, pass `-` or don't provide an argument at all.
infile: Option<String>,
},
}

#[derive(Debug)]
Expand Down Expand Up @@ -336,6 +347,40 @@ impl Stacrs {
load_and_serve(addr, backend, collections, items, create_collections).await
}
}
Command::Validate { ref infile } => {
let value = self.get(infile.as_deref()).await?;
let result = Handle::current()
.spawn_blocking(move || value.validate())
.await?;
if let Err(error) = result {
if let stac::Error::Validation(errors) = error {
if let Some(format) = self.output_format {
if let Format::Json(_) = format {
let value = errors
.into_iter()
.map(|error| error.into_json())
.collect::<Vec<_>>();
if self.compact_json.unwrap_or_default() {
serde_json::to_writer(std::io::stdout(), &value)?;
} else {
serde_json::to_writer_pretty(std::io::stdout(), &value)?;
}
println!("");
} else {
return Err(anyhow!("invalid output format: {}", format));
}
} else {
for error in errors {
println!("{}", error);
}
}
}
std::io::stdout().flush()?;
Err(anyhow!("one or more validation errors"))
} else {
Ok(())
}
}
}
}

Expand Down Expand Up @@ -591,4 +636,18 @@ mod tests {
Format::Geoparquet(Some(Compression::LZO))
);
}

#[rstest]
fn validate(mut command: Command) {
command
.arg("validate")
.arg("examples/simple-item.json")
.assert()
.success();
command
.arg("validate")
.arg("data/invalid-item.json")
.assert()
.failure();
}
}
4 changes: 4 additions & 0 deletions crates/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- `error::Validation::into_json` ([#613](https://github.com/stac-utils/stac-rs/pull/613))

### Removed

- Async validation ([#611](https://github.com/stac-utils/stac-rs/pull/611))
Expand Down
25 changes: 15 additions & 10 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,6 @@ impl Validation {
error: jsonschema::ValidationError<'_>,
value: Option<&serde_json::Value>,
) -> Validation {
use std::borrow::Cow;

// Cribbed from https://docs.rs/jsonschema/latest/src/jsonschema/error.rs.html#21-30
let error = jsonschema::ValidationError {
instance_path: error.instance_path.clone(),
instance: Cow::Owned(error.instance.into_owned()),
kind: error.kind,
schema_path: error.schema_path,
};
let mut id = None;
let mut r#type = None;
if let Some(value) = value.and_then(|v| v.as_object()) {
Expand All @@ -192,7 +183,21 @@ impl Validation {
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<crate::Type>().ok());
}
Validation { id, r#type, error }
Validation {
id,
r#type,
error: error.to_owned(),
}
}

/// Converts this validation error into a [serde_json::Value].
pub fn into_json(self) -> serde_json::Value {
let error_description = jsonschema::output::ErrorDescription::from(self.error);
serde_json::json!({
"id": self.id,
"type": self.r#type,
"error": error_description,
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ pub const STAC_VERSION: Version = Version::v1_1_0;
pub type Result<T> = std::result::Result<T, Error>;

/// Enum for the four "types" of STAC values.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
pub enum Type {
/// An item.
Item,
Expand Down