use serde::{Serialize, Deserialize};
use quick_xml::{reader::Reader, writer::Writer, errors::Result as XmlResult};
const SERVERS_TAG: &str = "servers";
const SERVERS_TAG_B: &[u8] = b"servers";
const SETTING_TAG: &str = "setting";
const SETTING_TAG_B: &[u8] = b"setting";
const NAME_ATTR: &str = "name";
const NAME_ATTR_B: &[u8] = b"name";
const CURRENT_GROUP_TAG: &str = "currentgroup";
const CURRENT_GROUP_TAG_B: &[u8] = b"currentgroup";
const CURRENT_SKU_TAG: &str = "currentsku";
const CURRENT_SKU_TAG_B: &[u8] = b"currentsku";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RobocraftServers {
pub current_group: String,
pub current_sku: String,
pub skus: std::collections::HashMap<String, RobocraftSku>,
}
impl RobocraftServers {
pub fn from_str(s: &str) -> XmlResult<Self> {
let mut current_group = None;
let mut current_sku = None;
let mut skus = std::collections::HashMap::new();
let mut reader = Reader::from_str(s);
reader.config_mut().trim_text(true);
if let quick_xml::events::Event::Start(e) = reader.read_event()? {
if e.name().0 != SERVERS_TAG_B {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: SERVERS_TAG.to_owned(),
found: String::from_utf8_lossy(e.name().0).to_string(),
}));
}
} else {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: SERVERS_TAG.to_owned(),
found: "".to_owned(),
}));
}
loop {
match reader.read_event()? {
quick_xml::events::Event::Start(e) => {
match e.name().0 {
CURRENT_GROUP_TAG_B => {
let text = reader.read_text(e.name())?;
current_group = Some(text.to_string());
},
CURRENT_SKU_TAG_B => {
let text = reader.read_text(e.name())?;
current_sku = Some(text.to_string());
},
_ => {
let sku = RobocraftSku::from_reader(&mut reader, e.name())?;
skus.insert(String::from_utf8_lossy(e.name().0).to_string(), sku);
}
}
},
quick_xml::events::Event::End(e) => {
if e.name().0 == SERVERS_TAG_B { break; }
},
quick_xml::events::Event::Eof => {
break;
},
_ => {},
}
}
if current_group.is_none() {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: CURRENT_GROUP_TAG.to_owned(),
found: "".to_owned(),
}));
}
if current_sku.is_none() {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: CURRENT_SKU_TAG.to_owned(),
found: "".to_owned(),
}));
}
Ok(Self {
current_group: current_group.unwrap(),
current_sku: current_sku.unwrap(),
skus,
})
}
pub fn to_writer<W: std::io::Write>(&self, writer: &mut Writer<W>) -> XmlResult<()> {
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(SERVERS_TAG)))?;
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(CURRENT_GROUP_TAG)))?;
writer.write_event(quick_xml::events::Event::Text(quick_xml::events::BytesText::new(&self.current_group)))?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(CURRENT_GROUP_TAG)))?;
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(CURRENT_SKU_TAG)))?;
writer.write_event(quick_xml::events::Event::Text(quick_xml::events::BytesText::new(&self.current_sku)))?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(CURRENT_SKU_TAG)))?;
for (name, sku) in self.skus.iter() {
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(name)))?;
sku.to_writer(writer)?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(name)))?;
}
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(SERVERS_TAG)))?;
Ok(())
}
pub fn as_xml_str(&self, indent: usize) -> XmlResult<String> {
let mut buf = Vec::new();
let mut writer = if indent != 0 {
Writer::new_with_indent(&mut buf, ' ' as u8, indent)
} else {
Writer::new(&mut buf)
};
self.to_writer(&mut writer)?;
Ok(String::from_utf8_lossy(&buf).into())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RobocraftSku {
pub groups: std::collections::HashMap<String, GroupConfig>,
}
impl RobocraftSku {
fn from_reader(reader: &mut Reader<&[u8]>, until: quick_xml::name::QName<'_>) -> XmlResult<Self> {
let mut groups = std::collections::HashMap::new();
loop {
match reader.read_event()? {
quick_xml::events::Event::Start(e) => {
let group = GroupConfig::from_reader(reader, e.name())?;
groups.insert(String::from_utf8_lossy(e.name().0).to_string(), group);
},
quick_xml::events::Event::End(e) => {
if e.name().0 == until.0 { break; }
},
quick_xml::events::Event::Eof => {
break;
},
_ => {},
}
}
Ok(Self {
groups,
})
}
fn to_writer<W: std::io::Write>(&self, writer: &mut Writer<W>) -> XmlResult<()> {
for (name, group) in self.groups.iter() {
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(name)))?;
group.to_writer(writer)?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(name)))?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RobocraftRoyaleServers {
pub current_group: String,
pub groups: std::collections::HashMap<String, GroupConfig>,
}
impl RobocraftRoyaleServers {
pub fn from_str(s: &str) -> XmlResult<Self> {
let mut current_group = None;
let mut groups = std::collections::HashMap::new();
let mut reader = Reader::from_str(s);
reader.config_mut().trim_text(true);
if let quick_xml::events::Event::Start(e) = reader.read_event()? {
if e.name().0 != SERVERS_TAG_B {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: SERVERS_TAG.to_owned(),
found: String::from_utf8_lossy(e.name().0).to_string(),
}));
}
} else {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: SERVERS_TAG.to_owned(),
found: "".to_owned(),
}));
}
loop {
match reader.read_event()? {
quick_xml::events::Event::Start(e) => {
match e.name().0 {
CURRENT_GROUP_TAG_B => {
let text = reader.read_text(e.name())?;
current_group = Some(text.to_string());
},
_ => {
let group = GroupConfig::from_reader(&mut reader, e.name())?;
groups.insert(String::from_utf8_lossy(e.name().0).to_string(), group);
}
}
},
quick_xml::events::Event::End(e) => {
if e.name().0 == SERVERS_TAG_B { break; }
},
quick_xml::events::Event::Eof => {
break;
},
_ => {},
}
}
if current_group.is_none() {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: CURRENT_GROUP_TAG.to_owned(),
found: "".to_owned(),
}));
}
Ok(Self {
current_group: current_group.unwrap(),
groups,
})
}
pub fn to_writer<W: std::io::Write>(&self, writer: &mut Writer<W>) -> XmlResult<()> {
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(SERVERS_TAG)))?;
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(CURRENT_GROUP_TAG)))?;
writer.write_event(quick_xml::events::Event::Text(quick_xml::events::BytesText::new(&self.current_group)))?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(CURRENT_GROUP_TAG)))?;
for (name, group) in self.groups.iter() {
writer.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(name)))?;
group.to_writer(writer)?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(name)))?;
}
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(SERVERS_TAG)))?;
Ok(())
}
pub fn as_xml_str(&self, indent: usize) -> XmlResult<String> {
let mut buf = Vec::new();
let mut writer = if indent != 0 {
Writer::new_with_indent(&mut buf, ' ' as u8, indent)
} else {
Writer::new(&mut buf)
};
self.to_writer(&mut writer)?;
Ok(String::from_utf8_lossy(&buf).into())
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct GroupConfig {
#[serde(rename = "setting")]
pub settings: Vec<GroupSetting>,
}
impl GroupConfig {
fn from_reader(reader: &mut Reader<&[u8]>, until: quick_xml::name::QName<'_>) -> XmlResult<Self> {
let mut settings = Vec::new();
loop {
match reader.read_event()? {
quick_xml::events::Event::Start(e) => {
if e.name().0 == SETTING_TAG_B {
let group = GroupSetting::from_reader(reader, e)?;
settings.push(group);
} else {
return Err(quick_xml::Error::IllFormed(quick_xml::errors::IllFormedError::MismatchedEndTag {
expected: SETTING_TAG.to_owned(),
found: String::from_utf8_lossy(e.name().0).to_string(),
}));
}
},
quick_xml::events::Event::End(e) => {
if e.name().0 == until.0 { break; }
},
quick_xml::events::Event::Eof => {
break;
},
_ => {},
}
}
Ok(Self {
settings,
})
}
fn to_writer<W: std::io::Write>(&self, writer: &mut Writer<W>) -> XmlResult<()> {
for setting in self.settings.iter() {
setting.to_writer(writer)?;
}
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct GroupSetting {
#[serde(rename = "@name")]
pub name: String,
#[serde(rename = "#text")]
pub content: String,
}
impl GroupSetting {
fn from_reader(reader: &mut Reader<&[u8]>, start: quick_xml::events::BytesStart<'_>) -> XmlResult<Self> {
if let quick_xml::events::Event::Text(t) = reader.read_event()? {
if let quick_xml::events::Event::End(_e) = reader.read_event()? {
if let Some(name) = start.try_get_attribute(NAME_ATTR_B)? {
Ok(Self {
name: String::from_utf8_lossy(&name.value).to_string(),
content: String::from_utf8_lossy(&t).to_string(),
})
} else {
Err(quick_xml::errors::Error::InvalidAttr(quick_xml::events::attributes::AttrError::ExpectedValue(0)))
}
} else {
Err(quick_xml::errors::Error::InvalidAttr(quick_xml::events::attributes::AttrError::ExpectedValue(0)))
}
} else {
Err(quick_xml::errors::Error::InvalidAttr(quick_xml::events::attributes::AttrError::ExpectedValue(0)))
}
}
fn to_writer<W: std::io::Write>(&self, writer: &mut Writer<W>) -> XmlResult<()> {
let start = quick_xml::events::BytesStart::new(SETTING_TAG)
.with_attributes([
(NAME_ATTR, &self.name as &str),
]);
writer.write_event(quick_xml::events::Event::Start(start))?;
writer.write_event(quick_xml::events::Event::Text(quick_xml::events::BytesText::new(&self.content)))?;
writer.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(SETTING_TAG)))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const RCR_CONFIG: &str = r#"<servers>
<currentgroup>ngram</currentgroup>
<dev>
<setting name="WebServicesServerAddress">127.0.0.1:4532</setting>
<setting name="WebServicesServerAddress__note__forwardedTo">127.0.0.1:4533</setting>
<setting name="SocialServerAddress">127.0.0.1:4534</setting>
<setting name="SocialServerAddress__note__forwardedTo">127.0.0.1:4535</setting>
<setting name="ChatServerAddress">127.0.0.1:4536</setting>
<setting name="ChatServerAddress__note__forwardedTo">127.0.0.1:4537</setting>
<setting name="SinglePlayerServerAddress">127.0.0.1:4538</setting>
<setting name="SinglePlayerServerAddress__note__forwardedTo">127.0.0.1:4539</setting>
<setting name="LobbyServerAddress">127.0.0.1:4540</setting>
<setting name="LobbyServerAddress__note__forwardedTo">127.0.0.1:4541</setting>
<setting name="authUrl">http://127.0.0.1:8001/</setting>
<setting name="S3URL">http://127.0.0.1:8010/royalejson/data.json</setting>
<setting name="UserMode">Dev</setting>
</dev>
<ngram>
<setting name="WebServicesServerAddress">ws.services.rcr.ngram.ca:4632</setting>
<setting name="WebServicesServerAddress__note__forwardedTo">:4633</setting>
<setting name="LobbyServerAddress">lobby.services.rcr.ngram.ca:4640</setting>
<setting name="LobbyServerAddress__note__forwardedTo">:4641</setting>
<setting name="authUrl">https://auth.rcr.ngram.ca/</setting>
<setting name="S3URL">https://static.rcr.ngram.ca/royalejson/data.json</setting>
<setting name="Payment__note__notUsedHere">https://pay.rcr.ngram.ca/</setting>
<setting name="Multiplayer__note__notUsedHere">mp.rcr.ngram.ca:4642</setting>
<setting name="UserMode">Dev</setting>
</ngram>
<mynewgroup>
<setting name="WebServicesServerAddress">domain_or_ip_address:4532</setting>
<setting name="WebServicesServerAddress__note__forwardedTo">:4533</setting>
<setting name="LobbyServerAddress">domain_or_ip_address:4540</setting>
<setting name="LobbyServerAddress__note__forwardedTo">:4541</setting>
<setting name="authUrl">http://domain_or_ip_address:8001/</setting>
<setting name="S3URL">http://domain_or_ip_address:8010/royalejson/data.json</setting>
<setting name="Multiplayer__note__notUsedHere">domain_or_ip_address:4542</setting>
</mynewgroup>
</servers>
"#;
const RC_CONFIG: &str = r#"<servers>
<currentgroup>ngram</currentgroup>
<currentsku>robocraft</currentsku>
<robocraft>
<dev>
<setting name="WebServicesServerAddress">127.0.0.1:4532</setting>
<setting name="WebServicesServerAddress__note__forwardedTo">127.0.0.1:4533</setting>
<setting name="SocialServerAddress">127.0.0.1:4534</setting>
<setting name="SocialServerAddress__note__forwardedTo">127.0.0.1:4535</setting>
<setting name="ChatServerAddress">127.0.0.1:4536</setting>
<setting name="ChatServerAddress__note__forwardedTo">127.0.0.1:4537</setting>
<setting name="SinglePlayerServerAddress">127.0.0.1:4538</setting>
<setting name="SinglePlayerServerAddress__note__forwardedTo">127.0.0.1:4539</setting>
<setting name="LobbyServerAddress">127.0.0.1:4540</setting>
<setting name="LobbyServerAddress__note__forwardedTo">127.0.0.1:4541</setting>
<setting name="authUrl">http://127.0.0.1:8001/</setting>
<setting name="S3URL">http://127.0.0.1:8010/live/data.json</setting>
</dev>
<ngram>
<setting name="WebServicesServerAddress">ws.services.rc.ngram.ca:4532</setting>
<setting name="WebServicesServerAddress__note__forwardedTo">:4533</setting>
<setting name="SocialServerAddress">s.services.rc.ngram.ca:4534</setting>
<setting name="SocialServerAddress__note__forwardedTo">:4535</setting>
<setting name="ChatServerAddress">c.services.rc.ngram.ca:4536</setting>
<setting name="ChatServerAddress__note__forwardedTo">:4537</setting>
<setting name="SinglePlayerServerAddress">sp.services.rc.ngram.ca:4538</setting>
<setting name="SinglePlayerServerAddress__note__forwardedTo">:4539</setting>
<setting name="LobbyServerAddress">lobby.services.rc.ngram.ca:4540</setting>
<setting name="LobbyServerAddress__note__forwardedTo">:4541</setting>
<setting name="authUrl">https://live-auth.rc.ngram.ca/</setting>
<setting name="S3URL">https://static.rc.ngram.ca/live/data.json</setting>
<setting name="Payment__note__notUsedHere">https://pay.rc.ngram.ca/</setting>
<setting name="Multiplayer__note__notUsedHere">mp.rc.ngram.ca:4542</setting>
</ngram>
<mynewgroup>
<setting name="WebServicesServerAddress">domain_or_ip_address:4532</setting>
<setting name="WebServicesServerAddress__note__forwardedTo">:4533</setting>
<setting name="SocialServerAddress">domain_or_ip_address:4534</setting>
<setting name="SocialServerAddress__note__forwardedTo">:4535</setting>
<setting name="ChatServerAddress">domain_or_ip_address:4536</setting>
<setting name="ChatServerAddress__note__forwardedTo">:4537</setting>
<setting name="SinglePlayerServerAddress">domain_or_ip_address:4538</setting>
<setting name="SinglePlayerServerAddress__note__forwardedTo">:4539</setting>
<setting name="LobbyServerAddress">domain_or_ip_address:4540</setting>
<setting name="LobbyServerAddress__note__forwardedTo">:4541</setting>
<setting name="authUrl">http://domain_or_ip_address:8001/</setting>
<setting name="S3URL">http://domain_or_ip_address:8010/live/data.json</setting>
<setting name="Payment__note__notUsedHere">http://domain_or_ip_address:8011/</setting>
<setting name="Multiplayer__note__notUsedHere">domain_or_ip_address:4542</setting>
</mynewgroup>
</robocraft>
</servers>
"#;
#[test]
fn test_rc_servenvmulti_full_des() {
let conf = RobocraftServers::from_str(RC_CONFIG).expect("Failed to parse embedded RC servenvmulti.config");
println!("Parsed RC config: {:?}", conf);
}
#[test]
fn test_rcr_servenvmulti_full_des() {
let conf = RobocraftRoyaleServers::from_str(RCR_CONFIG).expect("Failed to parse embedded RCR servenvmulti.config");
println!("Parsed RCR config: {:?}", conf);
}
#[test]
fn test_rc_servenvmulti_indempotence() {
let conf = RobocraftServers::from_str(RC_CONFIG).expect("Failed to parse embedded RC servenvmulti.config");
let xml = conf.as_xml_str(4).expect("Failed to serialize RC servenvmulti.config");
println!("Dumped RC config:\n{}", xml);
let conf2 = RobocraftServers::from_str(&xml).expect("Failed to re-parse RC servenvmulti.config");
assert_eq!(conf, conf2);
}
#[test]
fn test_rcr_servenvmulti_indempotence() {
let conf = RobocraftRoyaleServers::from_str(RCR_CONFIG).expect("Failed to parse embedded RCR servenvmulti.config");
let xml = conf.as_xml_str(4).expect("Failed to serialize RCR servenvmulti.config");
println!("Dumped RCR config:\n{}", xml);
let conf2 = RobocraftRoyaleServers::from_str(&xml).expect("Failed to re-parse RCR servenvmulti.config");
assert_eq!(conf, conf2);
}
}