yaml-rt 0.2.3

Facade crate for YAML 1.2.2 round-trip parsing and typed overlays
Documentation
use std::time::Duration;

use yaml_rt::{FromYamlDoc, ToYamlDoc, ToYamlFragment, YamlDoc, YamlRt};

#[derive(Debug, PartialEq, Eq, YamlRt)]
struct Disabled;

#[derive(Debug, PartialEq, Eq, YamlRt)]
struct Endpoint(String, u16);

mod duration_seconds {
    use std::time::Duration;

    use yaml_rt::YamlError;

    pub type Repr = u64;

    #[expect(
        clippy::unnecessary_wraps,
        reason = "yaml(with) adapters require fallible conversion signatures"
    )]
    pub fn from_yaml(value: Repr) -> Result<Duration, YamlError> {
        Ok(Duration::from_secs(value))
    }

    #[expect(
        clippy::unnecessary_wraps,
        reason = "yaml(with) adapters require fallible conversion signatures"
    )]
    pub fn to_yaml(value: &Duration) -> Result<Repr, YamlError> {
        Ok(value.as_secs())
    }
}

#[derive(Debug, PartialEq, Eq, YamlRt)]
struct Window<T, const N: usize>(T, [u8; N], #[yaml(with = "duration_seconds")] Duration)
where
    T: Copy;

#[derive(Debug, PartialEq, Eq, YamlRt)]
struct Config {
    disabled: Disabled,
    endpoint: Endpoint,
}

#[test]
fn unit_struct_accepts_and_preserves_core_null_spellings() {
    for yaml in ["null\n", "~\n", "Null\n", "NULL\n", "!!null Null\n"] {
        let mut doc = YamlDoc::parse(yaml).expect("valid null document");
        let value = Disabled::from_yaml_doc(&doc).expect("unit struct reads null");
        value
            .apply_to_yaml_doc(&mut doc)
            .expect("unchanged unit struct writes");
        assert_eq!(doc.to_string(), yaml);
    }
}

#[test]
fn unit_struct_replaces_non_null_nodes_and_reports_invalid_reads() {
    let doc = YamlDoc::parse("enabled\n").expect("valid scalar document");
    let error = Disabled::from_yaml_doc(&doc).expect_err("non-null unit struct must fail");
    assert!(error.diagnostic.message.contains("expected YAML null"));
    assert!(error.diagnostic.position.is_some());

    let mut nested = YamlDoc::parse("disabled: false # retain\nendpoint: [api, 80]\n")
        .expect("valid config document");
    Config {
        disabled: Disabled,
        endpoint: Endpoint("api".to_owned(), 80),
    }
    .apply_to_yaml_doc(&mut nested)
    .expect("unit struct replaces nested value");
    assert_eq!(
        nested.to_string(),
        "disabled: null # retain\nendpoint: [api, 80]\n"
    );
    assert_eq!(Disabled.to_yaml_fragment(0, "\n").unwrap(), "null");
}

#[test]
fn selected_documents_support_unit_struct_roots() {
    let mut doc = YamlDoc::parse("name: app\n---\n~\n").expect("valid stream");
    let value: Disabled = doc.read_document(1).expect("selected unit struct reads");
    doc.write_document(1, &value)
        .expect("selected unit struct writes");
    assert_eq!(doc.to_string(), "name: app\n---\n~\n");
}

#[test]
fn selected_documents_support_tuple_struct_roots() {
    let mut doc = YamlDoc::parse("name: app\n---\n[api, 80]\n").expect("valid stream");
    let mut endpoint: Endpoint = doc.read_document(1).expect("selected tuple struct reads");
    endpoint.1 = 443;
    doc.write_document(1, &endpoint)
        .expect("selected tuple struct writes");
    assert_eq!(doc.to_string(), "name: app\n---\n[api, 443]\n");
}

#[test]
fn tuple_struct_patches_flow_and_block_sequences_losslessly() {
    let mut flow = YamlDoc::parse("[api, 0x50] # endpoint\n").expect("valid flow tuple");
    let mut endpoint = Endpoint::from_yaml_doc(&flow).expect("flow tuple reads");
    endpoint.1 = 443;
    endpoint
        .apply_to_yaml_doc(&mut flow)
        .expect("flow tuple writes");
    assert_eq!(flow.to_string(), "[api, 443] # endpoint\n");

    let mut block = YamlDoc::parse("- api # host\n- 0x50 # port\n").expect("valid block tuple");
    endpoint = Endpoint::from_yaml_doc(&block).expect("block tuple reads");
    endpoint.0 = "edge".to_owned();
    endpoint
        .apply_to_yaml_doc(&mut block)
        .expect("block tuple writes");
    assert_eq!(block.to_string(), "- edge # host\n- 0x50 # port\n");
}

#[test]
fn tuple_structs_work_as_nested_values_and_fragments() {
    let mut doc =
        YamlDoc::parse("disabled: ~\nendpoint: [api, 80]\n").expect("valid nested tuple document");
    let mut config = Config::from_yaml_doc(&doc).expect("nested tuple reads");
    config.endpoint.1 = 8080;
    config
        .apply_to_yaml_doc(&mut doc)
        .expect("nested tuple writes");
    assert_eq!(doc.to_string(), "disabled: ~\nendpoint: [api, 8080]\n");
    assert_eq!(
        Endpoint("api".to_owned(), 80)
            .to_yaml_fragment(0, "\n")
            .unwrap(),
        "- api\n- 80"
    );
}

#[test]
fn tuple_structs_validate_kind_and_exact_arity() {
    for (yaml, message) in [
        ("host: api\n", "requires a sequence"),
        ("[api]\n", "expects 2 fields, found 1"),
        ("[api, 80, extra]\n", "expects 2 fields, found 3"),
    ] {
        let doc = YamlDoc::parse(yaml).expect("valid YAML");
        let error = Endpoint::from_yaml_doc(&doc).expect_err("tuple shape must fail");
        assert!(error.diagnostic.message.contains(message));
        assert!(error.diagnostic.position.is_some());
    }

    let mut wrong_arity = YamlDoc::parse("[api]\n").expect("valid short sequence");
    let error = Endpoint("edge".to_owned(), 443)
        .apply_to_yaml_doc(&mut wrong_arity)
        .expect_err("tuple write must reject the wrong arity");
    assert!(
        error
            .diagnostic
            .message
            .contains("expects 2 fields, found 1")
    );
    assert_eq!(wrong_arity.to_string(), "[api]\n");
}

#[test]
fn tuple_structs_support_generics_const_generics_and_adapters() {
    let mut doc = YamlDoc::parse("[0x10, [1, 2], 30]\n").expect("valid generic tuple");
    let mut window: Window<u16, 2> = Window::from_yaml_doc(&doc).expect("generic tuple reads");
    assert_eq!(window, Window(16, [1, 2], Duration::from_secs(30)));
    window.0 = 17;
    window.2 = Duration::from_secs(45);
    window
        .apply_to_yaml_doc(&mut doc)
        .expect("generic tuple writes");
    assert_eq!(doc.to_string(), "[17, [1, 2], 45]\n");
}