fit 0.2.0

A small crate used for reading and decoding FIT files generated by sports devices.
Documentation
use csv::ReaderBuilder;
use heck::{CamelCase, TitleCase};
use std::env;
use std::fs::File;
use std::io::{BufWriter, Read, Write};
use std::path::Path;

pub struct MessageField {
    pub name: String,
    pub kind: String,
    pub scale: f32,
    pub offset: f32,
}

fn main() {
    read_types_csv();
    read_messages_csv();
}

fn read_types_csv() {
    let out_path = Path::new(&env::var("OUT_DIR").unwrap()).join("types.rs");
    let mut out_file = BufWriter::new(File::create(&out_path).unwrap());

    let mut in_file = File::open("types.semi.csv").unwrap();
    let mut contents = String::new();
    in_file.read_to_string(&mut contents).unwrap();

    let mut rdr = ReaderBuilder::new()
        .delimiter(b';')
        .from_reader(contents.as_bytes());

    let mut subsequent = false;
    for r in rdr.records().flatten() {
        if !&r[0].is_empty() {
            let name = &r[0];
            if subsequent {
                write!(&mut out_file, "    _ => None\n  }}\n}}\n\n").unwrap();
            } else {
                subsequent = true;
            }
            write!(
                &mut out_file,
                "{}",
                format!(
                    "pub fn {}(key: &u32) -> Option<&'static str> {{\n  match key {{\n",
                    name
                )
            )
            .unwrap();
        }
        if !&r[3].is_empty() {
            let value = &r[2];
            if r[4].contains("Deprecated") {
                continue;
            }
            match parse_u32(&r[3]) {
                Some(key) => {
                    write!(
                        &mut out_file,
                        "{}",
                        format!("    {} => Some({:?}),\n", key, value)
                    )
                    .unwrap();
                }
                None => (),
            }
        }
    }
    write!(&mut out_file, "    _ => None\n  }}\n}}\n\n").unwrap();
}

fn read_messages_csv() {
    let def_path = Path::new(&env::var("OUT_DIR").unwrap()).join("message_definitions.rs");
    let mut def_file = BufWriter::new(File::create(&def_path).unwrap());
    let msg_path = Path::new(&env::var("OUT_DIR").unwrap()).join("messages.rs");
    let mut msg_file = BufWriter::new(File::create(&msg_path).unwrap());

    let mut in_file = File::open("messages.semi.csv").unwrap();
    let mut contents = String::new();
    in_file.read_to_string(&mut contents).unwrap();

    write!(
        &mut msg_file,
        "fn message(msg: &str) -> Option<Box<dyn DefinedMessageType>> {{\n
            match msg {{\n"
    )
    .unwrap();

    let mut rdr = ReaderBuilder::new()
        .delimiter(b';')
        .from_reader(contents.as_bytes());

    let mut subsequent = false;
    for r in rdr.records().flatten() {
        if !&r[0].is_empty() {
            let name = &r[0];
            if subsequent {
                write!(
                    &mut def_file,
                    "             _ => None,\n        }}\n    }}\n}}\n"
                )
                .unwrap();
            } else {
                subsequent = true;
            }
            write!(
                &mut def_file,
                "{}",
                format!(
                    r#"#[derive(Debug)]
pub struct {0} {{ values: HashMap<u16, Value> }}
impl DefinedMessageType for {0} {{
    fn new() -> Self {{
        Self {{ values: HashMap::new() }} 
    }}
    fn inner(&self) -> &HashMap<u16, Value> {{
        &self.values
    }}
    fn name(&self) -> &str {{
        "{1}"
    }}
    fn write_value(&mut self, num: u16, val: Value) {{
        self.values.insert(num, val);
    }}
    fn defined_message_field(&self, num: u16) -> Option<&DefinedMessageField> {{
        match num {{
"#,
                    name.to_camel_case(),
                    name.to_title_case()
                )
            )
            .unwrap();
            write!(
                &mut msg_file,
                "{}",
                format!(
                    "{:?} => Some(Box::new({}::new())),\n",
                    name,
                    name.to_camel_case()
                )
            )
            .unwrap();
        }
        if !&r[1].is_empty() {
            let value = &r[2];
            if value.contains("Deprecated") {
                continue;
            }
            match parse_u32(&r[1]) {
                Some(key) => write!(
                    &mut def_file,
                    "{}",
                    format!(
                        r#"            {0} => {{
                static F: DefinedMessageField = DefinedMessageField {{
                    num: {0},
                    name: "{1}",
                    kind: "{2}",
                    scale: {3:?},
                    offset: {4:?},
                }};
                Some(&F)
            }},
"#,
                        key,
                        value,
                        &r[3],
                        &r[6].parse::<f64>().ok(),
                        &r[7].parse::<f64>().ok(),
                    )
                )
                .unwrap(),
                None => (),
            }
        }
    }
    write!(
        &mut def_file,
        "            _ => None,\n        }}\n    }}\n}}\n"
    )
    .unwrap();
    write!(
        &mut msg_file,
        "            _ => None,\n        }}\n    }}\n"
    )
    .unwrap();
}

fn parse_u32(s: &str) -> Option<u32> {
    const HEX: &'static str = "0x";
    let l = s.to_lowercase();
    if l.starts_with(HEX) {
        u32::from_str_radix(&l.trim_start_matches(HEX), 16).ok()
    } else {
        u32::from_str_radix(&l, 10).ok()
    }
}