edit-xlsx 0.4.4

A quick and easy-to-use Rust library for Excel file editing.
Documentation
use std::collections::HashSet;
use std::fs::File;
use std::hash::Hash;
use std::io;
use std::io::Read;
use std::path::Path;
use quick_xml::{de, se};
use serde::{Deserialize, Serialize};
use zip::read::ZipFile;
use zip::ZipArchive;
use crate::file::{XlsxFileType, XlsxFileWriter};
use crate::xml::io::Io;
use crate::xml::relationships::Relationships;

#[derive(Debug, Deserialize, Serialize, Default)]
pub(crate) struct ContentTypes {
    #[serde(rename = "@xmlns")]
    xmlns: String,
    #[serde(rename = "$value")]
    content_types: HashSet<ContentType>,
}

#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
enum ContentType{
    Default {
        #[serde(rename = "@Extension")]
        extension: String,
        #[serde(rename = "@ContentType")]
        content_type: String,
    },
    Override {
        #[serde(rename = "@PartName")]
        part_name: String,
        #[serde(rename = "@ContentType")]
        content_type: String,
    }
}

impl ContentType {
    fn get_extension(&self) -> Option<&str> {
        if let ContentType::Default { extension, content_type } = self {
            Some(&extension)
        } else {
            None
        }
    }
}

impl ContentTypes {
    fn get_mut_by_extension(&self, extension: &str) -> bool {
        self.content_types.iter().find(|c| c.get_extension() == Some(extension)).is_some()
    }
}

impl ContentTypes {
    pub(crate) fn add_png(&mut self) {
        self.content_types.insert(ContentType::png_default());
    }
    pub(crate) fn add_bin(&mut self, extension: &str) {
        if self.get_mut_by_extension(extension) {
            return;
        }
        self.content_types.insert(ContentType::octet_stream_default(extension));
    }
    pub(crate) fn add_drawing(&mut self, id: u32) { self.content_types.insert(ContentType::drawing_override(id)); }
    pub(crate) fn add_metadata(&mut self) { self.content_types.insert(ContentType::metadata_override()); }
}

impl ContentType {
    fn png_default() -> ContentType {
        ContentType::Default {
            extension: "png".to_string(),
            content_type: "image/png".to_string(),
        }
    }

    fn octet_stream_default(extension: &str) -> ContentType {
        ContentType::Default {
            extension: extension.to_string(),
            content_type: "application/octet-stream".to_string(),
        }
    }

    fn drawing_override(id: u32) -> ContentType {
        ContentType::Override {
            part_name: format!("/xl/drawings/drawing{id}.xml"),
            content_type: "application/vnd.openxmlformats-officedocument.drawing+xml".to_string(),
        }
    }

    fn metadata_override() -> ContentType {
        ContentType::Override {
            part_name: "/xl/metadata.xml".to_string(),
            content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml".to_string(),
        }
    }
}

impl ContentTypes {
    pub(crate) fn from_file(file: &File) -> ContentTypes {
        let mut xml = String::new();
        let mut archive = zip::ZipArchive::new(file).unwrap();
        let file_path = "[Content_Types].xml";
        let content_types = match archive.by_name(&file_path) {
            Ok(mut file) => {
                file.read_to_string(&mut xml).unwrap();
                de::from_str(&xml).unwrap()
            }
            Err(_) => {
                ContentTypes::default()
            }
        };
        content_types
    }
}

impl Io<ContentTypes> for ContentTypes {
    fn save<P: AsRef<Path>>(& self, file_path: P) {
        let xml = se::to_string_with_root("Types", &self).unwrap();
        let xml = format!("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n{}", xml);
        let mut file = XlsxFileWriter::from_path(file_path, XlsxFileType::ContentTypes).unwrap();
        file.write_all(xml.as_ref()).unwrap();
    }
}