use super::{walk, BuilderKind, Cardinality, Field, Object, Scalar, Schema, Warning};
use crate::json::{self, parse};
static NAME_OBJ: Object = Object {
fields: &[
Field::required("first", Scalar::String),
Field::required("last", Scalar::String),
],
kind: BuilderKind::Ignore,
};
static NAME_SCHEMA: Schema = Schema::Object(&NAME_OBJ);
static PERSON_OBJ: Object = Object {
fields: &[
Field::optional("age", Scalar::Number),
Field::required_object("name", &NAME_OBJ),
],
kind: BuilderKind::Ignore,
};
static PERSON_SCHEMA: Schema = Schema::Object(&PERSON_OBJ);
static NAMES_ARRAY_SCHEMA: Schema = Schema::Array {
item: &NAME_SCHEMA,
cardinality: Cardinality::ZeroOrMore,
};
static NAMES_ARRAY_ONE_OR_MORE: Schema = Schema::Array {
item: &NAME_SCHEMA,
cardinality: Cardinality::OneOrMore,
};
#[test]
fn empty_object_reports_missing_required() {
let doc = parse("{}".into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$"],
vec![&Warning::MissingField { name: "name" }]
);
}
#[test]
fn unexpected_fields_reported() {
let doc = parse(r#"{"nane": {}, "extra": 1}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
let by_path = warnings.path_map();
assert_eq!(warnings.len_warnings(), 3);
assert_eq!(by_path["$.nane"], vec![&Warning::UnexpectedField]);
assert_eq!(by_path["$.extra"], vec![&Warning::UnexpectedField]);
assert_eq!(by_path["$"], vec![&Warning::MissingField { name: "name" }]);
}
#[test]
fn valid_object_is_clean() {
let doc = parse(r#"{"name": {"first": "John", "last": "Doe"}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn optional_field_present_does_not_warn() {
let doc = parse(r#"{"age": 30, "name": {"first": "J", "last": "D"}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn nested_unexpected_reported_with_path() {
let doc = parse(r#"{"name": {"first": "J", "last": "D", "middle": "X"}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.name.middle"],
vec![&Warning::UnexpectedField]
);
}
#[test]
fn nested_missing_required_reported_with_path() {
let doc = parse(r#"{"name": {"first": "J"}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.name"],
vec![&Warning::MissingField { name: "last" }]
);
}
#[test]
fn array_items_each_validated() {
let doc =
parse(r#"[{"first": "A", "last": "B"}, {"first": "C", "typo": "D"}]"#.into()).unwrap();
let warnings = walk(&doc, &NAMES_ARRAY_SCHEMA).into_warnings();
let by_path = warnings.path_map();
assert_eq!(warnings.len_warnings(), 2);
assert_eq!(by_path["$[1].typo"], vec![&Warning::UnexpectedField]);
assert_eq!(
by_path["$[1]"],
vec![&Warning::MissingField { name: "last" }]
);
}
#[test]
fn any_imposes_no_kind_constraint() {
let doc = parse(r#"{"anything": [1, 2, {"nested": true}]}"#.into()).unwrap();
let warnings = walk(&doc, &Schema::Scalar(Scalar::Any)).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn non_object_against_object_schema_reported() {
let doc = parse("[1, 2, 3]".into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::Object,
actual: json::ValueKind::Array,
}]
);
}
#[test]
fn scalar_where_object_expected_reported() {
let doc = parse(r#"{"name": "John Doe"}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.name"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::Object,
actual: json::ValueKind::String,
}]
);
}
#[test]
fn non_array_against_array_schema_reported() {
let doc = parse(r#"{"first": "J", "last": "D"}"#.into()).unwrap();
let warnings = walk(&doc, &NAMES_ARRAY_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::Array,
actual: json::ValueKind::Object,
}]
);
}
#[test]
fn scalar_type_mismatch_reported() {
let doc = parse(r#"{"age": true, "name": {"first": "J", "last": "D"}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.age"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::Number,
actual: json::ValueKind::Bool,
}]
);
}
#[test]
fn number_field_accepts_string_encoding() {
let doc = parse(r#"{"age": "30", "name": {"first": "J", "last": "D"}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn empty_one_or_more_array_reported() {
let doc = parse("[]".into()).unwrap();
let warnings = walk(&doc, &NAMES_ARRAY_ONE_OR_MORE).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$"],
vec![&Warning::Cardinality {
expected: Cardinality::OneOrMore,
len: 0,
}]
);
}
#[test]
fn empty_zero_or_more_array_is_clean() {
let doc = parse("[]".into()).unwrap();
let warnings = walk(&doc, &NAMES_ARRAY_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn non_empty_one_or_more_array_is_clean() {
let doc = parse(r#"[{"first": "A", "last": "B"}]"#.into()).unwrap();
let warnings = walk(&doc, &NAMES_ARRAY_ONE_OR_MORE).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn null_inside_any_subtree_is_reported() {
static OPAQUE_OBJ: Object = Object {
fields: &[Field::optional("blob", Scalar::Any)],
kind: BuilderKind::Ignore,
};
static OPAQUE_SCHEMA: Schema = Schema::Object(&OPAQUE_OBJ);
let doc = parse(r#"{"blob": {"nested": null}}"#.into()).unwrap();
let warnings = walk(&doc, &OPAQUE_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.blob.nested"],
vec![&Warning::NullField]
);
}
#[test]
fn invalid_type_subtree_is_not_walked() {
let doc = parse(r#"{"name": [null]}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.name"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::Object,
actual: json::ValueKind::Array,
}]
);
}
#[test]
fn unexpected_field_subtree_is_not_walked() {
let doc =
parse(r#"{"name": {"first": "J", "last": "D"}, "typo": {"x": null}}"#.into()).unwrap();
let warnings = walk(&doc, &PERSON_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.typo"],
vec![&Warning::UnexpectedField]
);
}
const COLOR_VALUES: &[&str] = &["RED", "GREEN", "BLUE"];
static PAINT_OBJ: Object = Object {
fields: &[Field::required("color", Scalar::Enum(COLOR_VALUES))],
kind: BuilderKind::Ignore,
};
static PAINT_SCHEMA: Schema = Schema::Object(&PAINT_OBJ);
#[test]
fn enum_permitted_value_is_clean() {
let doc = parse(r#"{"color": "GREEN"}"#.into()).unwrap();
let warnings = walk(&doc, &PAINT_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn enum_value_matched_case_insensitively() {
let doc = parse(r#"{"color": "green"}"#.into()).unwrap();
let warnings = walk(&doc, &PAINT_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn enum_value_matched_escape_aware_and_case_insensitively() {
let doc = parse(r#"{"color": "\u0067reen"}"#.into()).unwrap();
let warnings = walk(&doc, &PAINT_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn enum_unknown_value_reported() {
let doc = parse(r#"{"color": "PURPLE"}"#.into()).unwrap();
let warnings = walk(&doc, &PAINT_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.color"],
vec![&Warning::FieldInvalidValue {
expected: COLOR_VALUES,
actual: "PURPLE".to_owned(),
}]
);
}
#[test]
fn enum_non_string_reported_as_type_mismatch() {
let doc = parse(r#"{"color": 7}"#.into()).unwrap();
let warnings = walk(&doc, &PAINT_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.color"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::String,
actual: json::ValueKind::Number,
}]
);
}
static LABEL_OBJ: Object = Object {
fields: &[Field::required("code", Scalar::StringMax(3))],
kind: BuilderKind::Ignore,
};
static LABEL_SCHEMA: Schema = Schema::Object(&LABEL_OBJ);
#[test]
fn string_within_max_is_clean() {
let doc = parse(r#"{"code": "AB"}"#.into()).unwrap();
let warnings = walk(&doc, &LABEL_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn string_at_max_is_clean() {
let doc = parse(r#"{"code": "ABC"}"#.into()).unwrap();
let warnings = walk(&doc, &LABEL_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn string_over_max_reported() {
let doc = parse(r#"{"code": "ABCD"}"#.into()).unwrap();
let warnings = walk(&doc, &LABEL_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.code"],
vec![&Warning::StringTooLong { max: 3, len: 4 }]
);
}
#[test]
fn string_max_non_string_reported_as_type_mismatch() {
let doc = parse(r#"{"code": 7}"#.into()).unwrap();
let warnings = walk(&doc, &LABEL_SCHEMA).into_warnings();
assert_eq!(warnings.len_warnings(), 1);
assert_eq!(
warnings.path_map()["$.code"],
vec![&Warning::TypeMismatch {
expected: json::ValueKind::String,
actual: json::ValueKind::Number,
}]
);
}
#[test]
fn string_max_counts_decoded_characters() {
let doc = parse(r#"{"code": "\u0041\u0041\u0041"}"#.into()).unwrap();
let warnings = walk(&doc, &LABEL_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}
#[test]
fn scalar_any_is_not_type_checked() {
static OPAQUE_OBJ: Object = Object {
fields: &[Field::optional("blob", Scalar::Any)],
kind: BuilderKind::Ignore,
};
static OPAQUE_SCHEMA: Schema = Schema::Object(&OPAQUE_OBJ);
let doc = parse(r#"{"blob": {"nested": 1}}"#.into()).unwrap();
let warnings = walk(&doc, &OPAQUE_SCHEMA).into_warnings();
assert!(warnings.is_empty());
}