<commit_before>extern crate chrono;
#[macro_use]
extern crate clap;
extern crate diesel;

mod setup_error;

use chrono::*;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use diesel::{migrations, Connection};
use diesel::connection::PgConnection;
use self::setup_error::SetupError;
use std::{env, fs};
use std::error::Error;
use std::path::{PathBuf, Path};

fn main() {
    let database_arg = || Arg::with_name("DATABASE_URL")
        .long("database-url")
        .help("Specifies the database URL to connect to. Falls back to \
                   the DATABASE_URL environment variable if unspecified.")
        .takes_value(true);

    let migration_subcommand = SubCommand::with_name("migration")
        .setting(AppSettings::VersionlessSubcommands)
        .subcommand(
            SubCommand::with_name("run")
                .about("Runs all pending migrations")
                .arg(database_arg())
        ).subcommand(
            SubCommand::with_name("revert")
                .about("Reverts the latest run migration")
                .arg(database_arg())
        ).subcommand(
            SubCommand::with_name("redo")
                .about("Reverts and re-runs the latest migration. Useful \
                      for testing that a migration can in fact be reverted.")
                .arg(database_arg())
        ).subcommand(
            SubCommand::with_name("generate")
                .about("Generate a new migration with the given name, and \
                      the current timestamp as the version")
                .arg(Arg::with_name("MIGRATION_NAME")
                     .help("The name of the migration to create")
                     .required(true)
                 )
        ).setting(AppSettings::SubcommandRequiredElseHelp);

    let setup_subcommand = SubCommand::with_name("setup")
        .about("Creates the migrations directory, creates the database \
                specified in your DATABASE_URL, and runs existing migrations.")
        .arg(database_arg());

    let matches = App::new("diesel")
        .version(env!("CARGO_PKG_VERSION"))
        .setting(AppSettings::VersionlessSubcommands)
        .subcommand(migration_subcommand)
        .subcommand(setup_subcommand)
        .setting(AppSettings::SubcommandRequiredElseHelp)
        .get_matches();

    match matches.subcommand() {
        ("migration", Some(matches)) => run_migration_command(matches),
        ("setup", Some(matches)) => run_setup_command(matches),
        _ => unreachable!(),
    }
}

fn run_migration_command(matches: &ArgMatches) {
    match matches.subcommand() {
        ("run", Some(args)) => {
            migrations::run_pending_migrations(&connection(&database_url(args)))
                .map_err(handle_error).unwrap();
        }
        ("revert", Some(args)) => {
            migrations::revert_latest_migration(&connection(&database_url(args)))
                .map_err(handle_error).unwrap();
        }
        ("redo", Some(args)) => {
            let connection = connection(&database_url(args));
            connection.transaction(|| {
                let reverted_version = try!(migrations::revert_latest_migration(&connection));
                migrations::run_migration_with_version(&connection, &reverted_version)
            }).unwrap_or_else(handle_error);
        }
        ("generate", Some(args)) => {
            let migration_name = args.value_of("MIGRATION_NAME").unwrap();
            let timestamp = Local::now().format("%Y%m%d%H%M%S");
            let versioned_name = format!("{}_{}", &timestamp, migration_name);
            let migration_dir = migrations::find_migrations_directory()
                .map_err(handle_error).unwrap().join(versioned_name);
            fs::create_dir(&migration_dir).unwrap();

            // FIXME: It would be nice to print these as relative paths
            let up_path = migration_dir.join("up.sql");
            println!("Creating {}", up_path.display());
            fs::File::create(up_path).unwrap();
            let down_path = migration_dir.join("down.sql");
            println!("Creating {}", down_path.display());
            fs::File::create(down_path).unwrap();
        }
        _ => unreachable!("The cli parser should prevent reaching here"),
    }
}

fn run_setup_command(matches: &ArgMatches) {
    migrations::find_migrations_directory()
        .unwrap_or_else(|_|
                        create_migrations_directory()
                        .map_err(handle_error).unwrap()
                       );

    if PgConnection::establish(&database_url(matches)).is_err() {
        create_database(database_url(matches)).unwrap_or_else(handle_error);
    }

    let connection = connection(&database_url(matches));

    if !schema_table_exists(&connection).map_err(handle_error).unwrap() {
        migrations::create_schema_migrations_table_if_needed(&connection)
            .map_err(handle_error).unwrap();
        migrations::run_pending_migrations(&connection)
            .map_err(handle_error).unwrap();
    }
}

/// Creates the database specified in the connection url. It returns a
/// `SetupError::ConnectionError` if it can't connect to the postgres
/// connection url, and returns a `SetupError::QueryError` if it is unable
/// to create the database.
fn create_database(database_url: String) -> Result<(), SetupError> {
    let mut split: Vec<&str> = database_url.split("/").collect();
    let database = split.pop().unwrap();
    let postgres_url = split.join("/");
    let connection = try!(PgConnection::establish(&postgres_url));
    try!(connection.execute(&format!("CREATE DATABASE {};", database)));
    Ok(())
}

/// Looks for a migrations directory in the current path and all parent paths,
/// and creates one in the same directory as the Cargo.toml if it can't find
/// one. It also sticks a .gitkeep in the directory so git will pick it up.
/// Returns a `SetupError::CargoTomlNotFound` if no Cargo.toml is found.
fn create_migrations_directory() -> Result<PathBuf, SetupError> {
    let project_root = try!(find_project_root());
    try!(fs::create_dir(project_root.join("migrations")));
    try!(fs::File::create(project_root.join("migrations/.gitkeep")));
    Ok(project_root)
}

fn find_project_root() -> Result<PathBuf, SetupError> {
    search_for_cargo_toml_directory(&try!(env::current_dir()))
}

fn search_for_cargo_toml_directory(path: &Path) -> Result<PathBuf, SetupError> {
    let toml_path = path.join("Cargo.toml");
    if toml_path.is_file() {
        Ok(path.to_owned())
    } else {
        path.parent().map(search_for_cargo_toml_directory)
            .unwrap_or(Err(SetupError::CargoTomlNotFound))
    }
}

