e173 0.1.0-alpha.7

A library implementing various aspects of the ESTA BSR E1.73 standard
Documentation
use std::cmp::Ordering;
use std::fs::File;
use std::io::{BufWriter, Write};

use e173::controller::{DmxDriver, ParamRange, ParameterCombo, ParameterConstraint};
use e173::schema::E173Document;
use e173::schema::EstaDmx;
use itertools::Itertools;

fn main() {
    let doc: E173Document =
        serde_json::from_str(include_str!("data/martin_mac_encore_performance_cld.json")).unwrap();

    let device_class = doc
        .e173doc
        .device_classes
        .get("com.martin.dev.encore-performance-cold")
        .unwrap()
        .get("1.0.0")
        .unwrap();

    eprintln!("Deserialized UDR Document:");
    eprintln!(" -- Author: {}", device_class.author);
    eprintln!(" -- Manufacturer: {}", device_class.info.manufacturer.name);
    eprintln!(" -- Model: {}", device_class.info.model.name);

    eprint!("\nParsing ESTA DMX serializer...");

    let esta_dmx = serde_json::from_value::<EstaDmx>(
        device_class
            .serializers
            .get("dmx")
            .unwrap()
            .default
            .clone()
            .unwrap(),
    )
    .unwrap();

    eprintln!("Done.");
    eprint!("Transforming DMX data...");

    let transformed = DmxDriver::from(&esta_dmx).unwrap();

    eprintln!("Done.");

    println!("{}", serde_json::to_string_pretty(&transformed).unwrap());

    to_html_output(&transformed, "export.html").unwrap();
}

fn to_html_output(dmx_driver: &DmxDriver, file_name: &str) -> std::io::Result<()> {
    let mut out = BufWriter::new(File::create(file_name)?);
    out.write(b"<html><head>\n")?;
    out.write(b"<title>MAC Encore Performance - Standard</title>")?;
    out.write(
        b"<style>\n\
          table { border-collapse: collapse; }\n\
          td { border: 1px solid black; }\n\
          .dmx { color: grey; }\n\
          .none { color: grey; }\n\
          .calc { color: grey; }\n\
          </style>\n",
    )?;

    out.write(b"<h2>DMX Combinations</h2>")?;

    for cluster in &dmx_driver.clusters {
        let mut param_vec: Vec<_> = cluster.parameters.iter().collect();
        param_vec.sort();

        out.write(b"<table>\n")?;
        out.write(b"<tr>")?;
        for parameter in &param_vec {
            out.write(format!("<th>{}</th>", parameter).as_bytes())?;
        }
        out.write(b"</tr>")?;

        let mut combinations = cluster.combinations.clone();
        combinations.sort_by(compare_parameter_combo);

        for combination in combinations {
            out.write(b"<tr>\n")?;
            for parameter in &param_vec {
                out.write(b"<td>")?;
                if let Some(constraint) = combination.constraints.get(*parameter) {
                    out.write(range_to_html(&constraint.param_range).as_bytes())?;
                    if let Some(mapping) = &constraint.dmx_mapping {
                        let offsets = &dmx_driver.chunks.get(&mapping.chunk_id).unwrap().offsets;
                        out.write(b"<br><span class=\"dmx\">Ch ")?;
                        out.write(
                            offsets
                                .iter()
                                .map(|offset| (offset + 1).to_string())
                                .join("+")
                                .as_bytes(),
                        )?;
                        out.write(format!(": {}", mapping.start).as_bytes())?;
                        if mapping.end != mapping.start {
                            out.write(format!(" > {}", mapping.end).as_bytes())?;
                        }
                        out.write(b"</span>")?;
                    }
                    if constraint.calculated {
                        out.write(b"<br><span class=\"calc\">calculated</span>")?;
                    }
                }
                out.write(b"</td>")?;
            }
            out.write(b"</tr>")?;
        }
        out.write(b"</table>")?;
    }

    out.write(b"</body></html>")?;
    out.flush()
}

fn compare_parameter_combo(lhs: &ParameterCombo, rhs: &ParameterCombo) -> Ordering {
    if lhs.constraints.len() != rhs.constraints.len() {
        return lhs.constraints.len().cmp(&rhs.constraints.len());
    }

    let mut lhs_constraints: Vec<_> = lhs.constraints.iter().collect();
    let mut rhs_constraints: Vec<_> = rhs.constraints.iter().collect();
    lhs_constraints.sort_by(compare_constraint);
    rhs_constraints.sort_by(compare_constraint);

    let mismatch = lhs_constraints
        .into_iter()
        .zip(rhs_constraints.into_iter())
        .find(|(lhs, rhs)| lhs != rhs);

    match mismatch {
        None => Ordering::Equal,
        Some((lhs, rhs)) => compare_constraint(&lhs, &rhs),
    }
}

fn compare_constraint(
    lhs: &(&String, &ParameterConstraint),
    rhs: &(&String, &ParameterConstraint),
) -> Ordering {
    lhs.0.cmp(&rhs.0).then(
        lhs.1
            .param_range
            .partial_cmp(&rhs.1.param_range)
            .unwrap_or(Ordering::Equal),
    )
}

fn range_to_html(range: &ParamRange) -> String {
    match range {
        ParamRange::Null => "<span class=\"none\" />".into(),
        ParamRange::Bool { start, end } => {
            if start != end {
                format!("{start} > {end}")
            } else {
                format!("{start}")
            }
        }
        ParamRange::Numeric { start, end } => {
            if start != end {
                format!("{start} > {end}")
            } else {
                format!("{start}")
            }
        }
    }
}