umya-spreadsheet 3.1.1

umya-spreadsheet is a library written in pure Rust to read and write xlsx file.
Documentation
use std::io;

use quick_xml::{
    NsReader,
    events::Event,
    name::{
        Namespace,
        ResolveResult,
    },
};

use super::{
    XlsxError,
    driver::{
        get_attribute,
        xml_read_loop,
    },
};
use crate::{
    helper::const_str::{
        PIVOT_CACHE_DEF_NS,
        PKG_WORKBOOK_RELS,
        REL_NS,
    },
    structs::Workbook,
};

pub(crate) fn read<R: io::Read + io::Seek>(
    arv: &mut zip::read::ZipArchive<R>,
    wb: &mut Workbook,
) -> Result<Vec<(String, String, String)>, XlsxError> {
    let r = io::BufReader::new(super::driver::zip_by_name(arv, PKG_WORKBOOK_RELS)?);
    let mut reader = NsReader::from_reader(r);
    reader.config_mut().trim_text(true);

    let mut result: Vec<(String, String, String)> = Vec::new();

    xml_read_loop!(
        reader,
        Event::Empty(ref e) => {
            let (namespace, local_name) = reader.resolver().resolve_element(e.name());
            if namespace == ResolveResult::Bound(Namespace(REL_NS.as_bytes()))
                && local_name.as_ref() == b"Relationship"
            {
                let id_value = get_attribute(e, b"Id").unwrap();
                let type_value = get_attribute(e, b"Type").unwrap();
                let target_value = get_attribute(e, b"Target").unwrap();
                let target_value = target_value
                    .strip_prefix("/xl/")
                    .map(ToOwned::to_owned)
                    .unwrap_or(target_value);
                if type_value == PIVOT_CACHE_DEF_NS {
                    wb.update_pivot_caches(id_value.as_str(), target_value.as_str());
                }
                result.push((id_value, type_value, target_value));
            }
        },
        Event::Eof => break,
    );

    Ok(result)
}

#[cfg(test)]
mod tests {
    use std::io::{
        Cursor,
        Write,
    };

    use rstest::rstest;
    use zip::{
        ZipArchive,
        ZipWriter,
        write::SimpleFileOptions,
    };

    use super::*;

    fn relationships(xml: &str) -> Vec<(String, String, String)> {
        let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
        zip.start_file(PKG_WORKBOOK_RELS, SimpleFileOptions::default())
            .unwrap();
        zip.write_all(xml.as_bytes()).unwrap();
        let mut archive = ZipArchive::new(zip.finish().unwrap()).unwrap();
        read(&mut archive, &mut Workbook::default()).unwrap()
    }

    #[rstest]
    #[case("")]
    #[case("ns0:")]
    #[case("pkg:")]
    fn relationship_namespace_accepts_equivalent_spellings(#[case] prefix: &str) {
        let declaration = if prefix.is_empty() {
            "xmlns".to_string()
        } else {
            format!("xmlns:{}", prefix.trim_end_matches(':'))
        };
        let xml = format!(
            "<{prefix}Relationships {declaration}=\"{REL_NS}\"><{prefix}Relationship Id=\"rId1\" \
             Type=\"worksheet\" Target=\"/xl/worksheets/sheet1.xml\"/></{prefix}Relationships>"
        );
        assert_eq!(
            relationships(&xml),
            vec![(
                "rId1".to_string(),
                "worksheet".to_string(),
                "worksheets/sheet1.xml".to_string(),
            )]
        );
    }

    #[rstest]
    #[case(r#"<Relationship xmlns="urn:unrelated"/>"#)]
    #[case(r#"<other:Relationship xmlns:other="urn:unrelated"/>"#)]
    #[case(r#"<Relationship xmlns=""/>"#)]
    #[case("<Other/>")]
    fn unrelated_relationship_names_are_not_interpreted(#[case] child: &str) {
        let xml = format!(r#"<Relationships xmlns="{REL_NS}">{child}</Relationships>"#);
        assert!(relationships(&xml).is_empty());
    }

    #[test]
    fn child_namespace_declarations_are_resolved_in_scope() {
        let xml = format!(
            "<Relationships xmlns=\"{REL_NS}\" \
             xmlns:p=\"urn:unrelated\"><p:Relationship/><p:Relationship xmlns:p=\"{REL_NS}\" \
             Id=\"rId1\" Type=\"worksheet\" \
             Target=\"worksheets/sheet1.xml\"/><p:Relationship/></Relationships>"
        );
        assert_eq!(relationships(&xml).len(), 1);
    }
}