rplc 0.3.0

PLC programming in Rust
Documentation
use serde::Serialize;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};

pub const AUTO_GENERATED: &str = "// AUTO-GENERATED BY RPLC";

pub mod config;

use config::Config;

pub fn generate(config_file: &str) -> Result<(), Box<dyn Error>> {
    Builder::new(config_file).generate()
}

pub struct Builder<'a> {
    config_file: &'a str,
    context: tera::Context,
}

impl<'a> Builder<'a> {
    pub fn new(config_file: &'a str) -> Self {
        Self {
            config_file,
            context: tera::Context::new(),
        }
    }
    pub fn insert<T: Serialize + ?Sized, S: Into<String>>(&mut self, variable: S, value: &T) {
        self.context.insert(variable, value);
    }
    pub fn generate(&self) -> Result<(), Box<dyn Error>> {
        let config = Config::load(self.config_file, &self.context)?;
        prepare(&config)?;
        fs::create_dir_all("src/plc")?;
        config.generate_io("src/plc/io.rs")?;
        config.generate_context("src/plc/context.rs")?;
        Ok(())
    }
}

fn prepare(config: &Config) -> Result<(), Box<dyn Error>> {
    fs::create_dir_all("src/plc")?;
    let mut plc_mod = codegen::Scope::new();
    plc_mod.raw(AUTO_GENERATED);
    plc_mod.raw("use ::std::time::Duration;");
    plc_mod.raw("pub mod context;");
    plc_mod.raw("pub mod io;");
    for c in &["VERSION", "NAME", "DESCRIPTION"] {
        plc_mod.raw(format!(
            "pub const {c}: &str = \"{}\";",
            env::var(format!("CARGO_PKG_{c}"))?
        ));
    }
    plc_mod.raw(format!(
        "pub const STACK_SIZE: Option<usize> = {};",
        if let Some(ss) = config.core.stack_size {
            format!("Some({})", ss * 1000)
        } else {
            "None".to_owned()
        }
    ));
    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_sign_loss)]
    plc_mod.raw(format!(
        "#[allow(clippy::unreadable_literal)] pub const STOP_TIMEOUT: Duration = Duration::from_nanos({});",
        (config.core.stop_timeout.trunc() as u64) * 1_000_000_000
    ));
    #[cfg(feature = "eva")]
    plc_mod.raw(format!(
        "pub const EAPI_ACTION_POOL_SIZE: usize = {};",
        config.eapi.action_pool_size
    ));
    #[cfg(not(feature = "eva"))]
    plc_mod.raw("pub const EAPI_ACTION_POOL_SIZE: usize = 0;");
    write("src/plc/mod.rs", plc_mod.to_string())?;
    Ok(())
}

fn format_code(code: String) -> Result<String, Box<dyn Error>> {
    let mut child = Command::new("rustfmt")
        .arg("--edition=2021")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;
    let mut stdin = child
        .stdin
        .take()
        .ok_or_else(|| eva_common::Error::io("unable to take stdin"))?;
    std::thread::spawn(move || {
        stdin.write_all(code.as_bytes()).unwrap();
        stdin.flush().unwrap();
    });
    let output = child.wait_with_output()?;
    if !output.status.success() {
        return Err(eva_common::Error::failed(format!(
            "process exit code: {:?}",
            output.status.code()
        ))
        .into());
    }
    //let mut formatted_code = String::new();
    //for line in stdout {
    //writeln!(formatted_code, "{}", line.unwrap()).unwrap();
    //}
    let formatted_code = std::str::from_utf8(&output.stdout)?;
    Ok(formatted_code.to_owned())
}

fn write<T, D>(target: T, data: D) -> Result<(), Box<dyn Error>>
where
    T: AsRef<Path>,
    D: fmt::Display,
{
    let mut code = data.to_string();
    match format_code(code.clone()) {
        Ok(c) => code = c,
        Err(e) => println!("cargo:warning=unable to format code with rustfmt: {}", e),
    }
    if let Ok(x) = fs::read_to_string(&target) {
        if code == x {
            return Ok(());
        }
    }
    fs::write(target, code)?;
    Ok(())
}