csvsource 0.7.0

Converts a CSV file to SQL Insert Statements.
Documentation
use clap::{Arg, ArgMatches, App, ErrorKind};
use std::str::FromStr;
use std::str::ParseBoolError;

fn main() {
    let matches = App::new("CSVSource")
        .version("0.6.0")
        .author("Hildeberto Mendonca <me@hildeberto.com>")
        .about("Converts a CSV file to SQL Insert Statements.")
        .arg(Arg::new("csv")
            .long("csv")
            .short('f')
            .value_name("file")
            .required(true)
            .takes_value(true)
            .help("Relative or absolute path to the CSV file. The file's name is also used as table name and sql file's name, unless specified otherwise by the arguments `--table` and `--sql` respectivelly."))
        .arg(Arg::new("sql")
            .long("sql")
            .short('q')
            .value_name("file")
            .help("Relative or absolute path to the SQL file."))
        .arg(Arg::new("delimiter")
            .long("delimiter")
            .short('d')
            .default_value("comma")
            .value_name("comma | semicolon | tab")
            .help("The supported CSV value delimiter used in the file."))
        .arg(Arg::new("table")
            .long("table")
            .short('t')
            .value_name("database_table_name")
            .help("Database table name if it is different from the name of the CSV file."))
        .arg(Arg::new("headers")
            .long("headers")
            .short('h')
            .default_value("true")
            .value_name("true | false")
            .help("Consider the first line in the file as headers to columns. They are also used as sql column names unless specified otherwise."))
        .arg(Arg::new("columns")
            .long("column")
            .short('c')
            .required_if_eq("headers", "false")
            .multiple_occurrences(true)
            .value_name("database_column_names")
            .help("Columns of the database table if different from the name of the labels."))
        .arg(Arg::new("chunk")
            .long("chunk")
            .short('k')
            .default_value("0")
            .value_name("#")
            .help("Size of the transaction chunk, indicating how many insert statements are put within a transaction scope."))
        .arg(Arg::new("chunk_insert")
            .long("chunkinsert")
            .short('i')
            .default_value("0")
            .value_name("#")
            .help("Size of the insert chunk, indicating how many lines of the CSV files are put in a single insert statement."))
        .arg(Arg::new("prefix")
            .long("prefix")
            .short('p')
            .value_name("file")
            .help("File with the content to put at the beginning of the SQL file. Example: It can be used to create the target table."))
        .arg(Arg::new("suffix")
            .long("suffix")
            .short('s')
            .value_name("file")
            .help("File with the content to put at the end of the SQL file. Example: It can be used to create indexes."))
        .arg(Arg::new("with_transaction")
            .long("with_transaction")
            .short('w')
            .default_value("false")
            .value_name("true | false")
            .help("Indicates whether SQL statements are put in a transaction block or not. This argument is ignored if the argument chunk is used."))
        .arg(Arg::new("typed")
            .long("typed")
            .short('y')
            .default_value("false")
            .value_name("true | false")
            .help("Indicates whether the values type are declared, automatically detected or everything is taken as string."))
        .get_matches();

    let args = arguments_from_console(matches);

    match csvsource::convert_to_sql(args) {
        Ok(())   => println!("CSV file processed successfully!"),
        Err(err) => println!("Error: {}.", err)
    };
}

fn arguments_from_console(matches: ArgMatches) -> csvsource::Arguments {
    let mut arguments = csvsource::Arguments {
        csv: String::from(""),
        sql: String::from(""),
        delimiter: b',',
        has_headers: true,
        table: String::from(""),
        columns: Vec::new(),
        chunk: 0,
        chunk_insert: 0,
        prefix: String::from(""),
        suffix: String::from(""),
        with_transaction: false,
        typed: false,
    };

    if let Some(csv) = matches.value_of("csv") {
        arguments.csv = String::from(csv);
    }

    let sql = matches.value_of("sql");
    match sql {
        Some(q) => arguments.sql = String::from(q),
        None => arguments.sql = get_file_name_without_extension(&arguments.csv) + ".sql",
    }

    if let Some(delimiter) = matches.value_of("delimiter") {
        match delimiter {
            "comma"     => arguments.delimiter = b',',
            "semicolon" => arguments.delimiter = b';',
            "tab"       => arguments.delimiter = b'\t',
            _ => App::new("CSVSource").error(ErrorKind::InvalidValue, "Invalid delimiter. Use 'comma', 'semicolon', or 'tab'.").exit()
        }
    }

    if let Some(headers) = matches.value_of("headers") {
        let has_headers: Result<bool, ParseBoolError> = FromStr::from_str(headers);
        arguments.has_headers = has_headers.ok().unwrap();
    }

    let table = matches.value_of("table");
    match table {
        Some(tbl) => arguments.table = String::from(tbl),
        None => arguments.table = get_file_name_without_extension(&arguments.csv),
    }

    if let Some(cols) = matches.values_of("columns") {
        let columns: Vec<&str> = cols.collect();
        let mut columns_vec: Vec<String> = Vec::new();
        for s in &columns {
            columns_vec.push(s.to_string());
        }
        arguments.columns = columns_vec;
    }

    if let Some(chunk) = matches.value_of("chunk") {
        arguments.chunk = String::from(chunk).parse::<usize>().unwrap();
        if arguments.chunk > 0 {
            arguments.with_transaction = true;
        }
    }

    if let Some(insert_chunk) = matches.value_of("chunk_insert") {
        arguments.chunk_insert = String::from(insert_chunk).parse::<usize>().unwrap();
    }

    if let Some(prefix) = matches.value_of("prefix") {
        arguments.prefix = String::from(prefix);
    }

    if let Some(suffix) = matches.value_of("suffix") {
        arguments.suffix = String::from(suffix);
    }

    if let Some(with_transaction) = matches.value_of("with_transaction") {
        if arguments.chunk <= 0 {
            let result: Result<bool, ParseBoolError> = FromStr::from_str(with_transaction);
            arguments.with_transaction = result.ok().unwrap();
        }
    }

    if let Some(typed) = matches.value_of("typed") {
        let result: Result<bool, ParseBoolError> = FromStr::from_str(typed);
        arguments.typed = result.ok().unwrap();
    }

    return arguments;
}

fn get_file_name_without_extension(csv_file_name: &String) -> String {
    let last_dot_pos = csv_file_name.rfind('.');
    let last_slash_pos = csv_file_name.rfind('/');
    return match last_dot_pos {
        Some(pos_dot) => {
            match last_slash_pos {
                Some(pos_slash) => csv_file_name[(pos_slash + 1)..pos_dot].to_string(),
                None => csv_file_name[..pos_dot].to_string(),
            }
        },
        None => csv_file_name.to_string(),
    }
}