ruic 0.1.1

Load Qt Designer .ui files into Rust code at compile time
Documentation
use convert_case::{Case, Casing};
use regex::Regex;
use roxmltree::{Document, Node};
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::PathBuf;

type IO<T> = Result<T, Box<dyn std::error::Error>>;

mod config;
pub use config::Config;

/// An XML field representing a Qt object.
struct Field<'a> {
    /// Object class, e.g. QWidget, QAction, QVBoxLayout.
    widget: &'a str,
    /// Object name, e.g. actionCloseFile.
    name: &'a str,
    /// Object name converted to snake-case, e.g. action_close_file.
    name_rs: String,
}

impl<'a> Field<'a> {
    /// Parses a `Field` from an XML node if the node defines a Qt object.
    /// If the node is something else, such as a property or item, this function returns `None`.
    /// It also returns `None` if `config.all` is false and the field is on the ignore list.
    fn parse<'input>(node: Node<'a, 'input>, config: &Config) -> Option<Self> {
        let name = node.attribute("name")?;
        if !config.all
            && (name == "label" || name.starts_with("label_") || name.ends_with("_label"))
        {
            return None;
        }
        let widget = match node.tag_name().name() {
            "widget" => node.attribute("class"),
            "action" => Some("QAction"),
            "layout" if config.all || !node.has_children() => node.attribute("class"),
            _ => None,
        }?;
        if !config.all && widget == "QGroupBox" && node.has_children() {
            None
        } else {
            Some(Field {
                widget,
                name_rs: name.to_case(Case::Snake),
                name,
            })
        }
    }
}

/// The main runner of the program. Translates .ui code into .rs and writes it to a stream.
pub struct Ruic<W> {
    /// Program output.
    writer: W,
    /// Command-line arguments.
    config: Config,
    /// Removes all whitespace.
    trimmer: Regex,
}

impl<W: Write> Ruic<W> {
    /// Creates a new `Ruic<W>` from command-line arguments.
    pub fn new(writer: W, config: Config) -> Self {
        Self {
            writer,
            config,
            trimmer: Regex::new(r"\n\s*").unwrap(),
        }
    }

    /// Writes a header and then recursively processes directories and files.
    pub fn process(&mut self) -> io::Result<()> {
        let path = self.config.path.clone();
        writeln!(
            self.writer,
            r#"// This file is automatically generated.
use cpp_core::{{CastInto, Ptr}};
use qt_core::{{QBox, QPtr}};
use qt_ui_tools::QUiLoader;
use qt_widgets::*;
"#
        )?;
        if path.is_dir() {
            self.process_dir(path)
        } else {
            if let Err(e) = self.process_file(path) {
                eprintln!("{}", e);
            }
            Ok(())
        }
    }

    /// Processes the contents of a directory.
    fn process_dir(&mut self, path: PathBuf) -> io::Result<()> {
        for entry in fs::read_dir(path)? {
            let entry = entry?;
            let subpath = entry.path();
            let metadata = entry.metadata()?;
            if !self.config.no_recursive && metadata.is_dir() {
                self.process_dir(subpath)?;
            } else if subpath
                .extension()
                .and_then(OsStr::to_str)
                .unwrap_or("")
                .to_lowercase()
                == "ui"
            {
                if let Err(e) = self.process_file(subpath) {
                    eprintln!("{}", e)
                }
            }
        }
        Ok(())
    }

    /// Processes the contents of a file that ends in .ui.
    fn process_file(&mut self, path: PathBuf) -> IO<()> {
        let mut src = String::new();
        File::open(&path)?.read_to_string(&mut src)?;
        src = self.trimmer.replace_all(&src, "").into_owned();
        let doc = Document::parse(&src)?;
        let mut els = doc
            .root_element()
            .children()
            .find(|el| el.tag_name().name() == "widget")
            .ok_or_else(|| format!("{}: invalid format", path.to_string_lossy()))?
            .descendants()
            .filter_map(|node| Field::parse(node, &self.config));

        let base = els
            .next()
            .ok_or_else(|| format!("{}: invalid format", path.to_string_lossy()))?;
        let fields: Vec<_> = els.collect();
        writeln!(
            self.writer,
            "#[derive(Debug)]
pub struct {}{} {{
    pub widget: QBox<{}>,",
            base.name, self.config.suffix, base.widget
        )?;
        for field in &fields {
            writeln!(
                self.writer,
                "    pub {}: QPtr<{}>,",
                field.name_rs, field.widget
            )?;
        }
        writeln!(
            self.writer,
            r#"}}
impl {}{} {{
    pub fn load<P: CastInto<Ptr<QWidget>>>(parent: P) -> Self {{
        unsafe {{
            let loader = QUiLoader::new_0a();
            loader.set_language_change_enabled(true);
            let bytes = {:?}.as_bytes();
            let widget = loader.load_bytes_with_parent(bytes, parent);
            assert!(!widget.is_null(), "invalid ui file");
            Self {{"#,
            base.name, self.config.suffix, src
        )?;
        for field in &fields {
            writeln!(
                self.writer,
                r#"                {}: widget.find_child("{}").unwrap(),"#,
                field.name_rs, field.name
            )?;
        }
        if base.widget == "QWidget" {
            writeln!(self.writer, "                widget,")?;
        } else {
            writeln!(
                self.writer,
                "                widget: QBox::from_q_ptr(widget.into_q_ptr().dynamic_cast()),"
            )?;
        }
        writeln!(
            self.writer,
            r#"            }}
        }}
    }}
}}"#
        )?;
        Ok(())
    }
}