slip44 0.1.4

Mapping between SLIP-0044 coin types and the associated metadata.
Documentation
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::Path;

use itertools::Itertools;
use reqwest;

const SLIP_0044_MARKDOWN_URL: &str =
    "https://raw.githubusercontent.com/satoshilabs/slips/master/slip-0044.md";
const SLIP_044_MARKDOWN_HEADER: &str = "Coin type | Path component (`coin_type'`) | Symbol | Coin";

#[derive(Debug)]
struct CoinType {
    id: u32,
    ids: Vec<u32>,
    path_component: String,
    symbol: Option<String>,
    name: String,
    original_name: String,
    link: Option<String>,
    rustdoc_lines: Vec<String>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let coin_types = reqwest::blocking::get(SLIP_0044_MARKDOWN_URL)?
        .text()?
        .split("\n")
        .skip_while(|&line| line != SLIP_044_MARKDOWN_HEADER)
        .skip(2)
        .filter_map(|line| {
            let columns = line.split('|').collect::<Vec<_>>();
            if columns.len() != 4 {
                return None;
            }

            let (original_name, link) = parse_markdown_link(columns[3].trim());
            if original_name.is_empty() || original_name == "reserved" {
                return None;
            }

            let name = original_name_to_short(original_name).unwrap();

            Some(CoinType {
                id: columns[0].trim().parse().ok()?,
                ids: vec![],
                path_component: columns[1].trim().to_string(),
                symbol: Some(columns[2].trim())
                    .map(prepend_enum)
                    .map(|symbol| match symbol.as_str() {
                        "$DAG" => "DAG".to_string(),
                        symbol => symbol.to_string(),
                    })
                    .filter(|symbol| !symbol.is_empty()),
                name: name.to_string(),
                original_name: original_name.to_string(),
                link: link.map(ToString::to_string),
                rustdoc_lines: vec![],
            })
        })
        .fold(HashMap::<_, CoinType>::new(), |mut acc, coin_type| {
            let id = coin_type.id.clone();

            acc.entry((
                coin_type.symbol.clone(),
                coin_type.name.clone(),
                coin_type.original_name.clone(),
            ))
            .or_insert(coin_type)
            .ids
            .push(id);
            acc
        })
        .into_iter()
        .fold(HashMap::<_, Vec<_>>::new(), |mut acc, (_, coin_type)| {
            acc.entry(coin_type.name.clone())
                .or_default()
                .push(coin_type);
            acc
        })
        .into_iter()
        .map(|(_, coin_types)| {
            let coin_types = if coin_types.len() > 1 {
                coin_types
                    .into_iter()
                    .map(|coin_type| CoinType {
                        name: format!(
                            "{}_{}",
                            coin_type.name.clone(),
                            match coin_type.symbol.clone() {
                                Some(symbol) => symbol,
                                None => coin_type.ids.clone().into_iter().join("_").to_string(),
                            }
                        ),
                        ..coin_type
                    })
                    .collect()
            } else {
                coin_types
            };

            coin_types
                .into_iter()
                .map(|coin_type| CoinType {
                    rustdoc_lines: vec![
                        format!("/// Coin type: {}", coin_type.ids.iter().join(", ")),
                        if let Some(symbol) = coin_type.symbol.clone() {
                            format!("/// Symbol: {}", symbol)
                        } else {
                            "".to_string()
                        },
                        format!("/// Coin: {}", coin_type.original_name),
                    ],
                    ..coin_type
                })
                .collect::<Vec<_>>()
        })
        .flatten();

    let mut file = std::fs::File::create(
        Path::new(file!())
            .parent()
            .ok_or("can't get first parent")?
            .parent()
            .ok_or("can't get second parent")?
            .join("coin.rs"),
    )?;

    writeln!(&mut file, "// Code generated by {}; DO NOT EDIT.", file!())?;
    writeln!(&mut file, "use crate::coins;")?;
    writeln!(&mut file, "coins!(")?;

    let mut seen_symbols = HashSet::<String>::new();

    for coin_type in coin_types.sorted_by_key(|coin_type| coin_type.id) {
        writeln!(
            &mut file,
            "    (
        {}
        [{}], {}, \"{}\", {}, {}, {},
    ),",
            coin_type
                .rustdoc_lines
                .into_iter()
                .filter(|s| !s.is_empty())
                .join("\n        ///\n        "),
            coin_type.ids.into_iter().join(",").to_string(),
            coin_type.name,
            coin_type.original_name,
            match coin_type.link {
                Some(ref link) => format!("\"{}\"", link),
                None => "".to_string(),
            },
            match coin_type.symbol {
                Some(ref symbol) =>
                    if seen_symbols.contains(symbol) {
                        ""
                    } else {
                        symbol
                    },
                None => "",
            }
            .to_string(),
            match coin_type.symbol.clone() {
                Some(ref symbol) =>
                    if seen_symbols.contains(symbol) {
                        format!("\"{}\"", symbol)
                    } else {
                        seen_symbols.insert(symbol.clone());

                        "".to_string()
                    },
                None => "".to_string(),
            }
            .to_string(),
        )?;
    }
    writeln!(&mut file, ");")?;

    Ok(())
}

fn parse_markdown_link(input: &str) -> (&str, Option<&str>) {
    if input.starts_with('[') {
        (
            input.splitn(3, &['[', ']'][..]).nth(1).unwrap_or(input),
            input
                .trim_start_matches(']')
                .splitn(3, &['(', ')'][..])
                .nth(1),
        )
    } else {
        (input, None)
    }
}

fn original_name_to_short(original_name: &str) -> Result<String, String> {
    let mut name = original_name.replace(' ', "");
    name = name
        .split_once('(')
        .map_or(name.to_string(), |(name, _)| name.to_string());
    name = prepend_enum(&name);

    if name.contains(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_') {
        let name_match = match name.as_str() {
            "Pl^g" => Ok("Plug"),
            "BitcoinMatteo'sVision" => Ok("BitcoinMatteosVision"),
            "Crypto.orgChain" => Ok("CryptoOrgChain"),
            "Cocos-BCX" => Ok("CocosBCX"),
            "Capricoin+" => Ok("CapricoinPlus"),
            "Seele-N" => Ok("SeeleN"),
            "IQ-Cash" => Ok("IQCash"),
            "XinFin.Network" => Ok("XinFinNetwork"),
            "Unit-e" => Ok("UnitE"),
            "HARMONY-ONE" => Ok("HarmonyOne"),
            "ThePower.io" => Ok("ThePower"),
            "evan.network" => Ok("EvanNetwork"),
            "Ether-1" => Ok("EtherOne"),
            "æternity" => Ok("aeternity"),
            "θ" => Ok("Theta"),
            name => Err(format!("unknown original coin name `{}`", name)),
        };

        name_match.map(|name| name.to_string())
    } else {
        Ok(name)
    }
}

fn prepend_enum(name: &str) -> String {
    if name.starts_with(char::is_numeric) {
        ["_", name].join("")
    } else {
        name.to_string()
    }
}