use crate::error::JacsError;
use crate::protocol::canonicalize_json;
pub fn jacs_to_yaml(json_str: &str) -> Result<String, JacsError> {
let value: serde_json::Value = serde_json::from_str(json_str)
.map_err(|e| JacsError::conversion("JSON", "YAML", format!("invalid JSON input: {}", e)))?;
serde_yaml_ng::to_string(&value).map_err(|e| {
JacsError::conversion("JSON", "YAML", format!("YAML serialization failed: {}", e))
})
}
fn parse_yaml_value(yaml_str: &str) -> Result<serde_json::Value, JacsError> {
let value: serde_json::Value = serde_yaml_ng::from_str(yaml_str)
.map_err(|e| JacsError::conversion("YAML", "JSON", format!("invalid YAML input: {}", e)))?;
if !value.is_object() && !value.is_array() {
return Err(JacsError::conversion(
"YAML",
"JSON",
"YAML must deserialize to a JSON object or array, not a bare scalar",
));
}
Ok(value)
}
pub fn yaml_to_jacs(yaml_str: &str) -> Result<String, JacsError> {
let value = parse_yaml_value(yaml_str)?;
serde_json::to_string_pretty(&value).map_err(|e| {
JacsError::conversion("YAML", "JSON", format!("JSON serialization failed: {}", e))
})
}
pub fn yaml_to_jacs_canonical(yaml_str: &str) -> Result<String, JacsError> {
let value = parse_yaml_value(yaml_str)?;
Ok(canonicalize_json(&value))
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_canonical_round_trip(json_str: &str) {
let original: serde_json::Value =
serde_json::from_str(json_str).expect("test input should be valid JSON");
let original_canonical = canonicalize_json(&original);
let yaml = jacs_to_yaml(json_str).expect("jacs_to_yaml should succeed");
let round_tripped =
yaml_to_jacs_canonical(&yaml).expect("yaml_to_jacs_canonical should succeed");
assert_eq!(
original_canonical, round_tripped,
"Canonical JSON mismatch after YAML round-trip.\nOriginal: {}\nRound-tripped: {}",
original_canonical, round_tripped
);
}
#[test]
fn yaml_round_trip_simple_object() {
assert_canonical_round_trip(r#"{"key": "value"}"#);
}
#[test]
fn yaml_round_trip_nested_object() {
assert_canonical_round_trip(
r#"{"outer": {"inner": {"deep": [1, 2, 3]}, "sibling": "value"}}"#,
);
}
#[test]
fn yaml_round_trip_null_values() {
assert_canonical_round_trip(r#"{"key": null}"#);
}
#[test]
fn yaml_round_trip_boolean_not_stringified() {
let json_str = r#"{"flag": true}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let value: serde_json::Value = serde_json::from_str(&back).unwrap();
assert!(
value["flag"].is_boolean(),
"Boolean should remain a boolean, not become a string"
);
assert_eq!(value["flag"], true);
}
#[test]
fn yaml_round_trip_integer_preserved() {
let json_str = r#"{"count": 42}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let value: serde_json::Value = serde_json::from_str(&back).unwrap();
assert!(value["count"].is_number());
assert_eq!(value["count"], 42);
}
#[test]
fn yaml_round_trip_empty_object() {
assert_canonical_round_trip(r#"{}"#);
}
#[test]
fn yaml_round_trip_array() {
assert_canonical_round_trip(r#"[1, "two", null, true]"#);
}
#[test]
fn yaml_to_jacs_invalid_yaml_returns_error() {
let result = yaml_to_jacs("{{{{ not yaml ::::");
assert!(result.is_err());
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Conversion from YAML to JSON failed"),
"Error should mention conversion direction: {}",
msg
);
}
#[test]
fn yaml_to_jacs_bare_scalar_returns_error() {
let result = yaml_to_jacs("just a string");
assert!(result.is_err());
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("bare scalar"),
"Error should mention bare scalar: {}",
msg
);
}
#[test]
fn jacs_to_yaml_invalid_json_returns_error() {
let result = jacs_to_yaml("{not valid json}");
assert!(result.is_err());
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Conversion from JSON to YAML failed"),
"Error should mention conversion direction: {}",
msg
);
}
#[test]
fn yaml_output_is_valid_yaml() {
let json_str = r#"{"hello": "world", "count": 42}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let _: serde_json::Value =
serde_yaml_ng::from_str(&yaml).expect("YAML output should be valid YAML");
}
#[test]
fn yaml_round_trip_unicode() {
assert_canonical_round_trip(r#"{"name": "\u00e9"}"#);
assert_canonical_round_trip(r#"{"emoji": "Hello \ud83c\udf0d"}"#);
assert_canonical_round_trip(r#"{"cjk": "\u4e16\u754c"}"#);
}
#[test]
fn yaml_float_one_point_zero_round_trips() {
let json_str = r#"{"val": 1.0}"#;
let original: serde_json::Value = serde_json::from_str(json_str).unwrap();
let original_canonical = canonicalize_json(&original);
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let reconstituted: serde_json::Value = serde_json::from_str(&back).unwrap();
let reconstituted_canonical = canonicalize_json(&reconstituted);
assert_eq!(
original_canonical, reconstituted_canonical,
"Canonical JSON should match for float 1.0"
);
}
#[test]
fn yaml_integer_stays_integer() {
let json_str = r#"{"val": 42}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let value: serde_json::Value = serde_json::from_str(&back).unwrap();
assert!(
value["val"].is_number(),
"42 should remain a number after YAML round-trip"
);
assert_eq!(value["val"], 42);
}
#[test]
fn yaml_string_true_not_coerced_to_bool() {
let json_str = r#"{"val": "true"}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let value: serde_json::Value = serde_json::from_str(&back).unwrap();
assert!(
value["val"].is_string(),
"String 'true' should remain a string, got {:?}",
value["val"]
);
assert_eq!(value["val"].as_str().unwrap(), "true");
}
#[test]
fn yaml_string_yes_not_coerced_to_bool() {
let json_str = r#"{"val": "yes"}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let value: serde_json::Value = serde_json::from_str(&back).unwrap();
assert!(
value["val"].is_string(),
"String 'yes' should remain a string, got {:?}",
value["val"]
);
assert_eq!(value["val"].as_str().unwrap(), "yes");
}
#[test]
fn yaml_string_null_not_coerced() {
let json_str = r#"{"val": "null"}"#;
let yaml = jacs_to_yaml(json_str).unwrap();
let back = yaml_to_jacs(&yaml).unwrap();
let value: serde_json::Value = serde_json::from_str(&back).unwrap();
assert!(
value["val"].is_string(),
"String 'null' should remain a string, got {:?}",
value["val"]
);
assert_eq!(value["val"].as_str().unwrap(), "null");
}
#[test]
fn yaml_large_integer_preserved() {
let json_str = r#"{"val": 9007199254740993}"#;
assert_canonical_round_trip(json_str);
}
#[test]
fn yaml_negative_zero() {
let json_str = r#"{"val": -0.0}"#;
let original: serde_json::Value = serde_json::from_str(json_str).unwrap();
let original_canonical = canonicalize_json(&original);
let yaml = jacs_to_yaml(json_str).unwrap();
let back_canonical = yaml_to_jacs_canonical(&yaml).unwrap();
assert_eq!(
original_canonical, back_canonical,
"Canonical form of -0.0 should be consistent after round-trip"
);
}
#[test]
fn yaml_empty_string() {
assert_canonical_round_trip(r#"{"val": ""}"#);
}
#[test]
fn yaml_deeply_nested_object() {
let json_str =
r#"{"l1":{"l2":{"l3":{"l4":{"l5":{"l6":{"l7":{"l8":{"l9":{"l10":"deep"}}}}}}}}}}"#;
assert_canonical_round_trip(json_str);
}
#[test]
fn yaml_mixed_array_types() {
assert_canonical_round_trip(r#"[1, "two", null, true, [3], {"four": 4}]"#);
}
#[test]
fn yaml_special_json_keys() {
assert_canonical_round_trip(r#"{"@context": "http://example.com", "$schema": "test"}"#);
}
#[test]
fn yaml_multiline_string_value() {
let json_str = r#"{"text": "line1\nline2\nline3"}"#;
assert_canonical_round_trip(json_str);
}
#[test]
fn yaml_escaped_characters() {
let json_str = r#"{"val": "tab\there\nnewline"}"#;
assert_canonical_round_trip(json_str);
}
}