/// Returns true if the '__diesel_schema_migrations' table exists in the
/// database we connect to, returns false if it does not.
pub fn schema_table_exists(connection: &PgConnection) -> Result<bool, SetupError> {
    let result = try!(connection.execute("SELECT 1
        FROM information_schema.tables
        WHERE table_name = '__diesel_schema_migrations';"));
    Ok(result != 0)
}

fn handle_error<E: Error>(error: E) {
    println!("{}", error);
    std::process::exit(1);
}

fn database_url(matches: &ArgMatches) -> String {
    matches.value_of("DATABASE_URL")
        .map(|s| s.into())
        .or(env::var("DATABASE_URL").ok())
        .expect("The --database-url argument must be passed, \
                or the DATABASE_URL environment variable must be set.")
}

fn connection(database_url: &str) -> PgConnection {
    PgConnection::establish(database_url)
        .expect(&format!("Error connecting to {}", database_url))
}

#[cfg(test)]
mod tests {
    extern crate diesel;
    extern crate dotenv;
    extern crate tempdir;

    use self::tempdir::TempDir;
    use self::diesel::Connection;

    use setup_error::SetupError;

    use super::{schema_table_exists, search_for_cargo_toml_directory};

    use std::{env, fs};

    fn connection() -> diesel::connection::PgConnection {
        dotenv::dotenv().ok();
        let database_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL must be set in order to run diesel_cli tests");
        diesel::connection::PgConnection::establish(&database_url).unwrap()
    }

    #[test]
    fn toml_directory_find_cargo_toml() {
        let dir = TempDir::new("diesel").unwrap();
        let temp_path = dir.path().canonicalize().unwrap();
        let toml_path = temp_path.join("Cargo.toml");

        fs::File::create(&toml_path).unwrap();

        assert_eq!(Ok(temp_path.clone()), search_for_cargo_toml_directory(&temp_path));
    }

    #[test]
    fn cargo_toml_not_found_if_no_cargo_toml() {
        let dir = TempDir::new("diesel").unwrap();
        let temp_path = dir.path().canonicalize().unwrap();

        assert_eq!(Err(SetupError::CargoTomlNotFound),
            search_for_cargo_toml_directory(&temp_path));
    }

    #[test]
    fn schema_table_exists_finds_table() {
        let connection = connection();
        connection.silence_notices(|| {
            connection.execute("DROP TABLE IF EXISTS __diesel_schema_migrations").unwrap();
            connection.execute("CREATE TABLE __diesel_schema_migrations ()").unwrap();
        });

        assert!(schema_table_exists(&connection).unwrap());
    }

    #[test]
    fn schema_table_exists_doesnt_find_table() {
        let connection = connection();
        connection.silence_notices(|| {
            connection.execute("DROP TABLE IF EXISTS __diesel_schema_migrations").unwrap();
        });

        assert!(!schema_table_exists(&connection).unwrap());
    }
}
<commit_msg>Fix diesel_cli test race condition by using a transaction.<commit_after>extern crate chrono;
#[macro_use]
extern crate clap;
extern crate diesel;

mod setup_error;

use chrono::*;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use diesel::{migrations, Connection};
use diesel::connection::PgConnection;
use self::setup_error::SetupError;
use std::{env, fs};
use std::error::Error;
use std::path::{PathBuf, Path};

fn main() {
    let database_arg = || Arg::with_name("DATABASE_URL")
        .long("database-url")
        .help("Specifies the database URL to connect to. Falls back to \
                   the DATABASE_URL environment variable if unspecified.")
        .takes_value(true);

    let migration_subcommand = SubCommand::with_name("migration")
        .setting(AppSettings::VersionlessSubcommands)
        .subcommand(
            SubCommand::with_name("run")
                .about("Runs all pending migrations")
                .arg(database_arg())
        ).subcommand(
            SubCommand::with_name("revert")
                .about("Reverts the latest run migration")
                .arg(database_arg())
        ).subcommand(
            SubCommand::with_name("redo")
                .about("Reverts and re-runs the latest migration. Useful \
                      for testing that a migration can in fact be reverted.")
                .arg(database_arg())
        ).subcommand(
            SubCommand::with_name("generate")
                .about("Generate a new migration with the given name, and \
                      the current timestamp as the version")
                .arg(Arg::with_name("MIGRATION_NAME")
                     .help("The name of the migration to create")
                     .required(true)
                 )
        ).setting(AppSettings::SubcommandRequiredElseHelp);

    let setup_subcommand = SubCommand::with_name("setup")
        .about("Creates the migrations directory, creates the database \
                specified in your DATABASE_URL, and runs existing migrations.")
        .arg(database_arg());

    let matches = App::new("diesel")
        .version(env!("CARGO_PKG_VERSION"))
        .setting(AppSettings::VersionlessSubcommands)
        .subcommand(migration_subcommand)
        .subcommand(setup_subcommand)
        .setting(AppSettings::SubcommandRequiredElseHelp)
        .get_matches();

    match matches.subcommand() {
        ("migration", Some(matches)) => run_migration_command(matches),
        ("setup", Some(matches)) => run_setup_command(matches),
        _ => unreachable!(),
    }
}

fn run_migration_command(matches: &ArgMatches) {
    match matches.subcommand() {
        ("run", Some(args)) => {
            migrations::run_pending_migrations(&connection(&database_url(args)))
                .map_err(handle_error).unwrap();
        }
        ("revert", Some(args)) => {
            migrations::revert_latest_migration(&connection(&database_url(args)))
                .map_err(handle_error).unwrap();
        }
        ("redo", Some(args)) => {
            let connection = connection(&database_url(args));
            connection.transaction(|| {
                let reverted_version = try!(migrations::revert_latest_migration(&connection));
                migrations::run_migration_with_version(&connection, &reverted_version)
            }).unwrap_or_else(handle_error);
        }
        ("generate", Some(args)) => {
            let migration_name = args.value_of("MIGRATION_NAME").unwrap();
            let timestamp = Local::now().format("%Y%m%d%H%M%S");
            let versioned_name = format!("{}_{}", &timestamp, migration_name);
            let migration_dir = migrations::find_migrations_directory()
                .map_err(handle_error).unwrap().join(versioned_name);
            fs::create_dir(&migration_dir).unwrap();

            // FIXME: It would be nice to print these as relative paths
            let up_path = migration_dir.join("up.sql");
            println!("Creating {}", up_path.display());
            fs::File::create(up_path).unwrap();
            let down_path = migration_dir.join("down.sql");
            println!("Creating {}", down_path.display());
            fs::File::create(down_path).unwrap();
        }
        _ => unreachable!("The cli parser should prevent reaching here"),
    }
}

fn run_setup_command(matches: &ArgMatches) {
    migrations::find_migrations_directory()
        .unwrap_or_else(|_|
                        create_migrations_directory()
                        .map_err(handle_error).unwrap()
                       );

    if PgConnection::establish(&database_url(matches)).is_err() {
        create_database(database_url(matches)).unwrap_or_else(handle_error);
    }

    let connection = connection(&database_url(matches));

    if !schema_table_exists(&connection).map_err(handle_error).unwrap() {
        migrations::create_schema_migrations_table_if_needed(&connection)
            .map_err(handle_error).unwrap();
        migrations::run_pending_migrations(&connection)
            .map_err(handle_error).unwrap();
    }
}

/// Creates the database specified in the connection url. It returns a
/// `SetupError::ConnectionError` if it can't connect to the postgres
/// connection url, and returns a `SetupError::QueryError` if it is unable
/// to create the database.
fn create_database(database_url: String) -> Result<(), SetupError> {
    let mut split: Vec<&str> = database_url.split("/").collect();
    let database = split.pop().unwrap();
    let postgres_url = split.join("/");
    let connection = try!(PgConnection::establish(&postgres_url));
    try!(connection.execute(&format!("CREATE DATABASE {};", database)));
    Ok(())
}

/// Looks for a migrations directory in the current path and all parent paths,
/// and creates one in the same directory as the Cargo.toml if it can't find
/// one. It also sticks a .gitkeep in the directory so git will pick it up.
/// Returns a `SetupError::CargoTomlNotFound` if no Cargo.toml is found.
fn create_migrations_directory() -> Result<PathBuf, SetupError> {
    let project_root = try!(find_project_root());
    try!(fs::create_dir(project_root.join("migrations")));
    try!(fs::File::create(project_root.join("migrations/.gitkeep")));
    Ok(project_root)
}

fn find_project_root() -> Result<PathBuf, SetupError> {
    search_for_cargo_toml_directory(&try!(env::current_dir()))
}

fn search_for_cargo_toml_directory(path: &Path) -> Result<PathBuf, SetupError> {
    let toml_path = path.join("Cargo.toml");
    if toml_path.is_file() {
        Ok(path.to_owned())
    } else {
        path.parent().map(search_for_cargo_toml_directory)
            .unwrap_or(Err(SetupError::CargoTomlNotFound))
    }
}

/// Returns true if the '__diesel_schema_migrations' table exists in the
/// database we connect to, returns false if it does not.
pub fn schema_table_exists(connection: &PgConnection) -> Result<bool, SetupError> {
    let result = try!(connection.execute("SELECT 1
        FROM information_schema.tables
        WHERE table_name = '__diesel_schema_migrations';"));
    Ok(result != 0)
}

fn handle_error<E: Error>(error: E) {
    println!("{}", error);
    std::process::exit(1);
}

fn database_url(matches: &ArgMatches) -> String {
    matches.value_of("DATABASE_URL")
        .map(|s| s.into())
        .or(env::var("DATABASE_URL").ok())
        .expect("The --database-url argument must be passed, \
                or the DATABASE_URL environment variable must be set.")
}

fn connection(database_url: &str) -> PgConnection {
    PgConnection::establish(database_url)
        .expect(&format!("Error connecting to {}", database_url))
}

#[cfg(test)]
mod tests {
    extern crate diesel;
    extern crate dotenv;
    extern crate tempdir;

    use self::tempdir::TempDir;
    use self::diesel::Connection;
    use self::diesel::connection::PgConnection;

    use setup_error::SetupError;

    use super::{schema_table_exists, search_for_cargo_toml_directory};

    use std::{env, fs};

    fn connection() -> PgConnection {
        dotenv::dotenv().ok();
        let database_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL must be set in order to run diesel_cli tests");
        let connection = PgConnection::establish(&database_url).unwrap();
        connection.begin_test_transaction().unwrap();
        connection
    }

    #[test]
    fn toml_directory_find_cargo_toml() {
        let dir = TempDir::new("diesel").unwrap();
        let temp_path = dir.path().canonicalize().unwrap();
        let toml_path = temp_path.join("Cargo.toml");

        fs::File::create(&toml_path).unwrap();

        assert_eq!(Ok(temp_path.clone()), search_for_cargo_toml_directory(&temp_path));
    }

    #[test]
    fn cargo_toml_not_found_if_no_cargo_toml() {
        let dir = TempDir::new("diesel").unwrap();
        let temp_path = dir.path().canonicalize().unwrap();

        assert_eq!(Err(SetupError::CargoTomlNotFound),
            search_for_cargo_toml_directory(&temp_path));
    }

    #[test]
    fn schema_table_exists_finds_table() {
        let connection = connection();
        connection.silence_notices(|| {
            connection.execute("DROP TABLE IF EXISTS __diesel_schema_migrations;").unwrap();
            connection.execute("CREATE TABLE __diesel_schema_migrations ();").unwrap();
        });

        assert!(schema_table_exists(&connection).unwrap());
    }

    #[test]
    fn schema_table_exists_doesnt_find_table() {
        let connection = connection();
        connection.silence_notices(|| {
            connection.execute("DROP TABLE IF EXISTS __diesel_schema_migrations;").unwrap();
        });
        assert!(!schema_table_exists(&connection).unwrap());
    }
}
<|endoftext|>


<commit_before>//! This module handles the "Universal Chess Interface" protocol
//! communication.

use regex::Regex;
use std::io;
use std::io::{Read, Write, BufRead, BufReader, BufWriter, ErrorKind};

/// A command from the GUI to the engine.
pub enum UciCommand {
    Uci,
    Debug(bool),
    IsReady,
    SetOption(SetOptionParams),
    UciNewGame,
    Position(PositionParams),
    Go(GoParams),
    Stop,
    PonderHit,
    Quit,
}

pub struct SetOptionParams {
    name: String,
    value: String,
}

pub struct PositionParams {
    fen: String,
    moves: Vec<String>,
}

pub struct GoParams {
    searchmoves: Option<Vec<String>>,
    ponder: bool,
    wtime: Option<u64>,
    btime: Option<u64>,
    winc: Option<u64>,
    binc: Option<u64>,
    movestogo: Option<u64>,
    depth: Option<u64>,
    nodes: Option<u64>,
    mate: Option<u64>,
    movetime: Option<u64>,
    infinite: bool,
}

struct ParseError;

fn parse_uci_command(s: &str) -> Result<UciCommand, ParseError> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            format!(r"\b({})\s*(?:\s(.*)|$)",
                    "uci|debug|isready|setoption|register|\
                     ucinewgame|position|go|stop|ponderhit|quit",  // any UCI command
            ).as_str()
        ).unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        let command_str = captures.at(1).unwrap();
        let params_str = captures.at(2).unwrap_or("");
        match command_str {
            "uci" if params_str == "" => Ok(UciCommand::Uci),
            "stop" if params_str == "" => Ok(UciCommand::Stop),
            "quit" if params_str == "" => Ok(UciCommand::Quit),
            "isready" if params_str == "" => Ok(UciCommand::IsReady),
            "ponderhit" if params_str == "" => Ok(UciCommand::PonderHit),
            "ucinewgame" if params_str == "" => Ok(UciCommand::UciNewGame),
            "debug" => Ok(UciCommand::Debug(try!(parse_debug_params(params_str)))),
            "setoption" => Ok(UciCommand::SetOption(try!(parse_setoption_params(params_str)))),
            "postition" => Ok(UciCommand::Position(try!(parse_position_params(params_str)))),
            "go" => Ok(UciCommand::Go(parse_go_params(params_str))),
            _ => Err(ParseError),
        }
    } else {
        Err(ParseError)
    }
}

fn parse_debug_params(s: &str) -> Result<bool, ParseError> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            r"^(on|off)\s*$").unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        match captures.at(1).unwrap() {
            "on" => Ok(true),
            "off" => Ok(false),
            _ => Err(ParseError),
        }
    } else {
        Err(ParseError)
    }
}

