use std::mem;
use serde::Deserialize;
use super::*;
use crate::DecodeErrorKind;
#[derive(Debug, Deserialize)]
struct Row<'a> {
#[serde(borrow)]
value: Content<'a>,
}
fn row(document: &[u8]) -> Result<Row<'_>, DecodeError> {
codec::decode::<Row<'_>>(document)
}
fn encoded(content: &Content<'_>) -> String {
let mut buffer = Vec::new();
codec::encode_into(&mut buffer, content).expect("content always encodes");
String::from_utf8(buffer).expect("the codec emits UTF-8")
}
#[test]
fn content_stays_small() {
assert_eq!(
mem::size_of::<Content<'_>>(),
mem::size_of::<Cow<'_, str>>(),
"Content is {} bytes against {} for a Cow",
mem::size_of::<Content<'_>>(),
mem::size_of::<Cow<'_, str>>()
);
assert_eq!(mem::size_of::<Content<'_>>(), 24);
}
#[test]
fn a_string_decodes_as_text_and_borrows_the_input() {
let document = br#"{"value":"neutral or polite"}"#;
let decoded = row(document).expect("a string is valid content");
assert_eq!(decoded.value.as_text(), Some("neutral or polite"));
assert_eq!(decoded.value.as_json(), None);
let borrowed = decoded.value.as_text().expect("it is text").as_ptr().addr();
let start = document.as_ptr().addr();
assert!(
(start..start + document.len()).contains(&borrowed),
"text without escapes must point into the response buffer, not a copy"
);
}
#[test]
fn an_escaped_string_decodes_to_its_value() {
let document = br#"{"value":"caf\u00e9 \"quoted\" \\ \u2028"}"#;
let decoded = row(document).expect("escapes are valid content");
assert_eq!(decoded.value.as_text(), Some("caf\u{e9} \"quoted\" \\ \u{2028}"));
}
#[test]
fn an_object_and_an_array_keep_their_text() {
let object = br#"{"value":{"label":"ok","weight":2}}"#;
let array = br#"{"value":["great","best"]}"#;
let decoded_object = row(object).expect("an object is valid content");
let decoded_array = row(array).expect("an array is valid content");
assert_eq!(decoded_object.value.as_text(), None);
assert_eq!(
decoded_object.value.as_json().map(RawJson::as_str),
Some(r#"{"label":"ok","weight":2}"#)
);
assert_eq!(decoded_array.value.as_json().map(RawJson::as_str), Some(r#"["great","best"]"#));
}
#[test]
fn every_shape_re_encodes_to_what_it_came_from() {
for (document, field) in [
(&br#"{"value":"neutral or polite"}"#[..], r#""neutral or polite""#),
(&br#"{"value":"caf\u00e9 \"quoted\" \\"}"#[..], "\"caf\u{e9} \\\"quoted\\\" \\\\\""),
(&br#"{"value":{"label":"ok","weight":2}}"#[..], r#"{"label":"ok","weight":2}"#),
(&br#"{"value":["great","best"]}"#[..], r#"["great","best"]"#),
] {
let decoded = row(document).expect("valid content");
assert_eq!(
encoded(&decoded.value),
field,
"round trip of {}",
String::from_utf8_lossy(document)
);
}
}
#[test]
fn a_raw_value_is_spliced_in_rather_than_re_rendered() {
let document = br#"{"value":{"b":1,"a":[ ],"n":-0.0}}"#;
let decoded = row(document).expect("an object is valid content");
assert_eq!(
encoded(&decoded.value),
r#"{"b":1,"a":[ ],"n":-0.0}"#,
"key order, the space and the sign of the zero all survive"
);
}
#[test]
fn numbers_booleans_and_null_are_refused() {
for document in [
&br#"{"value":42}"#[..],
&br#"{"value":-0.5}"#[..],
&br#"{"value":true}"#[..],
&br#"{"value":false}"#[..],
&br#"{"value":null}"#[..],
] {
let error = row(document).expect_err("only string, object and array are content");
assert_eq!(
error.kind(),
DecodeErrorKind::Data,
"{} gave {error}",
String::from_utf8_lossy(document)
);
assert_eq!(error.path(), "value");
}
assert_eq!(Content::json(&42).expect_err("a number is not content"), ContentError::Shape);
assert_eq!(Content::json(&true).expect_err("a boolean is not content"), ContentError::Shape);
assert_eq!(
Content::json(&Option::<u8>::None).expect_err("null is not content"),
ContentError::Shape
);
}
#[test]
fn a_value_that_encodes_to_a_string_is_text() {
let from_value = Content::json(&"payments or invoices").expect("a string is content");
assert_eq!(from_value, Content::text("payments or invoices"));
assert_eq!(from_value.as_text(), Some("payments or invoices"));
}
#[test]
fn a_structured_value_becomes_raw_json() {
#[derive(Debug, serde::Serialize)]
struct Criterion {
label: &'static str,
weight: u8,
}
let content =
Content::json(&Criterion { label: "ok", weight: 2 }).expect("a struct is content");
assert_eq!(content.as_json().map(RawJson::as_str), Some(r#"{"label":"ok","weight":2}"#));
assert_eq!(encoded(&content), r#"{"label":"ok","weight":2}"#);
}
#[test]
fn borrowed_text_can_be_detached_from_its_input() {
let owned = {
let document = String::from(r#"{"value":"neutral or polite"}"#);
let decoded = row(document.as_bytes()).expect("a string is valid content");
decoded.value.into_owned()
};
assert_eq!(owned.as_text(), Some("neutral or polite"));
}
#[test]
fn text_is_escaped_on_the_way_out() {
let content = Content::text("a \"quoted\" \\ caf\u{e9}");
assert_eq!(encoded(&content), "\"a \\\"quoted\\\" \\\\ caf\u{e9}\"");
}
#[derive(Debug, serde::Serialize, Deserialize)]
struct Fields<'a> {
#[serde(borrow)]
instructions: Content<'a>,
#[serde(borrow)]
criteria: Content<'a>,
#[serde(borrow)]
label: Content<'a>,
}
#[test]
fn every_shape_round_trips_through_another_codec() {
for (document, data) in [
(&br#"{"value":"neutral or polite"}"#[..], serde_json::json!("neutral or polite")),
(
&br#"{"value":{"label":"ok","weight":2,"sub":{"deep":[1,2]}}}"#[..],
serde_json::json!({"label": "ok", "weight": 2, "sub": {"deep": [1, 2]}}),
),
(
&br#"{"value":["great","best",{"n":3}]}"#[..],
serde_json::json!(["great", "best", {"n": 3}]),
),
(
&br#"{"value":"caf\u00e9 \"quoted\" \\"}"#[..],
serde_json::json!("caf\u{e9} \"quoted\" \\"),
),
] {
let decoded = row(document).expect("valid content");
let written = serde_json::to_string(&decoded.value).expect("another codec writes it");
assert!(!written.contains("$sonic_rs"), "token leaked: {written}");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&written).expect("the output is JSON"),
data,
"from {}",
String::from_utf8_lossy(document)
);
let read_back: Content<'_> =
serde_json::from_str(&written).expect("another codec reads it back");
assert_eq!(
serde_json::to_value(&read_back).expect("it is JSON"),
data,
"round trip of {}",
String::from_utf8_lossy(document)
);
}
}
#[test]
fn a_struct_of_content_fields_carries_the_same_data_through_both_codecs() {
let document = br#"{
"instructions": "rank the replies",
"criteria": {"tone": ["warm", "brief"], "length": {"max": 40}},
"label": ["a", "b"]
}"#;
let fields: Fields<'_> = codec::decode(document).expect("the question decodes");
let mut buffer = Vec::new();
codec::encode_into(&mut buffer, &fields).expect("this codec writes it");
let ours = String::from_utf8(buffer).expect("the codec emits UTF-8");
let theirs = serde_json::to_string(&fields).expect("another codec writes it");
assert!(!theirs.contains("$sonic_rs"), "token leaked: {theirs}");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&ours).expect("our output is JSON"),
serde_json::from_str::<serde_json::Value>(&theirs).expect("theirs is too"),
"this codec wrote {ours}, the other wrote {theirs}"
);
assert!(ours.contains(r#"{"tone": ["warm", "brief"], "length": {"max": 40}}"#), "{ours}");
assert!(theirs.contains(r#"{"tone":["warm","brief"],"length":{"max":40}}"#), "{theirs}");
}
#[test]
fn text_that_is_not_one_json_value_never_becomes_content() {
use serde::de::value::{BorrowedStrDeserializer, Error as ValueError};
for (text, expected) in [
(r#"{"a":1}, "model": "evil""#, "invalid JSON syntax at line 1 column 8"),
("{not json", "invalid JSON syntax at line 1 column 2"),
("hello", "invalid JSON syntax at line 1 column 1"),
("", "invalid JSON syntax at line 1 column 1"),
] {
let refused = <Content<'_> as Deserialize>::deserialize(BorrowedStrDeserializer::<
ValueError,
>::new(text));
let error = refused.expect_err(text).to_string();
assert!(error.contains(expected), "{text:?} was refused as {error}");
assert!(!error.contains("evil"), "the refusal quoted the input: {error}");
assert!(!error.contains("not json"), "the refusal quoted the input: {error}");
}
let accepted = <Content<'_> as Deserialize>::deserialize(
BorrowedStrDeserializer::<ValueError>::new(r#"{"a":1}"#),
)
.expect("one complete value is content");
assert_eq!(accepted.as_json().map(RawJson::as_str), Some(r#"{"a":1}"#));
}
#[test]
fn text_converts_into_the_text_shape_and_keeps_its_borrow() {
let source = String::from("payments or invoices");
let from_str = Content::from(source.as_str());
let from_string = Content::from(source.clone());
let from_borrowed = Content::from(Cow::Borrowed(source.as_str()));
let from_owned = Content::from(Cow::<str>::Owned(source.clone()));
for (case, content) in [
("&str", &from_str),
("String", &from_string),
("Cow::Borrowed", &from_borrowed),
("Cow::Owned", &from_owned),
] {
assert_eq!(*content, Content::text(source.as_str()), "{case}");
assert_eq!(content.as_text(), Some("payments or invoices"), "{case}");
assert_eq!(content.as_json(), None, "{case}");
}
for (case, content) in [("&str", &from_str), ("Cow::Borrowed", &from_borrowed)] {
assert!(
matches!(content.repr, Repr::Text(Cow::Borrowed(_))),
"{case} stays borrowed: {content:?}"
);
let text = content.as_text().expect("text");
assert!(std::ptr::eq(text, source.as_str()), "{case} points into the source");
}
for (case, content) in [("String", &from_string), ("Cow::Owned", &from_owned)] {
assert!(matches!(content.repr, Repr::Text(Cow::Owned(_))), "{case} is owned: {content:?}");
}
}
#[test]
fn a_converted_string_round_trips_through_both_codecs() {
let text = String::from("quote \" backslash \\ newline \n tab \t nul \u{0000} earth \u{1F30D}");
let content = Content::from(text.clone());
let ours = encoded(&content);
assert_eq!(
ours,
r#""quote \" backslash \\ newline \n tab \t nul \u0000 earth "#.to_owned() + "\u{1F30D}\""
);
assert_eq!(codec::decode::<String>(ours.as_bytes()).expect("decodes"), text);
let theirs = serde_json::to_string(&content).expect("serde_json encodes text");
assert_eq!(serde_json::from_str::<String>(&theirs).expect("serde_json decodes"), text);
assert_eq!(
serde_json::from_str::<Content<'_>>(&theirs).expect("reads back as content"),
content
);
}