Skip to content

Commit 7002a5d

Browse files
committed
feat!(core): better validation messages
1 parent afdd30c commit 7002a5d

File tree

5 files changed

+96
-50
lines changed

5 files changed

+96
-50
lines changed

crates/cli/src/input.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use crate::{options::Options, Error, Result};
22
use stac::{Format, Value};
3-
use std::io::Read;
3+
use std::{
4+
fs::File,
5+
io::{BufReader, Read},
6+
};
47
use url::Url;
58

69
/// The input to a CLI run.
@@ -76,8 +79,16 @@ impl Input {
7679
}
7780
}
7881
} else {
79-
let value: Value = format.from_path(href)?;
80-
serde_json::to_value(value).map_err(Error::from)
82+
match format {
83+
Format::Json(..) => {
84+
let file = BufReader::new(File::open(href)?);
85+
serde_json::from_reader(file).map_err(Error::from)
86+
}
87+
_ => {
88+
let value: Value = format.from_path(href)?;
89+
serde_json::to_value(value).map_err(Error::from)
90+
}
91+
}
8192
}
8293
} else {
8394
let mut buf = Vec::new();

crates/cli/src/subcommand/validate.rs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,8 @@ impl Run for Args {
1717
let value = input.get_json().await?;
1818
let result = value.validate().await;
1919
if let Err(stac::Error::Validation(ref errors)) = result {
20-
let id = value
21-
.get("id")
22-
.and_then(|v| v.as_str())
23-
.map(|v| format!("={}", v))
24-
.unwrap_or_default();
25-
let message_base = match value
26-
.get("type")
27-
.and_then(|v| v.as_str())
28-
.unwrap_or_default()
29-
{
30-
"Feature" => format!("[item={}] ", id),
31-
"Catalog" => format!("[catalog={}] ", id),
32-
"Collection" => format!("[collection={}] ", id),
33-
"FeatureCollection" => "[item-collection] ".to_string(),
34-
_ => String::new(),
35-
};
3620
for error in errors {
37-
eprintln!(
38-
"{}{} (instance path: '{}', schema path: '{}')",
39-
message_base, error, error.instance_path, error.schema_path
40-
);
21+
eprintln!("{error}");
4122
}
4223
}
4324
result.and(Ok(None)).map_err(Error::from)

crates/core/src/error.rs

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
use crate::Version;
2-
#[cfg(feature = "validate")]
3-
use jsonschema::ValidationError;
42
use thiserror::Error;
53

64
/// Error enum for crate-specific errors.
@@ -135,33 +133,84 @@ pub enum Error {
135133
Url(#[from] url::ParseError),
136134

137135
/// A list of validation errors.
138-
///
139-
/// Since we usually don't have the original [serde_json::Value] (because we
140-
/// create them from the STAC objects), we need these errors to be `'static`
141-
/// lifetime.
142-
#[error("validation errors")]
136+
#[error("{} validation error(s)", .0.len())]
137+
#[cfg(feature = "validate")]
138+
Validation(Vec<Validation>),
139+
140+
/// [jsonschema::ValidationError]
143141
#[cfg(feature = "validate")]
144-
Validation(Vec<ValidationError<'static>>),
142+
#[error(transparent)]
143+
JsonschemaValidation(#[from] jsonschema::ValidationError<'static>),
144+
}
145+
146+
/// A validation error
147+
#[cfg(feature = "validate")]
148+
#[derive(Debug)]
149+
pub struct Validation {
150+
/// The ID of the STAC object that failed to validate.
151+
id: Option<String>,
152+
153+
/// The type of the STAC object that failed to validate.
154+
r#type: Option<crate::Type>,
155+
156+
/// The validation error.
157+
error: jsonschema::ValidationError<'static>,
158+
}
159+
160+
#[cfg(feature = "validate")]
161+
impl Validation {
162+
pub(crate) fn new(
163+
error: jsonschema::ValidationError<'_>,
164+
value: Option<&serde_json::Value>,
165+
) -> Validation {
166+
use std::borrow::Cow;
167+
168+
// Cribbed from https://docs.rs/jsonschema/latest/src/jsonschema/error.rs.html#21-30
169+
let error = jsonschema::ValidationError {
170+
instance_path: error.instance_path.clone(),
171+
instance: Cow::Owned(error.instance.into_owned()),
172+
kind: error.kind,
173+
schema_path: error.schema_path,
174+
};
175+
let mut id = None;
176+
let mut r#type = None;
177+
if let Some(value) = value.and_then(|v| v.as_object()) {
178+
id = value.get("id").and_then(|v| v.as_str()).map(String::from);
179+
r#type = value
180+
.get("type")
181+
.and_then(|v| v.as_str())
182+
.and_then(|s| s.parse::<crate::Type>().ok());
183+
}
184+
Validation { id, r#type, error }
185+
}
145186
}
146187

147188
#[cfg(feature = "validate")]
148189
impl Error {
149-
pub(crate) fn from_validation_errors<'a, I>(errors: I) -> Error
190+
pub(crate) fn from_validation_errors<'a, I>(
191+
errors: I,
192+
value: Option<&serde_json::Value>,
193+
) -> Error
150194
where
151-
I: Iterator<Item = ValidationError<'a>>,
195+
I: Iterator<Item = jsonschema::ValidationError<'a>>,
152196
{
153-
use std::borrow::Cow;
197+
Error::Validation(errors.map(|error| Validation::new(error, value)).collect())
198+
}
199+
}
154200

155-
let mut error_vec = Vec::new();
156-
for error in errors {
157-
// Cribbed from https://docs.rs/jsonschema/latest/src/jsonschema/error.rs.html#21-30
158-
error_vec.push(ValidationError {
159-
instance_path: error.instance_path.clone(),
160-
instance: Cow::Owned(error.instance.into_owned()),
161-
kind: error.kind,
162-
schema_path: error.schema_path,
163-
})
201+
#[cfg(feature = "validate")]
202+
impl std::fmt::Display for Validation {
203+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204+
if let Some(r#type) = self.r#type {
205+
if let Some(id) = self.id.as_ref() {
206+
write!(f, "{}[id={id}]: {}", r#type, self.error)
207+
} else {
208+
write!(f, "{}: {}", r#type, self.error)
209+
}
210+
} else if let Some(id) = self.id.as_ref() {
211+
write!(f, "[id={id}]: {}", self.error)
212+
} else {
213+
write!(f, "{}", self.error)
164214
}
165-
Error::Validation(error_vec)
166215
}
167216
}

crates/core/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ mod validate;
178178
mod value;
179179
mod version;
180180

181+
use std::fmt::Display;
182+
181183
#[cfg(feature = "validate-blocking")]
182184
pub use validate::ValidateBlocking;
183185
#[cfg(feature = "validate")]
@@ -297,6 +299,12 @@ where
297299
}
298300
}
299301

302+
impl Display for Type {
303+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304+
write!(f, "{}", self.as_str())
305+
}
306+
}
307+
300308
/// Utility function to deserialize the type field on an object.
301309
///
302310
/// Use this, via a wrapper function, for `#[serde(deserialize_with)]`.

crates/core/src/validate/validator.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ impl Validator {
154154
let value = Arc::new(Value::Object(object));
155155
let mut result = schema
156156
.validate(&value)
157-
.map_err(Error::from_validation_errors);
157+
.map_err(|e| Error::from_validation_errors(e, Some(&value)));
158158
if result.is_ok() {
159159
result = validator.validate_extensions(value.clone()).await
160160
}
@@ -201,7 +201,7 @@ impl Validator {
201201
let extension_schema = validator.schema(extension).await?;
202202
extension_schema
203203
.validate(&value)
204-
.map_err(Error::from_validation_errors)
204+
.map_err(|errors| Error::from_validation_errors(errors, Some(&value)))
205205
});
206206
}
207207
}
@@ -232,10 +232,7 @@ impl Validator {
232232
let schema = {
233233
let cache = self.cache.read().unwrap();
234234
let value = cache.get(&uri).expect("we just resolved it");
235-
let schema = self
236-
.validation_options
237-
.build(value)
238-
.map_err(|err| Error::from_validation_errors([err].into_iter()))?;
235+
let schema = self.validation_options.build(value)?;
239236
Arc::new(schema)
240237
};
241238
{

0 commit comments

Comments
 (0)