#![allow(clippy::pedantic)]
use std::collections::HashMap;
use std::path::PathBuf;
use apcore::errors::{ErrorCode, ModuleError};
use serde_json::Value;
fn find_fixtures_root() -> PathBuf {
if let Ok(spec_repo) = std::env::var("APCORE_SPEC_REPO") {
let p = PathBuf::from(&spec_repo)
.join("conformance")
.join("fixtures");
if p.is_dir() {
return p;
}
panic!("APCORE_SPEC_REPO={spec_repo} does not contain conformance/fixtures/");
}
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let sibling = manifest_dir
.parent()
.unwrap()
.join("apcore")
.join("conformance")
.join("fixtures");
if sibling.is_dir() {
return sibling;
}
panic!(
"Cannot find apcore conformance fixtures.\n\
Set APCORE_SPEC_REPO or clone apcore as a sibling of {}",
manifest_dir.parent().unwrap().display()
);
}
fn load_fixture() -> Value {
let path = find_fixtures_root().join("error_serialization.json");
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Failed to read fixture: {}", path.display()));
serde_json::from_str(&content).unwrap_or_else(|e| panic!("Invalid JSON: {e}"))
}
fn error_code_from_str(code: &str) -> ErrorCode {
serde_json::from_value(Value::String(code.to_string()))
.unwrap_or_else(|e| panic!("unknown error code {code:?}: {e}"))
}
fn build_error(input: &Value) -> ModuleError {
let code = error_code_from_str(input["code"].as_str().expect("input.code must be a string"));
let message = input["message"]
.as_str()
.expect("input.message must be a string");
let mut err = ModuleError::new(code, message);
if let Some(trace_id) = input.get("trace_id").and_then(Value::as_str) {
err = err.with_trace_id(trace_id);
}
if let Some(ai_guidance) = input.get("ai_guidance").and_then(Value::as_str) {
err = err.with_ai_guidance(ai_guidance);
}
if let Some(retryable) = input.get("retryable").and_then(Value::as_bool) {
err = err.with_retryable(retryable);
}
if let Some(user_fixable) = input.get("user_fixable").and_then(Value::as_bool) {
err.user_fixable = Some(user_fixable);
}
if let Some(details_obj) = input.get("details").and_then(Value::as_object) {
let details: HashMap<String, Value> = details_obj
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
err = err.with_details(details);
}
err
}
fn as_string_list(value: &Value) -> Vec<String> {
value
.as_array()
.map(|arr| {
arr.iter()
.map(|v| v.as_str().expect("expected a string").to_string())
.collect()
})
.unwrap_or_default()
}
#[test]
fn error_serialization_conformance() {
let fixture = load_fixture();
let cases = fixture["test_cases"]
.as_array()
.expect("test_cases must be an array");
for tc in cases {
let id = tc["id"].as_str().expect("each case needs an id");
let serialized = build_error(&tc["input"]).to_dict();
let obj = serialized.as_object().unwrap_or_else(|| {
panic!("case {id}: to_dict() must serialize to a JSON object, got {serialized}")
});
for key in as_string_list(&tc["expected_keys_present"]) {
assert!(
obj.contains_key(&key),
"case {id}: expected top-level key {key:?} to be present in {serialized}"
);
}
for key in as_string_list(&tc["expected_keys_absent"]) {
assert!(
!obj.contains_key(&key),
"case {id}: expected top-level key {key:?} to be ABSENT in {serialized}"
);
}
let detail_present = as_string_list(&tc["expected_detail_keys_present"]);
let detail_absent = as_string_list(&tc["expected_detail_keys_absent"]);
if !detail_present.is_empty() || !detail_absent.is_empty() {
let details = obj
.get("details")
.and_then(Value::as_object)
.unwrap_or_else(|| {
panic!("case {id}: expected a `details` object in {serialized}")
});
for key in detail_present {
assert!(
details.contains_key(&key),
"case {id}: expected detail key {key:?} to be present in {serialized}"
);
}
for key in detail_absent {
assert!(
!details.contains_key(&key),
"case {id}: expected detail key {key:?} to be ABSENT in {serialized}"
);
}
}
}
}