Skip to content

Commit ca0ea4e

Browse files
authored
feat: validate in cli (#613)
## Closes - Closes #612
1 parent 958f83e commit ca0ea4e

File tree

7 files changed

+169
-13
lines changed

7 files changed

+169
-13
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ $ stacrs search items.parquet
5252

5353
# Server
5454
$ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822
55+
56+
# Validate
57+
$ stacrs validate item.json
5558
```
5659

5760
## Python

crates/cli/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ $ stac search items.parquet
4343

4444
# Server
4545
$ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822
46+
47+
# Validate
48+
$ stacrs validate item.json
4649
```
4750

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

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

crates/cli/data/invalid-item.json

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"stac_version": "1.1.0",
3+
"stac_extensions": [],
4+
"type": "Feature",
5+
"id": "",
6+
"bbox": [
7+
172.91173669923782,
8+
1.3438851951615003,
9+
172.95469614953714,
10+
1.3690476620161975
11+
],
12+
"geometry": {
13+
"type": "Polygon",
14+
"coordinates": [
15+
[
16+
[
17+
172.91173669923782,
18+
1.3438851951615003
19+
],
20+
[
21+
172.95469614953714,
22+
1.3438851951615003
23+
],
24+
[
25+
172.95469614953714,
26+
1.3690476620161975
27+
],
28+
[
29+
172.91173669923782,
30+
1.3690476620161975
31+
],
32+
[
33+
172.91173669923782,
34+
1.3438851951615003
35+
]
36+
]
37+
]
38+
},
39+
"properties": {
40+
"datetime": "2020-12-11T22:38:32.125000Z"
41+
},
42+
"collection": "simple-collection",
43+
"links": [
44+
{
45+
"rel": "collection",
46+
"href": "./collection.json",
47+
"type": "application/json",
48+
"title": "Simple Example Collection"
49+
},
50+
{
51+
"rel": "root",
52+
"href": "./collection.json",
53+
"type": "application/json",
54+
"title": "Simple Example Collection"
55+
},
56+
{
57+
"rel": "parent",
58+
"href": "./collection.json",
59+
"type": "application/json",
60+
"title": "Simple Example Collection"
61+
}
62+
],
63+
"assets": {
64+
"visual": {
65+
"href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif",
66+
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
67+
"title": "3-Band Visual",
68+
"roles": [
69+
"visual"
70+
]
71+
},
72+
"thumbnail": {
73+
"href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg",
74+
"title": "Thumbnail",
75+
"type": "image/jpeg",
76+
"roles": [
77+
"thumbnail"
78+
]
79+
}
80+
}
81+
}

crates/cli/src/lib.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use anyhow::{anyhow, Error, Result};
22
use clap::{Parser, Subcommand};
3-
use stac::{geoparquet::Compression, Collection, Format, Item, Links, Migrate};
3+
use stac::{geoparquet::Compression, Collection, Format, Item, Links, Migrate, Validate};
44
use stac_api::{GetItems, GetSearch, Search};
55
use stac_server::Backend;
66
use std::{collections::HashMap, io::Write, str::FromStr};
7-
use tokio::{io::AsyncReadExt, net::TcpListener};
7+
use tokio::{io::AsyncReadExt, net::TcpListener, runtime::Handle};
88

99
/// stacrs: A command-line interface for the SpatioTemporal Asset Catalog (STAC)
1010
#[derive(Debug, Parser)]
@@ -193,6 +193,17 @@ pub enum Command {
193193
#[arg(long, default_value_t = true)]
194194
create_collections: bool,
195195
},
196+
197+
/// Validates a STAC value.
198+
///
199+
/// The default output format is plain text — use `--output-format=json` to
200+
/// get structured output.
201+
Validate {
202+
/// The input file.
203+
///
204+
/// To read from standard input, pass `-` or don't provide an argument at all.
205+
infile: Option<String>,
206+
},
196207
}
197208

