|
1 | 1 | use anyhow::{anyhow, Error, Result}; |
2 | 2 | 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}; |
4 | 4 | use stac_api::{GetItems, GetSearch, Search}; |
5 | 5 | use stac_server::Backend; |
6 | 6 | use std::{collections::HashMap, io::Write, str::FromStr}; |
7 | | -use tokio::{io::AsyncReadExt, net::TcpListener}; |
| 7 | +use tokio::{io::AsyncReadExt, net::TcpListener, runtime::Handle}; |
8 | 8 |
|
9 | 9 | /// stacrs: A command-line interface for the SpatioTemporal Asset Catalog (STAC) |
10 | 10 | #[derive(Debug, Parser)] |
@@ -193,6 +193,17 @@ pub enum Command { |
193 | 193 | #[arg(long, default_value_t = true)] |
194 | 194 | create_collections: bool, |
195 | 195 | }, |
| 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 | + }, |
196 | 207 | } |
197 | 208 |
|
198 | 209 | #[derive(Debug)] |
@@ -336,6 +347,40 @@ impl Stacrs { |
336 | 347 | load_and_serve(addr, backend, collections, items, create_collections).await |
337 | 348 | } |
338 | 349 | } |
| 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 | + } |
339 | 384 | } |
340 | 385 | } |
341 | 386 |
|
@@ -591,4 +636,18 @@ mod tests { |
591 | 636 | Format::Geoparquet(Some(Compression::LZO)) |
592 | 637 | ); |
593 | 638 | } |
| 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 | + } |
594 | 653 | } |
0 commit comments