type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
use inflector::Inflector;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, atomic::AtomicUsize};
use xsd_parser::{
Config, Ident2, Name, TypeIdent,
config::Schema,
generate,
models::{
NameBuilder as DefaultNameBuilder, format_ident, format_unknown_variant, make_type_name,
meta::MetaType,
},
traits::{NameBuilder, Naming},
};
const ORIGINAL_XSD_DIR: &str = "xsd";
const OUTPUT_FILE_NAME: &str = "generated_schema.rs";
const FEATURE_SCHEMAS: &[(&str, &str)] = &[
("net", "net_file.xsd"),
("routes", "routes_file.xsd"),
("additional", "additional_file.xsd"),
];
const WRITE_FEATURE: &str = "write";
fn feature_enabled(name: &str) -> bool {
env::var(format!(
"CARGO_FEATURE_{}",
name.to_uppercase().replace('-', "_")
))
.is_ok()
}
fn resolve_dtd_entities(content: &str) -> String {
let entities = parse_dtd_entities(content);
if entities.is_empty() {
return content.to_string();
}
let mut segments = content.split('&');
let first = segments.next().unwrap_or_default().to_string();
segments.fold(first, |mut resolved, segment| {
resolved.push_str(&resolve_entity_reference(segment, &entities));
resolved
})
}
fn parse_dtd_entities(content: &str) -> Vec<(&str, &str)> {
content
.lines()
.filter_map(|line| {
let rest = line.trim().strip_prefix("<!ENTITY ")?.strip_suffix('>')?;
let (name, value) = rest.split_once(char::is_whitespace)?;
let value = value.trim().strip_prefix('"')?.strip_suffix('"')?;
Some((name.trim(), value))
})
.collect()
}
fn resolve_entity_reference(segment: &str, entities: &[(&str, &str)]) -> String {
let Some((name, rest)) = segment.split_once(';') else {
return format!("&{segment}");
};
let value = match name {
"amp" | "lt" | "gt" | "apos" | "quot" => return format!("&{name};{rest}"),
_ => entities
.iter()
.find_map(|(n, v)| (*n == name).then_some(*v)),
};
match value {
Some(value) => format!("{value}{rest}"),
None => format!("&{segment}"),
}
}
const RENAMED_TYPES: &[(&str, &str)] = &[
(r#""idType""#, r#""sumoIdType""#),
(r#""floatType""#, r#""sumoFloatType""#),
(r#""timeType""#, r#""sumoTimeType""#),
];
fn rename_colliding_types(content: &str) -> String {
RENAMED_TYPES
.iter()
.fold(content.to_string(), |content, (from, to)| {
content.replace(from, to)
})
}
const PER_FILE_PATCHES: &[(&str, &[(&str, &str)])] = &[
(
"additional_file.xsd",
&[
(
" <xsd:include schemaLocation=\"types/metadata.xsd\"/>\n",
"",
),
(
" <xsd:element name=\"metadata\" type=\"metadataType\" minOccurs=\"0\" maxOccurs=\"1\"/>\n",
"",
),
(
" <xsd:choice minOccurs=\"2\" maxOccurs=\"unbounded\">\n\
\x20 <xsd:element name=\"detEntry\" type=\"detEntryExitType\" minOccurs=\"1\" maxOccurs=\"unbounded\"/>\n\
\x20 <xsd:element name=\"detExit\" type=\"detEntryExitType\" minOccurs=\"1\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:choice>\n",
" <xsd:group ref=\"detGateGroup\" minOccurs=\"2\" maxOccurs=\"unbounded\"/>\n",
),
(
" <xsd:complexType name=\"e3DetectorType\">\n",
" <xsd:group name=\"detGateGroup\">\n\
\x20 <xsd:choice>\n\
\x20 <xsd:element name=\"detEntry\" type=\"detEntryExitType\" minOccurs=\"1\" maxOccurs=\"unbounded\"/>\n\
\x20 <xsd:element name=\"detExit\" type=\"detEntryExitType\" minOccurs=\"1\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:choice>\n\
\x20 </xsd:group>\n\
\n\
\x20 <xsd:complexType name=\"e3DetectorType\">\n",
),
],
),
(
"route.xsd",
&[
(
" <xsd:sequence minOccurs=\"0\">\n\
\x20 <xsd:choice>\n\
\x20 <xsd:element name=\"carFollowing-IDM\" type=\"cfIDMType\"/>\n\
\x20 <xsd:element name=\"carFollowing-IDMM\" type=\"cfIDMMType\"/>\n\
\x20 <xsd:element name=\"carFollowing-EIDM\" type=\"cfEIDMType\"/>\n\
\x20 <xsd:element name=\"carFollowing-Krauss\" type=\"cfKraussType\"/>\n\
\x20 <xsd:element name=\"carFollowing-KraussPS\" type=\"cfKraussType\"/>\n\
\x20 <xsd:element name=\"carFollowing-KraussOrig1\" type=\"cfKraussType\"/>\n\
\x20 <xsd:element name=\"carFollowing-SmartSK\" type=\"cfSmartType\"/>\n\
\x20 <xsd:element name=\"carFollowing-Daniel1\" type=\"cfSmartType\"/>\n\
\x20 <xsd:element name=\"carFollowing-PWagner2009\" type=\"cfPWagType\"/>\n\
\x20 <xsd:element name=\"carFollowing-BKerner\" type=\"cfBKernerType\"/>\n\
\x20 <xsd:element name=\"carFollowing-Wiedemann\" type=\"cfWiedemannType\"/>\n\
\x20 <xsd:element name=\"carFollowing-W99\" type=\"cfW99Type\"/>\n\
\x20 <xsd:element name=\"carFollowing-ACC\" type=\"cfACCType\"/>\n\
\x20 <xsd:element name=\"carFollowing-CACC\" type=\"cfCACCType\"/>\n\
\x20 <xsd:element name=\"carFollowing-CC\" type=\"cfCCType\"/>\n\
\x20 </xsd:choice>\n\
\x20 <xsd:element name=\"param\" type=\"paramType\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:sequence>\n",
" <xsd:group ref=\"vTypeCarFollowingGroup\" minOccurs=\"0\"/>\n",
),
(
" <xsd:complexType name=\"vTypeType\">\n",
" <xsd:group name=\"vTypeCarFollowingGroup\">\n\
\x20 <xsd:sequence>\n\
\x20 <xsd:choice>\n\
\x20 <xsd:element name=\"carFollowing-IDM\" type=\"cfIDMType\"/>\n\
\x20 <xsd:element name=\"carFollowing-IDMM\" type=\"cfIDMMType\"/>\n\
\x20 <xsd:element name=\"carFollowing-EIDM\" type=\"cfEIDMType\"/>\n\
\x20 <xsd:element name=\"carFollowing-Krauss\" type=\"cfKraussType\"/>\n\
\x20 <xsd:element name=\"carFollowing-KraussPS\" type=\"cfKraussType\"/>\n\
\x20 <xsd:element name=\"carFollowing-KraussOrig1\" type=\"cfKraussType\"/>\n\
\x20 <xsd:element name=\"carFollowing-SmartSK\" type=\"cfSmartType\"/>\n\
\x20 <xsd:element name=\"carFollowing-Daniel1\" type=\"cfSmartType\"/>\n\
\x20 <xsd:element name=\"carFollowing-PWagner2009\" type=\"cfPWagType\"/>\n\
\x20 <xsd:element name=\"carFollowing-BKerner\" type=\"cfBKernerType\"/>\n\
\x20 <xsd:element name=\"carFollowing-Wiedemann\" type=\"cfWiedemannType\"/>\n\
\x20 <xsd:element name=\"carFollowing-W99\" type=\"cfW99Type\"/>\n\
\x20 <xsd:element name=\"carFollowing-ACC\" type=\"cfACCType\"/>\n\
\x20 <xsd:element name=\"carFollowing-CACC\" type=\"cfCACCType\"/>\n\
\x20 <xsd:element name=\"carFollowing-CC\" type=\"cfCCType\"/>\n\
\x20 </xsd:choice>\n\
\x20 <xsd:element name=\"param\" type=\"paramType\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:sequence>\n\
\x20 </xsd:group>\n\
\n\
\x20 <xsd:complexType name=\"vTypeType\">\n",
),
(
" <xsd:sequence minOccurs=\"0\">\n\
\x20 <xsd:choice>\n\
\x20 <xsd:element name=\"route\" type=\"vehicleRouteType\"/>\n\
\x20 <xsd:element name=\"routeDistribution\" type=\"vehicleRouteDistributionType\"/>\n\
\x20 </xsd:choice>\n\
\x20 <xsd:element name=\"param\" type=\"paramType\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:sequence>\n\
\x20 <xsd:sequence minOccurs=\"0\">\n\
\x20 <xsd:element name=\"stop\" type=\"stopType\" maxOccurs=\"unbounded\"/>\n\
\x20 <xsd:element name=\"param\" type=\"paramType\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:sequence>\n",
" <xsd:group ref=\"vehicleRouteChoiceGroup\" minOccurs=\"0\"/>\n\
\x20 <xsd:group ref=\"vehicleStopGroup\" minOccurs=\"0\"/>\n",
),
(
" <xsd:complexType name=\"vehicleType\">\n",
" <xsd:group name=\"vehicleRouteChoiceGroup\">\n\
\x20 <xsd:sequence>\n\
\x20 <xsd:choice>\n\
\x20 <xsd:element name=\"route\" type=\"vehicleRouteType\"/>\n\
\x20 <xsd:element name=\"routeDistribution\" type=\"vehicleRouteDistributionType\"/>\n\
\x20 </xsd:choice>\n\
\x20 <xsd:element name=\"param\" type=\"paramType\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:sequence>\n\
\x20 </xsd:group>\n\
\n\
\x20 <xsd:group name=\"vehicleStopGroup\">\n\
\x20 <xsd:sequence>\n\
\x20 <xsd:element name=\"stop\" type=\"stopType\" maxOccurs=\"unbounded\"/>\n\
\x20 <xsd:element name=\"param\" type=\"paramType\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n\
\x20 </xsd:sequence>\n\
\x20 </xsd:group>\n\
\n\
\x20 <xsd:complexType name=\"vehicleType\">\n",
),
],
),
];
fn apply_per_file_patches(file_name: &str, content: &str) -> Result<String> {
let Some((_, patches)) = PER_FILE_PATCHES.iter().find(|(name, _)| *name == file_name) else {
return Ok(content.to_string());
};
patches
.iter()
.try_fold(content.to_string(), |content, (from, to)| {
if !content.contains(from) {
return Err(format!(
"patch for {file_name} no longer matches; the vendored schema must have \
changed. Expected to find:\n{from}"
)
.into());
}
Ok(content.replace(from, to))
})
}
fn patch_xsd(file_name: &str, content: &str) -> Result<String> {
let content = apply_per_file_patches(file_name, content)?;
Ok(rename_colliding_types(&resolve_dtd_entities(&content)))
}
fn copy_and_patch_schemas(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
fs::read_dir(src)?.try_for_each(|entry| {
let entry = entry?;
let path = entry.path();
let dest_path = dst.join(entry.file_name());
if path.is_dir() {
return copy_and_patch_schemas(&path, &dest_path);
}
if path.extension().is_some_and(|ext| ext == "xsd") {
let content = fs::read_to_string(&path)?;
let file_name = entry.file_name();
let patched = patch_xsd(&file_name.to_string_lossy(), &content)?;
fs::write(&dest_path, patched)?;
} else {
fs::copy(&path, &dest_path)?;
}
Ok(())
})
}
#[derive(Debug, Clone, Default)]
struct SumoNaming(Arc<AtomicUsize>);
impl Naming for SumoNaming {
fn clone_boxed(&self) -> Box<dyn Naming> {
Box::new(self.clone())
}
fn builder(&self) -> Box<dyn NameBuilder> {
Box::new(DefaultNameBuilder::new(
self.0.clone(),
Box::new(self.clone()),
))
}
fn unify(&self, s: &str) -> String {
unify(s)
}
fn make_type_name(&self, postfixes: &[String], ty: &MetaType, ident: &TypeIdent) -> Name {
make_type_name(self, postfixes, ty, ident)
}
fn make_unknown_variant(&self, id: usize) -> Ident2 {
format_unknown_variant(id)
}
fn format_module_name(&self, s: &str) -> String {
format_ident(self.unify(s).to_snake_case())
}
fn format_type_name(&self, s: &str) -> String {
format_ident(self.unify(s))
}
fn format_field_name(&self, s: &str) -> String {
format_ident(self.unify(s).to_snake_case())
}
fn format_variant_name(&self, s: &str) -> String {
format_variant_name(s)
}
fn format_constant_name(&self, s: &str) -> String {
format_ident(self.unify(s).to_screaming_snake_case())
}
}
fn unify(s: &str) -> String {
let mut done = true;
let unified = s.replace(
|c: char| {
let replace = !c.is_alphanumeric();
if c != '_' && !replace {
done = false;
}
c != '_' && replace
},
"_",
);
if done {
unified
} else {
unified.to_screaming_snake_case().to_pascal_case()
}
}
fn format_variant_name(s: &str) -> String {
let sanitized: String = s
.chars()
.map(|c| {
if c.is_alphanumeric() {
c.to_string()
} else {
describe_symbol(c)
}
})
.collect();
format_ident(if sanitized.is_empty() {
"Empty".to_string()
} else {
sanitized
})
}
fn describe_symbol(c: char) -> String {
match c {
'-' => "Dash".to_string(),
'=' => "Eq".to_string(),
'_' => "_".to_string(),
other => format!("U{:x}", other as u32),
}
}
fn pretty_print(code: &str) -> String {
match syn::parse_file(code) {
Ok(parsed) => prettyplease::unparse(&parsed),
Err(error) => {
println!("cargo:warning=could not pretty-print the generated schema: {error}");
code.to_string()
}
}
}
fn main() {
if let Err(error) = run() {
eprintln!("error: {error}");
std::process::exit(1);
}
}
fn run() -> Result<()> {
println!("cargo:rerun-if-changed={ORIGINAL_XSD_DIR}");
let out_dir = PathBuf::from(
env::var("OUT_DIR")
.map_err(|error| format!("OUT_DIR environment variable is not set: {error}"))?,
);
let dest_path = out_dir.join(OUTPUT_FILE_NAME);
let active_schemas: Vec<&str> = FEATURE_SCHEMAS
.iter()
.filter(|(feature, _)| feature_enabled(feature))
.map(|(_, xsd)| *xsd)
.collect();
if active_schemas.is_empty() {
fs::write(&dest_path, "").map_err(|error| {
format!(
"Failed to write empty schema to {}: {error}",
dest_path.display()
)
})?;
return Ok(());
}
let patched_xsd_dir = out_dir.join("patched_xsd");
copy_and_patch_schemas(Path::new(ORIGINAL_XSD_DIR), &patched_xsd_dir)
.map_err(|error| format!("Failed to patch and copy XSD schemas: {error}"))?;
let mut config = Config::default()
.with_naming(SumoNaming::default())
.with_quick_xml_deserialize();
if feature_enabled(WRITE_FEATURE) {
config = config.with_quick_xml_serialize();
}
config.parser.schemas = active_schemas
.iter()
.map(|xsd| Schema::File(patched_xsd_dir.join(xsd)))
.collect();
let code = generate(config)
.map_err(|error| format!("Error generating code from XSD schema: {error:?}"))?;
fs::write(&dest_path, pretty_print(&code.to_string())).map_err(|error| {
format!(
"Failed to write generated file to {}: {error}",
dest_path.display()
)
})?;
Ok(())
}