ferrocat-po 2.2.0

Performance-first PO parsing, serialization, and catalog update primitives for ferrocat.
Documentation
use std::borrow::Cow;

use ferrocat_po::{MergeMessageInput, PoFile, merge_catalog, parse_po, parse_po_borrowed};

const CASES: &[(&str, &str)] = &[
    (
        "context plural with metadata",
        concat!(
            "msgid \"\"\n",
            "msgstr \"\"\n",
            "\"Language: de\\n\"\n",
            "\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n",
            "\n",
            "# Translator note\n",
            "#. Extracted note\n",
            "#: src/app.rs:1 src/lib.rs:2\n",
            "#, fuzzy, rust-format\n",
            "#@ owner: checkout\n",
            "msgctxt \"menu\"\n",
            "msgid \"file\"\n",
            "msgid_plural \"files\"\n",
            "msgstr[0] \"Datei\"\n",
            "msgstr[1] \"Dateien\"\n",
            "\n",
            "msgid \"Save\"\n",
            "msgstr \"Speichern\"\n",
        ),
    ),
    (
        "multiline escaped content",
        concat!(
            "msgid \"multi\"\n",
            "\" line\\n\"\n",
            "\"source\"\n",
            "msgstr \"mehr\"\n",
            "\" zeilig\\t\"\n",
            "\n",
            "msgctxt \"cta\"\n",
            "msgid \"Quote \\\"here\\\"\"\n",
            "msgstr \"Zitat \\\"hier\\\"\"\n",
        ),
    ),
];

#[test]
fn owned_and_borrowed_parsers_match_on_shared_lf_inputs() {
    for (name, input) in CASES {
        let owned = parse_po(input).unwrap_or_else(|error| panic!("{name}: owned parse: {error}"));
        let borrowed = parse_po_borrowed(input)
            .unwrap_or_else(|error| panic!("{name}: borrowed parse: {error}"))
            .into_owned();

        assert_eq!(borrowed, owned, "{name}");
    }
}

#[test]
fn owned_and_borrowed_parsers_match_on_crlf_and_bare_cr_inputs() {
    for line_ending in ["\r\n", "\r"] {
        let input = CASES[0].1.replace('\n', line_ending);
        let owned = parse_po(&input)
            .unwrap_or_else(|error| panic!("{line_ending:?}: owned parse: {error}"));
        let borrowed = parse_po_borrowed(&input)
            .unwrap_or_else(|error| panic!("{line_ending:?}: borrowed parse: {error}"))
            .into_owned();

        assert_eq!(borrowed, owned, "{line_ending:?}");
    }
}

#[test]
fn merge_parser_preserves_matching_existing_messages() {
    for (name, input) in CASES {
        let owned = parse_po(input).unwrap_or_else(|error| panic!("{name}: owned parse: {error}"));
        let extracted = extracted_messages_from(&owned);
        let merged = merge_catalog(input, &extracted)
            .unwrap_or_else(|error| panic!("{name}: merge parse: {error}"));
        let reparsed =
            parse_po(&merged).unwrap_or_else(|error| panic!("{name}: reparse merged: {error}"));

        assert_eq!(reparsed.items, owned.items, "{name}\nmerged:\n{merged}");
    }
}

#[test]
fn merge_parser_preserves_matching_crlf_and_bare_cr_messages() {
    for line_ending in ["\r\n", "\r"] {
        let input = CASES[0].1.replace('\n', line_ending);
        let owned = parse_po(&input)
            .unwrap_or_else(|error| panic!("{line_ending:?}: owned parse: {error}"));
        let extracted = extracted_messages_from(&owned);
        let merged = merge_catalog(&input, &extracted)
            .unwrap_or_else(|error| panic!("{line_ending:?}: merge parse: {error}"));
        let reparsed = parse_po(&merged)
            .unwrap_or_else(|error| panic!("{line_ending:?}: reparse merged: {error}"));

        assert_eq!(
            reparsed.items, owned.items,
            "{line_ending:?}\nmerged:\n{merged}"
        );
    }
}

fn extracted_messages_from(file: &PoFile) -> Vec<MergeMessageInput<'_>> {
    file.items
        .iter()
        .filter(|item| !item.obsolete)
        .map(|item| MergeMessageInput {
            msgctxt: item.msgctxt.as_deref().map(Cow::Borrowed),
            msgid: Cow::Borrowed(item.msgid.as_str()),
            msgid_plural: item.msgid_plural.as_deref().map(Cow::Borrowed),
            references: item
                .references
                .iter()
                .map(|reference| Cow::Borrowed(reference.as_str()))
                .collect(),
            extracted_comments: item
                .extracted_comments
                .iter()
                .map(|comment| Cow::Borrowed(comment.as_str()))
                .collect(),
            flags: item
                .flags
                .iter()
                .map(|flag| Cow::Borrowed(flag.as_str()))
                .collect(),
        })
        .collect()
}