fn parse_setoption_params(s: &str) -> Result<SetOptionParams, ParseError> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            r"^name\s+(.*?)(?:\s+value\s+(.*?))?\s*$").unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        Ok(SetOptionParams {
            name: captures.at(1).unwrap().to_string(),
            value: captures.at(2).unwrap_or("").to_string(),
        })
    } else {
        Err(ParseError)
    }
}

fn parse_position_params(s: &str) -> Result<PositionParams, ParseError> {
    const STARTPOS: &'static str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1";
    lazy_static! {
        static ref RE: Regex = Regex::new(
            format!(
                r"^(?:fen\s+(?P<fen>.*?)|startpos)(?:\s+moves(?P<moves>{}))?\s*$",
                r"(?:\s+[a-h][1-8][a-h][1-8][qrbn]?)*",  // a possibly empty list of moves
            ).as_str()
        ).unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        Ok(PositionParams {
            fen: if let Some(fen) = captures.name("fen") {
                fen.to_string()
            } else {
                STARTPOS.to_string()
            },
            moves: captures.name("moves")
                           .unwrap_or("")
                           .split_whitespace()
                           .map(|x| x.to_string())
                           .collect(),
        })
    } else {
        Err(ParseError)
    }
}

fn parse_go_params(s: &str) -> GoParams {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            format!(
                r"\b(?P<keyword>{})(?:\s+(?P<number>\d+)|(?P<moves>{}))?(?:\s+|$)",
                "wtime|btime|winc|binc|movestogo|depth|\
                 nodes|mate|movetime|ponder|infinite|searchmoves",  // any keyword
                r"(?:\s+[a-h][1-8][a-h][1-8][qrbn]?)+",  // a non-empty list of moves
            ).as_str()
        ).unwrap();
    }
    let mut params = GoParams {
        searchmoves: None,
        ponder: false,
        wtime: None,
        btime: None,
        winc: None,
        binc: None,
        movestogo: None,
        depth: None,
        nodes: None,
        mate: None,
        movetime: None,
        infinite: false,
    };
    for captures in RE.captures_iter(s) {
        let keyword = captures.name("keyword").unwrap();
        match keyword {
            "searchmoves" => {
                if let Some(moves) = captures.name("moves") {
                    params.searchmoves = Some(moves.split_whitespace()
                                                   .map(|x| x.to_string())
                                                   .collect());
                }
            }
            "infinite" => {
                params.infinite = true;
            }
            "ponder" => {
                params.ponder = true;
            }
            _ => {
                if let Some(number) = captures.name("number") {
                    let field = match keyword {
                        "wtime" => &mut params.wtime,
                        "btime" => &mut params.btime,
                        "winc" => &mut params.winc,
                        "binc" => &mut params.binc,
                        "movestogo" => &mut params.movestogo,
                        "depth" => &mut params.depth,
                        "nodes" => &mut params.nodes,
                        "mate" => &mut params.mate,
                        "movetime" => &mut params.movetime,
                        _ => panic!("invalid keyword"),
                    };
                    *field = number.parse::<u64>().ok();
                }
            }
        }
    }
    params
}

/// A response from the engine to the GUI .
pub enum UciResponse {
    Id {
        attribute: String,
        value: String,
    },
    UciOk,
    ReadyOk,
    BestMove {
        best_move: String,
        ponder: Option<String>,
    },
    Info(String),
    Option(OptionDescription),
}

/// A description of a single configuration option (name and value)
/// supported by the engine.
///
/// The GUI may use this information to configure the engine. It may
/// also build dialog boxes according to the received option
/// descriptions so that GUI users can configure the engine.
pub struct OptionDescription {
    name: String,
    description: ValueDescription,
}

/// A description of a single configurable value.
pub enum ValueDescription {
    Check {
        default: bool,
    },
    Spin {
        min: i32,
        max: i32,
        default: i32,
    },
    Combo {
        list: Vec<String>,
        default: String,
    },
    String {
        default: String,
    },
    Button,
}

/// The main UCI protocol serving loop.
pub struct UciServingLoop<R: Read, W: Write> {
    reader: BufReader<R>,
    writer: BufWriter<W>,
    // engine: Box<UciEngine>,
    engine_is_started: bool,
    engine_is_thinking: bool,
}

impl<R: Read, W: Write> UciServingLoop<R, W> {
    fn wait_for_hanshake(in_stream: R, out_stream: W) -> io::Result<Self> {
        let mut reader = BufReader::new(in_stream);
        let mut writer = BufWriter::new(out_stream);
        let mut line = String::new();
        if try!(reader.read_line(&mut line)) == 0 {
            return Err(io::Error::new(ErrorKind::UnexpectedEof, "EOF"));
        }
        if line.as_str() != "uci" {
            return Err(io::Error::new(ErrorKind::Other, "unrecognized protocol"));
        }
        Ok(UciServingLoop {
            reader: reader,
            writer: writer,
            engine_is_started: false,
            engine_is_thinking: false,
        })
    }
}

pub trait UciEngine {
    fn options(&self) -> Vec<OptionDescription>;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_go_params() {
        use super::parse_go_params;
        assert_eq!(parse_go_params(" wtime22000  ").wtime, None);
        assert_eq!(parse_go_params(" wtime    22000  ").wtime, Some(22000));
        assert_eq!(parse_go_params("wtime 22000").wtime, Some(22000));
        assert_eq!(parse_go_params("wtime 99999999999999998888888888999999999999999999").wtime,
                   None);
        assert_eq!(parse_go_params("wtime 22000").infinite, false);
        assert_eq!(parse_go_params("searchmoves   e2e4  c7c8q  ").searchmoves,
                   Some(vec!["e2e4".to_string(), "c7c8q".to_string()]));
        assert_eq!(parse_go_params("searchmoves   e2e4  c7c8q,ponder  ").searchmoves,
                   Some(vec!["e2e4".to_string()]));
        assert_eq!(parse_go_params("searchmoves aabb").searchmoves, None);
        assert_eq!(parse_go_params("infinite wtime 22000").wtime, Some(22000));
        assert_eq!(parse_go_params("infinite wtime 22000").infinite, true);
        assert_eq!(parse_go_params("wtime 22000 infinite btime 11000").infinite,
                   true);
        assert_eq!(parse_go_params("wtime fdfee / 22000 infinite btime 11000 fdfds").infinite,
                   true);
        assert_eq!(parse_go_params("wtime 22000 infinite btime 11000 ponder").btime,
                   Some(11000));
    }

    #[test]
    fn test_parse_debug_params() {
        use super::parse_debug_params;
        assert_eq!(parse_debug_params("on").ok().unwrap(), true);
        assert!(parse_debug_params(" on").is_err());
        assert_eq!(parse_debug_params("off   ").ok().unwrap(), false);
        assert!(parse_debug_params(" off   ").is_err());
        assert!(parse_debug_params("    ").is_err());
        assert!(parse_debug_params(" sgssgs  ").is_err());
    }

    #[test]
    fn test_parse_setoption_params() {
        use super::parse_setoption_params;
        assert_eq!(parse_setoption_params("name   xxx  value   yyy  ").ok().unwrap().name,
                   "xxx".to_string());
        assert_eq!(parse_setoption_params("name xxx value yyy").ok().unwrap().value,
                   "yyy".to_string());
        assert_eq!(parse_setoption_params("name xxx   value  ").ok().unwrap().value,
                   "".to_string());
        assert_eq!(parse_setoption_params("name xxx    ").ok().unwrap().value,
                   "".to_string());
        assert_eq!(parse_setoption_params("name     ").ok().unwrap().name,
                   "".to_string());
        assert_eq!(parse_setoption_params("name     ").ok().unwrap().value,
                   "".to_string());
        assert!(parse_setoption_params("namexxx     ").is_err());
    }

    #[test]
    fn test_parse_position_params() {
        use super::parse_position_params;
        assert_eq!(parse_position_params("startpos  ").ok().unwrap().fen,
                   "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1");
        assert_eq!(parse_position_params("startpos ").ok().unwrap().moves.len(),
                   0);
        assert_eq!(parse_position_params("startpos   moves  ").ok().unwrap().moves.len(),
                   0);
        assert_eq!(parse_position_params("startpos   moves   e2e4   d2d4 ")
                       .ok()
                       .unwrap()
                       .moves
                       .len(),
                   2);
        assert_eq!(parse_position_params("fen xxx moves e2e4").ok().unwrap().moves.len(),
                   1);
        assert_eq!(parse_position_params("fen xxx moves e2e4").ok().unwrap().fen,
                   "xxx".to_string());
        assert_eq!(parse_position_params("fen   xxx    moves e2e4").ok().unwrap().fen,
                   "xxx".to_string());
        assert_eq!(parse_position_params("fen   xxx    moves").ok().unwrap().fen,
                   "xxx".to_string());
        assert_eq!(parse_position_params("fen   xxx   ").ok().unwrap().fen,
                   "xxx".to_string());
    }
}
<commit_msg>added tests in uci.rs<commit_after>//! This module handles the "Universal Chess Interface" protocol
//! communication.