198209
#[derive(Debug)]
@@ -336,6 +347,40 @@ impl Stacrs {
336347
load_and_serve(addr, backend, collections, items, create_collections).await
337348
}
338349
}
350+
Command::Validate { ref infile } => {
351+
let value = self.get(infile.as_deref()).await?;
352+
let result = Handle::current()
353+
.spawn_blocking(move || value.validate())
354+
.await?;
355+
if let Err(error) = result {
356+
if let stac::Error::Validation(errors) = error {
357+
if let Some(format) = self.output_format {
358+
if let Format::Json(_) = format {
359+
let value = errors
360+
.into_iter()
361+
.map(|error| error.into_json())
362+
.collect::<Vec<_>>();
363+
if self.compact_json.unwrap_or_default() {
364+
serde_json::to_writer(std::io::stdout(), &value)?;
365+
} else {
366+
serde_json::to_writer_pretty(std::io::stdout(), &value)?;
367+
}
368+
println!("");
369+
} else {
370+
return Err(anyhow!("invalid output format: {}", format));
371+
}
372+
} else {
373+
for error in errors {
374+
println!("{}", error);
375+
}
376+
}
377+
}
378+
std::io::stdout().flush()?;
379+
Err(anyhow!("one or more validation errors"))
380+
} else {
381+
Ok(())
382+
}
383+
}
339384
}
340385
}
341386

@@ -591,4 +636,18 @@ mod tests {
591636
Format::Geoparquet(Some(Compression::LZO))
592637
);
593638
}
639+
640+
#[rstest]
641+
fn validate(mut command: Command) {
642+
command
643+
.arg("validate")
644+
.arg("examples/simple-item.json")
645+
.assert()
646+
.success();
647+
command
648+
.arg("validate")
649+
.arg("data/invalid-item.json")
650+
.assert()
651+
.failure();
652+
}
594653
}

crates/core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `error::Validation::into_json` ([#613](https://github.com/stac-utils/stac-rs/pull/613))
12+
913
### Removed
1014

1115
- Async validation ([#611](https://github.com/stac-utils/stac-rs/pull/611))

crates/core/src/error.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,6 @@ impl Validation {
174174
error: jsonschema::ValidationError<'_>,
175175
value: Option<&serde_json::Value>,
176176
) -> Validation {
177-
use std::borrow::Cow;
178-
179-
// Cribbed from https://docs.rs/jsonschema/latest/src/jsonschema/error.rs.html#21-30
180-
let error = jsonschema::ValidationError {
181-
instance_path: error.instance_path.clone(),
182-
instance: Cow::Owned(error.instance.into_owned()),
183-
kind: error.kind,
184-
schema_path: error.schema_path,
185-
};
186177
let mut id = None;
187178
let mut r#type = None;
188179
if let Some(value) = value.and_then(|v| v.as_object()) {
@@ -192,7 +183,21 @@ impl Validation {
192183
.and_then(|v| v.as_str())
193184
.and_then(|s| s.parse::<crate::Type>().ok());
194185
}
195-
Validation { id, r#type, error }
186+
Validation {
187+
id,
188+
r#type,
189+
error: error.to_owned(),
190+
}
191+
}
192+
193+
/// Converts this validation error into a [serde_json::Value].
194+
pub fn into_json(self) -> serde_json::Value {
195+
let error_description = jsonschema::output::ErrorDescription::from(self.error);
196+
serde_json::json!({
197+
"id": self.id,
198+
"type": self.r#type,
199+
"error": error_description,
200+
})
196201
}
197202
}
198203

crates/core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ pub const STAC_VERSION: Version = Version::v1_1_0;
221221
pub type Result<T> = std::result::Result<T, Error>;
222222

223223
/// Enum for the four "types" of STAC values.
224-
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
224+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
225225
pub enum Type {
226226
/// An item.
227227
Item,

0 commit comments

Comments
 (0)