This commit is contained in:
303
src/lib.rs
Normal file
303
src/lib.rs
Normal 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
25
src/main.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user