use regex::Regex;
use std::io;
use std::io::{Read, Write, BufRead, BufReader, BufWriter, ErrorKind};

/// A command from the GUI to the engine.
pub enum UciCommand {
    Uci,
    Debug(bool),
    IsReady,
    SetOption(SetOptionParams),
    UciNewGame,
    Position(PositionParams),
    Go(GoParams),
    Stop,
    PonderHit,
    Quit,
}

pub struct SetOptionParams {
    name: String,
    value: String,
}

pub struct PositionParams {
    fen: String,
    moves: Vec<String>,
}

pub struct GoParams {
    searchmoves: Option<Vec<String>>,
    ponder: bool,
    wtime: Option<u64>,
    btime: Option<u64>,
    winc: Option<u64>,
    binc: Option<u64>,
    movestogo: Option<u64>,
    depth: Option<u64>,
    nodes: Option<u64>,
    mate: Option<u64>,
    movetime: Option<u64>,
    infinite: bool,
}

struct ParseError;

fn parse_uci_command(s: &str) -> Result<UciCommand, ParseError> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            format!(r"\b({})\s*(?:\s(.*)|$)",
                    "uci|debug|isready|setoption|register|\
                     ucinewgame|position|go|stop|ponderhit|quit",  // any UCI command
            ).as_str()
        ).unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        let command_str = captures.at(1).unwrap();
        let params_str = captures.at(2).unwrap_or("");
        match command_str {
            "uci" if params_str == "" => Ok(UciCommand::Uci),
            "stop" if params_str == "" => Ok(UciCommand::Stop),
            "quit" if params_str == "" => Ok(UciCommand::Quit),
            "isready" if params_str == "" => Ok(UciCommand::IsReady),
            "ponderhit" if params_str == "" => Ok(UciCommand::PonderHit),
            "ucinewgame" if params_str == "" => Ok(UciCommand::UciNewGame),
            "debug" => Ok(UciCommand::Debug(try!(parse_debug_params(params_str)))),
            "setoption" => Ok(UciCommand::SetOption(try!(parse_setoption_params(params_str)))),
            "position" => Ok(UciCommand::Position(try!(parse_position_params(params_str)))),
            "go" => Ok(UciCommand::Go(parse_go_params(params_str))),
            _ => Err(ParseError),
        }
    } else {
        Err(ParseError)
    }
}

fn parse_debug_params(s: &str) -> Result<bool, ParseError> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            r"^(on|off)\s*$").unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        match captures.at(1).unwrap() {
            "on" => Ok(true),
            "off" => Ok(false),
            _ => Err(ParseError),
        }
    } else {
        Err(ParseError)
    }
}

fn parse_setoption_params(s: &str) -> Result<SetOptionParams, ParseError> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            r"^name\s+(.*?)(?:\s+value\s+(.*?))?\s*$").unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        Ok(SetOptionParams {
            name: captures.at(1).unwrap().to_string(),
            value: captures.at(2).unwrap_or("").to_string(),
        })
    } else {
        Err(ParseError)
    }
}

fn parse_position_params(s: &str) -> Result<PositionParams, ParseError> {
    const STARTPOS: &'static str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1";
    lazy_static! {
        static ref RE: Regex = Regex::new(
            format!(
                r"^(?:fen\s+(?P<fen>.*?)|startpos)(?:\s+moves(?P<moves>{}))?\s*$",
                r"(?:\s+[a-h][1-8][a-h][1-8][qrbn]?)*",  // a possibly empty list of moves
            ).as_str()
        ).unwrap();
    }
    if let Some(captures) = RE.captures(s) {
        Ok(PositionParams {
            fen: if let Some(fen) = captures.name("fen") {
                fen.to_string()
            } else {
                STARTPOS.to_string()
            },
            moves: captures.name("moves")
                           .unwrap_or("")
                           .split_whitespace()
                           .map(|x| x.to_string())
                           .collect(),
        })
    } else {
        Err(ParseError)
    }
}

fn parse_go_params(s: &str) -> GoParams {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            format!(
                r"\b(?P<keyword>{})(?:\s+(?P<number>\d+)|(?P<moves>{}))?(?:\s+|$)",
                "wtime|btime|winc|binc|movestogo|depth|\
                 nodes|mate|movetime|ponder|infinite|searchmoves",  // any keyword
                r"(?:\s+[a-h][1-8][a-h][1-8][qrbn]?)+",  // a non-empty list of moves
            ).as_str()
        ).unwrap();
    }
    let mut params = GoParams {
        searchmoves: None,
        ponder: false,
        wtime: None,
        btime: None,
        winc: None,
        binc: None,
        movestogo: None,
        depth: None,
        nodes: None,
        mate: None,
        movetime: None,
        infinite: false,
    };
    for captures in RE.captures_iter(s) {
        let keyword = captures.name("keyword").unwrap();
        match keyword {
            "searchmoves" => {
                if let Some(moves) = captures.name("moves") {
                    params.searchmoves = Some(moves.split_whitespace()
                                                   .map(|x| x.to_string())
                                                   .collect());
                }
            }
            "infinite" => {
                params.infinite = true;
            }
            "ponder" => {
                params.ponder = true;
            }
            _ => {
                if let Some(number) = captures.name("number") {
                    let field = match keyword {
                        "wtime" => &mut params.wtime,
                        "btime" => &mut params.btime,
                        "winc" => &mut params.winc,
                        "binc" => &mut params.binc,
                        "movestogo" => &mut params.movestogo,
                        "depth" => &mut params.depth,
                        "nodes" => &mut params.nodes,
                        "mate" => &mut params.mate,
                        "movetime" => &mut params.movetime,
                        _ => panic!("invalid keyword"),
                    };
                    *field = number.parse::<u64>().ok();
                }
            }
        }
    }
    params
}

/// A response from the engine to the GUI .
pub enum UciResponse {
    Id {
        attribute: String,
        value: String,
    },
    UciOk,
    ReadyOk,
    BestMove {
        best_move: String,
        ponder: Option<String>,
    },
    Info(String),
    Option(OptionDescription),
}

/// A description of a single configuration option (name and value)
/// supported by the engine.
///
/// The GUI may use this information to configure the engine. It may
/// also build dialog boxes according to the received option
/// descriptions so that GUI users can configure the engine.
pub struct OptionDescription {
    name: String,
    description: ValueDescription,
}

/// A description of a single configurable value.
pub enum ValueDescription {
    Check {
        default: bool,
    },
    Spin {
        min: i32,
        max: i32,
        default: i32,
    },
    Combo {
        list: Vec<String>,
        default: String,
    },
    String {
        default: String,
    },
    Button,
}

/// The main UCI protocol serving loop.
pub struct UciServingLoop<R: Read, W: Write> {
    reader: BufReader<R>,
    writer: BufWriter<W>,
    // engine: Box<UciEngine>,
    engine_is_started: bool,
    engine_is_thinking: bool,
}

impl<R: Read, W: Write> UciServingLoop<R, W> {
    fn wait_for_hanshake(in_stream: R, out_stream: W) -> io::Result<Self> {
        let mut reader = BufReader::new(in_stream);
        let mut writer = BufWriter::new(out_stream);
        let mut line = String::new();
        if try!(reader.read_line(&mut line)) == 0 {
            return Err(io::Error::new(ErrorKind::UnexpectedEof, "EOF"));
        }
        if line.as_str() != "uci" {
            return Err(io::Error::new(ErrorKind::Other, "unrecognized protocol"));
        }
        Ok(UciServingLoop {
            reader: reader,
            writer: writer,
            engine_is_started: false,
            engine_is_thinking: false,
        })
    }
}

pub trait UciEngine {
    fn options(&self) -> Vec<OptionDescription>;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_go_params() {
        use super::parse_go_params;
        assert_eq!(parse_go_params(" wtime22000  ").wtime, None);
        assert_eq!(parse_go_params(" wtime    22000  ").wtime, Some(22000));
        assert_eq!(parse_go_params("wtime 22000").wtime, Some(22000));
        assert_eq!(parse_go_params("wtime 99999999999999998888888888999999999999999999").wtime,
                   None);
        assert_eq!(parse_go_params("wtime 22000").infinite, false);
        assert_eq!(parse_go_params("searchmoves   e2e4  c7c8q  ").searchmoves,
                   Some(vec!["e2e4".to_string(), "c7c8q".to_string()]));
        assert_eq!(parse_go_params("searchmoves   e2e4  c7c8q,ponder  ").searchmoves,
                   Some(vec!["e2e4".to_string()]));
        assert_eq!(parse_go_params("searchmoves aabb").searchmoves, None);
        assert_eq!(parse_go_params("infinite wtime 22000").wtime, Some(22000));
        assert_eq!(parse_go_params("infinite wtime 22000").infinite, true);
        assert_eq!(parse_go_params("wtime 22000 infinite btime 11000").infinite,
                   true);
        assert_eq!(parse_go_params("wtime fdfee / 22000 infinite btime 11000 fdfds").infinite,
                   true);
        assert_eq!(parse_go_params("wtime 22000 infinite btime 11000 ponder").btime,
                   Some(11000));
    }

    #[test]
    fn test_parse_debug_params() {
        use super::parse_debug_params;
        assert_eq!(parse_debug_params("on").ok().unwrap(), true);
        assert!(parse_debug_params(" on").is_err());
        assert_eq!(parse_debug_params("off   ").ok().unwrap(), false);
        assert!(parse_debug_params(" off   ").is_err());
        assert!(parse_debug_params("    ").is_err());
        assert!(parse_debug_params(" sgssgs  ").is_err());
    }

