623 lines
16 KiB
Rust
623 lines
16 KiB
Rust
use std::{
|
|
borrow::Cow,
|
|
fmt::{Display, Formatter, Write},
|
|
};
|
|
|
|
use serde::{
|
|
ser::{SerializeMap, SerializeSeq},
|
|
Serialize,
|
|
};
|
|
|
|
use crate::{util::*, ErrorKind, InstanceLocation, ValidationError};
|
|
|
|
impl<'s> ValidationError<'s, '_> {
|
|
fn absolute_keyword_location(&self) -> AbsoluteKeywordLocation<'s> {
|
|
if let ErrorKind::Reference { url, .. } = &self.kind {
|
|
AbsoluteKeywordLocation {
|
|
schema_url: url,
|
|
keyword_path: None,
|
|
}
|
|
} else {
|
|
AbsoluteKeywordLocation {
|
|
schema_url: self.schema_url,
|
|
keyword_path: self.kind.keyword_path(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn skip(&self) -> bool {
|
|
self.causes.len() == 1 && matches!(self.kind, ErrorKind::Reference { .. })
|
|
}
|
|
|
|
/// The `Flag` output format, merely the boolean result.
|
|
pub fn flag_output(&self) -> FlagOutput {
|
|
FlagOutput { valid: false }
|
|
}
|
|
|
|
/// The `Basic` structure, a flat list of output units.
|
|
pub fn basic_output(&self) -> OutputUnit<'_, '_, '_> {
|
|
let mut outputs = vec![];
|
|
|
|
let mut in_ref = InRef::default();
|
|
let mut kw_loc = KeywordLocation::default();
|
|
for node in DfsIterator::new(self) {
|
|
match node {
|
|
DfsItem::Pre(e) => {
|
|
in_ref.pre(e);
|
|
kw_loc.pre(e);
|
|
if e.skip() || matches!(e.kind, ErrorKind::Schema { .. }) {
|
|
continue;
|
|
}
|
|
let absolute_keyword_location = if in_ref.get() {
|
|
Some(e.absolute_keyword_location())
|
|
} else {
|
|
None
|
|
};
|
|
outputs.push(OutputUnit {
|
|
valid: false,
|
|
keyword_location: kw_loc.get(e),
|
|
absolute_keyword_location,
|
|
instance_location: &e.instance_location,
|
|
error: OutputError::Leaf(&e.kind),
|
|
});
|
|
}
|
|
DfsItem::Post(e) => {
|
|
in_ref.post();
|
|
kw_loc.post();
|
|
if e.skip() || matches!(e.kind, ErrorKind::Schema { .. }) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let error = if outputs.is_empty() {
|
|
OutputError::Leaf(&self.kind)
|
|
} else {
|
|
OutputError::Branch(outputs)
|
|
};
|
|
OutputUnit {
|
|
valid: false,
|
|
keyword_location: String::new(),
|
|
absolute_keyword_location: None,
|
|
instance_location: &self.instance_location,
|
|
error,
|
|
}
|
|
}
|
|
|
|
/// The `Detailed` structure, based on the schema.
|
|
pub fn detailed_output(&self) -> OutputUnit<'_, '_, '_> {
|
|
let mut root = None;
|
|
let mut stack: Vec<OutputUnit> = vec![];
|
|
|
|
let mut in_ref = InRef::default();
|
|
let mut kw_loc = KeywordLocation::default();
|
|
for node in DfsIterator::new(self) {
|
|
match node {
|
|
DfsItem::Pre(e) => {
|
|
in_ref.pre(e);
|
|
kw_loc.pre(e);
|
|
if e.skip() {
|
|
continue;
|
|
}
|
|
let absolute_keyword_location = if in_ref.get() {
|
|
Some(e.absolute_keyword_location())
|
|
} else {
|
|
None
|
|
};
|
|
stack.push(OutputUnit {
|
|
valid: false,
|
|
keyword_location: kw_loc.get(e),
|
|
absolute_keyword_location,
|
|
instance_location: &e.instance_location,
|
|
error: OutputError::Leaf(&e.kind),
|
|
});
|
|
}
|
|
DfsItem::Post(e) => {
|
|
in_ref.post();
|
|
kw_loc.post();
|
|
if e.skip() {
|
|
continue;
|
|
}
|
|
let output = stack.pop().unwrap();
|
|
if let Some(parent) = stack.last_mut() {
|
|
match &mut parent.error {
|
|
OutputError::Leaf(_) => {
|
|
parent.error = OutputError::Branch(vec![output]);
|
|
}
|
|
OutputError::Branch(v) => v.push(output),
|
|
}
|
|
} else {
|
|
root.replace(output);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
root.unwrap()
|
|
}
|
|
}
|
|
|
|
// DfsIterator --
|
|
|
|
impl Display for ValidationError<'_, '_> {
|
|
/// Formats error hierarchy. Use `#` to show the schema location.
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let mut indent = Indent::default();
|
|
let mut sloc = SchemaLocation::default();
|
|
// let mut kw_loc = KeywordLocation::default();
|
|
for node in DfsIterator::new(self) {
|
|
match node {
|
|
DfsItem::Pre(e) => {
|
|
// kw_loc.pre(e);
|
|
if e.skip() {
|
|
continue;
|
|
}
|
|
indent.pre(f)?;
|
|
if f.alternate() {
|
|
sloc.pre(e);
|
|
}
|
|
if let ErrorKind::Schema { .. } = &e.kind {
|
|
write!(f, "jsonschema {}", e.kind)?;
|
|
} else {
|
|
write!(f, "at {}", quote(&e.instance_location.to_string()))?;
|
|
if f.alternate() {
|
|
write!(f, " [{}]", sloc)?;
|
|
// write!(f, " [{}]", kw_loc.get(e))?;
|
|
// write!(f, " [{}]", e.absolute_keyword_location())?;
|
|
}
|
|
write!(f, ": {}", e.kind)?;
|
|
}
|
|
}
|
|
DfsItem::Post(e) => {
|
|
// kw_loc.post();
|
|
if e.skip() {
|
|
continue;
|
|
}
|
|
indent.post();
|
|
sloc.post();
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct DfsIterator<'a, 'v, 's> {
|
|
root: Option<&'a ValidationError<'v, 's>>,
|
|
stack: Vec<Frame<'a, 'v, 's>>,
|
|
}
|
|
|
|
impl<'a, 'v, 's> DfsIterator<'a, 'v, 's> {
|
|
fn new(err: &'a ValidationError<'v, 's>) -> Self {
|
|
DfsIterator {
|
|
root: Some(err),
|
|
stack: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, 'v, 's> Iterator for DfsIterator<'a, 'v, 's> {
|
|
type Item = DfsItem<&'a ValidationError<'v, 's>>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let Some(mut frame) = self.stack.pop() else {
|
|
if let Some(err) = self.root.take() {
|
|
self.stack.push(Frame::from(err));
|
|
return Some(DfsItem::Pre(err));
|
|
} else {
|
|
return None;
|
|
}
|
|
};
|
|
|
|
if frame.causes.is_empty() {
|
|
return Some(DfsItem::Post(frame.err));
|
|
}
|
|
|
|
let err = &frame.causes[0];
|
|
frame.causes = &frame.causes[1..];
|
|
self.stack.push(frame);
|
|
self.stack.push(Frame::from(err));
|
|
Some(DfsItem::Pre(err))
|
|
}
|
|
}
|
|
|
|
struct Frame<'a, 'v, 's> {
|
|
err: &'a ValidationError<'v, 's>,
|
|
causes: &'a [ValidationError<'v, 's>],
|
|
}
|
|
|
|
impl<'a, 'v, 's> Frame<'a, 'v, 's> {
|
|
fn from(err: &'a ValidationError<'v, 's>) -> Self {
|
|
Self {
|
|
err,
|
|
causes: &err.causes,
|
|
}
|
|
}
|
|
}
|
|
|
|
enum DfsItem<T> {
|
|
Pre(T),
|
|
Post(T),
|
|
}
|
|
|
|
// Indent --
|
|
|
|
#[derive(Default)]
|
|
struct Indent {
|
|
n: usize,
|
|
}
|
|
|
|
impl Indent {
|
|
fn pre(&mut self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
if self.n > 0 {
|
|
writeln!(f)?;
|
|
for _ in 0..self.n - 1 {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "- ")?;
|
|
}
|
|
self.n += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn post(&mut self) {
|
|
self.n -= 1;
|
|
}
|
|
}
|
|
|
|
// SchemaLocation
|
|
|
|
#[derive(Default)]
|
|
struct SchemaLocation<'a, 's, 'v> {
|
|
stack: Vec<&'a ValidationError<'s, 'v>>,
|
|
}
|
|
|
|
impl<'a, 's, 'v> SchemaLocation<'a, 's, 'v> {
|
|
fn pre(&mut self, e: &'a ValidationError<'s, 'v>) {
|
|
self.stack.push(e);
|
|
}
|
|
|
|
fn post(&mut self) {
|
|
self.stack.pop();
|
|
}
|
|
}
|
|
|
|
impl Display for SchemaLocation<'_, '_, '_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
let mut iter = self.stack.iter().cloned();
|
|
let cur = iter.next_back().unwrap();
|
|
let cur: Cow<str> = match &cur.kind {
|
|
ErrorKind::Schema { url } => Cow::Borrowed(url),
|
|
ErrorKind::Reference { url, .. } => Cow::Borrowed(url),
|
|
_ => Cow::Owned(cur.absolute_keyword_location().to_string()),
|
|
};
|
|
|
|
let Some(prev) = iter.next_back() else {
|
|
return write!(f, "{cur}");
|
|
};
|
|
|
|
let p = match &prev.kind {
|
|
ErrorKind::Schema { url } => {
|
|
let (p, _) = split(url);
|
|
p
|
|
}
|
|
ErrorKind::Reference { url, .. } => {
|
|
let (p, _) = split(url);
|
|
p
|
|
}
|
|
_ => {
|
|
let (p, _) = split(prev.schema_url);
|
|
p
|
|
}
|
|
};
|
|
let (c, frag) = split(cur.as_ref());
|
|
if c == p {
|
|
write!(f, "S#{frag}")
|
|
} else {
|
|
write!(f, "{cur}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// KeywordLocation --
|
|
|
|
#[derive(Default)]
|
|
struct KeywordLocation<'a> {
|
|
loc: String,
|
|
stack: Vec<(&'a str, usize)>, // (schema_url, len)
|
|
}
|
|
|
|
impl<'a> KeywordLocation<'a> {
|
|
fn pre(&mut self, e: &'a ValidationError) {
|
|
let cur = match &e.kind {
|
|
ErrorKind::Schema { url } => url,
|
|
ErrorKind::Reference { url, .. } => url,
|
|
_ => e.schema_url,
|
|
};
|
|
|
|
if let Some((prev, _)) = self.stack.last() {
|
|
self.loc.push_str(&e.schema_url[prev.len()..]); // todo: url-decode
|
|
if let ErrorKind::Reference { kw, .. } = &e.kind {
|
|
self.loc.push('/');
|
|
self.loc.push_str(kw);
|
|
}
|
|
}
|
|
self.stack.push((cur, self.loc.len()));
|
|
}
|
|
|
|
fn post(&mut self) {
|
|
self.stack.pop();
|
|
if let Some((_, len)) = self.stack.last() {
|
|
self.loc.truncate(*len);
|
|
}
|
|
}
|
|
|
|
fn get(&mut self, cur: &'a ValidationError) -> String {
|
|
if let ErrorKind::Reference { .. } = &cur.kind {
|
|
self.loc.clone()
|
|
} else if let Some(kw_path) = &cur.kind.keyword_path() {
|
|
let len = self.loc.len();
|
|
self.loc.push('/');
|
|
write!(self.loc, "{}", kw_path).expect("write kw_path to String should not fail");
|
|
let loc = self.loc.clone();
|
|
self.loc.truncate(len);
|
|
loc
|
|
} else {
|
|
self.loc.clone()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct InRef {
|
|
stack: Vec<bool>,
|
|
}
|
|
|
|
impl InRef {
|
|
fn pre(&mut self, e: &ValidationError) {
|
|
let in_ref: bool = self.get() || matches!(e.kind, ErrorKind::Reference { .. });
|
|
self.stack.push(in_ref);
|
|
}
|
|
|
|
fn post(&mut self) {
|
|
self.stack.pop();
|
|
}
|
|
|
|
fn get(&self) -> bool {
|
|
self.stack.last().cloned().unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
// output formats --
|
|
|
|
/// Simplest output format, merely the boolean result.
|
|
pub struct FlagOutput {
|
|
pub valid: bool,
|
|
}
|
|
|
|
impl Serialize for FlagOutput {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut map = serializer.serialize_map(Some(1))?;
|
|
map.serialize_entry("valid", &self.valid)?;
|
|
map.end()
|
|
}
|
|
}
|
|
|
|
impl Display for FlagOutput {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write_json_to_fmt(f, self)
|
|
}
|
|
}
|
|
|
|
/// Single OutputUnit used in Basic/Detailed output formats.
|
|
pub struct OutputUnit<'e, 's, 'v> {
|
|
pub valid: bool,
|
|
pub keyword_location: String,
|
|
/// The absolute, dereferenced location of the validating keyword
|
|
pub absolute_keyword_location: Option<AbsoluteKeywordLocation<'s>>,
|
|
/// The location of the JSON value within the instance being validated
|
|
pub instance_location: &'e InstanceLocation<'v>,
|
|
pub error: OutputError<'e, 's, 'v>,
|
|
}
|
|
|
|
impl Serialize for OutputUnit<'_, '_, '_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let n = 4 + self.absolute_keyword_location.as_ref().map_or(0, |_| 1);
|
|
let mut map = serializer.serialize_map(Some(n))?;
|
|
map.serialize_entry("valid", &self.valid)?;
|
|
map.serialize_entry("keywordLocation", &self.keyword_location.to_string())?;
|
|
if let Some(s) = &self.absolute_keyword_location {
|
|
map.serialize_entry("absoluteKeywordLocation", &s.to_string())?;
|
|
}
|
|
map.serialize_entry("instanceLocation", &self.instance_location.to_string())?;
|
|
let pname = match self.error {
|
|
OutputError::Leaf(_) => "error",
|
|
OutputError::Branch(_) => "errors",
|
|
};
|
|
map.serialize_entry(pname, &self.error)?;
|
|
map.end()
|
|
}
|
|
}
|
|
|
|
impl Display for OutputUnit<'_, '_, '_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write_json_to_fmt(f, self)
|
|
}
|
|
}
|
|
|
|
/// Error of [`OutputUnit`].
|
|
pub enum OutputError<'e, 's, 'v> {
|
|
/// Single.
|
|
Leaf(&'e ErrorKind<'s, 'v>),
|
|
/// Nested.
|
|
Branch(Vec<OutputUnit<'e, 's, 'v>>),
|
|
}
|
|
|
|
impl Serialize for OutputError<'_, '_, '_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
match self {
|
|
OutputError::Leaf(kind) => serializer.serialize_str(&kind.to_string()),
|
|
OutputError::Branch(units) => {
|
|
let mut seq = serializer.serialize_seq(Some(units.len()))?;
|
|
for unit in units {
|
|
seq.serialize_element(unit)?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AbsoluteKeywordLocation --
|
|
|
|
impl<'s> ErrorKind<'s, '_> {
|
|
pub fn keyword_path(&self) -> Option<KeywordPath<'s>> {
|
|
#[inline(always)]
|
|
fn kw(kw: &'static str) -> Option<KeywordPath<'static>> {
|
|
Some(KeywordPath {
|
|
keyword: kw,
|
|
token: None,
|
|
})
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn kw_prop<'s>(kw: &'static str, prop: &'s str) -> Option<KeywordPath<'s>> {
|
|
Some(KeywordPath {
|
|
keyword: kw,
|
|
token: Some(SchemaToken::Prop(prop)),
|
|
})
|
|
}
|
|
|
|
use ErrorKind::*;
|
|
match self {
|
|
Group => None,
|
|
Schema { .. } => None,
|
|
ContentSchema => kw("contentSchema"),
|
|
PropertyName { .. } => kw("propertyNames"),
|
|
Reference { kw: kword, .. } => kw(kword),
|
|
RefCycle { .. } => None,
|
|
FalseSchema => None,
|
|
Type { .. } => kw("type"),
|
|
Enum { .. } => kw("enum"),
|
|
Const { .. } => kw("const"),
|
|
Format { .. } => kw("format"),
|
|
MinProperties { .. } => kw("minProperties"),
|
|
MaxProperties { .. } => kw("maxProperties"),
|
|
AdditionalProperties { .. } => kw("additionalProperty"),
|
|
Required { .. } => kw("required"),
|
|
Dependency { prop, .. } => kw_prop("dependencies", prop),
|
|
DependentRequired { prop, .. } => kw_prop("dependentRequired", prop),
|
|
MinItems { .. } => kw("minItems"),
|
|
MaxItems { .. } => kw("maxItems"),
|
|
Contains => kw("contains"),
|
|
MinContains { .. } => kw("minContains"),
|
|
MaxContains { .. } => kw("maxContains"),
|
|
UniqueItems { .. } => kw("uniqueItems"),
|
|
AdditionalItems { .. } => kw("additionalItems"),
|
|
MinLength { .. } => kw("minLength"),
|
|
MaxLength { .. } => kw("maxLength"),
|
|
Pattern { .. } => kw("pattern"),
|
|
ContentEncoding { .. } => kw("contentEncoding"),
|
|
ContentMediaType { .. } => kw("contentMediaType"),
|
|
Minimum { .. } => kw("minimum"),
|
|
Maximum { .. } => kw("maximum"),
|
|
ExclusiveMinimum { .. } => kw("exclusiveMinimum"),
|
|
ExclusiveMaximum { .. } => kw("exclusiveMaximum"),
|
|
MultipleOf { .. } => kw("multipleOf"),
|
|
Not => kw("not"),
|
|
AllOf => kw("allOf"),
|
|
AnyOf => kw("anyOf"),
|
|
OneOf(_) => kw("oneOf"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The absolute, dereferenced location of the validating keyword
|
|
#[derive(Debug, Clone)]
|
|
pub struct AbsoluteKeywordLocation<'s> {
|
|
/// The absolute, dereferenced schema location.
|
|
pub schema_url: &'s str,
|
|
/// Location within the `schema_url`.
|
|
pub keyword_path: Option<KeywordPath<'s>>,
|
|
}
|
|
|
|
impl Display for AbsoluteKeywordLocation<'_> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.schema_url.fmt(f)?;
|
|
if let Some(path) = &self.keyword_path {
|
|
f.write_str("/")?;
|
|
path.keyword.fmt(f)?;
|
|
if let Some(token) = &path.token {
|
|
f.write_str("/")?;
|
|
match token {
|
|
SchemaToken::Prop(p) => write!(f, "{}", escape(p))?, // todo: url-encode
|
|
SchemaToken::Item(i) => write!(f, "{i}")?,
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
/// JsonPointer in schema.
|
|
pub struct KeywordPath<'s> {
|
|
/// The first token.
|
|
pub keyword: &'static str,
|
|
/// Optinal token within keyword.
|
|
pub token: Option<SchemaToken<'s>>,
|
|
}
|
|
|
|
impl Display for KeywordPath<'_> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.keyword.fmt(f)?;
|
|
if let Some(token) = &self.token {
|
|
f.write_str("/")?;
|
|
token.fmt(f)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Token for schema.
|
|
#[derive(Debug, Clone)]
|
|
pub enum SchemaToken<'s> {
|
|
/// Token for property.
|
|
Prop(&'s str),
|
|
/// Token for array item.
|
|
Item(usize),
|
|
}
|
|
|
|
impl Display for SchemaToken<'_> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
SchemaToken::Prop(p) => write!(f, "{}", escape(p)),
|
|
SchemaToken::Item(i) => write!(f, "{i}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// helpers --
|
|
|
|
fn write_json_to_fmt<T>(f: &mut std::fmt::Formatter, value: &T) -> Result<(), std::fmt::Error>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
let s = if f.alternate() {
|
|
serde_json::to_string_pretty(value)
|
|
} else {
|
|
serde_json::to_string(value)
|
|
};
|
|
let s = s.map_err(|_| std::fmt::Error)?;
|
|
f.write_str(&s)
|
|
}
|