Initial commit
All checks were successful
test / test (push) Successful in 3m2s

This commit is contained in:
2025-04-17 07:44:47 +09:30
commit 99c8aa8e70
8 changed files with 820 additions and 0 deletions

303
src/lib.rs Normal file
View File

@@ -0,0 +1,303 @@
use std::{
fs::File,
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use anyhow::Result;
use log::{debug, info};
use quick_xml::{Reader, events::Event};
pub fn xsd_to_dts(xsd: &PathBuf, output: &PathBuf) -> Result<()> {
let mut reader = Reader::from_file(xsd)?;
let mut buf = Vec::new();
if let Some(parent_dir) = output.parent() {
std::fs::create_dir_all(parent_dir)?;
}
let output_file = File::create(output)?;
let mut writer = BufWriter::new(output_file);
let mut in_class = false;
let mut in_nested_class = false;
let mut other_classes = vec![];
let mut in_sequence = false;
loop {
match reader.read_event_into(&mut buf).unwrap() {
Event::Eof => break,
Event::Start(start) => {
match start.name().as_ref() {
b"xs:sequence" => {
in_sequence = true;
}
b"xs:element" => {
if let Some(type_attr) = start.try_get_attribute("type")? {
let mut ts_type = std::str::from_utf8(&type_attr.value)?;
if ts_type.starts_with("xs:") {
ts_type = &ts_type[3..];
}
if ts_type.eq_ignore_ascii_case("datetime") {
ts_type = "XMLGregorianCalendar";
} else if ts_type.eq_ignore_ascii_case("int")
|| ts_type.eq_ignore_ascii_case("double")
{
ts_type = "number";
}
let mut ts_type_final = ts_type.to_owned();
if !ts_type.eq("string") {
ts_type_final = capitalize(ts_type);
}
if let Some(occurs_attr) = start.try_get_attribute("maxOccurs")? {
if std::str::from_utf8(&occurs_attr.value)? == "unbounded" {
ts_type_final = format!("List<{}>", ts_type_final);
}
}
if let Some(name_attr) = start.try_get_attribute("name")? {
if in_class && !in_nested_class {
writer.write(
format!(
"\tget{}(): {};\n\n",
capitalize(std::str::from_utf8(&name_attr.value)?),
ts_type_final
)
.as_bytes(),
)?;
writer.write(
format!(
"\tset{}({}: {}): void;\n\n",
capitalize(std::str::from_utf8(&name_attr.value)?),
std::str::from_utf8(&name_attr.value)?,
ts_type_final
)
.as_bytes(),
)?;
} else if in_nested_class {
let class_index = other_classes.len() - 1;
other_classes[class_index] = format!(
"{}\n\n\tget{}(): {};\n\n",
other_classes[other_classes.len() - 1],
capitalize(std::str::from_utf8(&name_attr.value)?),
ts_type_final
);
other_classes[class_index] = format!(
"{}\tset{}({}: {}): void;\n\n",
other_classes[other_classes.len() - 1],
capitalize(std::str::from_utf8(&name_attr.value)?),
std::str::from_utf8(&name_attr.value)?,
ts_type_final
);
}
}
} else {
// Got a new type, if we're not in a class we should start
// writing the class, otherwise leave it until the end
if let Some(name_attr) = start.try_get_attribute("name")? {
if !in_class {
in_class = true;
writer.write(
format!(
"declare class {} {{\n",
capitalize(std::str::from_utf8(&name_attr.value)?)
)
.as_bytes(),
)?;
} else {
in_nested_class = true;
other_classes.push(format!(
"declare class {} {{\n",
capitalize(std::str::from_utf8(&name_attr.value)?)
));
}
}
}
}
b"xs:complexType" => {
if let Some(name_attr) = start.try_get_attribute("name")? {
in_class = true;
writer.write(
format!(
"declare class {} {{\n",
capitalize(std::str::from_utf8(&name_attr.value)?)
)
.as_bytes(),
)?;
}
}
b"xs:attribute" => {
if let Some(type_attr) = start.try_get_attribute("type")? {
let mut ts_type = std::str::from_utf8(&type_attr.value)?;
if ts_type.starts_with("xs:") {
ts_type = &ts_type[3..];
}
if ts_type.eq_ignore_ascii_case("datetime") {
ts_type = "XMLGregorianCalendar";
} else if ts_type.eq_ignore_ascii_case("int")
|| ts_type.eq_ignore_ascii_case("double")
{
ts_type = "number";
}
if in_class {
if let Some(name_attr) = start.try_get_attribute("name")? {
writer.write(
format!(
"\tget{}(): {};\n\n",
capitalize(std::str::from_utf8(&name_attr.value)?),
ts_type
)
.as_bytes(),
)?;
writer.write(
format!(
"\tset{}({}: {}): void;\n\n",
capitalize(std::str::from_utf8(&name_attr.value)?),
std::str::from_utf8(&name_attr.value)?,
ts_type
)
.as_bytes(),
)?;
}
}
}
}
name => {
info!(
"Found unknown element type: {}",
String::from_utf8_lossy(name)
);
}
}
debug!("{:?}", start);
}
Event::End(end) => {
match end.name().as_ref() {
b"xs:sequence" => {
in_sequence = false;
}
b"xs:element" => {
if in_sequence {
continue;
}
if in_nested_class {
in_nested_class = false;
let class_index = other_classes.len() - 1;
other_classes[class_index] =
format!("{}\n}}", other_classes[other_classes.len() - 1]);
} else if in_class && !in_nested_class {
in_class = false;
writer.write("}\n".as_bytes())?;
}
}
b"xs:complexType" => {
if in_class && !in_nested_class {
in_class = false;
writer.write("}\n".as_bytes())?;
}
}
name => {
info!(
"Found unknown element type: {}",
String::from_utf8_lossy(name)
);
}
}
debug!("{:?}", end);
}
Event::Empty(empty) => {
if let Some(type_attr) = empty.try_get_attribute("type")? {
let mut ts_type = std::str::from_utf8(&type_attr.value)?;
if ts_type.starts_with("xs:") {
ts_type = &ts_type[3..];
}
if ts_type.eq_ignore_ascii_case("datetime") {
ts_type = "XMLGregorianCalendar";
} else if ts_type.eq_ignore_ascii_case("int")
|| ts_type.eq_ignore_ascii_case("double")
{
ts_type = "number";
}
let mut ts_type_final = ts_type.to_owned();
if !ts_type.eq("string") {
ts_type_final = capitalize(ts_type);
}
if let Some(occurs_attr) = empty.try_get_attribute("maxOccurs")? {
if std::str::from_utf8(&occurs_attr.value)? == "unbounded" {
ts_type_final = format!("List<{}>", ts_type_final);
}
}
if let Some(name_attr) = empty.try_get_attribute("name")? {
if in_class && !in_nested_class {
writer.write(
format!(
"\tget{}(): {};\n\n",
capitalize(std::str::from_utf8(&name_attr.value)?),
ts_type_final
)
.as_bytes(),
)?;
writer.write(
format!(
"\tset{}({}: {}): void;\n\n",
capitalize(std::str::from_utf8(&name_attr.value)?),
std::str::from_utf8(&name_attr.value)?,
ts_type_final
)
.as_bytes(),
)?;
} else if in_nested_class {
let class_index = other_classes.len() - 1;
other_classes[class_index] = format!(
"{}\tget{}(): {};\n\n",
other_classes[other_classes.len() - 1],
capitalize(std::str::from_utf8(&name_attr.value)?),
ts_type_final
);
other_classes[class_index] = format!(
"{}\tset{}({}: {}): void;\n\n",
other_classes[other_classes.len() - 1],
capitalize(std::str::from_utf8(&name_attr.value)?),
std::str::from_utf8(&name_attr.value)?,
ts_type_final
);
}
}
}
}
name => {
debug!("Got Something: {:?}", name);
}
}
}
for other_class in other_classes {
writer.write(format!("\n\n{}", other_class).as_bytes())?;
}
Ok(())
}
/// Capitalizes the first character in s.
pub fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use anyhow::Result;
use crate::xsd_to_dts;
#[test]
fn ppm2xsd() -> Result<()> {
xsd_to_dts(
&PathBuf::from("test_resources/test.xsd"),
&PathBuf::from("test_output/test.d.ts"),
)
}
}

25
src/main.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use xsd_to_dts::xsd_to_dts;
#[derive(Parser)]
#[command(name = "xsd-to-dts")]
#[command(author = "Pivato M. <contact@michaelpivato.dev>")]
#[command(version = "0.0.1")]
#[command(about = "Convert an XML Schema (.xsd) file to a .d.ts file using java naming conventions", long_about = None)]
struct Cli {
#[arg(short, long, value_name = "FILE")]
input_xsd: PathBuf,
#[arg(short, long, value_name = "FILE")]
output_dts: PathBuf,
}
fn main() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
xsd_to_dts(&cli.input_xsd, &cli.output_dts)
}