    #[test]
    fn test_parse_setoption_params() {
        use super::parse_setoption_params;
        assert_eq!(parse_setoption_params("name   xxx  value   yyy  ").ok().unwrap().name,
                   "xxx".to_string());
        assert_eq!(parse_setoption_params("name xxx value yyy").ok().unwrap().value,
                   "yyy".to_string());
        assert_eq!(parse_setoption_params("name xxx   value  ").ok().unwrap().value,
                   "".to_string());
        assert_eq!(parse_setoption_params("name xxx    ").ok().unwrap().value,
                   "".to_string());
        assert_eq!(parse_setoption_params("name     ").ok().unwrap().name,
                   "".to_string());
        assert_eq!(parse_setoption_params("name     ").ok().unwrap().value,
                   "".to_string());
        assert!(parse_setoption_params("namexxx     ").is_err());
    }

    #[test]
    fn test_parse_position_params() {
        use super::parse_position_params;
        assert_eq!(parse_position_params("startpos  ").ok().unwrap().fen,
                   "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1");
        assert_eq!(parse_position_params("startpos ").ok().unwrap().moves.len(),
                   0);
        assert_eq!(parse_position_params("startpos   moves  ").ok().unwrap().moves.len(),
                   0);
        assert_eq!(parse_position_params("startpos   moves   e2e4   d2d4 ")
                       .ok()
                       .unwrap()
                       .moves
                       .len(),
                   2);
        assert_eq!(parse_position_params("fen xxx moves e2e4").ok().unwrap().moves.len(),
                   1);
        assert_eq!(parse_position_params("fen xxx moves e2e4").ok().unwrap().fen,
                   "xxx".to_string());
        assert_eq!(parse_position_params("fen   xxx    moves e2e4").ok().unwrap().fen,
                   "xxx".to_string());
        assert_eq!(parse_position_params("fen   xxx    moves").ok().unwrap().fen,
                   "xxx".to_string());
        assert_eq!(parse_position_params("fen   xxx   ").ok().unwrap().fen,
                   "xxx".to_string());
    }

    #[test]
    fn test_parse_uci_command() {
        use super::parse_uci_command;
        assert!(match parse_uci_command("uci").ok().unwrap() {
            UciCommand::Uci => true,
            _ => false,
        });
        assert!(match parse_uci_command("  uci  ").ok().unwrap() {
            UciCommand::Uci => true,
            _ => false,
        });
        assert!(match parse_uci_command(" foo  uci  ").ok().unwrap() {
            UciCommand::Uci => true,
            _ => false,
        });
        assert!(match parse_uci_command("isready  ").ok().unwrap() {
            UciCommand::IsReady => true,
            _ => false,
        });
        assert!(parse_uci_command("isready xxx ").is_err());
        assert!(parse_uci_command("quit xxx ").is_err());
        assert!(parse_uci_command("stop xxx ").is_err());
        assert!(parse_uci_command("ponderhit xxx ").is_err());
        assert!(match parse_uci_command("ponderhit  ").ok().unwrap() {
            UciCommand::PonderHit => true,
            _ => false,
        });
        assert!(match parse_uci_command(" foo quit  ").ok().unwrap() {
            UciCommand::Quit => true,
            _ => false,
        });
        assert!(match parse_uci_command("  stop  ").ok().unwrap() {
            UciCommand::Stop => true,
            _ => false,
        });
        assert!(match parse_uci_command("ucinewgame").ok().unwrap() {
            UciCommand::UciNewGame => true,
            _ => false,
        });
        assert!(match parse_uci_command("position startpos").ok().unwrap() {
            UciCommand::Position(_) => true,
            _ => false,
        });
        assert!(match parse_uci_command("position fen k7/8/8/8/8/8/8/7K w - - 0 1")
                          .ok()
                          .unwrap() {
            UciCommand::Position(_) => true,
            _ => false,
        });
        assert!(match parse_uci_command("position fen k7/8/8/8/8/8/8/7K w - - 0 1 moves h1h2")
                          .ok()
                          .unwrap() {
            UciCommand::Position(_) => true,
            _ => false,
        });
        assert!(match parse_uci_command("setoption name x value y").ok().unwrap() {
            UciCommand::SetOption(_) => true,
            _ => false,
        });
        assert!(match parse_uci_command("debug on").ok().unwrap() {
            UciCommand::Debug(true) => true,
            _ => false,
        });
        assert!(match parse_uci_command("go infinite").ok().unwrap() {
            UciCommand::Go(_) => true,
            _ => false,
        });
    }
}
<|endoftext|>


<commit_before>// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::fmt;
use std::str::FromStr;
use std::num;
use std::str;

#[derive(Clone, PartialEq, Eq)]
pub struct Url {
    pub scheme: String,
    pub user: Option<UserInfo>,
    pub host: String,
    pub port: Option<u16>,
    pub path: Path,
}

#[derive(Clone, PartialEq, Eq)]
pub struct Path {
    pub path: String,
    pub query: Query,
    pub fragment: Option<String>
}

#[derive(Clone, PartialEq, Eq)]
pub struct UserInfo {
    pub user: String,
    pub pass: Option<String>
}

pub type Query = Vec<(String, String)>;

impl Url {
    pub fn new(scheme: String,
               user: Option<UserInfo>,
               host: String,
               port: Option<u16>,
               path: String,
               query: Query,
               fragment: Option<String>)
               -> Url {
        Url {
            scheme: scheme,
            user: user,
            host: host,
            port: port,
            path: Path::new(path, query, fragment)
        }
    }

    pub fn parse(rawurl: &str) -> DecodeResult<Url> {
        // scheme
        let (scheme, rest) = try!(get_scheme(rawurl));

        // authority
        let (userinfo, host, port, rest) = try!(get_authority(rest));

        // path
        let has_authority = host.len() > 0;
        let (path, rest) = try!(get_path(rest, has_authority));

        // query and fragment
        let (query, fragment) = try!(get_query_fragment(rest));

        let url = Url::new(scheme.to_string(),
                            userinfo,
                            host.to_string(),
                            port,
                            path,
                            query,
                            fragment);
        Ok(url)
    }
}

impl Path {
    pub fn new(path: String,
               query: Query,
               fragment: Option<String>)
               -> Path {
        Path {
            path: path,
            query: query,
            fragment: fragment,
        }
    }

    pub fn parse(rawpath: &str) -> DecodeResult<Path> {
        let (path, rest) = try!(get_path(rawpath, false));

        // query and fragment
        let (query, fragment) = try!(get_query_fragment(rest.as_slice()));

        Ok(Path{ path: path, query: query, fragment: fragment })
    }
}

impl UserInfo {
    #[inline]
    pub fn new(user: String, pass: Option<String>) -> UserInfo {
        UserInfo { user: user, pass: pass }
    }
}

pub type DecodeResult<T> = Result<T, String>;

pub fn decode_component(container: &str) -> DecodeResult<String> {
    decode_inner(container, false)
}

fn decode_inner(c: &str, full_url: bool) -> DecodeResult<String> {
    let mut out = String::new();
    let mut iter = c.as_bytes().iter().map(|&b| b);

    loop {
        match iter.next() {
            Some(b) => match b as char {
                '%' => {
                    let bytes = match (iter.next(), iter.next()) {
                        (Some(one), Some(two)) => [one as u8, two as u8],
                        _ => return Err(format!("Malformed input: found '%' \
                                                without two trailing bytes")),
                    };

                    // Only decode some characters if full_url:
                    match num::from_str_radix::<usize>(str::from_utf8(&bytes).unwrap(), 16).unwrap() as u8 as char {
                        // gen-delims:
                        ':' | '/' | '?' | '#' | '[' | ']' | '@' |

                        // sub-delims:
                        '!' | '$' | '&' | '"' | '(' | ')' | '*' |
                        '+' | ',' | ';' | '='
                            if full_url => {
                            out.push('%');
                            out.push(bytes[0] as char);
                            out.push(bytes[1] as char);
                        }

                        ch => out.push(ch)
                    }
                }
                ch => out.push(ch)
            },
            None => return Ok(out),
        }
    }
}

fn split_char_first(s: &str, c: char) -> (&str, &str) {
    let mut iter = s.splitn(1, c);

    match (iter.next(), iter.next()) {
        (Some(a), Some(b)) => (a, b),
        (Some(a), None) => (a, ""),
        (None, _) => unreachable!(),
    }
}

impl fmt::Debug for UserInfo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.pass {
            Some(ref pass) => write!(f, "{}:{}@", self.user, *pass),
            None => write!(f, "{}@", self.user),
        }
    }
}

fn query_from_str(rawquery: &str) -> DecodeResult<Query> {
    let mut query: Query = vec!();
    if !rawquery.is_empty() {
        for p in rawquery.split('&') {
            let (k, v) = split_char_first(p, '=');
            query.push((try!(decode_component(k)),
                        try!(decode_component(v))));
        }
    }

    Ok(query)
}

pub fn get_scheme(rawurl: &str) -> DecodeResult<(&str, &str)> {
    for (i,c) in rawurl.chars().enumerate() {
        let result = match c {
            'A' ... 'Z'
            | 'a' ... 'z' => continue,
            '0' ... '9' | '+' | '-' | '.' => {
                if i != 0 { continue }

                Err("url: Scheme must begin with a letter.".to_string())
            }
            ':' => {
                if i == 0 {
                    Err("url: Scheme cannot be empty.".to_string())
                } else {
                    Ok((&rawurl[0..i], &rawurl[i+1..rawurl.len()]))
                }
            }
            _ => Err("url: Invalid character in scheme.".to_string()),
        };

        return result;
    }

    Err("url: Scheme must be terminated with a colon.".to_string())
}

