pub mod decode;
mod parser;
pub(crate) mod schema;
pub(crate) mod walk;
pub mod write;
#[cfg(test)]
mod test_parser;
use std::{
borrow::Cow,
collections::BTreeMap,
fmt::{self, Write as _},
rc::Rc,
sync::Arc,
};
use serde::Serialize;
use tracing::{trace, Level};
use crate::{warning, Verdict};
use decode::unescape_str;
use parser::{Parser, Span};
pub use parser::{line_col, Error, ErrorKind, ErrorReport, LineCol};
pub(crate) use parser::{parse, RawStr};
const PATH_SEPARATOR: char = '.';
const PATH_ROOT: &str = "$";
pub(crate) trait FromJson<'elem, 'buf>: Sized {
type WarningKind: warning::Kind;
fn from_json(elem: &'elem Element<'buf>) -> Verdict<Self, Self::WarningKind>;
}
#[derive(Debug, Eq, PartialEq)]
pub struct Element<'buf> {
id: ElemId,
path_node: PathNodeRef<'buf>,
span: Span,
value: Value<'buf>,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub struct ElemId(usize);
impl fmt::Display for ElemId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl<'buf> Element<'buf> {
fn new(id: ElemId, path: PathNodeRef<'buf>, span: Span, value: Value<'buf>) -> Element<'buf> {
Element {
id,
path_node: path,
span,
value,
}
}
pub(crate) const fn id(&self) -> ElemId {
self.id
}
pub fn path(&self) -> PathRef<'buf> {
PathRef(self.path_node())
}
pub(crate) fn path_node(&self) -> PathNodeRef<'buf> {
Rc::clone(&self.path_node)
}
pub fn source_json(&self, source_json: &'buf str) -> SourceStr<'buf> {
if let PathNode::Object { key, .. } = *self.path_node {
let span = Span {
start: key.span().start,
end: self.span.end,
};
let field_str = &source_json
.get(span.start..span.end)
.expect("The disconnection between the source JSON and the `Element` will be fixed in a future PR");
let field = RawStr::from_str(field_str, span);
let (key, value) = field_str
.split_once(':')
.expect("An objects field always contains a delimiting `:`");
SourceStr::Field { field, key, value }
} else {
let span = self.span;
let s = source_json
.get(span.start..span.end)
.expect("The disconnection between the source JSON and the `Element` will be fixed in a future PR");
SourceStr::Value(RawStr::from_str(s, span))
}
}
pub fn source_json_value(&self, source_json: &'buf str) -> &'buf str {
source_json
.get(self.span.start..self.span.end)
.expect("The disconnection between the source JSON and the `Element` will be fixed in a future PR")
}
pub(crate) fn value(&self) -> &Value<'buf> {
&self.value
}
pub(crate) fn as_value(&self) -> &Value<'buf> {
&self.value
}
pub(crate) fn as_raw_str(&self) -> Option<&RawStr<'buf>> {
self.value.as_raw_str()
}
pub(crate) fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
self.value.as_object_fields()
}
pub(crate) fn as_array(&self) -> Option<&[Element<'buf>]> {
self.value.as_array()
}
pub fn as_number_str(&self) -> Option<&str> {
self.value.as_number()
}
}
#[derive(Debug)]
pub enum SourceStr<'buf> {
Value(RawStr<'buf>),
Field {
field: RawStr<'buf>,
key: &'buf str,
value: &'buf str,
},
}
impl fmt::Display for SourceStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SourceStr::Value(s) => f.write_str(s.as_raw()),
SourceStr::Field { field, .. } => f.write_str(field.as_raw()),
}
}
}
impl PartialEq<&str> for SourceStr<'_> {
fn eq(&self, other: &&str) -> bool {
match self {
SourceStr::Value(s) => s.as_raw() == *other,
SourceStr::Field { field, .. } => field.as_raw() == *other,
}
}
}
impl PartialEq<String> for SourceStr<'_> {
fn eq(&self, other: &String) -> bool {
self.eq(&&**other)
}
}
impl PartialOrd for Element<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Element<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.path_node.cmp(&other.path_node)
}
}
impl fmt::Display for Element<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} = {}", self.path_node, self.value)
}
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct Field<'buf>(Element<'buf>);
impl<'buf> Field<'buf> {
#[expect(
clippy::unreachable,
reason = "A Field is created by the parser when the type is an Object."
)]
pub(crate) fn key(&self) -> RawStr<'buf> {
let PathNode::Object { key, .. } = *self.0.path_node else {
unreachable!();
};
key
}
pub(crate) fn into_element(self) -> Element<'buf> {
self.0
}
pub(crate) fn element(&self) -> &Element<'buf> {
&self.0
}
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Value<'buf> {
Null,
True,
False,
String(RawStr<'buf>),
Number(&'buf str),
Array(Vec<Element<'buf>>),
Object(Vec<Field<'buf>>),
}
impl<'buf> Value<'buf> {
pub(crate) fn kind(&self) -> ValueKind {
match self {
Value::Null => ValueKind::Null,
Value::True | Value::False => ValueKind::Bool,
Value::String(_) => ValueKind::String,
Value::Number(_) => ValueKind::Number,
Value::Array(_) => ValueKind::Array,
Value::Object(_) => ValueKind::Object,
}
}
pub(crate) fn is_scalar(&self) -> bool {
matches!(
self,
Value::Null | Value::True | Value::False | Value::String(_) | Value::Number(_)
)
}
pub(crate) fn as_array(&self) -> Option<&[Element<'buf>]> {
match self {
Value::Array(elems) => Some(elems),
_ => None,
}
}
pub(crate) fn as_number(&self) -> Option<&str> {
match self {
Value::Number(s) => Some(s),
_ => None,
}
}
pub(crate) fn as_raw_str(&self) -> Option<&RawStr<'buf>> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub(crate) fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
match self {
Value::Object(fields) => Some(fields),
_ => None,
}
}
}
impl fmt::Display for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Null => write!(f, "null"),
Self::True => write!(f, "true"),
Self::False => write!(f, "false"),
Self::String(s) => write!(f, "{s}"),
Self::Number(s) => write!(f, "{s}"),
Self::Array(..) => f.write_str("[...]"),
Self::Object(..) => f.write_str("{...}"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub enum ValueKind {
Null,
Bool,
Number,
String,
Array,
Object,
}
impl fmt::Display for ValueKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValueKind::Null => write!(f, "null"),
ValueKind::Bool => write!(f, "bool"),
ValueKind::Number => write!(f, "number"),
ValueKind::String => write!(f, "string"),
ValueKind::Array => write!(f, "array"),
ValueKind::Object => write!(f, "object"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum ObjectKind {
Object,
Array,
}
type RawMap<'buf> = BTreeMap<RawStr<'buf>, Element<'buf>>;
type RawRefMap<'a, 'buf> = BTreeMap<RawStr<'buf>, &'a Element<'buf>>;
#[allow(dead_code, reason = "pending use in `tariff::lint`")]
pub(crate) trait FieldsIntoExt<'buf> {
fn into_map(self) -> RawMap<'buf>;
}
pub(crate) trait FieldsAsExt<'buf> {
fn as_raw_map(&self) -> RawRefMap<'_, 'buf>;
fn find_field(&self, key: &str) -> Option<&Field<'buf>>;
}
impl<'buf> FieldsIntoExt<'buf> for Vec<Field<'buf>> {
fn into_map(self) -> RawMap<'buf> {
self.into_iter()
.map(|field| (field.key(), field.into_element()))
.collect()
}
}
impl<'buf> FieldsAsExt<'buf> for Vec<Field<'buf>> {
fn as_raw_map(&self) -> RawRefMap<'_, 'buf> {
self.iter()
.map(|field| (field.key(), field.element()))
.collect()
}
fn find_field(&self, key: &str) -> Option<&Field<'buf>> {
self.iter().find(|field| field.key().as_raw() == key)
}
}
impl<'buf> FieldsAsExt<'buf> for [Field<'buf>] {
fn as_raw_map(&self) -> RawRefMap<'_, 'buf> {
self.iter()
.map(|field| (field.key(), field.element()))
.collect()
}
fn find_field(&self, key: &str) -> Option<&Field<'buf>> {
self.iter().find(|field| field.key().as_raw() == key)
}
}
pub(crate) type RawValue = serde_json::value::RawValue;
pub(crate) trait RawValueExt {
fn kind(&self) -> ValueKind;
fn is_string(&self) -> bool;
fn as_str(&self) -> Option<Cow<'_, str>>;
}
impl RawValueExt for RawValue {
fn kind(&self) -> ValueKind {
let s = self.get();
let first = s
.as_bytes()
.first()
.expect("A RawValue can`t be an empty string, it has to contain a JSON value");
match *first {
b'n' => ValueKind::Null,
b't' | b'f' => ValueKind::Bool,
b'"' => ValueKind::String,
b'[' => ValueKind::Array,
b'{' => ValueKind::Object,
_ => ValueKind::Number,
}
}
fn is_string(&self) -> bool {
matches!(self.kind(), ValueKind::String)
}
fn as_str(&self) -> Option<Cow<'_, str>> {
if !self.is_string() {
return None;
}
let s = self.get().trim_matches('"');
let elem = Element {
id: ElemId(0),
path_node: Rc::new(PathNode::Root),
span: Span::default(),
value: Value::Null,
};
let (s, _warnings) = unescape_str(s, &elem).into_parts();
Some(s)
}
}
#[derive(Clone, Debug)]
pub struct UnexpectedFields<'buf>(Vec<PathNodeRef<'buf>>);
impl fmt::Display for UnexpectedFields<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.write_str("[\n")?;
for entry in &self.0 {
writeln!(f, "\t\"{entry}\",")?;
}
f.write_str("]\n")?;
} else {
f.write_char('[')?;
for entry in &self.0 {
write!(f, "{entry},")?;
}
f.write_char(']')?;
}
Ok(())
}
}
impl<'buf> UnexpectedFields<'buf> {
pub(crate) fn empty() -> Self {
Self(vec![])
}
pub(crate) fn from_vec(v: Vec<PathNodeRef<'buf>>) -> Self {
Self(v)
}
pub fn to_strings(&self) -> Vec<String> {
self.0.iter().map(ToString::to_string).collect()
}
pub fn into_strings(self) -> Vec<String> {
self.0.into_iter().map(|path| path.to_string()).collect()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter<'a>(&'a self) -> UnexpectedFieldsIter<'a, 'buf> {
UnexpectedFieldsIter(self.0.iter())
}
}
impl<'buf> IntoIterator for UnexpectedFields<'buf> {
type Item = PathRef<'buf>;
type IntoIter = UnexpectedFieldsIntoIter<'buf>;
fn into_iter(self) -> Self::IntoIter {
UnexpectedFieldsIntoIter(self.0.into_iter())
}
}
pub struct UnexpectedFieldsIntoIter<'buf>(std::vec::IntoIter<PathNodeRef<'buf>>);
impl<'buf> Iterator for UnexpectedFieldsIntoIter<'buf> {
type Item = PathRef<'buf>;
fn next(&mut self) -> Option<Self::Item> {
let path_node = self.0.next()?;
Some(PathRef(path_node))
}
}
impl<'a, 'buf> IntoIterator for &'a UnexpectedFields<'buf> {
type Item = PathRef<'buf>;
type IntoIter = UnexpectedFieldsIter<'a, 'buf>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct UnexpectedFieldsIter<'a, 'buf>(std::slice::Iter<'a, PathNodeRef<'buf>>);
impl<'buf> Iterator for UnexpectedFieldsIter<'_, 'buf> {
type Item = PathRef<'buf>;
fn next(&mut self) -> Option<Self::Item> {
let path_node = self.0.next()?;
Some(PathRef(Rc::clone(path_node)))
}
}
pub struct PathRef<'buf>(PathNodeRef<'buf>);
impl fmt::Debug for PathRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl<'buf> PathRef<'buf> {
pub fn components(&self) -> PathComponents<'buf> {
PathComponents(PathIter::new(Rc::clone(&self.0)))
}
}
pub struct PathComponents<'buf>(PathIter<'buf>);
impl<'buf> Iterator for PathComponents<'buf> {
type Item = PathComponent<'buf>;
fn next(&mut self) -> Option<Self::Item> {
let path_node = self.0.next()?;
Some(PathComponent(path_node))
}
}
impl PartialEq<&str> for PathRef<'_> {
fn eq(&self, other: &&str) -> bool {
match_path_node(&self.0, other, |_| false)
}
}
impl PartialEq<String> for PathRef<'_> {
fn eq(&self, other: &String) -> bool {
match_path_node(&self.0, other, |_| false)
}
}
impl fmt::Display for PathRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
#[cfg(test)]
mod test_path_node_matches_str {
use std::rc::Rc;
use crate::test;
use super::PathNode;
#[test]
fn should_match_path() {
test::setup();
let root = Rc::new(PathNode::Root);
let path_a = Rc::new(PathNode::Array {
parent: Rc::clone(&root),
index: 1,
});
let path_b = Rc::new(PathNode::Object {
parent: Rc::clone(&path_a),
key: r#""name""#.into(),
});
let path_c = PathNode::Object {
parent: Rc::clone(&path_b),
key: r#""gene""#.into(),
};
assert_eq!(*root, "$");
assert_eq!(*path_a, "$.1");
assert_eq!(*path_b, "$.1.name");
assert_eq!(path_c, "$.1.name.gene");
}
}
pub(crate) type PathNodeRef<'buf> = Rc<PathNode<'buf>>;
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum PathNode<'buf> {
#[default]
Root,
Array {
parent: PathNodeRef<'buf>,
index: usize,
},
Object {
parent: PathNodeRef<'buf>,
key: RawStr<'buf>,
},
}
pub enum PathNodeKind {
Root,
Array,
Object,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct Path(Vec<PathPiece>);
impl Path {
const fn root() -> Self {
Self(vec![])
}
fn from_node(path: PathNodeRef<'_>) -> Self {
let paths: Vec<_> = PathIter::new(path).collect();
let pieces = paths
.into_iter()
.rev()
.filter_map(|path_node| match *path_node {
PathNode::Root => None,
PathNode::Array { index, .. } => Some(PathPiece::Array(index)),
PathNode::Object { key, .. } => Some(PathPiece::Object(key.to_string())),
})
.collect();
Self(pieces)
}
}
impl PartialEq<&str> for Path {
fn eq(&self, other: &&str) -> bool {
match_path(self, other)
}
}
impl PartialEq<String> for Path {
fn eq(&self, other: &String) -> bool {
match_path(self, other)
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let iter = self.0.iter();
write!(f, "$")?;
for path in iter {
write!(f, ".{path}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum PathPiece {
Array(usize),
Object(String),
}
impl fmt::Display for PathPiece {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathPiece::Array(index) => write!(f, "{index}"),
PathPiece::Object(key) => write!(f, "{key}"),
}
}
}
pub struct PathComponent<'buf>(PathNodeRef<'buf>);
impl PathComponent<'_> {
pub fn kind(&self) -> PathNodeKind {
match *self.0 {
PathNode::Root => PathNodeKind::Root,
PathNode::Array { .. } => PathNodeKind::Array,
PathNode::Object { .. } => PathNodeKind::Object,
}
}
}
impl fmt::Display for PathComponent<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self.0 {
PathNode::Root => f.write_str(PATH_ROOT),
PathNode::Array { index, .. } => write!(f, "{index}"),
PathNode::Object { key, .. } => write!(f, "{key}"),
}
}
}
impl fmt::Display for PathNode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let paths: Vec<_> = PathIter::new(Rc::new(self.clone())).collect();
let mut iter = paths.into_iter().rev();
if f.alternate() {
for path in iter {
match *path {
PathNode::Root => f.write_str("")?,
PathNode::Array { .. } | PathNode::Object { .. } => f.write_str("...|")?,
}
}
} else {
if let Some(path) = iter.next() {
write!(f, "{}", PathComponent(path))?;
}
for path in iter {
write!(f, ".{}", PathComponent(path))?;
}
}
Ok(())
}
}
impl<'buf> PathNode<'buf> {
pub(crate) fn is_root(&self) -> bool {
matches!(self, PathNode::Root)
}
pub(crate) fn is_array(&self) -> bool {
matches!(self, PathNode::Array { .. })
}
pub(crate) fn as_object_key(&self) -> Option<&RawStr<'buf>> {
match self {
PathNode::Object { key, .. } => Some(key),
PathNode::Root | PathNode::Array { .. } => None,
}
}
}
fn match_path_node<F>(path: &PathNode<'_>, s: &str, mut skip: F) -> bool
where
F: FnMut(&str) -> bool,
{
let mut parts = s.rsplit(PATH_SEPARATOR);
let mut paths_iter = PathIter::new(Rc::new(path.clone()));
loop {
let node_segment = paths_iter.next();
let str_segment = parts.next();
let (node_segment, str_segment) = match (node_segment, str_segment) {
(None, None) => return true,
(None, Some(_)) | (Some(_), None) => return false,
(Some(a), Some(b)) => (a, b),
};
if skip(str_segment) {
continue;
}
let yip = match *node_segment {
PathNode::Root => str_segment == PATH_ROOT,
PathNode::Array { index, .. } => {
let Ok(b) = str_segment.parse::<usize>() else {
return false;
};
index == b
}
PathNode::Object { key, .. } => key.as_raw() == str_segment,
};
if !yip {
return false;
}
}
}
fn match_path(path: &Path, s: &str) -> bool {
let mut parts = s.split(PATH_SEPARATOR);
let mut paths_iter = path.0.iter();
let Some(str_segment) = parts.next() else {
return false;
};
if str_segment != PATH_ROOT {
return false;
}
loop {
let node_segment = paths_iter.next();
let str_segment = parts.next();
let (node_segment, str_segment) = match (node_segment, str_segment) {
(None, None) => return true,
(None, Some(_)) | (Some(_), None) => return false,
(Some(a), Some(b)) => (a, b),
};
let yip = match node_segment {
PathPiece::Array(index) => {
let Ok(b) = str_segment.parse::<usize>() else {
return false;
};
*index == b
}
PathPiece::Object(key) => key == str_segment,
};
if !yip {
return false;
}
}
}
impl PartialEq<&str> for PathNode<'_> {
fn eq(&self, other: &&str) -> bool {
match_path_node(self, other, |_| false)
}
}
impl PartialEq<String> for PathNode<'_> {
fn eq(&self, other: &String) -> bool {
match_path_node(self, other, |_| false)
}
}
struct PathIter<'buf> {
complete: bool,
path: PathNodeRef<'buf>,
}
impl<'buf> PathIter<'buf> {
fn new(path: PathNodeRef<'buf>) -> Self {
Self {
complete: false,
path,
}
}
}
impl<'buf> Iterator for PathIter<'buf> {
type Item = PathNodeRef<'buf>;
fn next(&mut self) -> Option<Self::Item> {
if self.complete {
return None;
}
match &*self.path {
PathNode::Root => {
self.complete = true;
Some(Rc::clone(&self.path))
}
PathNode::Array { parent, .. } | PathNode::Object { parent, .. } => {
let next = Rc::clone(&self.path);
self.path = Rc::clone(parent);
Some(next)
}
}
}
}
struct DisplayExpectStack<'a>(&'a [schema::Expect]);
impl fmt::Display for DisplayExpectStack<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut iter = self.0.iter().rev();
let last = iter.next();
f.write_str("~")?;
for _ in iter {
f.write_str("...~")?;
}
if let Some(exp) = last {
match exp {
schema::Expect::Scalar => f.write_str("~")?,
schema::Expect::Array(element) => match &**element {
schema::Element::Scalar => f.write_str("~")?,
schema::Element::Array(element) => write!(f, "[{element:?}]")?,
schema::Element::Object(fields) => {
write!(f, "[{{{:}}}])", DisplayExpectFields(&**fields))?;
}
},
schema::Expect::Object(fields) => {
write!(f, "{{{:}}}", DisplayExpectFields(&**fields))?;
}
schema::Expect::UnmatchedScalar => write!(f, "unmatched(scalar)")?,
schema::Expect::UnmatchedArray => write!(f, "unmatched(array)")?,
schema::Expect::UnmatchedObject => write!(f, "unmatched(object)")?,
schema::Expect::OutOfSchema => write!(f, "no_schema")?,
}
}
Ok(())
}
}
struct DisplayExpectFields<'a, V>(&'a BTreeMap<&'a str, V>);
impl<V> fmt::Display for DisplayExpectFields<'_, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const MAX_FIELDS: usize = 8;
let mut count = 0;
let mut iter = self.0.keys().peekable();
loop {
if count >= MAX_FIELDS {
f.write_str("...")?;
break;
}
let Some(field) = iter.next() else {
break;
};
count += 1;
write!(f, "{field}")?;
let Some(_) = iter.peek() else {
break;
};
f.write_str(", ")?;
}
Ok(())
}
}
#[derive(Debug)]
struct UnbalancedExpectStack;
impl fmt::Display for UnbalancedExpectStack {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unbalanced expectation stack")
}
}
impl std::error::Error for UnbalancedExpectStack {}
pub(crate) fn parse_with_schema<'buf>(
json: &'buf str,
schema: &schema::Element,
) -> Result<Report<'buf>, Error> {
let parser = Parser::new(json);
let mut unexpected_fields = vec![];
let mut expectation_stack = vec![schema.to_expectation()];
for event in parser {
match event? {
parser::Event::Open { kind, parent_path } => {
let Some(expectation) = expectation_stack.pop() else {
return Err(ErrorKind::Internal(Box::new(UnbalancedExpectStack))
.into_partial_error_without_token()
.with_root_path());
};
if tracing::enabled!(Level::DEBUG) {
match kind {
ObjectKind::Array => {
trace!("{parent_path} [ {}", DisplayExpectStack(&expectation_stack));
}
ObjectKind::Object => trace!(
"{parent_path} {{ {}",
DisplayExpectStack(&expectation_stack)
),
}
}
match expectation {
schema::Expect::Array(elem) => {
if parent_path.is_root() {
let next = match kind {
ObjectKind::Array => schema::Expect::Array(elem),
ObjectKind::Object => schema::Expect::UnmatchedArray,
};
expectation_stack.push(next);
trace!("{}", DisplayExpectStack(&expectation_stack));
continue;
}
if !parent_path.is_array() {
expectation_stack.push(schema::Expect::UnmatchedArray);
trace!("{}", DisplayExpectStack(&expectation_stack));
continue;
}
expectation_stack.push(schema::Expect::Array(Arc::clone(&elem)));
expectation_stack.push(elem.to_expectation());
}
schema::Expect::Object(fields) => {
if parent_path.is_root() {
let next = match kind {
ObjectKind::Array => schema::Expect::UnmatchedObject,
ObjectKind::Object => schema::Expect::Object(fields),
};
expectation_stack.push(next);
trace!("{}", DisplayExpectStack(&expectation_stack));
continue;
}
let Some(key) = parent_path.as_object_key() else {
expectation_stack.push(schema::Expect::UnmatchedObject);
trace!("{}", DisplayExpectStack(&expectation_stack));
continue;
};
let next = if let Some(elem) = fields.get(key.as_raw()) {
open_object(kind, elem.as_ref())
} else {
unexpected_fields.push(parent_path);
schema::Expect::OutOfSchema
};
expectation_stack.push(schema::Expect::Object(fields));
expectation_stack.push(next);
}
schema::Expect::OutOfSchema => {
expectation_stack.push(expectation);
expectation_stack.push(schema::Expect::OutOfSchema);
}
schema::Expect::UnmatchedArray | schema::Expect::UnmatchedObject => {
expectation_stack.push(expectation);
expectation_stack.push(schema::Expect::OutOfSchema);
}
_ => {
expectation_stack.push(expectation);
}
}
trace!("{}", DisplayExpectStack(&expectation_stack));
}
parser::Event::Element { kind, parent_path } => {
let Some(expectation) = expectation_stack.pop() else {
return Err(ErrorKind::Internal(Box::new(UnbalancedExpectStack))
.into_partial_error_without_token()
.with_root_path());
};
if let ValueKind::Array | ValueKind::Object = kind {
if tracing::enabled!(Level::DEBUG) {
match kind {
ValueKind::Array => {
trace!(
"{parent_path} ] {}",
DisplayExpectStack(&expectation_stack)
);
}
ValueKind::Object => trace!(
"{parent_path} }} {}",
DisplayExpectStack(&expectation_stack)
),
_ => (),
}
}
continue;
}
match expectation {
#[expect(
clippy::unreachable,
reason = "The parser only emits an `Event::Complete` for a scalar object at the root"
)]
schema::Expect::Object(fields) => match &*parent_path {
PathNode::Root => unreachable!(),
PathNode::Array { .. } => {
expectation_stack.push(schema::Expect::UnmatchedObject);
}
PathNode::Object { parent, key } => {
trace!("{parent:#}.{key}");
if !fields.contains_key(key.as_raw()) {
unexpected_fields.push(parent_path);
}
expectation_stack.push(schema::Expect::Object(fields));
}
},
schema::Expect::OutOfSchema => {
unexpected_fields.push(parent_path);
expectation_stack.push(expectation);
}
_ => {
expectation_stack.push(expectation);
}
}
}
parser::Event::Complete(element) => {
if element.value().is_scalar() {
unexpected_fields.push(element.path_node());
}
return Ok(Report {
element,
unexpected_fields: UnexpectedFields::from_vec(unexpected_fields),
});
}
}
}
Err(ErrorKind::UnexpectedEOF
.into_partial_error_without_token()
.with_root_path())
}
fn open_object(kind: ObjectKind, elem: Option<&Arc<schema::Element>>) -> schema::Expect {
let Some(schema) = elem else {
return schema::Expect::OutOfSchema;
};
match (kind, &**schema) {
(ObjectKind::Object | ObjectKind::Array, schema::Element::Scalar) => {
schema::Expect::UnmatchedScalar
}
(ObjectKind::Object, schema::Element::Array(_)) => schema::Expect::UnmatchedArray,
(ObjectKind::Object, schema::Element::Object(fields)) => {
schema::Expect::Object(Arc::clone(fields))
}
(ObjectKind::Array, schema::Element::Array(element)) => {
schema::Expect::Array(Arc::clone(element))
}
(ObjectKind::Array, schema::Element::Object(_)) => schema::Expect::UnmatchedObject,
}
}
#[derive(Debug)]
pub(crate) struct Report<'buf> {
pub element: Element<'buf>,
pub unexpected_fields: UnexpectedFields<'buf>,
}
#[cfg(test)]
pub mod test {
#![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
#![allow(clippy::panic, reason = "tests are allowed panic")]
use std::borrow::Cow;
use crate::json::match_path_node;
use super::{
parser::Span, walk::DepthFirst, ElemId, Element, Field, FieldsAsExt as _, PathNode,
PathNodeRef, PathRef, RawStr, UnexpectedFields, Value,
};
impl<'buf> Element<'buf> {
pub fn span(&self) -> Span {
self.span
}
pub(crate) fn into_value(self) -> Value<'buf> {
self.value
}
pub(crate) fn into_parts(self) -> (ElemId, PathNodeRef<'buf>, Span, Value<'buf>) {
let Self {
id,
path_node: path,
span,
value,
} = self;
(id, path, span, value)
}
pub(crate) fn find_field(&self, key: &str) -> Option<&Field<'buf>> {
self.as_object_fields()
.and_then(|fields| fields.find_field(key))
}
}
impl<'buf> Value<'buf> {
pub(crate) fn is_array(&self) -> bool {
matches!(self, Value::Array(_))
}
pub(crate) fn is_object(&self) -> bool {
matches!(self, Value::Object(_))
}
pub(crate) fn as_string(&self) -> Option<&RawStr<'buf>> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
}
impl<'buf> Field<'buf> {
pub fn id(&self) -> ElemId {
self.0.id()
}
pub fn into_parts(self) -> (ElemId, PathNodeRef<'buf>, Span, Value<'buf>) {
self.0.into_parts()
}
}
impl<'buf> UnexpectedFields<'buf> {
pub(crate) fn into_inner(self) -> Vec<PathNodeRef<'buf>> {
self.0
}
pub(crate) fn filter_matches(&mut self, glob: &PathGlob<'_>) {
self.0.retain(|path| !glob.matches(path));
}
}
#[derive(Debug)]
pub(crate) struct PathGlob<'a>(Cow<'a, str>);
impl PathGlob<'_> {
pub(crate) fn matches(&self, path: &PathNode<'_>) -> bool {
const WILDCARD: &str = "*";
match_path_node(path, &self.0, |s| {
s == WILDCARD
})
}
}
impl From<usize> for ElemId {
fn from(value: usize) -> Self {
Self(value)
}
}
impl<'a> From<&'a str> for PathGlob<'a> {
fn from(s: &'a str) -> Self {
Self(s.into())
}
}
impl From<String> for PathGlob<'_> {
fn from(s: String) -> Self {
Self(s.into())
}
}
impl<'de, 'buf> serde::Deserialize<'de> for PathGlob<'buf> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
let s = <Cow<'buf, str> as ::serde::Deserialize>::deserialize(deserializer)?;
Ok(Self(s))
}
}
pub struct ElementMap<'a, 'bin>(Vec<&'a Element<'bin>>);
impl<'a, 'bin> ElementMap<'a, 'bin> {
pub fn for_elem(root: &'a Element<'bin>) -> Self {
let walker = DepthFirst::new(root);
Self(walker.collect())
}
pub fn get(&self, id: ElemId) -> &Element<'bin> {
self.0.get(id.0).map(|e| &**e).unwrap()
}
pub fn path(&self, id: ElemId) -> PathRef<'bin> {
self.0.get(id.0).map(|elem| elem.path()).unwrap()
}
}
#[cfg(test)]
mod test_path_matches_glob {
use std::rc::Rc;
use crate::test;
use super::{PathGlob, PathNode};
#[test]
fn should_match_path() {
test::setup();
let root = Rc::new(PathNode::Root);
let path_a = Rc::new(PathNode::Array {
parent: Rc::clone(&root),
index: 1,
});
let path_b = Rc::new(PathNode::Object {
parent: Rc::clone(&path_a),
key: r#""name""#.into(),
});
let path_c = PathNode::Object {
parent: Rc::clone(&path_b),
key: r#""gene""#.into(),
};
assert!(PathGlob::from("$").matches(&root));
assert!(PathGlob::from("*").matches(&root));
assert!(!PathGlob::from("*").matches(&path_a));
assert!(PathGlob::from("*.*").matches(&path_a));
assert!(PathGlob::from("$.*").matches(&path_a));
assert!(PathGlob::from("$.1").matches(&path_a));
assert!(!PathGlob::from("*").matches(&path_b));
assert!(!PathGlob::from("*.*").matches(&path_b));
assert!(PathGlob::from("*.*.*").matches(&path_b));
assert!(PathGlob::from("$.*.*").matches(&path_b));
assert!(PathGlob::from("$.1.*").matches(&path_b));
assert!(PathGlob::from("$.*.name").matches(&path_b));
assert!(PathGlob::from("$.1.name").matches(&path_b));
assert!(PathGlob::from("$.1.name.gene").matches(&path_c));
}
}
}
#[cfg(test)]
mod test_path {
use super::{Path, PathPiece};
#[test]
fn path_should_cmp_with_str() {
assert_ne!(Path::root(), "");
assert_eq!(Path::root(), "$");
assert_eq!(Path(vec![PathPiece::Object("field_a".into())]), "$.field_a");
assert_eq!(Path(vec![PathPiece::Array(1)]), "$.1");
assert_eq!(
Path(vec![
PathPiece::Object("field_a".into()),
PathPiece::Array(1)
]),
"$.field_a.1"
);
}
#[test]
fn path_should_display() {
assert_eq!(Path::root().to_string(), "$");
assert_eq!(
Path(vec![PathPiece::Object("field_a".into())]).to_string(),
"$.field_a"
);
assert_eq!(Path(vec![PathPiece::Array(1)]).to_string(), "$.1");
assert_eq!(
Path(vec![
PathPiece::Object("field_a".into()),
PathPiece::Array(1)
])
.to_string(),
"$.field_a.1"
);
}
}
#[cfg(test)]
mod test_parse_with_schema {
use crate::{json_schema, test};
use super::{parse_with_schema, Report};
#[test]
fn should_report_unexpected_fields_for_root_element() {
const JSON: &str = "null";
test::setup();
let schema = json_schema!({
"id",
"currency",
});
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a] = unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$");
}
}
#[test]
fn should_report_unexpected_fields_in_flat_object() {
const JSON: &str = r#"{
"id": "tariff_id",
"currency": "EUR",
"name": "Barry",
"address": "Barrystown"
}"#;
test::setup();
let schema = json_schema!({
"id",
"currency",
});
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a, field_b] = unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$.name");
assert_eq!(*field_b, "$.address");
}
}
#[test]
fn should_report_unexpected_fields_in_nested_object() {
const JSON: &str = r#"{
"id": "tariff_id",
"currency": "EUR",
"owner": {
"id": "456856",
"subscription_id": "tedi4568",
"name": "Barry",
"address": "Barrystown"
}
}"#;
test::setup();
let schema = json_schema!({
"id",
"currency",
"owner": {
"id",
"subscription_id"
}
});
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a, field_b] = unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$.owner.name");
assert_eq!(*field_b, "$.owner.address");
}
}
#[test]
fn should_parse_nested_object_out_of_schema() {
const JSON: &str = r#"{
"id": "tariff_id",
"owner": {
"id": "456856",
"subscription_id": "tedi4568",
"name": "Barry",
"address": {
"city": "Barrystown",
"street": "Barrysstreet"
}
},
"currency": "EUR",
"country": "NL"
}"#;
test::setup();
let schema = json_schema!({
"id",
"currency",
"owner"
});
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a, field_b, field_c, field_d, field_e, field_f] =
unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$.owner.id");
assert_eq!(*field_b, "$.owner.subscription_id");
assert_eq!(*field_c, "$.owner.name");
assert_eq!(*field_d, "$.owner.address.city");
assert_eq!(*field_e, "$.owner.address.street");
assert_eq!(*field_f, "$.country");
}
}
#[test]
fn should_report_unexpected_fields_in_array_with_nested_object() {
const JSON: &str = r#"{
"id": "tariff_id",
"currency": "EUR",
"elements": [{
"id": "456856",
"subscription_id": "tedi4568",
"name": "Barry",
"address": "Barrystown"
}]
}"#;
test::setup();
let schema = json_schema!({
"id",
"currency",
"elements": [{
"id",
"subscription_id"
}]
});
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a, field_b] = unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$.elements.0.name");
assert_eq!(*field_b, "$.elements.0.address");
}
}
#[test]
fn should_report_unexpected_fields_in_array_of_nested_objects() {
const JSON: &str = r#"{
"id": "tariff_id",
"currency": "EUR",
"elements": [
{
"id": "456856",
"subscription_id": "tedi4568",
"name": "Barry",
"address": "Barrystown"
},
{
"id": "8746we",
"subscription_id": "dfr345",
"name": "Gerry",
"address": "Gerrystown"
}
]
}"#;
test::setup();
let schema = json_schema!({
"id",
"currency",
"elements": [{
"id",
"subscription_id"
}]
});
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a, field_b, field_c, field_d] =
unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$.elements.0.name");
assert_eq!(*field_b, "$.elements.0.address");
assert_eq!(*field_c, "$.elements.1.name");
assert_eq!(*field_d, "$.elements.1.address");
}
}
#[test]
fn should_report_unexpected_fields_in_array_of_objects() {
const JSON: &str = r#"[
{
"id": "456856",
"subscription_id": "tedi4568",
"name": "Barry",
"address": "Barrystown"
},
{
"id": "8746we",
"subscription_id": "dfr345",
"name": "Gerry",
"address": "Gerrystown"
}
]"#;
test::setup();
let schema = json_schema!([
{
"id",
"subscription_id"
}
]);
let report = parse_with_schema(JSON, &schema).unwrap();
let Report {
element: _,
unexpected_fields,
} = report;
{
let [field_a, field_b, field_c, field_d] =
unexpected_fields.into_inner().try_into().unwrap();
assert_eq!(*field_a, "$.0.name");
assert_eq!(*field_b, "$.0.address");
assert_eq!(*field_c, "$.1.name");
assert_eq!(*field_d, "$.1.address");
}
}
}
#[cfg(test)]
mod test_source_json {
use super::{parse, walk};
#[test]
fn should_resolve_to_source_json() {
const JSON: &str = r#"{
"name": "David Byrne",
"hobbies": ["song writing", "thinking about society"]
}"#;
let element = parse(JSON).unwrap();
let mut walk = walk::DepthFirst::new(&element);
let root = walk.next().unwrap();
assert_eq!(root.source_json(JSON), JSON);
let field_name = walk.next().unwrap();
assert_eq!(field_name.source_json(JSON), r#""name": "David Byrne""#);
assert_eq!(field_name.source_json_value(JSON), r#""David Byrne""#);
let field_hobbies = walk.next().unwrap();
assert_eq!(
field_hobbies.source_json(JSON),
r#""hobbies": ["song writing", "thinking about society"]"#
);
assert_eq!(
field_hobbies.source_json_value(JSON),
r#"["song writing", "thinking about society"]"#
);
let hobbies_one = walk.next().unwrap();
assert_eq!(hobbies_one.source_json(JSON), r#""song writing""#);
assert_eq!(hobbies_one.source_json_value(JSON), r#""song writing""#);
let hobbies_two = walk.next().unwrap();
assert_eq!(hobbies_two.source_json(JSON), r#""thinking about society""#);
assert_eq!(
hobbies_two.source_json_value(JSON),
r#""thinking about society""#
);
}
}
#[cfg(test)]
mod test_path_node {
use std::rc::Rc;
use super::{
parser::{RawStr, Token, TokenType},
PathNode, Span,
};
#[test]
fn should_display_path() {
let root = Rc::new(PathNode::Root);
let path_a = Rc::new(PathNode::Array {
parent: Rc::clone(&root),
index: 1,
});
let path_b = Rc::new(PathNode::Object {
parent: Rc::clone(&path_a),
key: r#""name""#.into(),
});
let path_c = Rc::new(PathNode::Object {
parent: Rc::clone(&path_b),
key: r#""gene""#.into(),
});
assert_eq!(*root, "$");
assert_eq!(*path_a, "$.1");
assert_eq!(*path_b, "$.1.name");
assert_eq!(*path_c, "$.1.name.gene");
}
impl<'buf> From<&'buf str> for RawStr<'buf> {
#[track_caller]
fn from(s: &'buf str) -> Self {
RawStr::from_quoted_str(
s,
Token {
kind: TokenType::String,
span: Span::default(),
},
)
.unwrap()
}
}
}
#[cfg(test)]
mod test_raw_json {
use serde::{de::IntoDeserializer, Deserialize};
use super::RawValue;
use crate::json::RawValueExt as _;
const TITLE: &str = "همّا مين واØÙ†Ø§ مين (Who Are They and Who Are We?)";
#[test]
fn should_fail_to_parse_whitespace_only_string_as_json() {
const JSON: &str = " ";
let err = serde_json::from_str::<&RawValue>(JSON).unwrap_err();
assert_eq!(
err.classify(),
serde_json::error::Category::Eof,
"A JSON string can't be empty"
);
let err = RawValue::from_string(JSON.to_string()).unwrap_err();
assert_eq!(
err.classify(),
serde_json::error::Category::Eof,
"A JSON string can't be empty"
);
}
#[test]
fn should_validate_json_without_allocating_for_each_token() {
#[derive(Deserialize)]
struct Song {
title: String,
}
let json = format!(r#"{{ "title": "{TITLE}" }}"#);
let json: Box<RawValue> = serde_json::from_str(&json).unwrap();
let song = Song::deserialize(json.into_deserializer()).unwrap();
assert_eq!(song.title, TITLE);
}
#[test]
fn should_compare_raw_title_correctly() {
#[derive(Deserialize)]
struct Song<'a> {
#[serde(borrow)]
title: &'a RawValue,
}
let json = format!(r#"{{ "title": "{TITLE}" }}"#);
let song: Song<'_> = serde_json::from_str(&json).unwrap();
assert_ne!(
song.title.get(),
TITLE,
"The raw `title` field contains the delimiting '\"' and so is technically not directly equal to the `TITLE` const"
);
let title = song.title.as_str().unwrap();
assert_eq!(
title, TITLE,
"When the quotes are removed the `title` field is the same as the `TITLE` const"
);
}
#[test]
fn should_fail_to_parse_invalid_json() {
const JSON: &str = r#"{ "title": }"#;
let err = serde_json::from_str::<Box<RawValue>>(JSON).unwrap_err();
assert_eq!(
err.classify(),
serde_json::error::Category::Syntax,
"The bytes contained within `RawValue` are valid JSON"
);
}
#[test]
fn should_parse_raw_json_to_rust_struct() {
#[derive(Deserialize)]
struct Song {
title: String,
}
struct SongMessage {
song: Song,
original: Box<RawValue>,
}
let json = format!(r#"{{ "title": "{TITLE}" }}"#);
let raw_json: Box<RawValue> = serde_json::from_str(&json)
.expect("Typically we want to parse some JSON from an endpoint first");
let song = Song::deserialize(raw_json.into_deserializer()).expect("And then introspect it");
let message = SongMessage {
song,
original: raw_json,
};
assert_eq!(
message.song.title, TITLE,
"The title is a normal `String` and so can be directly compared"
);
assert_eq!(
message.original.get(),
&json,
"The original has not been modified in any way"
);
}
#[test]
fn should_parse_borrowed_raw_json_to_rust_struct() {
#[derive(Deserialize)]
struct Song<'a> {
title: &'a str,
}
struct SongMessage<'a> {
song: Song<'a>,
original: &'a RawValue,
}
let json = format!(r#"{{ "title": "{TITLE}" }}"#);
let raw_json: &RawValue = serde_json::from_str(&json)
.expect("Typically we want to parse some JSON from an endpoint first");
let song =
Song::<'_>::deserialize(raw_json.into_deserializer()).expect("And then introspect it");
let message = SongMessage {
song,
original: raw_json,
};
assert_eq!(
message.song.title, TITLE,
"The title is a normal `&str` and so can be directly compared"
);
assert_eq!(
message.original.get(),
&json,
"The original has not been modified in any way"
);
}
#[test]
fn should_deser_number_as_i64() {
const JSON: &str = "123";
let json: &RawValue = serde_json::from_str(JSON).unwrap();
let n = i64::deserialize(json.into_deserializer()).unwrap();
assert_eq!(n, 123);
}
#[test]
fn should_convert_json_string_to_str() {
#[derive(Deserialize)]
struct Song<'a> {
title: &'a str,
}
struct SongMessage<'a> {
song: Song<'a>,
original: &'a RawValue,
}
let json = format!(r#"{{ "title": "{TITLE}" }}"#);
let raw_json: &RawValue = serde_json::from_str(&json)
.expect("Typically we want to parse some JSON from an endpoint first");
let song =
Song::<'_>::deserialize(raw_json.into_deserializer()).expect("And then introspect it");
let message = SongMessage {
song,
original: raw_json,
};
assert_eq!(
message.song.title, TITLE,
"The title is a normal `&str` and so can be directly compared"
);
assert_eq!(
message.original.get(),
&json,
"The original has not been modified in any way"
);
}
}