use std::{env, path::Path};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR")?;
let out_path = Path::new(&out_dir);
println!("cargo:rerun-if-changed=dimensions.txt");
dimensions::generate(&out_path.join("dimensions_generated.rs"))?;
println!("cargo:rerun-if-changed=units.txt");
units::generate(&out_path.join("units_generated.rs"))?;
Ok(())
}
type Error = Box<dyn std::error::Error>;
fn to_pascal_case(s: &str) -> String {
s.split(|c: char| c == '_' || c == '-' || c.is_whitespace())
.filter(|word| !word.is_empty())
.map(|word| {
let mut chars = word.chars();
chars
.next()
.map(|first| {
first
.to_uppercase()
.chain(chars.as_str().to_lowercase().chars())
.collect::<String>()
})
.unwrap_or_default()
})
.collect()
}
mod dimensions {
use super::*;
use std::{fmt::Write, fs, path::PathBuf};
#[derive(Debug, Clone)]
struct Dimension {
name: String,
exponents: [i8; 7],
doc: Option<String>,
}
pub fn generate(output_path: &PathBuf) -> Result<(), Error> {
let content = fs::read_to_string("dimensions.txt")?;
let dimensions = parse_dimensions(&content)?;
validate_dimensions(&dimensions)?;
let code = generate_code(&dimensions)?;
fs::write(output_path, code)?;
Ok(())
}
fn parse_dimensions(content: &str) -> Result<Vec<Dimension>, Error> {
let mut dimensions = Vec::new();
let mut line_num = 0;
for line in content.lines() {
line_num += 1;
let trimmed_line = line.trim();
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
continue;
}
let Some(dim) = parse_dimension_line(trimmed_line) else {
panic!("invalid dimension on line {}: {}", line_num, trimmed_line);
};
dimensions.push(dim);
}
assert!(!dimensions.is_empty());
Ok(dimensions)
}
fn parse_dimension_line(line: &str) -> Option<Dimension> {
let parts: Vec<&str> = line.splitn(2, '#').collect();
let definition = parts[0].trim();
let doc = parts.get(1).map(|s| s.trim().to_string());
let mut parts = definition.splitn(2, ':');
let name = parts.next()?.trim().to_string();
let exponents = parts.next()?.trim();
let exponents: Vec<i8> = exponents
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if exponents.len() != 7 {
return None;
}
Some(Dimension {
name,
exponents: exponents.try_into().ok()?,
doc,
})
}
fn validate_dimensions(dimensions: &[Dimension]) -> Result<(), Error> {
let mut seen = std::collections::HashSet::new();
for dim in dimensions {
if !seen.insert(&dim.name) {
panic!("duplicate dimension name: {}", dim.name);
}
}
Ok(())
}
fn generate_code(dimensions: &[Dimension]) -> Result<String, Error> {
let mut code = String::new();
writeln!(
&mut code,
"// This file is automatically generated by build.rs"
)?;
writeln!(&mut code, "// Do not edit manually!")?;
writeln!(&mut code, "// Source: dimensions.txt")?;
writeln!(&mut code)?;
writeln!(&mut code, "use typenum::*;")?;
writeln!(&mut code)?;
for dim in dimensions {
if let Some(ref doc) = dim.doc {
writeln!(code, "/// {}", doc)?;
}
write!(code, "pub type {} = Dimension<", to_pascal_case(&dim.name))?;
for (i, &exp) in dim.exponents.iter().enumerate() {
if i > 0 {
write!(code, ", ")?;
}
write!(
code,
"{}",
match exp {
-4 => "N4",
-3 => "N3",
-2 => "N2",
-1 => "N1",
0 => "Z0",
1 => "P1",
2 => "P2",
3 => "P3",
4 => "P4",
_ => unreachable!(),
}
)?;
}
writeln!(code, ">;")?;
writeln!(code)?;
}
Ok(code)
}
}
mod units {
use super::*;
use std::{fmt::Write, fs, path::PathBuf};
#[derive(Debug, Clone)]
struct Unit {
name: String,
symbol: String,
dimension: String,
}
pub fn generate(output_path: &PathBuf) -> Result<(), Error> {
let content = fs::read_to_string("units.txt")?;
let units = parse_units(&content)?;
let code = generate_code(&units)?;
fs::write(output_path, code)?;
Ok(())
}
fn parse_units(content: &str) -> Result<Vec<Unit>, Error> {
let mut units = Vec::new();
for line in content.lines() {
let trimmed_line = line.trim();
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
continue;
}
let parts: Vec<&str> = trimmed_line.split_whitespace().collect();
assert_eq!(parts.len(), 3);
units.push(Unit {
name: parts[0].into(),
symbol: parts[1].into(),
dimension: parts[2].into(),
});
}
Ok(units)
}
fn generate_code(units: &[Unit]) -> Result<String, Error> {
let mut code = String::new();
writeln!(
&mut code,
"// This file is automatically generated by build.rs"
)?;
writeln!(&mut code, "// Do not edit manually!")?;
writeln!(&mut code, "// Source: units.txt")?;
writeln!(&mut code)?;
writeln!(&mut code, "define_units! {{")?;
for unit in units {
let dimension_type = to_pascal_case(&unit.dimension);
writeln!(
&mut code,
" {} ({}): {},",
unit.name, unit.symbol, dimension_type
)?;
}
writeln!(&mut code, "}}")?;
Ok(code)
}
}