// returns userinfo, host, port, and unparsed part, or an error
fn get_authority(rawurl: &str) ->
    DecodeResult<(Option<UserInfo>, &str, Option<u16>, &str)> {
    enum State {
        Start, // starting state
        PassHostPort, // could be in user or port
        Ip6Port, // either in ipv6 host or port
        Ip6Host, // are in an ipv6 host
        InHost, // are in a host - may be ipv6, but don't know yet
        InPort // are in port
    }

    #[derive(Clone, PartialEq)]
    enum Input {
        Digit, // all digits
        Hex, // digits and letters a-f
        Unreserved // all other legal characters
    }

    if !rawurl.starts_with("//") {
        // there is no authority.
        return Ok((None, "", None, rawurl));
    }

    let len = rawurl.len();
    let mut st = State::Start;
    let mut input = Input::Digit; // most restricted, start here.

    let mut userinfo = None;
    let mut host = "";
    let mut port = None;

    let mut colon_count = 0usize;
    let mut pos = 0;
    let mut begin = 2;
    let mut end = len;

    for (i,c) in rawurl.chars().enumerate()
                               // ignore the leading '//' handled by early return
                               .skip(2) {
        // deal with input class first
        match c {
            '0' ... '9' => (),
            'A' ... 'F'
            | 'a' ... 'f' => {
                if input == Input::Digit {
                    input = Input::Hex;
                }
            }
            'G' ... 'Z'
            | 'g' ... 'z'
            | '-' | '.' | '_' | '~' | '%'
            | '&' |'\'' | '(' | ')' | '+'
            | '!' | '*' | ',' | ';' | '=' => input = Input::Unreserved,
            ':' | '@' | '?' | '#' | '/' => {
                // separators, don't change anything
            }
            _ => return Err("Illegal character in authority".to_string()),
        }

        // now process states
        match c {
          ':' => {
            colon_count += 1;
            match st {
              State::Start => {
                pos = i;
                st = State::PassHostPort;
              }
              State::PassHostPort => {
                // multiple colons means ipv6 address.
                if input == Input::Unreserved {
                    return Err(
                        "Illegal characters in IPv6 address.".to_string());
                }
                st = State::Ip6Host;
              }
              State::InHost => {
                pos = i;
                if input == Input::Unreserved {
                    // must be port
                    host = &rawurl[begin..i];
                    st = State::InPort;
                } else {
                    // can't be sure whether this is an ipv6 address or a port
                    st = State::Ip6Port;
                }
              }
              State::Ip6Port => {
                if input == Input::Unreserved {
                    return Err("Illegal characters in authority.".to_string());
                }
                st = State::Ip6Host;
              }
              State::Ip6Host => {
                if colon_count > 7 {
                    host = &rawurl[begin..i];
                    pos = i;
                    st = State::InPort;
                }
              }
              _ => return Err("Invalid ':' in authority.".to_string()),
            }
            input = Input::Digit; // reset input class
          }

          '@' => {
            input = Input::Digit; // reset input class
            colon_count = 0; // reset count
            match st {
              State::Start => {
                let user = rawurl[begin..i].to_string();
                userinfo = Some(UserInfo::new(user, None));
                st = State::InHost;
              }
              State::PassHostPort => {
                let user = rawurl[begin..pos].to_string();
                let pass = rawurl[pos+1..i].to_string();
                userinfo = Some(UserInfo::new(user, Some(pass)));
                st = State::InHost;
              }
              _ => return Err("Invalid '@' in authority.".to_string()),
            }
            begin = i+1;
          }

          '?' | '#' | '/' => {
            end = i;
            break;
          }
          _ => ()
        }
    }

    // finish up
    match st {
      State::Start => host = &rawurl[begin..end],
      State::PassHostPort
      | State::Ip6Port => {
        if input != Input::Digit {
            return Err("Non-digit characters in port.".to_string());
        }
        host = &rawurl[begin..pos];
        port = Some(&rawurl[pos+1..end]);
      }
      State::Ip6Host
      | State::InHost => host = &rawurl[begin..end],
      State::InPort => {
        if input != Input::Digit {
            return Err("Non-digit characters in port.".to_string());
        }
        port = Some(&rawurl[pos+1..end]);
      }
    }

    let rest = &rawurl[end..len];
    // If we have a port string, ensure it parses to u16.
    let port = match port {
        None => None,
        opt => match opt.and_then(|p| FromStr::from_str(p).ok()) {
            None => return Err(format!("Failed to parse port: {:?}", port)),
            opt => opt
        }
    };

    Ok((userinfo, host, port, rest))
}

// returns the path and unparsed part of url, or an error
fn get_path(rawurl: &str, is_authority: bool) -> DecodeResult<(String, &str)> {
    let len = rawurl.len();
    let mut end = len;
    for (i,c) in rawurl.chars().enumerate() {
        match c {
          'A' ... 'Z'
          | 'a' ... 'z'
          | '0' ... '9'
          | '&' |'\'' | '(' | ')' | '.'
          | '@' | ':' | '%' | '/' | '+'
          | '!' | '*' | ',' | ';' | '='
          | '_' | '-' | '~' => continue,
          '?' | '#' => {
            end = i;
            break;
          }
          _ => return Err("Invalid character in path.".to_string())
        }
    }

    if is_authority && end != 0 && !rawurl.starts_with("/") {
        Err("Non-empty path must begin with \
            '/' in presence of authority.".to_string())
    } else {
        Ok((try!(decode_component(&rawurl[0..end])), &rawurl[end..len]))
    }
}

// returns the parsed query and the fragment, if present
fn get_query_fragment(rawurl: &str) -> DecodeResult<(Query, Option<String>)> {
    let (before_fragment, raw_fragment) = split_char_first(rawurl, '#');

    // Parse the fragment if available
    let fragment = match raw_fragment {
        "" => None,
        raw => Some(try!(decode_component(raw)))
    };

    match before_fragment.slice_shift_char() {
        Some(('?', rest)) => Ok((try!(query_from_str(rest)), fragment)),
        None => Ok((vec!(), fragment)),
        _ => Err(format!("Query didn't start with '?': '{}..'", before_fragment)),
    }
}

impl FromStr for Url {
    type Err = String;
    fn from_str(s: &str) -> Result<Url, String> {
        Url::parse(s)
    }
}

impl FromStr for Path {
    type Err = String;
    fn from_str(s: &str) -> Result<Path, String> {
        Path::parse(s)
    }
}

<commit_msg>Remove some unused stuff from url<commit_after>// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::str::FromStr;
use std::num;
use std::str;

pub struct Url {
    pub scheme: String,
    pub user: Option<UserInfo>,
    pub host: String,
    pub port: Option<u16>,
    pub path: Path,
}

pub struct Path {
    pub path: String,
    pub query: Query,
    pub fragment: Option<String>
}

pub struct UserInfo {
    pub user: String,
    pub pass: Option<String>
}

pub type Query = Vec<(String, String)>;

impl Url {
    pub fn new(scheme: String,
               user: Option<UserInfo>,
               host: String,
               port: Option<u16>,
               path: String,
               query: Query,
               fragment: Option<String>)
               -> Url {
        Url {
            scheme: scheme,
            user: user,
            host: host,
            port: port,
            path: Path::new(path, query, fragment)
        }
    }

    pub fn parse(rawurl: &str) -> DecodeResult<Url> {
        // scheme
        let (scheme, rest) = try!(get_scheme(rawurl));

        // authority
        let (userinfo, host, port, rest) = try!(get_authority(rest));

        // path
        let has_authority = host.len() > 0;
        let (path, rest) = try!(get_path(rest, has_authority));

        // query and fragment
        let (query, fragment) = try!(get_query_fragment(rest));

        let url = Url::new(scheme.to_string(),
                            userinfo,
                            host.to_string(),
                            port,
                            path,
                            query,
                            fragment);
        Ok(url)
    }
}

impl Path {
    pub fn new(path: String,
               query: Query,
               fragment: Option<String>)
               -> Path {
        Path {
            path: path,
            query: query,
            fragment: fragment,
        }
    }

    pub fn parse(rawpath: &str) -> DecodeResult<Path> {
        let (path, rest) = try!(get_path(rawpath, false));

        // query and fragment
        let (query, fragment) = try!(get_query_fragment(rest.as_slice()));

        Ok(Path{ path: path, query: query, fragment: fragment })
    }
}

impl UserInfo {
    #[inline]
    pub fn new(user: String, pass: Option<String>) -> UserInfo {
        UserInfo { user: user, pass: pass }
    }
}

pub type DecodeResult<T> = Result<T, String>;

pub fn decode_component(container: &str) -> DecodeResult<String> {
    decode_inner(container, false)
}

fn decode_inner(c: &str, full_url: bool) -> DecodeResult<String> {
    let mut out = String::new();
    let mut iter = c.as_bytes().iter().map(|&b| b);

    loop {
        match iter.next() {
            Some(b) => match b as char {
                '%' => {
                    let bytes = match (iter.next(), iter.next()) {
                        (Some(one), Some(two)) => [one as u8, two as u8],
                        _ => return Err(format!("Malformed input: found '%' \
                                                without two trailing bytes")),
                    };

                    // Only decode some characters if full_url:
                    match num::from_str_radix::<usize>(str::from_utf8(&bytes).unwrap(), 16).unwrap() as u8 as char {
                        // gen-delims:
                        ':' | '/' | '?' | '#' | '[' | ']' | '@' |

                        // sub-delims:
                        '!' | '$' | '&' | '"' | '(' | ')' | '*' |
                        '+' | ',' | ';' | '='
                            if full_url => {
                            out.push('%');
                            out.push(bytes[0] as char);
                            out.push(bytes[1] as char);
                        }

                        ch => out.push(ch)
                    }
                }
                ch => out.push(ch)
            },
            None => return Ok(out),
        }
    }
}

fn split_char_first(s: &str, c: char) -> (&str, &str) {
    let mut iter = s.splitn(1, c);

    match (iter.next(), iter.next()) {
        (Some(a), Some(b)) => (a, b),
        (Some(a), None) => (a, ""),
        (None, _) => unreachable!(),
    }
}

