noyalib 0.0.5

A pure Rust YAML library with zero unsafe code and full serde integration
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Noyalib. All rights reserved.

//! Tests for bugs found in competitor libraries (yaml-rust2).
//!
//! Each test reproduces a specific issue from yaml-rust2's tracker and
//! verifies that noyalib handles it correctly.

use noyalib::{DuplicateKeyPolicy, ParserConfig, Value, from_str, from_str_with_config};

// ── yaml-rust2#70: UTF-8 in flow mappings ────────────────────────────
// Multi-byte UTF-8 characters in flow sequences/mappings caused column
// miscounting, leading to a parse error.

#[test]
fn yr2_70_utf8_in_flow_mapping() {
    let yaml = r#"- "(":
    - test:
        if: $SpeechStyle = 'ClearSpeak' or $SpeechStyle = 'SimpleSpeak'
        then: [test: {if: "$Verbosity='Terse'", then: [T: "mở ngoặc"], else: [T: "mở ngoặc đơn"]}]
        else: [T: "mở ngoặc đơn"]
"#;
    let result: Result<Value, _> = from_str(yaml);
    assert!(
        result.is_ok(),
        "UTF-8 flow mapping should parse: {:?}",
        result.err()
    );
}

#[test]
fn yr2_70_utf8_scalar_values() {
    let yaml = "name: héllo wörld\ncity: Zürich\nemoji: 🦀\n";
    let v: Value = from_str(yaml).unwrap();
    assert_eq!(v["name"].as_str(), Some("héllo wörld"));
    assert_eq!(v["city"].as_str(), Some("Zürich"));
    assert_eq!(v["emoji"].as_str(), Some("🦀"));
}

// ── yaml-rust2#69/#30: Flow collection closing bracket indentation ───
// Closing ] or } at the same indentation as the key (not the content)
// should be accepted by lenient parsers.

#[test]
fn yr2_69_flow_seq_closing_bracket_indentation() {
    // This is a common real-world pattern (accepted by js-yaml, serde_yaml)
    let yaml = "foo: [\n  'a',  'b',  'c'\n]\n";
    let result: Result<Value, _> = from_str(yaml);
    // noyalib should accept this lenient pattern
    if let Ok(v) = &result {
        let seq = v["foo"].as_sequence().unwrap();
        assert_eq!(seq.len(), 3);
    }
    // If it fails, that's spec-correct but document it
}

#[test]
fn yr2_30_multiline_flow_seq_trailing_comma() {
    let yaml = "foo: [\n  'a',\n  'b',\n  'c',\n]\n";
    let result: Result<Value, _> = from_str(yaml);
    if let Ok(v) = &result {
        let seq = v["foo"].as_sequence().unwrap();
        assert_eq!(seq.len(), 3);
    }
}

#[test]
fn yr2_30_flow_mapping_multiline() {
    let yaml = "foo: {\n  a: 1,\n  b: 2\n}\n";
    let result: Result<Value, _> = from_str(yaml);
    if let Ok(v) = &result {
        assert_eq!(v["foo"]["a"].as_i64(), Some(1));
        assert_eq!(v["foo"]["b"].as_i64(), Some(2));
    }
}

// ── yaml-rust2#23: Duplicate keys should be detectable ───────────────
// The YAML 1.2 spec requires unique keys. noyalib supports configurable
// duplicate key handling.

#[test]
fn yr2_23_duplicate_keys_default_last_wins() {
    let yaml = "a: hello\na: world\n";
    let v: Value = from_str(yaml).unwrap();
    // Default policy: last wins
    assert_eq!(v["a"].as_str(), Some("world"));
}

#[test]
fn yr2_23_duplicate_keys_error_policy() {
    let yaml = "a: hello\na: world\n";
    let config = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Error);
    let result: Result<Value, _> = from_str_with_config(yaml, &config);
    assert!(result.is_err(), "duplicate keys should error");
}

#[test]
fn yr2_23_duplicate_keys_first_wins() {
    let yaml = "a: hello\na: world\n";
    let config = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::First);
    let v: Value = from_str_with_config(yaml, &config).unwrap();
    assert_eq!(v["a"].as_str(), Some("hello"));
}

