use std::str::FromStr;
use qubit_budget::json::JsonDecodeLimits;
use qubit_budget::json::JsonResource;
use qubit_json::decode::DiagnosticPolicy;
use qubit_json::decode::JsonDecodeErrorKind;
use qubit_json::decode::JsonDecodeErrorSource;
use qubit_json::decode::JsonDecodeStage;
use qubit_json::decode::JsonDecoder;
use qubit_json::decode::JsonMaterializationErrorReason;
use qubit_json::decode::JsonRootKind;
use qubit_json::decode::JsonSyntaxErrorReason;
use qubit_json::decode::MarkdownFencePolicy;
use qubit_json::decode::NormalizingJsonDecodePolicy;
use qubit_json::decode::NormalizingJsonDecoder;
fn no_normalization_policy() -> NormalizingJsonDecodePolicy {
NormalizingJsonDecodePolicy::builder()
.trim_whitespace(false)
.strip_utf8_bom(false)
.markdown_fence_policy(MarkdownFencePolicy::Disabled)
.escape_control_chars_in_strings(false)
.build()
}
#[test]
fn test_json_decode_error_reports_strict_parse_failure() {
let mut decoder = JsonDecoder::unlimited();
let error = decoder
.validate_str("{")
.expect_err("an incomplete object must fail validation");
assert_eq!(error.kind(), JsonDecodeErrorKind::InvalidJson);
assert_eq!(error.stage(), JsonDecodeStage::Parse);
assert_eq!(error.raw_input_bytes(), 1);
assert!(error.syntax_error().is_some());
assert!(error.budget_error().is_none());
}
#[test]
fn test_redacted_syntax_diagnostics_do_not_reveal_unexpected_input_bytes() {
let mut decoder = JsonDecoder::unlimited();
let error = decoder
.validate_utf8(b"@")
.expect_err("an unexpected byte must fail validation");
let syntax = error.syntax_error().expect("expected a syntax error");
assert_eq!(error.diagnostic_policy(), DiagnosticPolicy::Redacted);
assert_eq!(syntax.reason(), JsonSyntaxErrorReason::UnexpectedByte);
assert_eq!(
syntax.to_string(),
"unexpected byte at line 1 column 1 (byte offset 0)"
);
assert!(!format!("{error:?}").contains("0x40"));
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn test_redacted_deserialize_without_coordinates_omits_location() {
let mut decoder = JsonDecoder::unlimited();
let error = decoder
.decode_unique_str::<bool>("1")
.expect_err("type mismatch");
assert_eq!(error.kind(), JsonDecodeErrorKind::Deserialize);
assert_eq!(error.line(), None);
assert_eq!(error.column(), None);
assert_eq!(error.to_string(), "Failed to deserialize JSON value");
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn test_deserialize_without_coordinates_preserves_diagnostic_policy() {
for policy in [DiagnosticPolicy::Redacted, DiagnosticPolicy::Detailed] {
let mut decoder = JsonDecoder::unlimited().with_diagnostic_policy(policy);
let error = decoder
.decode_unique_str::<bool>(r#""RJ_SECRET""#)
.expect_err("type mismatch");
assert_eq!(error.line(), None);
assert_eq!(error.column(), None);
let message = error.to_string();
assert!(!message.contains("line 0"));
assert!(!message.contains("column 0"));
match policy {
DiagnosticPolicy::Redacted => {
assert!(!message.contains("RJ_SECRET"));
assert!(!format!("{error:?}").contains("RJ_SECRET"));
assert!(std::error::Error::source(&error).is_none());
}
DiagnosticPolicy::Detailed => {
assert!(std::error::Error::source(&error).is_some());
}
}
}
}
#[test]
fn test_deserialize_with_coordinates_retains_location() {
let mut strict = JsonDecoder::unlimited();
let error = strict.decode_str::<bool>("1").expect_err("type mismatch");
let line = error.line().expect("known line");
let column = error.column().expect("known column");
assert!(
error
.to_string()
.ends_with(&format!("at line {line} column {column}"))
);
let mut normalizing = NormalizingJsonDecoder::with_limits(
no_normalization_policy(),
JsonDecodeLimits::<JsonResource, usize>::default(),
);
let error = normalizing
.decode_str::<bool>("1")
.expect_err("type mismatch");
let line = error.line().expect("known normalized line");
let column = error.column().expect("known normalized column");
assert!(
error
.to_string()
.ends_with(&format!("at normalized line {line} column {column}"))
);
}
#[test]
fn test_json_decode_error_reports_normalization_failure() {
let mut decoder = NormalizingJsonDecoder::with_limits(
NormalizingJsonDecodePolicy::lenient(),
JsonDecodeLimits::<JsonResource, usize>::default(),
);
let error = decoder
.decode_value("")
.expect_err("empty normalized input must fail");
assert_eq!(error.kind(), JsonDecodeErrorKind::EmptyInput);
assert_eq!(error.stage(), JsonDecodeStage::Normalize);
assert_eq!(error.raw_input_bytes(), 0);
}
#[test]
fn test_decoder_facades_share_error_classification() {
let mut strict = JsonDecoder::unlimited();
let mut normalizing = NormalizingJsonDecoder::with_limits(
no_normalization_policy(),
JsonDecodeLimits::<JsonResource, usize>::default(),
);
let strict_syntax = strict
.decode_str::<serde_json::Value>("{")
.expect_err("syntax error");
let normalizing_syntax = normalizing
.decode_str::<serde_json::Value>("{")
.expect_err("syntax error");
assert_eq!(strict_syntax.kind(), normalizing_syntax.kind());
assert_eq!(strict_syntax.stage(), normalizing_syntax.stage());
let strict_target = strict.decode_str::<bool>("1").expect_err("target error");
let normalizing_target = normalizing
.decode_str::<bool>("1")
.expect_err("target error");
assert_eq!(strict_target.kind(), normalizing_target.kind());
assert_eq!(strict_target.stage(), normalizing_target.stage());
let limits = JsonDecodeLimits::builder().max_input_bytes(1_usize).build();
let mut strict = JsonDecoder::with_limits(limits);
let mut normalizing = NormalizingJsonDecoder::with_limits(no_normalization_policy(), limits);
let strict_budget = strict.decode_str::<bool>("true").expect_err("input budget");
let normalizing_budget = normalizing
.decode_str::<bool>("true")
.expect_err("input budget");
assert_eq!(strict_budget.kind(), normalizing_budget.kind());
assert_eq!(strict_budget.stage(), normalizing_budget.stage());
}
#[test]
fn test_json_decode_error_kind_and_stage_round_trip() {
let kinds = [
(JsonDecodeErrorKind::Budget, "budget"),
(JsonDecodeErrorKind::EmptyInput, "empty_input"),
(JsonDecodeErrorKind::InvalidUtf8, "invalid_utf8"),
(JsonDecodeErrorKind::InvalidJson, "invalid_json"),
(
JsonDecodeErrorKind::UnexpectedTopLevel,
"unexpected_top_level",
),
(JsonDecodeErrorKind::Deserialize, "deserialize"),
];
for (kind, text) in kinds {
assert_eq!(kind.to_string(), text);
assert_eq!(JsonDecodeErrorKind::from_str(text), Ok(kind));
}
assert!(JsonDecodeErrorKind::from_str("unknown").is_err());
let stages = [
(JsonDecodeStage::Input, "input"),
(JsonDecodeStage::DecodeText, "decode_text"),
(JsonDecodeStage::Normalize, "normalize"),
(JsonDecodeStage::Admission, "admission"),
(JsonDecodeStage::Parse, "parse"),
(JsonDecodeStage::TopLevelCheck, "top_level_check"),
(JsonDecodeStage::Deserialize, "deserialize"),
];
for (stage, text) in stages {
assert_eq!(stage.to_string(), text);
assert_eq!(JsonDecodeStage::from_str(text), Ok(stage));
}
assert!(JsonDecodeStage::from_str("unknown").is_err());
}
#[test]
fn test_json_decode_error_model_representative_matrix() {
let mut invalid_utf8 = JsonDecoder::unlimited();
let error = invalid_utf8
.validate_utf8(&[0xff])
.expect_err("invalid UTF-8");
assert_eq!(
(error.kind(), error.stage()),
(
JsonDecodeErrorKind::InvalidUtf8,
JsonDecodeStage::DecodeText
),
);
assert_eq!(error.utf8_valid_up_to(), Some(0));
let mut decoder = NormalizingJsonDecoder::with_limits(
NormalizingJsonDecodePolicy::lenient(),
JsonDecodeLimits::<JsonResource, usize>::default(),
);
let document = decoder.prepare_str("null").expect("prepare scalar");
let error = decoder
.decode_precharged_object_document::<serde_json::Value>(&document)
.expect_err("object contract");
assert_eq!(
(error.kind(), error.stage()),
(
JsonDecodeErrorKind::UnexpectedTopLevel,
JsonDecodeStage::TopLevelCheck,
),
);
let error = decoder
.decode_precharged_document::<bool>(&document)
.expect_err("target mismatch");
assert_eq!(
(error.kind(), error.stage()),
(
JsonDecodeErrorKind::Deserialize,
JsonDecodeStage::Deserialize,
),
);
}
#[test]
fn test_json_decode_error_into_source_exposes_owned_variants() {
let limits = JsonDecodeLimits::builder().max_input_bytes(1_usize).build();
let mut decoder = JsonDecoder::with_limits(limits);
match decoder
.validate_str("null")
.expect_err("input budget")
.into_source()
{
JsonDecodeErrorSource::Budget {
stage,
raw_input_bytes,
source,
..
} => {
assert_eq!(stage, JsonDecodeStage::Input);
assert_eq!(raw_input_bytes, 4);
assert_eq!(source.resource(), &JsonResource::InputBytes);
}
source => panic!("expected budget source, got {source:?}"),
}
let mut normalizing = NormalizingJsonDecoder::with_limits(
NormalizingJsonDecodePolicy::lenient(),
JsonDecodeLimits::<JsonResource, usize>::default(),
);
match normalizing
.decode_value("")
.expect_err("empty input")
.into_source()
{
JsonDecodeErrorSource::EmptyInput {
stage,
raw_input_bytes,
..
} => {
assert_eq!(stage, JsonDecodeStage::Normalize);
assert_eq!(raw_input_bytes, 0);
}
source => panic!("expected empty-input source, got {source:?}"),
}
let mut decoder = JsonDecoder::unlimited().with_diagnostic_policy(DiagnosticPolicy::Detailed);
match decoder
.validate_utf8(&[0xff])
.expect_err("invalid UTF-8")
.into_source()
{
JsonDecodeErrorSource::InvalidUtf8 {
valid_up_to,
error_len,
source,
..
} => {
assert_eq!(valid_up_to, 0);
assert_eq!(error_len, Some(1));
assert!(source.is_some());
}
source => panic!("expected invalid-UTF-8 source, got {source:?}"),
}
match decoder
.validate_str("{")
.expect_err("invalid JSON")
.into_source()
{
JsonDecodeErrorSource::InvalidJson { syntax, source, .. } => {
assert_eq!(syntax.offset(), 1);
assert!(source.is_some());
}
source => panic!("expected invalid-JSON source, got {source:?}"),
}
match decoder
.decode_object_str::<serde_json::Value>("null")
.expect_err("object contract")
.into_source()
{
JsonDecodeErrorSource::UnexpectedTopLevel {
expected, actual, ..
} => {
assert_eq!(expected, JsonRootKind::Object);
assert_eq!(actual, JsonRootKind::Other);
}
source => panic!("expected top-level source, got {source:?}"),
}
match decoder
.decode_str::<bool>("1")
.expect_err("target mismatch")
.into_source()
{
JsonDecodeErrorSource::Deserialize {
line,
column,
source,
..
} => {
assert_eq!(line, 1);
assert_eq!(column, 1);
assert!(source.is_some());
}
source => panic!("expected deserialization source, got {source:?}"),
}
}
#[test]
fn test_unique_decode_materialization_reason_matrix() {
let mut decoder = JsonDecoder::unlimited();
let error = decoder
.decode_unique_str::<serde_json::Value>(r#"{"RJ_SECRET":1,"RJ_SECRET":2}"#)
.expect_err("duplicate decoded keys must fail construction");
assert_eq!(error.kind(), JsonDecodeErrorKind::Deserialize);
assert_eq!(error.stage(), JsonDecodeStage::Deserialize);
assert_eq!(
error.materialization_error_reason(),
Some(JsonMaterializationErrorReason::DuplicateKey)
);
assert_eq!(
error.clone().materialization_error_reason(),
error.materialization_error_reason()
);
assert!(!error.to_string().contains("RJ_SECRET"));
assert!(!format!("{error:?}").contains("RJ_SECRET"));
assert!(std::error::Error::source(&error).is_none());
let reason = error.materialization_error_reason();
assert!(matches!(
error.into_source(),
JsonDecodeErrorSource::Deserialize { .. }
));
assert_eq!(reason, Some(JsonMaterializationErrorReason::DuplicateKey));
let mut detailed = JsonDecoder::unlimited().with_diagnostic_policy(DiagnosticPolicy::Detailed);
let error = detailed
.decode_unique_str::<serde_json::Value>(r#"{"x":1,"x":2}"#)
.expect_err("detailed duplicate construction must fail");
assert_eq!(
error.materialization_error_reason(),
Some(JsonMaterializationErrorReason::DuplicateKey)
);
assert!(std::error::Error::source(&error).is_some());
let legacy_depth_reason = JsonMaterializationErrorReason::DepthLimit { limit: 127 };
assert_eq!(
format!("{legacy_depth_reason:?}"),
"DepthLimit { limit: 127 }"
);
let mut decoder = JsonDecoder::unlimited();
let target_error = decoder
.decode_unique_str::<bool>("1")
.expect_err("target type mismatch");
assert_eq!(target_error.materialization_error_reason(), None);
let syntax_error = decoder
.decode_unique_str::<serde_json::Value>("{")
.expect_err("invalid syntax");
assert_eq!(syntax_error.materialization_error_reason(), None);
let ordinary_error = decoder
.decode_str::<serde_json::Value>("{")
.expect_err("ordinary syntax error");
assert_eq!(ordinary_error.materialization_error_reason(), None);
let utf8_error = decoder
.decode_unique_utf8::<serde_json::Value>(&[0xff])
.expect_err("invalid UTF-8");
assert_eq!(utf8_error.materialization_error_reason(), None);
let limits = JsonDecodeLimits::builder().max_depth(2_usize).build();
let mut decoder = JsonDecoder::with_limits(limits);
let budget_error = decoder
.decode_unique_str::<serde_json::Value>("[[[0]]]")
.expect_err("configured depth budget is checked before materialization");
assert_eq!(budget_error.kind(), JsonDecodeErrorKind::Budget);
assert_eq!(budget_error.materialization_error_reason(), None);
let input_limits = JsonDecodeLimits::builder().max_input_bytes(0_usize).build();
let mut decoder = JsonDecoder::with_limits(input_limits);
let input_error = decoder
.decode_unique_str::<serde_json::Value>("null")
.expect_err("input budget must reject the document");
assert_eq!(input_error.materialization_error_reason(), None);
}