fn query_from_str(rawquery: &str) -> DecodeResult<Query> {
    let mut query: Query = vec!();
    if !rawquery.is_empty() {
        for p in rawquery.split('&') {
            let (k, v) = split_char_first(p, '=');
            query.push((try!(decode_component(k)),
                        try!(decode_component(v))));
        }
    }

    Ok(query)
}

pub fn get_scheme(rawurl: &str) -> DecodeResult<(&str, &str)> {
    for (i,c) in rawurl.chars().enumerate() {
        let result = match c {
            'A' ... 'Z'
            | 'a' ... 'z' => continue,
            '0' ... '9' | '+' | '-' | '.' => {
                if i != 0 { continue }

                Err("url: Scheme must begin with a letter.".to_string())
            }
            ':' => {
                if i == 0 {
                    Err("url: Scheme cannot be empty.".to_string())
                } else {
                    Ok((&rawurl[0..i], &rawurl[i+1..rawurl.len()]))
                }
            }
            _ => Err("url: Invalid character in scheme.".to_string()),
        };

        return result;
    }

    Err("url: Scheme must be terminated with a colon.".to_string())
}

// returns userinfo, host, port, and unparsed part, or an error
fn get_authority(rawurl: &str) ->
    DecodeResult<(Option<UserInfo>, &str, Option<u16>, &str)> {
    enum State {
        Start, // starting state
        PassHostPort, // could be in user or port
        Ip6Port, // either in ipv6 host or port
        Ip6Host, // are in an ipv6 host
        InHost, // are in a host - may be ipv6, but don't know yet
        InPort // are in port
    }

    #[derive(Clone, PartialEq)]
    enum Input {
        Digit, // all digits
        Hex, // digits and letters a-f
        Unreserved // all other legal characters
    }

    if !rawurl.starts_with("//") {
        // there is no authority.
        return Ok((None, "", None, rawurl));
    }

    let len = rawurl.len();
    let mut st = State::Start;
    let mut input = Input::Digit; // most restricted, start here.

    let mut userinfo = None;
    let mut host = "";
    let mut port = None;

    let mut colon_count = 0usize;
    let mut pos = 0;
    let mut begin = 2;
    let mut end = len;

    for (i,c) in rawurl.chars().enumerate()
                               // ignore the leading '//' handled by early return
                               .skip(2) {
        // deal with input class first
        match c {
            '0' ... '9' => (),
            'A' ... 'F'
            | 'a' ... 'f' => {
                if input == Input::Digit {
                    input = Input::Hex;
                }
            }
            'G' ... 'Z'
            | 'g' ... 'z'
            | '-' | '.' | '_' | '~' | '%'
            | '&' |'\'' | '(' | ')' | '+'
            | '!' | '*' | ',' | ';' | '=' => input = Input::Unreserved,
            ':' | '@' | '?' | '#' | '/' => {
                // separators, don't change anything
            }
            _ => return Err("Illegal character in authority".to_string()),
        }

        // now process states
        match c {
          ':' => {
            colon_count += 1;
            match st {
              State::Start => {
                pos = i;
                st = State::PassHostPort;
              }
              State::PassHostPort => {
                // multiple colons means ipv6 address.
                if input == Input::Unreserved {
                    return Err(
                        "Illegal characters in IPv6 address.".to_string());
                }
                st = State::Ip6Host;
              }
              State::InHost => {
                pos = i;
                if input == Input::Unreserved {
                    // must be port
                    host = &rawurl[begin..i];
                    st = State::InPort;
                } else {
                    // can't be sure whether this is an ipv6 address or a port
                    st = State::Ip6Port;
                }
              }
              State::Ip6Port => {
                if input == Input::Unreserved {
                    return Err("Illegal characters in authority.".to_string());
                }
                st = State::Ip6Host;
              }
              State::Ip6Host => {
                if colon_count > 7 {
                    host = &rawurl[begin..i];
                    pos = i;
                    st = State::InPort;
                }
              }
              _ => return Err("Invalid ':' in authority.".to_string()),
            }
            input = Input::Digit; // reset input class
          }

          '@' => {
            input = Input::Digit; // reset input class
            colon_count = 0; // reset count
            match st {
              State::Start => {
                let user = rawurl[begin..i].to_string();
                userinfo = Some(UserInfo::new(user, None));
                st = State::InHost;
              }
              State::PassHostPort => {
                let user = rawurl[begin..pos].to_string();
                let pass = rawurl[pos+1..i].to_string();
                userinfo = Some(UserInfo::new(user, Some(pass)));
                st = State::InHost;
              }
              _ => return Err("Invalid '@' in authority.".to_string()),
            }
            begin = i+1;
          }

          '?' | '#' | '/' => {
            end = i;
            break;
          }
          _ => ()
        }
    }

    // finish up
    match st {
      State::Start => host = &rawurl[begin..end],
      State::PassHostPort
      | State::Ip6Port => {
        if input != Input::Digit {
            return Err("Non-digit characters in port.".to_string());
        }
        host = &rawurl[begin..pos];
        port = Some(&rawurl[pos+1..end]);
      }
      State::Ip6Host
      | State::InHost => host = &rawurl[begin..end],
      State::InPort => {
        if input != Input::Digit {
            return Err("Non-digit characters in port.".to_string());
        }
        port = Some(&rawurl[pos+1..end]);
      }
    }

    let rest = &rawurl[end..len];
    // If we have a port string, ensure it parses to u16.
    let port = match port {
        None => None,
        opt => match opt.and_then(|p| FromStr::from_str(p).ok()) {
            None => return Err(format!("Failed to parse port: {:?}", port)),
            opt => opt
        }
    };

    Ok((userinfo, host, port, rest))
}

// returns the path and unparsed part of url, or an error
fn get_path(rawurl: &str, is_authority: bool) -> DecodeResult<(String, &str)> {
    let len = rawurl.len();
    let mut end = len;
    for (i,c) in rawurl.chars().enumerate() {
        match c {
          'A' ... 'Z'
          | 'a' ... 'z'
          | '0' ... '9'
          | '&' |'\'' | '(' | ')' | '.'
          | '@' | ':' | '%' | '/' | '+'
          | '!' | '*' | ',' | ';' | '='
          | '_' | '-' | '~' => continue,
          '?' | '#' => {
            end = i;
            break;
          }
          _ => return Err("Invalid character in path.".to_string())
        }
    }

    if is_authority && end != 0 && !rawurl.starts_with("/") {
        Err("Non-empty path must begin with \
            '/' in presence of authority.".to_string())
    } else {
        Ok((try!(decode_component(&rawurl[0..end])), &rawurl[end..len]))
    }
}

// returns the parsed query and the fragment, if present
fn get_query_fragment(rawurl: &str) -> DecodeResult<(Query, Option<String>)> {
    let (before_fragment, raw_fragment) = split_char_first(rawurl, '#');

    // Parse the fragment if available
    let fragment = match raw_fragment {
        "" => None,
        raw => Some(try!(decode_component(raw)))
    };

    match before_fragment.slice_shift_char() {
        Some(('?', rest)) => Ok((try!(query_from_str(rest)), fragment)),
        None => Ok((vec!(), fragment)),
        _ => Err(format!("Query didn't start with '?': '{}..'", before_fragment)),
    }
}

impl FromStr for Url {
    type Err = String;
    fn from_str(s: &str) -> Result<Url, String> {
        Url::parse(s)
    }
}

impl FromStr for Path {
    type Err = String;
    fn from_str(s: &str) -> Result<Path, String> {
        Path::parse(s)
    }
}

<|endoftext|>


<commit_before>use std::borrow::Cow;
use std::env;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};

use glob;
use strum_macros::EnumString;

use crate::template::{self, RenderTemplateError, TemplateInput};
use crate::util;
use crate::version::{self, Version};

#[derive(Debug, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum BumpVersion {
    Major,
    Minor,
    Patch,
    Prerelease,
    Build,
}

pub enum VutError {
    VersionFileOpen(util::FileError),
    VersionFileRead(io::Error),
    VersionFileWrite(io::Error),
    TemplateGenerate(RenderTemplateError),
    Other(Cow<'static, str>),
}

pub struct Vut {
    root_path: PathBuf,
    version_file_path: PathBuf,
}

impl Vut {
    const VERSION_FILENAME: &'static str = "VERSION";

    pub fn new(path: impl AsRef<Path>) -> Self {
        let path = path.as_ref();

        Self {
            root_path: path.to_path_buf(),
            version_file_path: path.join(Self::VERSION_FILENAME),
        }
    }

    pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
        if let Some(version_file_path) = util::locate_config_file(path, Self::VERSION_FILENAME) {
            // Can this actually fail?
            let root_path = version_file_path.parent().unwrap();

            Some(Self {
                root_path: root_path.to_path_buf(),
                version_file_path,
            })
        } else {
            None
        }
    }

    pub fn from_current_dir() -> Option<Self> {
        let current_dir = env::current_dir().unwrap();

        Self::from_path(current_dir)
    }

    pub fn exists(&self) -> bool {
        self.version_file_path.exists()
    }

    pub fn get_root_path(&self) -> &Path {
        &self.root_path
    }

    pub fn get_version_file_path(&self) -> &Path {
        &self.version_file_path
    }

    pub fn get_version(&self) -> Result<Version, VutError> {
        let version_str = {
            let mut file = util::open_file(&self.version_file_path)
                .map_err(|err| VutError::VersionFileOpen(err))?;

            let mut version_str = String::new();

            file.read_to_string(&mut version_str)
                .map_err(|err| VutError::VersionFileRead(err))?;

            version_str
        };

        let version = version_str.parse()
            .map_err(|err| VutError::Other(Cow::Owned(err)))?;

        Ok(version)
    }