// ── yaml-rust2#25: Folded scalar trailing newline ────────────────────
// Default "clip" chomping adds one trailing newline. This is correct per spec.

#[test]
fn yr2_25_folded_scalar_clip_chomping() {
    let yaml = ">\n  This is\n  multiline\n  string\n";
    let v: Value = from_str(yaml).unwrap();
    let s = v.as_str().unwrap();
    // Clip chomping: single trailing newline
    assert_eq!(s, "This is multiline string\n");
}

#[test]
fn yr2_25_folded_scalar_strip_chomping() {
    let yaml = ">-\n  This is\n  multiline\n  string\n";
    let v: Value = from_str(yaml).unwrap();
    let s = v.as_str().unwrap();
    // Strip chomping: no trailing newline
    assert_eq!(s, "This is multiline string");
}

#[test]
fn yr2_25_folded_scalar_keep_chomping() {
    let yaml = ">+\n  This is\n  multiline\n  string\n\n";
    let v: Value = from_str(yaml).unwrap();
    let s = v.as_str().unwrap();
    // Keep chomping: preserve all trailing newlines
    assert!(s.ends_with("string\n\n"));
}

// ── yaml-rust2#11: Tag suffix with extended data ─────────────────────
// Tags with numeric suffixes and anchors on the same line.

#[test]
fn yr2_11_tag_with_numeric_suffix() {
    // Basic tag parsing
    let yaml = "--- !custom 42\n";
    let result: Result<Value, _> = from_str(yaml);
    assert!(
        result.is_ok(),
        "tag with value should parse: {:?}",
        result.err()
    );
}

#[test]
fn yr2_11_tag_directive_and_custom_tag() {
    let yaml = "--- !mytag value\n";
    let v: Value = from_str(yaml).unwrap();
    // Should parse as tagged value or plain string
    match &v {
        Value::Tagged(t) => {
            assert!(t.tag().as_str().contains("mytag"));
        }
        Value::String(s) => {
            assert_eq!(s, "value");
        }
        _ => {}
    }
}

// ── yaml-rust2#29: Deep indentation block scalars ────────────────────
// Deeply nested structures with block scalars should parse correctly.

#[test]
fn yr2_29_deep_indentation_block_scalar() {
    let yaml = "\
a:
  b:
    c:
      d:
        e:
          f:
            g:
              h:
                i: |
                  deeply indented block scalar
";
    let v: Value = from_str(yaml).unwrap();
    let inner = v.get_path("a.b.c.d.e.f.g.h.i").and_then(|v| v.as_str());
    assert_eq!(inner, Some("deeply indented block scalar\n"));
}

#[test]
fn yr2_29_comment_between_directive_and_doc_start() {
    let yaml = "%YAML 1.2\n# This is a comment\n---\nfoo: bar\n";
    let result: Result<Value, _> = from_str(yaml);
    // Should either parse or gracefully handle the directive
    let _ = result;
}

// ── yaml-rust2#21: Comment handling ──────────────────────────────────
// Comments should be stripped during parsing (YAML spec) but data preserved.

#[test]
fn yr2_21_comments_stripped_data_preserved() {
    let yaml = "value:\n  # comment here\n  '002':\n    - name: foo\n";
    let v: Value = from_str(yaml).unwrap();
    let name = v
        .get_path("value.002")
        .and_then(|v| v.as_sequence())
        .and_then(|s| s.first())
        .and_then(|v| v.get("name"))
        .and_then(|v| v.as_str());
    assert_eq!(name, Some("foo"));
}

#[test]
fn yr2_21_inline_comments_stripped() {
    let yaml = "host: localhost  # primary host\nport: 5432       # postgres default\n";
    let v: Value = from_str(yaml).unwrap();
    assert_eq!(v["host"].as_str(), Some("localhost"));
    assert_eq!(v["port"].as_i64(), Some(5432));
}

// ── yaml-rust2#27: Source spans (Marker) on parsed nodes ─────────────
// noyalib supports this via Spanned<T>.

