Skip to content

Commit

Permalink
crates/doc: refactor inference into sub-module shape
Browse files Browse the repository at this point in the history
doc::shape has a variety of other submodules, some of them public and
others not, which focus on particular features and capabilities of Shape.

No behavior changes, and no significant changes to tests,
though I moved quite a few things around.
  • Loading branch information
jgraettinger committed Aug 9, 2023
1 parent 8b4f042 commit 26780b7
Show file tree
Hide file tree
Showing 14 changed files with 4,087 additions and 3,869 deletions.
3,862 changes: 0 additions & 3,862 deletions crates/doc/src/inference.rs

This file was deleted.

8 changes: 5 additions & 3 deletions crates/doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ pub use validation::{
// Doc implementations may be reduced.
pub mod reduce;

pub mod schema;

// Documents may be combined.
#[cfg(feature = "combine")]
pub mod combine;
Expand All @@ -110,7 +108,11 @@ pub use combine::Combiner;
// Nodes may be packed as FoundationDB tuples.
pub mod tuple_pack;

pub mod inference;
// Shape is a description of the valid shapes that a document may take.
// It's similar to (and built from) a JSON Schema, but includes only
// those inferences which can be statically proven for all documents.
pub mod shape;
pub use shape::Shape;

// Fancy diff support for documents.
pub mod diff;
Expand Down
4 changes: 2 additions & 2 deletions crates/doc/src/reduce/schema.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{count_nodes_heap, Cursor, Error, Result};
use crate::{inference::Shape, schema::SchemaBuilder, AsNode, HeapNode};
use crate::{shape::limits, shape::schema::SchemaBuilder, AsNode, HeapNode, Shape};
use json::schema::index::IndexBuilder;

pub fn json_schema_merge<'alloc, L: AsNode, R: AsNode>(
Expand Down Expand Up @@ -32,7 +32,7 @@ pub fn json_schema_merge<'alloc, L: AsNode, R: AsNode>(
let right = shape_from_node(rhs).map_err(|e| Error::with_location(e, loc))?;

let mut merged_shape = Shape::union(left, right);
merged_shape.enforce_field_count_limits(json::Location::Root);
limits::enforce_field_count_limits(&mut merged_shape, json::Location::Root);

// Union together the LHS and RHS, and convert back from `Shape` into `HeapNode`.
let merged_doc = serde_json::to_value(&SchemaBuilder::new(merged_shape).root_schema())
Expand Down
1,420 changes: 1,420 additions & 0 deletions crates/doc/src/shape/inference.rs

Large diffs are not rendered by default.

216 changes: 216 additions & 0 deletions crates/doc/src/shape/inspections.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/// This module implements various inspections which can be performed over Shapes.
use super::*;
use json::{LocatedProperty, Location};

#[derive(thiserror::Error, Debug, Eq, PartialEq)]
pub enum Error {
#[error("'{0}' must exist, but is constrained to always be invalid")]
ImpossibleMustExist(String),
#[error("'{0}' has reduction strategy, but its parent does not")]
ChildWithoutParentReduction(String),
#[error("{0} has 'sum' reduction strategy, restricted to numbers, but has types {1:?}")]
SumNotNumber(String, types::Set),
#[error(
"{0} has 'merge' reduction strategy, restricted to objects & arrays, but has types {1:?}"
)]
MergeNotObjectOrArray(String, types::Set),
#[error("{0} has 'set' reduction strategy, restricted to objects, but has types {1:?}")]
SetNotObject(String, types::Set),
#[error(
"{0} location's parent has 'set' reduction strategy, restricted to 'add'/'remove'/'intersect' properties"
)]
SetInvalidProperty(String),
#[error("{0} default value is invalid: {1}")]
InvalidDefaultValue(String, crate::FailedValidation),
}

