use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::Value;
pub fn assert_schema_matches<T>(raw: &str)
where
T: DeserializeOwned + Serialize,
{
let value: Value =
serde_json::from_str(raw).unwrap_or_else(|e| panic!("payload is not valid JSON: {e}"));
assert_schema_value::<T>(&value);
}
pub fn assert_schema_value<T>(value: &Value)
where
T: DeserializeOwned + Serialize,
{
let parsed: T = serde_json::from_value(value.clone()).unwrap_or_else(|e| {
panic!(
"payload does not deserialize into `{}`: {e}",
std::any::type_name::<T>()
)
});
let schema_value = serde_json::to_value(&parsed)
.unwrap_or_else(|e| panic!("reserializing must not fail: {e}"));
let mut problems = Vec::new();
diff_keys("$", value, &schema_value, &mut problems);
assert!(
problems.is_empty(),
"schema mismatch with `{}`:\n{}",
std::any::type_name::<T>(),
problems.join("\n")
);
}
pub fn check_no_extra_data<T>(value: &Value) -> Result<(), String>
where
T: DeserializeOwned + Serialize,
{
let parsed: T = serde_json::from_value(value.clone()).map_err(|e| {
format!(
"payload does not deserialize into `{}`: {e}",
std::any::type_name::<T>()
)
})?;
let schema_value = serde_json::to_value(&parsed)
.unwrap_or_else(|e| panic!("reserializing must not fail: {e}"));
let mut problems = Vec::new();
diff_extra_keys("$", value, &schema_value, &mut problems);
if problems.is_empty() {
Ok(())
} else {
Err(format!(
"unknown fields in live API response for `{}`:\n{}",
std::any::type_name::<T>(),
problems.join("\n")
))
}
}
fn diff_extra_keys(path: &str, api: &Value, schema: &Value, problems: &mut Vec<String>) {
match (api, schema) {
(Value::Object(api_obj), Value::Object(schema_obj)) => {
for key in api_obj.keys() {
if !schema_obj.contains_key(key) {
problems.push(format!(
"{path}.{key}: present in API response but absent from library struct (extra data)"
));
}
}
for key in api_obj.keys().filter(|k| schema_obj.contains_key(*k)) {
diff_extra_keys(
&format!("{path}.{key}"),
&api_obj[key],
&schema_obj[key],
problems,
);
}
}
(Value::Array(api_arr), Value::Array(schema_arr)) => {
for (i, (api_item, schema_item)) in api_arr.iter().zip(schema_arr.iter()).enumerate() {
diff_extra_keys(&format!("{path}[{i}]"), api_item, schema_item, problems);
}
}
_ => {}
}
}
fn diff_keys(path: &str, api: &Value, schema: &Value, problems: &mut Vec<String>) {
match (api, schema) {
(Value::Object(api_obj), Value::Object(schema_obj)) => {
for key in api_obj.keys() {
if !schema_obj.contains_key(key) {
problems.push(format!(
"{path}.{key}: present in API response but absent from library struct (extra data)"
));
}
}
for key in schema_obj.keys() {
if !api_obj.contains_key(key) {
problems.push(format!(
"{path}.{key}: declared in library struct but absent from API response"
));
}
}
for key in api_obj.keys().filter(|k| schema_obj.contains_key(*k)) {
diff_keys(
&format!("{path}.{key}"),
&api_obj[key],
&schema_obj[key],
problems,
);
}
}
(Value::Array(api_arr), Value::Array(schema_arr)) => {
for (i, (api_item, schema_item)) in api_arr.iter().zip(schema_arr.iter()).enumerate() {
diff_keys(&format!("{path}[{i}]"), api_item, schema_item, problems);
}
if api_arr.len() != schema_arr.len() {
problems.push(format!(
"{path}: array length differs (API: {}, library: {})",
api_arr.len(),
schema_arr.len()
));
}
}
_ => {}
}
}
pub fn load_fixture(rel_path: &str) -> String {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(rel_path);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read fixture `{}`: {e}", path.display()))
}