#[test]
fn yr2_27_source_spans_via_spanned() {
    use noyalib::Spanned;
    use serde::Deserialize;

    #[derive(Deserialize)]
    struct Config {
        host: Spanned<String>,
        port: Spanned<u16>,
    }

    let yaml = "host: localhost\nport: 8080\n";
    let config: Config = from_str(yaml).unwrap();
    assert_eq!(config.host.value, "localhost");
    assert!(config.host.start.line() >= 1);
    assert_eq!(config.port.value, 8080);
    assert!(config.port.start.line() >= 1);
}

// ── yaml-rust2#34: Tag handle resolution ─────────────────────────────

#[test]
fn yr2_34_basic_tag_handles() {
    let yaml = "--- !e!foo bar\n";
    let result: Result<Value, _> = from_str(yaml);
    // Should parse (tag may or may not be preserved depending on resolution)
    let _ = result;
}

#[test]
fn yr2_34_standard_tags() {
    let yaml = "!!int 42\n";
    let v: Value = from_str(yaml).unwrap();
    // !!int should resolve to integer
    match &v {
        Value::Number(_) | Value::Tagged(_) => {} // either is acceptable
        other => panic!("expected number or tagged, got {:?}", other),
    }
}

// ── yaml-rust2#22: Multi-document parsing ────────────────────────────

#[test]
fn yr2_22_multi_document_parsing() {
    let yaml = "---\nfoo: bar\n---\nbaz: qux\n";
    let docs = noyalib::load_all(yaml).unwrap();
    let docs: Vec<_> = docs.collect::<Result<Vec<_>, _>>().unwrap();
    assert_eq!(docs.len(), 2);
    assert_eq!(docs[0]["foo"].as_str(), Some("bar"));
    assert_eq!(docs[1]["baz"].as_str(), Some("qux"));
}

#[test]
fn yr2_22_multi_document_typed() {
    use serde::Deserialize;

    #[derive(Debug, Deserialize, PartialEq)]
    struct Doc {
        name: String,
    }

    let yaml = "---\nname: first\n---\nname: second\n";
    let docs: Vec<Doc> = noyalib::load_all_as(yaml).unwrap();
    assert_eq!(docs.len(), 2);
    assert_eq!(docs[0].name, "first");
    assert_eq!(docs[1].name, "second");
}

// ── Additional edge cases inspired by the issues ─────────────────────

#[test]
fn mixed_utf8_in_keys_and_values() {
    let yaml = "日本語: こんにちは\nclé: valeur\n";
    let v: Value = from_str(yaml).unwrap();
    assert_eq!(v["日本語"].as_str(), Some("こんにちは"));
    assert_eq!(v["clé"].as_str(), Some("valeur"));
}

#[test]
fn deeply_nested_flow_with_utf8() {
    let yaml = "{a: {b: {c: \"café\"}}}\n";
    let v: Value = from_str(yaml).unwrap();
    assert_eq!(v["a"]["b"]["c"].as_str(), Some("café"));
}

#[test]
fn empty_flow_collections() {
    let yaml = "empty_seq: []\nempty_map: {}\n";
    let v: Value = from_str(yaml).unwrap();
    assert!(v["empty_seq"].as_sequence().unwrap().is_empty());
    assert!(v["empty_map"].as_mapping().unwrap().is_empty());
}

#[test]
fn anchor_alias_in_flow() {
    let yaml = "[&a 1, *a, *a]\n";
    let v: Value = from_str(yaml).unwrap();
    let seq = v.as_sequence().unwrap();
    assert_eq!(seq[0].as_i64(), Some(1));
    assert_eq!(seq[1].as_i64(), Some(1));
    assert_eq!(seq[2].as_i64(), Some(1));
}

#[test]
fn literal_block_scalar_with_explicit_indent() {
    let yaml = "script: |2\n    #!/bin/bash\n    echo hello\n";
    let result: Result<Value, _> = from_str(yaml);
    if let Ok(v) = result {
        let s = v["script"].as_str().unwrap();
        assert!(s.contains("#!/bin/bash"));
    }
}