    pub fn set_version(&self, version: &Version) -> Result<(), VutError> {
        let mut file = util::create_file(&self.version_file_path)
            .map_err(|err| VutError::VersionFileOpen(err))?;

        file.write(version.to_string().as_bytes())
            .map_err(|err| VutError::VersionFileWrite(err))?;

        Ok(())
    }

    pub fn bump_version(&self, bump_version: BumpVersion) -> Result<Version, VutError> {
        let version = self.get_version()?;

        let version = match bump_version {
            BumpVersion::Major => version.bump_major(),
            BumpVersion::Minor => version.bump_minor(),
            BumpVersion::Patch => version.bump_patch(),
            BumpVersion::Prerelease => version.bump_prerelease(),
            BumpVersion::Build => version.bump_build(),
        };

        self.set_version(&version)?;

        Ok(version)
    }

    pub fn generate_template_input(&self) -> Result<TemplateInput, VutError> {
        let version = self.get_version()?;

        let mut template_input = TemplateInput::new();

        let split_prerelease = version::split_numbered_prerelease(&version.prerelease);
        let split_build = version::split_numbered_prerelease(&version.build);

        template_input.values.insert("FullVersion".to_owned(), version.to_string());
        template_input.values.insert("Version".to_owned(), Version { build: "".to_owned(), ..version.clone() }.to_string());
        template_input.values.insert("MajorMinorPatch".to_owned(), format!("{}.{}.{}", version.major, version.minor, version.patch));
        template_input.values.insert("MajorMinor".to_owned(), format!("{}.{}", version.major, version.minor));
        template_input.values.insert("Major".to_owned(), format!("{}", version.major));
        template_input.values.insert("Minor".to_owned(), format!("{}", version.minor));
        template_input.values.insert("Patch".to_owned(), format!("{}", version.patch));
        template_input.values.insert("Prerelease".to_owned(), version.prerelease.to_owned());
        template_input.values.insert("PrereleasePrefix".to_owned(), split_prerelease.and_then(|sp| Some(sp.0.to_owned())).unwrap_or_else(|| "".to_owned()));
        template_input.values.insert("PrereleaseNumber".to_owned(), split_prerelease.and_then(|sp| Some(format!("{}", sp.1))).unwrap_or_else(|| "".to_owned()));
        template_input.values.insert("Build".to_owned(), version.build.to_owned());
        template_input.values.insert("BuildPrefix".to_owned(), split_build.and_then(|sp| Some(sp.0.to_owned())).unwrap_or_else(|| "".to_owned()));
        template_input.values.insert("BuildNumber".to_owned(), split_build.and_then(|sp| Some(format!("{}", sp.1))).unwrap_or_else(|| "".to_owned()));

        Ok(template_input)
    }

    pub fn generate_output(&self) -> Result<(), VutError> {
        let root_path = &self.root_path;
        let template_input = self.generate_template_input()?;

        // Set current path to the root path.
        // This is currently required to ensure that relative paths in the configuration
        // are resolved correctly.
        env::set_current_dir(root_path)
            .map_err(|err| VutError::Other(Cow::Owned(err.to_string())))?;

        let files: Vec<PathBuf> = glob::glob("*.vutemplate")
        .expect("No glob!")
        .filter_map(|path| path.ok())
        // Make paths absolute
        .map(|path| util::normalize_path(&path).into_owned())
        // Exclude paths outside the root path
        .filter(|path| path.starts_with(&root_path))
        .collect();

        let mut processed_files: Vec<PathBuf> = Vec::new();
        let mut generated_files: Vec<PathBuf> = Vec::new();

        for file in files {
            let generated_file = template::generate_template::<template::processor::ClassicProcessor>(&file, &template_input, None)
                .map_err(|err| VutError::TemplateGenerate(err))?;

            processed_files.push(file);
            generated_files.push(generated_file);
        }

        Ok(())
    }
}
<commit_msg>fix: Templates in subdirectories not being generated<commit_after>use std::borrow::Cow;
use std::env;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};

use glob;
use strum_macros::EnumString;

use crate::template::{self, RenderTemplateError, TemplateInput};
use crate::util;
use crate::version::{self, Version};

#[derive(Debug, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum BumpVersion {
    Major,
    Minor,
    Patch,
    Prerelease,
    Build,
}

pub enum VutError {
    VersionFileOpen(util::FileError),
    VersionFileRead(io::Error),
    VersionFileWrite(io::Error),
    TemplateGenerate(RenderTemplateError),
    Other(Cow<'static, str>),
}

pub struct Vut {
    root_path: PathBuf,
    version_file_path: PathBuf,
}

impl Vut {
    const VERSION_FILENAME: &'static str = "VERSION";

    pub fn new(path: impl AsRef<Path>) -> Self {
        let path = path.as_ref();

        Self {
            root_path: path.to_path_buf(),
            version_file_path: path.join(Self::VERSION_FILENAME),
        }
    }

    pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
        if let Some(version_file_path) = util::locate_config_file(path, Self::VERSION_FILENAME) {
            // Can this actually fail?
            let root_path = version_file_path.parent().unwrap();

            Some(Self {
                root_path: root_path.to_path_buf(),
                version_file_path,
            })
        } else {
            None
        }
    }

    pub fn from_current_dir() -> Option<Self> {
        let current_dir = env::current_dir().unwrap();

        Self::from_path(current_dir)
    }

    pub fn exists(&self) -> bool {
        self.version_file_path.exists()
    }

    pub fn get_root_path(&self) -> &Path {
        &self.root_path
    }

    pub fn get_version_file_path(&self) -> &Path {
        &self.version_file_path
    }

    pub fn get_version(&self) -> Result<Version, VutError> {
        let version_str = {
            let mut file = util::open_file(&self.version_file_path)
                .map_err(|err| VutError::VersionFileOpen(err))?;

            let mut version_str = String::new();

            file.read_to_string(&mut version_str)
                .map_err(|err| VutError::VersionFileRead(err))?;

            version_str
        };

        let version = version_str.parse()
            .map_err(|err| VutError::Other(Cow::Owned(err)))?;

        Ok(version)
    }

    pub fn set_version(&self, version: &Version) -> Result<(), VutError> {
        let mut file = util::create_file(&self.version_file_path)
            .map_err(|err| VutError::VersionFileOpen(err))?;

        file.write(version.to_string().as_bytes())
            .map_err(|err| VutError::VersionFileWrite(err))?;

        Ok(())
    }

    pub fn bump_version(&self, bump_version: BumpVersion) -> Result<Version, VutError> {
        let version = self.get_version()?;

        let version = match bump_version {
            BumpVersion::Major => version.bump_major(),
            BumpVersion::Minor => version.bump_minor(),
            BumpVersion::Patch => version.bump_patch(),
            BumpVersion::Prerelease => version.bump_prerelease(),
            BumpVersion::Build => version.bump_build(),
        };

        self.set_version(&version)?;

        Ok(version)
    }

    pub fn generate_template_input(&self) -> Result<TemplateInput, VutError> {
        let version = self.get_version()?;

        let mut template_input = TemplateInput::new();

        let split_prerelease = version::split_numbered_prerelease(&version.prerelease);
        let split_build = version::split_numbered_prerelease(&version.build);

        template_input.values.insert("FullVersion".to_owned(), version.to_string());
        template_input.values.insert("Version".to_owned(), Version { build: "".to_owned(), ..version.clone() }.to_string());
        template_input.values.insert("MajorMinorPatch".to_owned(), format!("{}.{}.{}", version.major, version.minor, version.patch));
        template_input.values.insert("MajorMinor".to_owned(), format!("{}.{}", version.major, version.minor));
        template_input.values.insert("Major".to_owned(), format!("{}", version.major));
        template_input.values.insert("Minor".to_owned(), format!("{}", version.minor));
        template_input.values.insert("Patch".to_owned(), format!("{}", version.patch));
        template_input.values.insert("Prerelease".to_owned(), version.prerelease.to_owned());
        template_input.values.insert("PrereleasePrefix".to_owned(), split_prerelease.and_then(|sp| Some(sp.0.to_owned())).unwrap_or_else(|| "".to_owned()));
        template_input.values.insert("PrereleaseNumber".to_owned(), split_prerelease.and_then(|sp| Some(format!("{}", sp.1))).unwrap_or_else(|| "".to_owned()));
        template_input.values.insert("Build".to_owned(), version.build.to_owned());
        template_input.values.insert("BuildPrefix".to_owned(), split_build.and_then(|sp| Some(sp.0.to_owned())).unwrap_or_else(|| "".to_owned()));
        template_input.values.insert("BuildNumber".to_owned(), split_build.and_then(|sp| Some(format!("{}", sp.1))).unwrap_or_else(|| "".to_owned()));

        Ok(template_input)
    }

    pub fn generate_output(&self) -> Result<(), VutError> {
        let root_path = &self.root_path;
        let template_input = self.generate_template_input()?;

        // Set current path to the root path.
        // This is currently required to ensure that relative paths in the configuration
        // are resolved correctly.
        env::set_current_dir(root_path)
            .map_err(|err| VutError::Other(Cow::Owned(err.to_string())))?;

        let files: Vec<PathBuf> = glob::glob("**/*.vutemplate")
        .expect("No glob!")
        .filter_map(|path| path.ok())
        // Make paths absolute
        .map(|path| util::normalize_path(&path).into_owned())
        // Exclude paths outside the root path
        .filter(|path| path.starts_with(&root_path))
        .collect();

        let mut processed_files: Vec<PathBuf> = Vec::new();
        let mut generated_files: Vec<PathBuf> = Vec::new();

        for file in files {
            let generated_file = template::generate_template::<template::processor::ClassicProcessor>(&file, &template_input, None)
                .map_err(|err| VutError::TemplateGenerate(err))?;

            processed_files.push(file);
            generated_files.push(generated_file);
        }

        Ok(())
    }
}
<|endoftext|>