impl Shape {
pub fn inspect(&self) -> Vec<Error> {
let mut v = Vec::new();
self.inspect_inner(Location::Root, true, &mut v);
v
}

fn inspect_inner(&self, loc: Location, must_exist: bool, out: &mut Vec<Error>) {
// Enumerations over array sub-locations.
let items = self.array.tuple.iter().enumerate().map(|(index, s)| {
(
loc.push_item(index),
self.type_ == types::ARRAY && index < self.array.min.unwrap_or(0),
s,
)
});
let addl_items = self
.array
.additional
.iter()
.map(|s| (loc.push_end_of_array(), false, s.as_ref()));

// Enumerations over object sub-locations.
let props = self.object.properties.iter().map(|op| {
(
loc.push_prop(&op.name),
self.type_ == types::OBJECT && op.is_required,
&op.shape,
)
});
let patterns = self
.object
.patterns
.iter()
.map(|op| (loc.push_prop(op.re.as_str()), false, &op.shape));
let addl_props = self
.object
.additional
.iter()
.map(|shape| (loc.push_prop("*"), false, shape.as_ref()));

if self.type_ == types::INVALID && must_exist {
out.push(Error::ImpossibleMustExist(loc.pointer_str().to_string()));
}

// Invalid values for default values.
if let Some((_, Some(err))) = &self.default {
out.push(Error::InvalidDefaultValue(
loc.pointer_str().to_string(),
err.to_owned(),
));
};

if matches!(self.reduction, Reduction::Sum)
&& self.type_ - types::INT_OR_FRAC != types::INVALID
{
out.push(Error::SumNotNumber(
loc.pointer_str().to_string(),
self.type_,
));
}
if matches!(self.reduction, Reduction::Merge)
&& self.type_ - (types::OBJECT | types::ARRAY) != types::INVALID
{
out.push(Error::MergeNotObjectOrArray(
loc.pointer_str().to_string(),
self.type_,
));
}
if matches!(self.reduction, Reduction::Set) {
if self.type_ != types::OBJECT {
out.push(Error::SetNotObject(
loc.pointer_str().to_string(),
self.type_,
));
}

for (loc, _, _) in props.clone().chain(patterns.clone()) {
if !matches!(loc, Location::Property(LocatedProperty { name, .. })
if name == "add" || name == "intersect" || name == "remove")
{
out.push(Error::SetInvalidProperty(loc.pointer_str().to_string()));
}
}
}

for (loc, child_must_exist, child) in items
.chain(addl_items)
.chain(props)
.chain(patterns)
.chain(addl_props)
{
if matches!(self.reduction, Reduction::Unset)
&& !matches!(child.reduction, Reduction::Unset)
{
out.push(Error::ChildWithoutParentReduction(
loc.pointer_str().to_string(),
))
}

child.inspect_inner(loc, must_exist && child_must_exist, out);
}
}
}

#[cfg(test)]
mod test {
use super::{shape_from, Error};
use json::schema::types;
use pretty_assertions::assert_eq;

#[test]
fn test_error_collection() {
let obj = shape_from(
r#"
type: object
reduce: {strategy: merge}
properties:
sum-wrong-type:
reduce: {strategy: sum}
type: [number, string]
must-exist-but-cannot: false
may-not-exist: false
nested-obj-or-string:
type: [object, string]
properties:
must-exist-and-cannot-but-parent-could-be-string: false
required: [must-exist-and-cannot-but-parent-could-be-string]
nested-array:
type: array
items: [true, false, false]
minItems: 2
nested-array-or-string:
oneOf:
- $ref: '#/properties/nested-array'
- type: string
patternProperties:
merge-wrong-type:
reduce: {strategy: merge}
type: boolean
required: [must-exist-but-cannot, nested-obj-or-string, nested-array, nested-array-or-string]
additionalProperties:
type: object
# Valid child, but parent is missing reduce annotation.
properties:
nested-sum:
reduce: {strategy: sum}
type: integer
items:
# Set without type restriction.
- reduce: {strategy: set}
additionalItems:
type: object
properties:
add: true
intersect: true
whoops1: true
patternProperties:
remove: true
whoops2: true
reduce: {strategy: set}
"#,
);

assert_eq!(
obj.inspect(),
vec![
Error::SetNotObject("/0".to_owned(), types::ANY),
Error::SetInvalidProperty("/-/whoops1".to_owned()),
Error::SetInvalidProperty("/-/whoops2".to_owned()),
Error::ImpossibleMustExist("/must-exist-but-cannot".to_owned()),
Error::ImpossibleMustExist("/nested-array/1".to_owned()),
Error::SumNotNumber(
"/sum-wrong-type".to_owned(),
types::INT_OR_FRAC | types::STRING
),
Error::MergeNotObjectOrArray("/merge-wrong-type".to_owned(), types::BOOLEAN),
Error::ChildWithoutParentReduction("/*/nested-sum".to_owned()),
]
);
}
}
Loading

0 comments on commit 26780b7

Please sign in to comment.