403 lines
13 KiB
Rust
403 lines
13 KiB
Rust
//! Implementation of json schema output formats specified in <https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.12.2>
|
|
//!
|
|
//! Currently the "flag" and "basic" formats are supported. The "flag" format is
|
|
//! idential to the [`JSONSchema::is_valid`] method and so is uninteresting. The
|
|
//! main contribution of this module is [`Output::basic`]. See the documentation
|
|
//! of that method for more information.
|
|
|
|
use std::{
|
|
borrow::Cow,
|
|
collections::VecDeque,
|
|
fmt,
|
|
iter::{FromIterator, Sum},
|
|
ops::AddAssign,
|
|
};
|
|
|
|
use crate::{validator::PartialApplication, ValidationError};
|
|
use ahash::AHashMap;
|
|
use serde::ser::SerializeMap;
|
|
|
|
use crate::{
|
|
paths::{AbsolutePath, InstancePath, JSONPointer},
|
|
schema_node::SchemaNode,
|
|
JSONSchema,
|
|
};
|
|
|
|
/// The output format resulting from the application of a schema. This can be
|
|
/// converted into various representations based on the definitions in
|
|
/// <https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.12.2>
|
|
///
|
|
/// Currently only the "flag" and "basic" output formats are supported
|
|
#[derive(Debug, Clone)]
|
|
pub struct Output<'a, 'b> {
|
|
schema: &'a JSONSchema,
|
|
root_node: &'a SchemaNode,
|
|
instance: &'b serde_json::Value,
|
|
}
|
|
|
|
impl<'a, 'b> Output<'a, 'b> {
|
|
pub(crate) const fn new<'c, 'd>(
|
|
schema: &'c JSONSchema,
|
|
root_node: &'c SchemaNode,
|
|
instance: &'d serde_json::Value,
|
|
) -> Output<'c, 'd> {
|
|
Output {
|
|
schema,
|
|
root_node,
|
|
instance,
|
|
}
|
|
}
|
|
|
|
/// Indicates whether the schema was valid, corresponds to the "flag" output
|
|
/// format
|
|
#[must_use]
|
|
pub fn flag(&self) -> bool {
|
|
self.schema.is_valid(self.instance)
|
|
}
|
|
|
|
/// Output a list of errors and annotations for each element in the schema
|
|
/// according to the basic output format. [`BasicOutput`] implements
|
|
/// `serde::Serialize` in a manner which conforms to the json core spec so
|
|
/// one way to use this is to serialize the `BasicOutput` and examine the
|
|
/// JSON which is produced. However, for rust programs this is not
|
|
/// necessary. Instead you can match on the `BasicOutput` and examine the
|
|
/// results. To use this API you'll need to understand a few things:
|
|
///
|
|
/// Regardless of whether the the schema validation was successful or not
|
|
/// the `BasicOutput` is a sequence of [`OutputUnit`]s. An `OutputUnit` is
|
|
/// some metadata about where the output is coming from (where in the schema
|
|
/// and where in the instance). The difference between the
|
|
/// `BasicOutput::Valid` and `BasicOutput::Invalid` cases is the value which
|
|
/// is associated with each `OutputUnit`. For `Valid` outputs the value is
|
|
/// an annotation, whilst for `Invalid` outputs it's an `ErrorDescription`
|
|
/// (a `String` really).
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use crate::jsonschema::{Draft, output::{Output, BasicOutput}, JSONSchema};
|
|
/// # let schema_json = serde_json::json!({
|
|
/// # "title": "string value",
|
|
/// # "type": "string"
|
|
/// # });
|
|
/// # let instance = serde_json::json!{"some string"};
|
|
/// # let schema = JSONSchema::options().compile(&schema_json).unwrap();
|
|
/// let output: BasicOutput = schema.apply(&instance).basic();
|
|
/// match output {
|
|
/// BasicOutput::Valid(annotations) => {
|
|
/// for annotation in annotations {
|
|
/// println!(
|
|
/// "Value: {} at path {}",
|
|
/// annotation.value(),
|
|
/// annotation.instance_location()
|
|
/// )
|
|
/// }
|
|
/// },
|
|
/// BasicOutput::Invalid(errors) => {
|
|
/// for error in errors {
|
|
/// println!(
|
|
/// "Error: {} at path {}",
|
|
/// error.error_description(),
|
|
/// error.instance_location()
|
|
/// )
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[must_use]
|
|
pub fn basic(&self) -> BasicOutput<'a> {
|
|
self.root_node
|
|
.apply_rooted(self.instance, &InstancePath::new())
|
|
}
|
|
}
|
|
|
|
/// The "basic" output format. See the documentation for [`Output::basic`] for
|
|
/// examples of how to use this.
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum BasicOutput<'a> {
|
|
/// The schema was valid, collected annotations can be examined
|
|
Valid(VecDeque<OutputUnit<Annotations<'a>>>),
|
|
/// The schema was invalid
|
|
Invalid(VecDeque<OutputUnit<ErrorDescription>>),
|
|
}
|
|
|
|
impl<'a> BasicOutput<'a> {
|
|
/// A shortcut to check whether the output represents passed validation.
|
|
#[must_use]
|
|
pub const fn is_valid(&self) -> bool {
|
|
match self {
|
|
BasicOutput::Valid(..) => true,
|
|
BasicOutput::Invalid(..) => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<OutputUnit<Annotations<'a>>> for BasicOutput<'a> {
|
|
fn from(unit: OutputUnit<Annotations<'a>>) -> Self {
|
|
let mut units = VecDeque::new();
|
|
units.push_front(unit);
|
|
BasicOutput::Valid(units)
|
|
}
|
|
}
|
|
|
|
impl<'a> AddAssign for BasicOutput<'a> {
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
match (&mut *self, rhs) {
|
|
(BasicOutput::Valid(ref mut anns), BasicOutput::Valid(anns_rhs)) => {
|
|
anns.extend(anns_rhs);
|
|
}
|
|
(BasicOutput::Valid(..), BasicOutput::Invalid(errors)) => {
|
|
*self = BasicOutput::Invalid(errors)
|
|
}
|
|
(BasicOutput::Invalid(..), BasicOutput::Valid(..)) => {}
|
|
(BasicOutput::Invalid(errors), BasicOutput::Invalid(errors_rhs)) => {
|
|
errors.extend(errors_rhs)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Sum for BasicOutput<'a> {
|
|
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
|
let result = BasicOutput::Valid(VecDeque::new());
|
|
iter.fold(result, |mut acc, elem| {
|
|
acc += elem;
|
|
acc
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'a> Default for BasicOutput<'a> {
|
|
fn default() -> Self {
|
|
BasicOutput::Valid(VecDeque::new())
|
|
}
|
|
}
|
|
|
|
impl<'a> From<BasicOutput<'a>> for PartialApplication<'a> {
|
|
fn from(output: BasicOutput<'a>) -> Self {
|
|
match output {
|
|
BasicOutput::Valid(anns) => PartialApplication::Valid {
|
|
annotations: None,
|
|
child_results: anns,
|
|
},
|
|
BasicOutput::Invalid(errors) => PartialApplication::Invalid {
|
|
errors: Vec::new(),
|
|
child_results: errors,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> FromIterator<BasicOutput<'a>> for PartialApplication<'a> {
|
|
fn from_iter<T: IntoIterator<Item = BasicOutput<'a>>>(iter: T) -> Self {
|
|
iter.into_iter().sum::<BasicOutput<'_>>().into()
|
|
}
|
|
}
|
|
|
|
/// An output unit is a reference to a place in a schema and a place in an
|
|
/// instance along with some value associated to that place. For annotations the
|
|
/// value will be an [`Annotations`] and for errors it will be an
|
|
/// [`ErrorDescription`]. See the documentation for [`Output::basic`] for a
|
|
/// detailed example.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct OutputUnit<T> {
|
|
keyword_location: JSONPointer,
|
|
instance_location: JSONPointer,
|
|
absolute_keyword_location: Option<AbsolutePath>,
|
|
value: T,
|
|
}
|
|
|
|
impl<T> OutputUnit<T> {
|
|
pub(crate) const fn annotations(
|
|
keyword_location: JSONPointer,
|
|
instance_location: JSONPointer,
|
|
absolute_keyword_location: Option<AbsolutePath>,
|
|
annotations: Annotations<'_>,
|
|
) -> OutputUnit<Annotations<'_>> {
|
|
OutputUnit {
|
|
keyword_location,
|
|
instance_location,
|
|
absolute_keyword_location,
|
|
value: annotations,
|
|
}
|
|
}
|
|
|
|
pub(crate) const fn error(
|
|
keyword_location: JSONPointer,
|
|
instance_location: JSONPointer,
|
|
absolute_keyword_location: Option<AbsolutePath>,
|
|
error: ErrorDescription,
|
|
) -> OutputUnit<ErrorDescription> {
|
|
OutputUnit {
|
|
keyword_location,
|
|
instance_location,
|
|
absolute_keyword_location,
|
|
value: error,
|
|
}
|
|
}
|
|
|
|
/// The location in the schema of the keyword
|
|
pub const fn keyword_location(&self) -> &JSONPointer {
|
|
&self.keyword_location
|
|
}
|
|
|
|
/// The absolute location in the schema of the keyword. This will be
|
|
/// different to `keyword_location` if the schema is a resolved reference.
|
|
pub const fn absolute_keyword_location(&self) -> &Option<AbsolutePath> {
|
|
&self.absolute_keyword_location
|
|
}
|
|
|
|
/// The location in the instance
|
|
pub const fn instance_location(&self) -> &JSONPointer {
|
|
&self.instance_location
|
|
}
|
|
}
|
|
|
|
impl OutputUnit<Annotations<'_>> {
|
|
/// The annotations found at this output unit
|
|
#[must_use]
|
|
pub fn value(&self) -> Cow<'_, serde_json::Value> {
|
|
self.value.value()
|
|
}
|
|
}
|
|
|
|
impl OutputUnit<ErrorDescription> {
|
|
/// The error for this output unit
|
|
#[must_use]
|
|
pub const fn error_description(&self) -> &ErrorDescription {
|
|
&self.value
|
|
}
|
|
}
|
|
|
|
/// Annotations associated with an output unit.
|
|
#[derive(serde::Serialize, Debug, Clone, PartialEq)]
|
|
pub struct Annotations<'a>(AnnotationsInner<'a>);
|
|
|
|
impl<'a> Annotations<'a> {
|
|
/// The `serde_json::Value` of the annotation
|
|
#[must_use]
|
|
pub fn value(&'a self) -> Cow<'a, serde_json::Value> {
|
|
match &self.0 {
|
|
AnnotationsInner::Value(v) => Cow::Borrowed(v),
|
|
AnnotationsInner::ValueRef(v) => Cow::Borrowed(v),
|
|
AnnotationsInner::UnmatchedKeywords(kvs) => {
|
|
let value = serde_json::to_value(kvs)
|
|
.expect("&AHashMap<String, serde_json::Value> cannot fail serializing");
|
|
Cow::Owned(value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
enum AnnotationsInner<'a> {
|
|
UnmatchedKeywords(&'a AHashMap<String, serde_json::Value>),
|
|
ValueRef(&'a serde_json::Value),
|
|
Value(Box<serde_json::Value>),
|
|
}
|
|
|
|
impl<'a> From<&'a AHashMap<String, serde_json::Value>> for Annotations<'a> {
|
|
fn from(anns: &'a AHashMap<String, serde_json::Value>) -> Self {
|
|
Annotations(AnnotationsInner::UnmatchedKeywords(anns))
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a serde_json::Value> for Annotations<'a> {
|
|
fn from(v: &'a serde_json::Value) -> Self {
|
|
Annotations(AnnotationsInner::ValueRef(v))
|
|
}
|
|
}
|
|
|
|
impl<'a> From<serde_json::Value> for Annotations<'a> {
|
|
fn from(v: serde_json::Value) -> Self {
|
|
Annotations(AnnotationsInner::Value(Box::new(v)))
|
|
}
|
|
}
|
|
|
|
/// An error associated with an `OutputUnit`
|
|
#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq)]
|
|
pub struct ErrorDescription(String);
|
|
|
|
impl fmt::Display for ErrorDescription {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(&self.0)
|
|
}
|
|
}
|
|
|
|
impl From<ValidationError<'_>> for ErrorDescription {
|
|
fn from(e: ValidationError<'_>) -> Self {
|
|
ErrorDescription(e.to_string())
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a str> for ErrorDescription {
|
|
fn from(s: &'a str) -> Self {
|
|
ErrorDescription(s.to_string())
|
|
}
|
|
}
|
|
|
|
impl<'a> serde::Serialize for BasicOutput<'a> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut map_ser = serializer.serialize_map(Some(2))?;
|
|
match self {
|
|
BasicOutput::Valid(outputs) => {
|
|
map_ser.serialize_entry("valid", &true)?;
|
|
map_ser.serialize_entry("annotations", outputs)?;
|
|
}
|
|
BasicOutput::Invalid(errors) => {
|
|
map_ser.serialize_entry("valid", &false)?;
|
|
map_ser.serialize_entry("errors", errors)?;
|
|
}
|
|
}
|
|
map_ser.end()
|
|
}
|
|
}
|
|
|
|
impl<'a> serde::Serialize for AnnotationsInner<'a> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
match self {
|
|
Self::UnmatchedKeywords(kvs) => kvs.serialize(serializer),
|
|
Self::Value(v) => v.serialize(serializer),
|
|
Self::ValueRef(v) => v.serialize(serializer),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> serde::Serialize for OutputUnit<Annotations<'a>> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut map_ser = serializer.serialize_map(Some(4))?;
|
|
map_ser.serialize_entry("keywordLocation", &self.keyword_location)?;
|
|
map_ser.serialize_entry("instanceLocation", &self.instance_location)?;
|
|
if let Some(absolute) = &self.absolute_keyword_location {
|
|
map_ser.serialize_entry("absoluteKeywordLocation", &absolute)?;
|
|
}
|
|
map_ser.serialize_entry("annotations", &self.value)?;
|
|
map_ser.end()
|
|
}
|
|
}
|
|
|
|
impl serde::Serialize for OutputUnit<ErrorDescription> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut map_ser = serializer.serialize_map(Some(4))?;
|
|
map_ser.serialize_entry("keywordLocation", &self.keyword_location)?;
|
|
map_ser.serialize_entry("instanceLocation", &self.instance_location)?;
|
|
if let Some(absolute) = &self.absolute_keyword_location {
|
|
map_ser.serialize_entry("absoluteKeywordLocation", &absolute)?;
|
|
}
|
|
map_ser.serialize_entry("error", &self.value)?;
|
|
map_ser.end()
|
|
}
|
|
}
|