A friend of mine and me are running a modded DayZ server. One of the common tasks when adding mods that include new items is to update the server's types.xml file (example).
My mate did this by hand until now, but asked me to write a program so that he can automate this annoying task. So I wrote the below library and program to merge two such XML files.
The target is that a base XML file is extended by an extension XML file that may override types in the original file:
Cargo.toml
[package] name = "typesxml" version = "0.1.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { "version" = "4.2.6", features = ["derive"] } quick-xml = { version = "0.28.2", features = ["serialize"] } serde = { version = "1.0.164", features = ["derive"] }
src/lib.rs
use serde::ser::Error; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::ops::Add; use std::str::FromStr; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Types { #[serde(rename = "type")] types: Vec<Type>, } impl Add for Types { type Output = Self; fn add(self, rhs: Self) -> Self::Output { let mut map = HashMap::from(&self); map.extend(HashMap::from(&rhs)); let mut types: Vec<Type> = map.into_values().cloned().collect(); types.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); Self { types } } } impl Display for Types { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { quick_xml::se::to_writer(f, self).map_err(std::fmt::Error::custom) } } impl<'a> From<&'a Types> for HashMap<&'a str, &'a Type> { fn from(types: &'a Types) -> Self { types .types .iter() .map(|typ| (typ.name.as_str(), typ)) .collect() } } impl FromStr for Types { type Err = quick_xml::de::DeError; fn from_str(xml: &str) -> Result<Self, Self::Err> { quick_xml::de::from_str(xml) } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Type { #[serde(rename = "@name")] name: String, nominal: u8, lifetime: u32, restock: u32, min: u8, quantmin: i64, quantmax: i64, cost: u32, flags: Flags, category: Option<Named>, usage: Option<Vec<Named>>, value: Option<Vec<Named>>, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Flags { #[serde(rename = "@count_in_cargo")] count_in_cargo: u64, #[serde(rename = "@count_in_hoarder")] count_in_hoarder: u64, #[serde(rename = "@count_in_map")] count_in_map: u64, #[serde(rename = "@count_in_player")] count_in_player: u64, #[serde(rename = "@crafted")] crafted: u64, #[serde(rename = "@deloot")] deloot: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Named { #[serde(rename = "@name")] name: String, }
src/bin/mergetypesxml.rs
use clap::Parser; use std::fs::{read_to_string, write}; use std::process::exit; use std::str::FromStr; use typesxml::Types; const DESCRIPTION: &str = "Merge types.xml files for DayZ servers."; #[derive(Parser, Debug)] #[command(author, version, about, long_about = DESCRIPTION)] struct Args { #[arg(index = 1)] base: String, #[arg(index = 2)] extension: String, #[arg(long, short)] output: Option<String>, } fn main() { let args = Args::parse(); let merger = types_from_file(&args.base) + types_from_file(&args.extension); if let Some(file) = args.output { write(file, merger.to_string()).unwrap_or_else(|error| { eprintln!("{}", error); exit(3); }) } else { println!("{}", merger); } } fn types_from_file(filename: &str) -> Types { Types::from_str(&read_to_string(filename).unwrap_or_else(|error| { eprintln!("{}\n{}", filename, error); exit(1); })) .unwrap_or_else(|error| { eprintln!("{} in {}", error, filename); exit(2); }) }
How may I improve the program?