0
\$\begingroup\$

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?

\$\endgroup\$

    1 Answer 1

    1
    \$\begingroup\$

    Why is this split into a library and a program? The task is pretty simple, so there doesn't seem any reason for the split.

    You've been pretty excessive in implementing traits: Add, Display, FromStr, From. They don't really help you, because you aren't using any functions that need those traits and they are each only invoked in one (maybe two) places. The code would be easier to follow if they were just functions you could call. These seems to be specialized bits of your algorithm and not the sort of general functionality that implementing these traits would suggest.

    As for your merging algorithm, I'd probably do sort/dedup like so:

     let mut types = self.types; types.extend(rhs.types); types.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); types.dedup_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); Self { types } 
    \$\endgroup\$
    1
    • \$\begingroup\$Thank you. Yes, I might have over-engineered here. :-)\$\endgroup\$CommentedJun 22, 2023 at 8:59

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.