<commit_before>extern crate bf;
extern crate clap;

use std::io::{self, Read, Write};
use std::fs::File;
use std::process::exit;

use clap::{Arg, App};

use bf::ast;
use bf::rle_ast;
use bf::flat;
use bf::peephole;

#[cfg(feature = "jit")]
use bf::jit;

use bf::traits::Interpretable;

const VERSION: &'static str = "0.2.0";

#[derive(Debug, Clone)]
struct Options {
    program_text:  Vec<u8>,
    memory_size:   Option<usize>,
    compiler_pass: Pass,
}

#[derive(Debug, Clone, Copy)]
enum Pass {
    Ast,
    Rle,
    Flat,
    Peep,
    #[cfg(feature = "jit")]
    Jit,
}

fn main() {
    // Process command-line options:
    let options = get_options();

    // Parse the program to AST:
    let program = parse(&options);

    match options.compiler_pass {
        Pass::Ast => {
            interpret(&*program, &options);
        }

        Pass::Rle => {
            let program = rle_ast::compile(&program);
            interpret(&*program, &options);
        }

        Pass::Flat => {
            let program = rle_ast::compile(&program);
            let program = flat::compile(&program);
            interpret(&*program, &options);
        }

        Pass::Peep => {
            let program = rle_ast::compile(&program);
            let program = peephole::compile(&program);
            interpret(&*program, &options);
        }

        #[cfg(feature = "jit")]
        Pass::Jit => {
            let program = rle_ast::compile(&program);
            let program = peephole::compile(&program);
            let program = jit::compile(&program);
            interpret(&program, &options);
        }
    }
}

fn parse(options: &Options) -> Box<ast::Program> {
    ast::parse_program(&options.program_text)
        .unwrap_or_else(|e| error_exit(2, format!("Syntax error: {}.", e)))
}

fn interpret<P: Interpretable + ?Sized>(program: &P, options: &Options) {
    program.interpret_stdin(options.memory_size)
        .unwrap_or_else(|e| error_exit(3, format!("Runtime error: {}.", e)))
}

fn get_options() -> Options {
    let mut result = Options {
        program_text:  Vec::new(),
        memory_size:   None,
        compiler_pass: Pass::Peep,
    };

    let matches = build_clap_app().get_matches();

    if let Some(size) = matches.value_of("size") {
        let size = size.parse()
            .unwrap_or_else(|e| error_exit(1, format!("Could not parse memory size: {}", e)));
        result.memory_size = Some(size);
    }

    if matches.is_present("ast") {
        result.compiler_pass = Pass::Ast;
    } else if matches.is_present("rle") {
        result.compiler_pass = Pass::Rle;
    } else if matches.is_present("flat") {
        result.compiler_pass = Pass::Flat;
    } else if matches.is_present("peep") {
        result.compiler_pass = Pass::Peep;
    } else if matches.is_present("jit") {
        #[cfg(feature = "jit")]
        let _ = result.compiler_pass = Pass::Jit;
    }

    if let Some(exprs) = matches.values_of("expr") {
        for e in exprs {
            result.program_text.extend(e.as_bytes());
        }
    } else if let Some(files) = matches.values_of("INPUT") {
        for f in files {
            let mut file = File::open(f)
                .unwrap_or_else(|e| error_exit(1, format!("{}: {}", e, f)));
            file.read_to_end(&mut result.program_text)
                .unwrap_or_else(|e| error_exit(1, format!("{}: {}", e, f)));
        }
    } else {
        error_exit(1, "No program given.".to_owned());
    }

    result
}

fn build_clap_app() -> App<'static, 'static> {
    let app = App::new("bfi")
        .version(VERSION)
        .author("Jesse A. Tov <jesse.tov@gmail.com>")
        .about("A Brainfuck interpreter")
        .arg(Arg::with_name("expr")
            .short("e")
            .long("expr")
            .value_name("CODE")
            .help("BF code to execute")
            .multiple(true)
            .takes_value(true)
            .conflicts_with("INPUT"))
        .arg(Arg::with_name("INPUT")
            .help("The source file(s) to interpret")
            .multiple(true)
            .conflicts_with("expr")
            .index(1))
        .arg(Arg::with_name("size")
            .short("s")
            .long("size")
            .value_name("SIZE")
            .help("Memory size in bytes")
            .takes_value(true))
        .arg(Arg::with_name("ast")
            .long("ast")
            .help("Interpret the AST directly")
            .conflicts_with_all(&["flat", "rle", "peep", "jit"]))
        .arg(Arg::with_name("rle")
            .long("rle")
            .help("Interpret the run-length encoded AST directly")
            .conflicts_with_all(&["ast", "flat", "peep", "jit"]))
        .arg(Arg::with_name("flat")
            .long("flat")
            .help("Interpret the flattened bytecode")
            .conflicts_with_all(&["ast", "rle", "peep", "jit"]))
        .arg(Arg::with_name("peep")
            .long("peep")
            .help("Interpret the peephole optimized bytecode (default)")
            .conflicts_with_all(&["ast", "rle", "flat", "jit"]));

    #[cfg(feature = "jit")]
    let app = app
        .arg(Arg::with_name("jit")
            .long("jit")
            .help("Use native x64 JIT")
            .conflicts_with_all(&["ast", "rle", "flat", "peep"]));

    app
}

fn error_exit(code: i32, msg: String) -> ! {
    writeln!(io::stderr(), "{}", msg).unwrap();
    exit(code)
}

<commit_msg>Improved --help.<commit_after>extern crate bf;
extern crate clap;

use std::io::{self, Read, Write};
use std::fs::File;
use std::process::exit;

use clap::{Arg, App};

use bf::ast;
use bf::rle_ast;
use bf::flat;
use bf::peephole;

#[cfg(feature = "jit")]
use bf::jit;

use bf::traits::Interpretable;

const VERSION: &'static str = "0.2.0";

#[derive(Debug, Clone)]
struct Options {
    program_text:  Vec<u8>,
    memory_size:   Option<usize>,
    compiler_pass: Pass,
}

#[derive(Debug, Clone, Copy)]
enum Pass {
    Ast,
    Rle,
    Flat,
    Peep,
    #[cfg(feature = "jit")]
    Jit,
}

fn main() {
    // Process command-line options:
    let options = get_options();

    // Parse the program to AST:
    let program = parse(&options);

    match options.compiler_pass {
        Pass::Ast => {
            interpret(&*program, &options);
        }

        Pass::Rle => {
            let program = rle_ast::compile(&program);
            interpret(&*program, &options);
        }

        Pass::Flat => {
            let program = rle_ast::compile(&program);
            let program = flat::compile(&program);
            interpret(&*program, &options);
        }

        Pass::Peep => {
            let program = rle_ast::compile(&program);
            let program = peephole::compile(&program);
            interpret(&*program, &options);
        }

        #[cfg(feature = "jit")]
        Pass::Jit => {
            let program = rle_ast::compile(&program);
            let program = peephole::compile(&program);
            let program = jit::compile(&program);
            interpret(&program, &options);
        }
    }
}

fn parse(options: &Options) -> Box<ast::Program> {
    ast::parse_program(&options.program_text)
        .unwrap_or_else(|e| error_exit(2, format!("Syntax error: {}.", e)))
}

fn interpret<P: Interpretable + ?Sized>(program: &P, options: &Options) {
    program.interpret_stdin(options.memory_size)
        .unwrap_or_else(|e| error_exit(3, format!("Runtime error: {}.", e)))
}

fn get_options() -> Options {
    let mut result = Options {
        program_text:  Vec::new(),
        memory_size:   None,
        compiler_pass: Pass::Peep,
    };

    let matches = build_clap_app().get_matches();

    if let Some(size) = matches.value_of("size") {
        let size = size.parse()
            .unwrap_or_else(|e| error_exit(1, format!("Could not parse memory size: {}", e)));
        result.memory_size = Some(size);
    }

    if matches.is_present("ast") {
        result.compiler_pass = Pass::Ast;
    } else if matches.is_present("rle") {
        result.compiler_pass = Pass::Rle;
    } else if matches.is_present("flat") {
        result.compiler_pass = Pass::Flat;
    } else if matches.is_present("peep") {
        result.compiler_pass = Pass::Peep;
    } else if matches.is_present("jit") {
        #[cfg(feature = "jit")]
        let _ = result.compiler_pass = Pass::Jit;
    }

    if let Some(exprs) = matches.values_of("expr") {
        for e in exprs {
            result.program_text.extend(e.as_bytes());
        }
    } else if let Some(files) = matches.values_of("INPUT") {
        for f in files {
            let mut file = File::open(f)
                .unwrap_or_else(|e| error_exit(1, format!("{}: {}", e, f)));
            file.read_to_end(&mut result.program_text)
                .unwrap_or_else(|e| error_exit(1, format!("{}: {}", e, f)));
        }
    } else {
        error_exit(1, "No program given.".to_owned());
    }

    result
}

fn build_clap_app() -> App<'static, 'static> {
    let app = App::new("bfi")
        .version(VERSION)
        .author("Jesse A. Tov <jesse.tov@gmail.com>")
        .about("A Brainfuck interpreter")
        .arg(Arg::with_name("expr")
            .short("e")
            .long("expr")
            .value_name("CODE")
            .help("BF code to execute")
            .multiple(true)
            .takes_value(true)
            .conflicts_with("INPUT"))
        .arg(Arg::with_name("INPUT")
            .help("The source file(s) to interpret")
            .multiple(true)
            .conflicts_with("expr")
            .index(1))
        .arg(Arg::with_name("size")
            .short("s")
            .long("size")
            .value_name("SIZE")
            .help("Memory size in bytes (default 30,000)")
            .takes_value(true))
        .arg(Arg::with_name("ast")
            .long("ast")
            .help("Interpret the AST directly")
            .conflicts_with_all(&["flat", "rle", "peep", "jit"]))
        .arg(Arg::with_name("rle")
            .long("rle")
            .help("Interpret the run-length encoded AST directly")
            .conflicts_with_all(&["ast", "flat", "peep", "jit"]))
        .arg(Arg::with_name("flat")
            .long("flat")
            .help("Interpret the flattened bytecode")
            .conflicts_with_all(&["ast", "rle", "peep", "jit"]))
        .arg(Arg::with_name("peep")
            .long("peep")
            .help("Interpret the peephole optimized bytecode (default)")
            .conflicts_with_all(&["ast", "rle", "flat", "jit"]));

    #[cfg(feature = "jit")]
    let app = app
        .arg(Arg::with_name("jit")
            .long("jit")
            .help("Use native x64 JIT")
            .conflicts_with_all(&["ast", "rle", "flat", "peep"]));

    app
}

fn error_exit(code: i32, msg: String) -> ! {
    writeln!(io::stderr(), "{}", msg).unwrap();
    exit(code)
}

<|endoftext|>


<commit_before>use clap;
use std::io::{self, Write};
use std::process;
use std::path::PathBuf;
use std::error;
use std::result;
use std::fmt;
use structopt::StructOpt;
use mprovision as mp;

#[derive(Debug, PartialEq, StructOpt)]
/// A tool that helps iOS developers to manage mobileprovision files.
pub enum Command {
    #[structopt(name = "list", raw(display_order = "0"))]
    /// Lists provisioning profiles
    List(ListParams),
    #[structopt(name = "show", raw(display_order = "1"))]
    /// Shows details of a provisioning profile using its uuid
    ShowUuid(ShowUuidParams),
    #[structopt(name = "show-file", raw(display_order = "2"))]
    /// Shows details of a provisioning profile
    ShowFile(ShowFileParams),
    #[structopt(name = "remove", raw(display_order = "3"))]
    /// Removes provisioning profiles
    Remove(RemoveParams),
    #[structopt(name = "clean", raw(display_order = "4"))]
    /// Removes expired provisioning profiles
    Clean(CleanParams),
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct ListParams {
    #[structopt(short = "t", long = "text", raw(empty_values = "false"))]
    /// Lists provisioning profiles that contain this text
    pub text: Option<String>,
    #[structopt(short = "d", long = "expire-in-days", parse(try_from_str = "parse_days"))]
    /// Lists provisioning profiles that will expire in days
    pub expire_in_days: Option<i64>,
    #[structopt(long = "source", parse(from_os_str))]
    /// A directory where to search provisioning profiles
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct ShowUuidParams {
    #[structopt(raw(empty_values = "false"))]
    /// An uuid of a provisioning profile
    pub uuid: String,
    #[structopt(long = "source", parse(from_os_str))]
    /// A directory where to search provisioning profiles
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct ShowFileParams {
    #[structopt(parse(from_os_str))]
    /// A file path of a provisioning profile
    pub file: PathBuf,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct RemoveParams {
    /// uuid(s) of provisioning profiles
    pub uuids: Vec<String>,
    #[structopt(long = "source", parse(from_os_str))]
    /// A directory where to search provisioning profiles
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct CleanParams {
    #[structopt(long = "source", parse(from_os_str))]
    /// A directory where to clean
    pub directory: Option<PathBuf>,
}

pub type Result = result::Result<Command, Error>;

#[derive(Debug)]
pub enum Error {
    Lib(mp::Error),
    Clap(clap::Error),
    Custom(String),
}

impl Error {
    pub fn exit(&self) -> ! {
        match *self {
            Error::Clap(ref e) => e.exit(),
            _ => {
                writeln!(&mut io::stderr(), "{}", self).unwrap();
                process::exit(1);
            }
        }
    }
}

impl error::Error for Error {
    fn description(&self) -> &str {
        match *self {
            Error::Lib(ref e) => e.description(),
            Error::Clap(ref e) => e.description(),
            Error::Custom(ref e) => e,
        }
    }

    fn cause(&self) -> Option<&error::Error> {
        match *self {
            Error::Lib(ref e) => Some(e),
            Error::Clap(ref e) => Some(e),
            Error::Custom(_) => None,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Error::Lib(ref e) => error::Error::description(e).fmt(f),
            Error::Clap(ref e) => error::Error::description(e).fmt(f),
            Error::Custom(ref e) => e.fmt(f),
        }
    }
}

impl From<mp::Error> for Error {
    fn from(e: mp::Error) -> Self {
        Error::Lib(e)
    }
}

impl From<clap::Error> for Error {
    fn from(e: clap::Error) -> Self {
        Error::Clap(e)
    }
}

/// Parses arguments and returns a `Command`.
pub fn parse<I, S>(args: I) -> Result
where
    I: IntoIterator<Item = S>,
    S: Clone,
    ::std::ffi::OsString: From<S>,
{
    let app = Command::clap();
    let matches = app.get_matches_from_safe(args)?;
    Ok(Command::from_clap(&matches))
}

/// Parses and validates days argument.
fn parse_days(s: &str) -> result::Result<i64, Error> {
    let days = s.parse::<i64>()
        .map_err(|err| Error::Custom(err.to_string()))?;
    if days < 0 || days > 365 {
        let message = format!("should be between 0 and 365, got {}", days);
        return Err(Error::Custom(message));
    }
    Ok(days)
}

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

    #[test]
    fn list_command() {
        expect!(parse(&["mprovision", "list"]))
            .to(be_ok().value(Command::List(ListParams::default())));

        expect!(parse(&["mprovision", "list", "--source", "."])).to(be_ok().value(Command::List(
            ListParams {
                text: None,
                expire_in_days: None,
                directory: Some(".".into()),
            },
        )));

        expect!(parse(&["mprovision", "list", "--text", "abc"])).to(be_ok().value(Command::List(
            ListParams {
                text: Some("abc".to_string()),
                expire_in_days: None,
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "list", "-t", "abc"])).to(be_ok().value(Command::List(
            ListParams {
                text: Some("abc".to_string()),
                expire_in_days: None,
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "list", "--text", ""])).to(be_err());

        expect!(parse(&["mprovision", "list", "-t", ""])).to(be_err());

        expect!(parse(&["mprovision", "list", "--expire-in-days", "3"])).to(be_ok().value(
            Command::List(ListParams {
                text: None,
                expire_in_days: Some(3),
                directory: None,
            }),
        ));

        expect!(parse(&["mprovision", "list", "-d", "3"])).to(be_ok().value(Command::List(
            ListParams {
                text: None,
                expire_in_days: Some(3),
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "list", "--expire-in-days", "-3"])).to(be_err());
        expect!(parse(&["mprovision", "list", "-d", "-3"])).to(be_err());
        expect!(parse(&["mprovision", "list", "--expire-in-days", "366"])).to(be_err());
        expect!(parse(&["mprovision", "list", "-d", "366"])).to(be_err());

        expect!(parse(&[
            "mprovision",
            "list",
            "--text",
            "abc",
            "--expire-in-days",
            "3",
            "--source",
            "."
        ])).to(be_ok().value(Command::List(ListParams {
            text: Some("abc".to_string()),
            expire_in_days: Some(3),
            directory: Some(".".into()),
        })));

        expect!(parse(&[
            "mprovision",
            "list",
            "-t",
            "abc",
            "-d",
            "3",
            "--source",
            "."
        ])).to(be_ok().value(Command::List(ListParams {
            text: Some("abc".to_string()),
            expire_in_days: Some(3),
            directory: Some(".".into()),
        })));
    }

    #[test]
    fn show_uuid_command() {
        expect!(parse(&["mprovision", "show", "abcd"])).to(be_ok().value(Command::ShowUuid(
            ShowUuidParams {
                uuid: "abcd".to_string(),
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "show", ""])).to(be_err());

        expect!(parse(&["mprovision", "show", "abcd", "--source", "."])).to(be_ok().value(
            Command::ShowUuid(ShowUuidParams {
                uuid: "abcd".to_string(),
                directory: Some(".".into()),
            }),
        ));
    }

    #[test]
    fn show_path_command() {
        expect!(parse(&["mprovision", "show-file", "file.mprovision"])).to(be_ok().value(
            Command::ShowFile(ShowFileParams {
                file: "file.mprovision".into(),
            }),
        ));

        expect!(parse(&["mprovision", "show-file", "file.mprovision", "."])).to(be_err());
    }

    #[test]
    fn remove_uuid_command() {
        expect!(parse(&["mprovision", "remove", "abcd"])).to(be_ok().value(Command::Remove(
            RemoveParams {
                uuids: vec!["abcd".to_string()],
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "remove", "abcd", "ef"])).to(be_ok().value(
            Command::Remove(RemoveParams {
                uuids: vec!["abcd".to_string(), "ef".to_string()],
                directory: None,
            }),
        ));

        expect!(parse(&["mprovision", "remove", "abcd", "--source", "."])).to(be_ok().value(
            Command::Remove(RemoveParams {
                uuids: vec!["abcd".to_string()],
                directory: Some(".".into()),
            }),
        ));

        expect!(parse(&[
            "mprovision",
            "remove",
            "abcd",
            "ef",
            "--source",
            "."
        ])).to(be_ok().value(Command::Remove(RemoveParams {
            uuids: vec!["abcd".to_string(), "ef".to_string()],
            directory: Some(".".into()),
        })));
    }

    #[test]
    fn clean_command() {
        expect!(parse(&["mprovision", "clean"]))
            .to(be_ok().value(Command::Clean(CleanParams { directory: None })));

        expect!(parse(&["mprovision", "clean", "--source", "."])).to(be_ok().value(
            Command::Clean(CleanParams {
                directory: Some(".".into()),
            }),
        ));
    }
}
<commit_msg>Forbid empty values for all arguments<commit_after>use clap;
use std::io::{self, Write};
use std::process;
use std::path::PathBuf;
use std::error;
use std::result;
use std::fmt;
use structopt::StructOpt;
use mprovision as mp;

#[derive(Debug, PartialEq, StructOpt)]
/// A tool that helps iOS developers to manage mobileprovision files.
pub enum Command {
    #[structopt(name = "list", raw(display_order = "0"))]
    /// Lists provisioning profiles
    List(ListParams),
    #[structopt(name = "show", raw(display_order = "1"))]
    /// Shows details of a provisioning profile using its uuid
    ShowUuid(ShowUuidParams),
    #[structopt(name = "show-file", raw(display_order = "2"))]
    /// Shows details of a provisioning profile
    ShowFile(ShowFileParams),
    #[structopt(name = "remove", raw(display_order = "3"))]
    /// Removes provisioning profiles
    Remove(RemoveParams),
    #[structopt(name = "clean", raw(display_order = "4"))]
    /// Removes expired provisioning profiles
    Clean(CleanParams),
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct ListParams {
    #[structopt(short = "t", long = "text", raw(empty_values = "false"))]
    /// Lists provisioning profiles that contain this text
    pub text: Option<String>,
    #[structopt(short = "d", long = "expire-in-days", parse(try_from_str = "parse_days"))]
    /// Lists provisioning profiles that will expire in days
    pub expire_in_days: Option<i64>,
    #[structopt(long = "source", parse(from_os_str), raw(empty_values = "false"))]
    /// A directory where to search provisioning profiles
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct ShowUuidParams {
    #[structopt(raw(empty_values = "false"))]
    /// An uuid of a provisioning profile
    pub uuid: String,
    #[structopt(long = "source", parse(from_os_str), raw(empty_values = "false"))]
    /// A directory where to search provisioning profiles
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct ShowFileParams {
    #[structopt(parse(from_os_str), raw(empty_values = "false"))]
    /// A file path of a provisioning profile
    pub file: PathBuf,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct RemoveParams {
    #[structopt(raw(empty_values = "false"))]
    /// uuid(s) of provisioning profiles
    pub uuids: Vec<String>,
    #[structopt(long = "source", parse(from_os_str), raw(empty_values = "false"))]
    /// A directory where to search provisioning profiles
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Default, PartialEq, StructOpt)]
pub struct CleanParams {
    #[structopt(long = "source", parse(from_os_str), raw(empty_values = "false"))]
    /// A directory where to clean
    pub directory: Option<PathBuf>,
}

pub type Result = result::Result<Command, Error>;

#[derive(Debug)]
pub enum Error {
    Lib(mp::Error),
    Clap(clap::Error),
    Custom(String),
}

impl Error {
    pub fn exit(&self) -> ! {
        match *self {
            Error::Clap(ref e) => e.exit(),
            _ => {
                writeln!(&mut io::stderr(), "{}", self).unwrap();
                process::exit(1);
            }
        }
    }
}

impl error::Error for Error {
    fn description(&self) -> &str {
        match *self {
            Error::Lib(ref e) => e.description(),
            Error::Clap(ref e) => e.description(),
            Error::Custom(ref e) => e,
        }
    }

    fn cause(&self) -> Option<&error::Error> {
        match *self {
            Error::Lib(ref e) => Some(e),
            Error::Clap(ref e) => Some(e),
            Error::Custom(_) => None,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Error::Lib(ref e) => error::Error::description(e).fmt(f),
            Error::Clap(ref e) => error::Error::description(e).fmt(f),
            Error::Custom(ref e) => e.fmt(f),
        }
    }
}

impl From<mp::Error> for Error {
    fn from(e: mp::Error) -> Self {
        Error::Lib(e)
    }
}

impl From<clap::Error> for Error {
    fn from(e: clap::Error) -> Self {
        Error::Clap(e)
    }
}

/// Parses arguments and returns a `Command`.
pub fn parse<I, S>(args: I) -> Result
where
    I: IntoIterator<Item = S>,
    S: Clone,
    ::std::ffi::OsString: From<S>,
{
    let app = Command::clap();
    let matches = app.get_matches_from_safe(args)?;
    Ok(Command::from_clap(&matches))
}

/// Parses and validates days argument.
fn parse_days(s: &str) -> result::Result<i64, Error> {
    let days = s.parse::<i64>()
        .map_err(|err| Error::Custom(err.to_string()))?;
    if days < 0 || days > 365 {
        let message = format!("should be between 0 and 365, got {}", days);
        return Err(Error::Custom(message));
    }
    Ok(days)
}

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

    #[test]
    fn list_command() {
        expect!(parse(&["mprovision", "list"]))
            .to(be_ok().value(Command::List(ListParams::default())));

        expect!(parse(&["mprovision", "list", "--source", "."])).to(be_ok().value(Command::List(
            ListParams {
                text: None,
                expire_in_days: None,
                directory: Some(".".into()),
            },
        )));

        expect!(parse(&["mprovision", "list", "--source", ""])).to(be_err());

        expect!(parse(&["mprovision", "list", "--text", "abc"])).to(be_ok().value(Command::List(
            ListParams {
                text: Some("abc".to_string()),
                expire_in_days: None,
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "list", "-t", "abc"])).to(be_ok().value(Command::List(
            ListParams {
                text: Some("abc".to_string()),
                expire_in_days: None,
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "list", "--text", ""])).to(be_err());

        expect!(parse(&["mprovision", "list", "-t", ""])).to(be_err());

        expect!(parse(&["mprovision", "list", "--expire-in-days", "3"])).to(be_ok().value(
            Command::List(ListParams {
                text: None,
                expire_in_days: Some(3),
                directory: None,
            }),
        ));

        expect!(parse(&["mprovision", "list", "-d", "3"])).to(be_ok().value(Command::List(
            ListParams {
                text: None,
                expire_in_days: Some(3),
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "list", "--expire-in-days", "-3"])).to(be_err());
        expect!(parse(&["mprovision", "list", "-d", "-3"])).to(be_err());
        expect!(parse(&["mprovision", "list", "--expire-in-days", "366"])).to(be_err());
        expect!(parse(&["mprovision", "list", "-d", "366"])).to(be_err());

        expect!(parse(&[
            "mprovision",
            "list",
            "--text",
            "abc",
            "--expire-in-days",
            "3",
            "--source",
            "."
        ])).to(be_ok().value(Command::List(ListParams {
            text: Some("abc".to_string()),
            expire_in_days: Some(3),
            directory: Some(".".into()),
        })));

        expect!(parse(&[
            "mprovision",
            "list",
            "-t",
            "abc",
            "-d",
            "3",
            "--source",
            "."
        ])).to(be_ok().value(Command::List(ListParams {
            text: Some("abc".to_string()),
            expire_in_days: Some(3),
            directory: Some(".".into()),
        })));
    }

    #[test]
    fn show_uuid_command() {
        expect!(parse(&["mprovision", "show", "abcd"])).to(be_ok().value(Command::ShowUuid(
            ShowUuidParams {
                uuid: "abcd".to_string(),
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "show", ""])).to(be_err());

        expect!(parse(&["mprovision", "show", "abcd", "--source", "."])).to(be_ok().value(
            Command::ShowUuid(ShowUuidParams {
                uuid: "abcd".to_string(),
                directory: Some(".".into()),
            }),
        ));

        expect!(parse(&["mprovision", "show", "abcd", "--source", ""])).to(be_err());
    }

    #[test]
    fn show_file_command() {
        expect!(parse(&["mprovision", "show-file", "file.mprovision"])).to(be_ok().value(
            Command::ShowFile(ShowFileParams {
                file: "file.mprovision".into(),
            }),
        ));

        expect!(parse(&["mprovision", "show-file", "file.mprovision", "."])).to(be_err());

        expect!(parse(&["mprovision", "show-file", ""])).to(be_err());
    }

    #[test]
    fn remove_uuid_command() {
        expect!(parse(&["mprovision", "remove", "abcd"])).to(be_ok().value(Command::Remove(
            RemoveParams {
                uuids: vec!["abcd".to_string()],
                directory: None,
            },
        )));

        expect!(parse(&["mprovision", "remove", "abcd", "ef"])).to(be_ok().value(
            Command::Remove(RemoveParams {
                uuids: vec!["abcd".to_string(), "ef".to_string()],
                directory: None,
            }),
        ));

        expect!(parse(&["mprovision", "remove", ""])).to(be_err());

        expect!(parse(&["mprovision", "remove", "abcd", "--source", "."])).to(be_ok().value(
            Command::Remove(RemoveParams {
                uuids: vec!["abcd".to_string()],
                directory: Some(".".into()),
            }),
        ));

        expect!(parse(&[
            "mprovision",
            "remove",
            "abcd",
            "ef",
            "--source",
            "."
        ])).to(be_ok().value(Command::Remove(RemoveParams {
            uuids: vec!["abcd".to_string(), "ef".to_string()],
            directory: Some(".".into()),
        })));

        expect!(parse(&["mprovision", "remove", "abcd", "--source", ""])).to(be_err());
    }

    #[test]
    fn clean_command() {
        expect!(parse(&["mprovision", "clean"]))
            .to(be_ok().value(Command::Clean(CleanParams { directory: None })));

        expect!(parse(&["mprovision", "clean", "--source", "."])).to(be_ok().value(
            Command::Clean(CleanParams {
                directory: Some(".".into()),
            }),
        ));

        expect!(parse(&["mprovision", "clean", "--source", ""])).to(be_err());
    }
}
<|endoftext|>


<commit_before>use std::io::Cursor;
use std::cmp::PartialEq;

use FromCursor;
use types::{CString, CStringList, CInet};

// Event types
const TOPOLOGY_CHANGE: &'static str = "TOPOLOGY_CHANGE";
const STATUS_CHANGE: &'static str = "STATUS_CHANGE";
const SCHEMA_CHANGE: &'static str = "SCHEMA_CHANGE";

// Topologe changes
const NEW_NODE: &'static str = "NEW_NODE";
const REMOVED_NODE: &'static str = "REMOVED_NODE";

// Status changes
const UP: &'static str = "UP";
const DOWN: &'static str = "DOWN";

// Schema changes
const CREATED: &'static str = "CREATED";
const UPDATED: &'static str = "UPDATED";
const DROPPED: &'static str = "DROPPED";

// Schema change targets
const KEYSPACE: &'static str = "KEYSPACE";
const TABLE: &'static str = "TABLE";
const TYPE: &'static str = "TYPE";
const FUNCTION: &'static str = "FUNCTION";
const AGGREGATE: &'static str = "AGGREGATE";

/// Simplified `ServerEvent` that does not contain details
/// about a concrete change. It may be useful for subscription
/// when you need only string representation of an event.
#[derive(Debug, PartialEq)]
pub enum SimpleServerEvent {
    TopologyChange,
    StatusChange,
    SchemaChange
}

impl SimpleServerEvent {
    pub fn as_string(&self) -> String {
        match self {
            &SimpleServerEvent::TopologyChange => String::from(TOPOLOGY_CHANGE),
            &SimpleServerEvent::StatusChange => String::from(STATUS_CHANGE),
            &SimpleServerEvent::SchemaChange => String::from(SCHEMA_CHANGE)
        }
    }
}

impl From<ServerEvent> for SimpleServerEvent {
    fn from(event: ServerEvent) -> SimpleServerEvent {
        match event {
            ServerEvent::TopologyChange(_) => SimpleServerEvent::TopologyChange,
            ServerEvent::StatusChange(_) => SimpleServerEvent::TopologyChange,
            ServerEvent::SchemaChange(_) => SimpleServerEvent::TopologyChange,
        }
    }
}

impl<'a> From<&'a ServerEvent> for SimpleServerEvent {
    fn from(event: &'a ServerEvent) -> SimpleServerEvent {
        match event {
            &ServerEvent::TopologyChange(_) => SimpleServerEvent::TopologyChange,
            &ServerEvent::StatusChange(_) => SimpleServerEvent::TopologyChange,
            &ServerEvent::SchemaChange(_) => SimpleServerEvent::TopologyChange,
        }
    }
}

impl PartialEq<ServerEvent> for SimpleServerEvent {
    fn eq(&self, full_event: &ServerEvent) -> bool {
        self == &SimpleServerEvent::from(full_event)
    }
}

/// Full server event that contains all details about a concreate change.
#[derive(Debug)]
pub enum ServerEvent {
    /// Events related to change in the cluster topology
    TopologyChange(TopologyChange),
    /// Events related to change of node status.
    StatusChange(StatusChange),
    /// Events related to schema change.
    SchemaChange(SchemaChange)
}

impl PartialEq<SimpleServerEvent> for ServerEvent {
    fn eq(&self, event: &SimpleServerEvent) -> bool {
        &SimpleServerEvent::from(self) == event
    }
}

impl FromCursor for ServerEvent {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> ServerEvent {
        let event_type = CString::from_cursor(&mut cursor);
        match event_type.as_str() {
            TOPOLOGY_CHANGE => ServerEvent::TopologyChange(
                TopologyChange::from_cursor(&mut cursor)
            ),
            STATUS_CHANGE => ServerEvent::StatusChange(
                StatusChange::from_cursor(&mut cursor)
            ),
            SCHEMA_CHANGE => ServerEvent::SchemaChange(
                SchemaChange::from_cursor(&mut cursor)
            ),
            _ => unreachable!()
        }
    }
}

/// Events related to change in the cluster topology
#[derive(Debug)]
pub struct TopologyChange {
    pub change_type: TopologyChangeType,
    pub addr: CInet
}

impl FromCursor for TopologyChange {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> TopologyChange {
        let change_type = TopologyChangeType::from_cursor(&mut cursor);
        let addr = CInet::from_cursor(&mut cursor);

        TopologyChange {
            change_type: change_type,
            addr: addr
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum TopologyChangeType {
    NewNode,
    RemovedNode
}

impl FromCursor for TopologyChangeType {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> TopologyChangeType {
        match CString::from_cursor(&mut cursor).as_str() {
            NEW_NODE => TopologyChangeType::NewNode,
            REMOVED_NODE => TopologyChangeType::RemovedNode,
            _ => unreachable!()
        }
    }
}

/// Events related to change of node status.
#[derive(Debug)]
pub struct StatusChange {
    pub change_type: StatusChangeType,
    pub addr: CInet
}

impl FromCursor for StatusChange {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> StatusChange {
        let change_type = StatusChangeType::from_cursor(&mut cursor);
        let addr = CInet::from_cursor(&mut cursor);

        StatusChange {
            change_type: change_type,
            addr: addr
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum StatusChangeType {
    Up,
    Down
}

impl FromCursor for StatusChangeType {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> StatusChangeType {
        match CString::from_cursor(&mut cursor).as_str() {
            UP => StatusChangeType::Up,
            DOWN => StatusChangeType::Down,
            _ => unreachable!()
        }
    }
}

/// Events related to schema change.
#[derive(Debug, PartialEq)]
pub struct SchemaChange {
    pub change_type: ChangeType,
    pub target: Target,
    pub options: ChangeSchemeOptions
}

impl FromCursor for SchemaChange {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> SchemaChange {
        let change_type = ChangeType::from_cursor(&mut cursor);
        let target = Target::from_cursor(&mut cursor);
        let options = ChangeSchemeOptions::from_cursor_and_target(&mut cursor, &target);

        SchemaChange {
            change_type: change_type,
            target: target,
            options: options
        }
    }
}

/// Represents type of changes.
#[derive(Debug, PartialEq)]
pub enum ChangeType {
    Created,
    Updated,
    Dropped
}

impl FromCursor for ChangeType {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeType {
        match CString::from_cursor(&mut cursor).as_str() {
            CREATED => ChangeType::Created,
            UPDATED => ChangeType::Updated,
            DROPPED => ChangeType::Dropped,
            _ => unreachable!()
        }
    }
}

/// Refers to a target of changes were made.
#[derive(Debug, PartialEq)]
pub enum Target {
    Keyspace,
    Table,
    Type,
    Function,
    Aggregate
}

impl FromCursor for Target {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> Target {
        match CString::from_cursor(&mut cursor).as_str() {
            KEYSPACE => Target::Keyspace,
            TABLE => Target::Table,
            TYPE => Target::Type,
            FUNCTION => Target::Function,
            AGGREGATE => Target::Aggregate,
            _ => unreachable!()
        }
    }
}

/// Option that contains an information about changes were made.
#[derive(Debug, PartialEq)]
pub enum ChangeSchemeOptions {
    /// Changes related to keyspaces. Contains keyspace name.
    Keyspace(String),
    /// Changes related to tables. Contains keyspace and table names.
    Table((String, String)),
    /// Changes related to functions and aggregations. Contains:
    /// * keyspace containing the user defined function / aggregate
    /// * the function/aggregate name
    /// * list of strings, one string for each argument type (as CQL type)
    Function((String, String, Vec<String>))
}

impl ChangeSchemeOptions {
    fn from_cursor_and_target(mut cursor: &mut Cursor<Vec<u8>>, target: &Target)
        -> ChangeSchemeOptions {
        match target {
            &Target::Keyspace => ChangeSchemeOptions::from_cursor_keyspace(&mut cursor),
            &Target::Table | &Target::Type => ChangeSchemeOptions::from_cursor_table(&mut cursor),
            &Target::Function | &Target::Aggregate => ChangeSchemeOptions::from_cursor_function(&mut cursor)
        }
    }

    fn from_cursor_keyspace(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeSchemeOptions {
         ChangeSchemeOptions::Keyspace(CString::from_cursor(&mut cursor).into_plain())
    }

    fn from_cursor_table(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeSchemeOptions {
        let keyspace = CString::from_cursor(&mut cursor).into_plain();
        let name = CString::from_cursor(&mut cursor).into_plain();
        ChangeSchemeOptions::Table((keyspace, name))
    }

    fn from_cursor_function(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeSchemeOptions {
        let keyspace = CString::from_cursor(&mut cursor).into_plain();
        let name = CString::from_cursor(&mut cursor).into_plain();
        let types = CStringList::from_cursor(&mut cursor).into_plain();
        ChangeSchemeOptions::Function((keyspace, name, types))
    }
}
<commit_msg>Create tests for frame/events 1<commit_after>use std::io::Cursor;
use std::cmp::PartialEq;

use FromCursor;
use types::{CString, CStringList, CInet};

// Event types
const TOPOLOGY_CHANGE: &'static str = "TOPOLOGY_CHANGE";
const STATUS_CHANGE: &'static str = "STATUS_CHANGE";
const SCHEMA_CHANGE: &'static str = "SCHEMA_CHANGE";

// Topologe changes
const NEW_NODE: &'static str = "NEW_NODE";
const REMOVED_NODE: &'static str = "REMOVED_NODE";

// Status changes
const UP: &'static str = "UP";
const DOWN: &'static str = "DOWN";

// Schema changes
const CREATED: &'static str = "CREATED";
const UPDATED: &'static str = "UPDATED";
const DROPPED: &'static str = "DROPPED";

// Schema change targets
const KEYSPACE: &'static str = "KEYSPACE";
const TABLE: &'static str = "TABLE";
const TYPE: &'static str = "TYPE";
const FUNCTION: &'static str = "FUNCTION";
const AGGREGATE: &'static str = "AGGREGATE";

/// Simplified `ServerEvent` that does not contain details
/// about a concrete change. It may be useful for subscription
/// when you need only string representation of an event.
#[derive(Debug, PartialEq)]
pub enum SimpleServerEvent {
    TopologyChange,
    StatusChange,
    SchemaChange
}

impl SimpleServerEvent {
    pub fn as_string(&self) -> String {
        match self {
            &SimpleServerEvent::TopologyChange => String::from(TOPOLOGY_CHANGE),
            &SimpleServerEvent::StatusChange => String::from(STATUS_CHANGE),
            &SimpleServerEvent::SchemaChange => String::from(SCHEMA_CHANGE)
        }
    }
}

impl From<ServerEvent> for SimpleServerEvent {
    fn from(event: ServerEvent) -> SimpleServerEvent {
        match event {
            ServerEvent::TopologyChange(_) => SimpleServerEvent::TopologyChange,
            ServerEvent::StatusChange(_) => SimpleServerEvent::TopologyChange,
            ServerEvent::SchemaChange(_) => SimpleServerEvent::TopologyChange,
        }
    }
}

impl<'a> From<&'a ServerEvent> for SimpleServerEvent {
    fn from(event: &'a ServerEvent) -> SimpleServerEvent {
        match event {
            &ServerEvent::TopologyChange(_) => SimpleServerEvent::TopologyChange,
            &ServerEvent::StatusChange(_) => SimpleServerEvent::TopologyChange,
            &ServerEvent::SchemaChange(_) => SimpleServerEvent::TopologyChange,
        }
    }
}

impl PartialEq<ServerEvent> for SimpleServerEvent {
    fn eq(&self, full_event: &ServerEvent) -> bool {
        self == &SimpleServerEvent::from(full_event)
    }
}

/// Full server event that contains all details about a concreate change.
#[derive(Debug)]
pub enum ServerEvent {
    /// Events related to change in the cluster topology
    TopologyChange(TopologyChange),
    /// Events related to change of node status.
    StatusChange(StatusChange),
    /// Events related to schema change.
    SchemaChange(SchemaChange)
}

impl PartialEq<SimpleServerEvent> for ServerEvent {
    fn eq(&self, event: &SimpleServerEvent) -> bool {
        &SimpleServerEvent::from(self) == event
    }
}

impl FromCursor for ServerEvent {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> ServerEvent {
        let event_type = CString::from_cursor(&mut cursor);
        match event_type.as_str() {
            TOPOLOGY_CHANGE => ServerEvent::TopologyChange(
                TopologyChange::from_cursor(&mut cursor)
            ),
            STATUS_CHANGE => ServerEvent::StatusChange(
                StatusChange::from_cursor(&mut cursor)
            ),
            SCHEMA_CHANGE => ServerEvent::SchemaChange(
                SchemaChange::from_cursor(&mut cursor)
            ),
            _ => unreachable!()
        }
    }
}

/// Events related to change in the cluster topology
#[derive(Debug)]
pub struct TopologyChange {
    pub change_type: TopologyChangeType,
    pub addr: CInet
}

impl FromCursor for TopologyChange {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> TopologyChange {
        let change_type = TopologyChangeType::from_cursor(&mut cursor);
        let addr = CInet::from_cursor(&mut cursor);

        TopologyChange {
            change_type: change_type,
            addr: addr
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum TopologyChangeType {
    NewNode,
    RemovedNode
}

impl FromCursor for TopologyChangeType {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> TopologyChangeType {
        match CString::from_cursor(&mut cursor).as_str() {
            NEW_NODE => TopologyChangeType::NewNode,
            REMOVED_NODE => TopologyChangeType::RemovedNode,
            _ => unreachable!()
        }
    }
}

/// Events related to change of node status.
#[derive(Debug)]
pub struct StatusChange {
    pub change_type: StatusChangeType,
    pub addr: CInet
}

impl FromCursor for StatusChange {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> StatusChange {
        let change_type = StatusChangeType::from_cursor(&mut cursor);
        let addr = CInet::from_cursor(&mut cursor);

        StatusChange {
            change_type: change_type,
            addr: addr
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum StatusChangeType {
    Up,
    Down
}

impl FromCursor for StatusChangeType {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> StatusChangeType {
        match CString::from_cursor(&mut cursor).as_str() {
            UP => StatusChangeType::Up,
            DOWN => StatusChangeType::Down,
            _ => unreachable!()
        }
    }
}

/// Events related to schema change.
#[derive(Debug, PartialEq)]
pub struct SchemaChange {
    pub change_type: ChangeType,
    pub target: Target,
    pub options: ChangeSchemeOptions
}

impl FromCursor for SchemaChange {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> SchemaChange {
        let change_type = ChangeType::from_cursor(&mut cursor);
        let target = Target::from_cursor(&mut cursor);
        let options = ChangeSchemeOptions::from_cursor_and_target(&mut cursor, &target);

        SchemaChange {
            change_type: change_type,
            target: target,
            options: options
        }
    }
}

/// Represents type of changes.
#[derive(Debug, PartialEq)]
pub enum ChangeType {
    Created,
    Updated,
    Dropped
}

impl FromCursor for ChangeType {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeType {
        match CString::from_cursor(&mut cursor).as_str() {
            CREATED => ChangeType::Created,
            UPDATED => ChangeType::Updated,
            DROPPED => ChangeType::Dropped,
            _ => unreachable!()
        }
    }
}

/// Refers to a target of changes were made.
#[derive(Debug, PartialEq)]
pub enum Target {
    Keyspace,
    Table,
    Type,
    Function,
    Aggregate
}

impl FromCursor for Target {
    fn from_cursor(mut cursor: &mut Cursor<Vec<u8>>) -> Target {
        match CString::from_cursor(&mut cursor).as_str() {
            KEYSPACE => Target::Keyspace,
            TABLE => Target::Table,
            TYPE => Target::Type,
            FUNCTION => Target::Function,
            AGGREGATE => Target::Aggregate,
            _ => unreachable!()
        }
    }
}

/// Option that contains an information about changes were made.
#[derive(Debug, PartialEq)]
pub enum ChangeSchemeOptions {
    /// Changes related to keyspaces. Contains keyspace name.
    Keyspace(String),
    /// Changes related to tables. Contains keyspace and table names.
    Table((String, String)),
    /// Changes related to functions and aggregations. Contains:
    /// * keyspace containing the user defined function / aggregate
    /// * the function/aggregate name
    /// * list of strings, one string for each argument type (as CQL type)
    Function((String, String, Vec<String>))
}

impl ChangeSchemeOptions {
    fn from_cursor_and_target(mut cursor: &mut Cursor<Vec<u8>>, target: &Target)
        -> ChangeSchemeOptions {
        match target {
            &Target::Keyspace => ChangeSchemeOptions::from_cursor_keyspace(&mut cursor),
            &Target::Table | &Target::Type => ChangeSchemeOptions::from_cursor_table(&mut cursor),
            &Target::Function | &Target::Aggregate => ChangeSchemeOptions::from_cursor_function(&mut cursor)
        }
    }

    fn from_cursor_keyspace(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeSchemeOptions {
         ChangeSchemeOptions::Keyspace(CString::from_cursor(&mut cursor).into_plain())
    }

    fn from_cursor_table(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeSchemeOptions {
        let keyspace = CString::from_cursor(&mut cursor).into_plain();
        let name = CString::from_cursor(&mut cursor).into_plain();
        ChangeSchemeOptions::Table((keyspace, name))
    }

    fn from_cursor_function(mut cursor: &mut Cursor<Vec<u8>>) -> ChangeSchemeOptions {
        let keyspace = CString::from_cursor(&mut cursor).into_plain();
        let name = CString::from_cursor(&mut cursor).into_plain();
        let types = CStringList::from_cursor(&mut cursor).into_plain();
        ChangeSchemeOptions::Function((keyspace, name, types))
    }
}

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

    #[test]
    fn as_string() {
        assert_eq!(SimpleServerEvent::TopologyChange.as_string(), "TOPOLOGY_CHANGE".to_string());
        assert_eq!(SimpleServerEvent::StatusChange.as_string(), "STATUS_CHANGE".to_string());
        assert_eq!(SimpleServerEvent::SchemaChange.as_string(), "SCHEMA_CHANGE".to_string());
    }
}

#[cfg(test)]
mod topology_change_type_test {
    use super::*;
    use std::io::Cursor;
    use FromCursor;

    #[test]
    fn from_cursor() {
        let mut new_node: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 8, 78, 69, 87, 95, 78, 79, 68, 69]);
        assert_eq!(TopologyChangeType::from_cursor(&mut new_node),
            TopologyChangeType::NewNode);

        let mut removed_node: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 12, 82, 69, 77, 79, 86, 69, 68, 95, 78, 79, 68, 69]);
        assert_eq!(TopologyChangeType::from_cursor(&mut removed_node),
            TopologyChangeType::RemovedNode);
    }

    #[test]
    #[should_panic]
    fn from_cursor_wrong() {
        let mut wrong: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 1, 78]);
        let _ = TopologyChangeType::from_cursor(&mut wrong);
    }
}

#[cfg(test)]
mod status_change_type_test {
    use super::*;
    use std::io::Cursor;
    use FromCursor;

    #[test]
    fn from_cursor() {
        let mut up: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 2, 85, 80]);
        assert_eq!(StatusChangeType::from_cursor(&mut up),
            StatusChangeType::Up);

        let mut down: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 4, 68, 79, 87, 78]);
        assert_eq!(StatusChangeType::from_cursor(&mut down),
            StatusChangeType::Down);
    }

    #[test]
    #[should_panic]
    fn from_cursor_wrong() {
        let mut wrong: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 1, 78]);
        let _ = StatusChangeType::from_cursor(&mut wrong);
    }
}

#[cfg(test)]
mod schema_change_type_test {
    use super::*;
    use std::io::Cursor;
    use FromCursor;

    #[test]
    fn from_cursor() {
        let mut created: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 7, 67, 82, 69, 65, 84, 69, 68]);
        assert_eq!(ChangeType::from_cursor(&mut created),
            ChangeType::Created);

        let mut updated: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 7, 85, 80, 68, 65, 84, 69, 68]);
        assert_eq!(ChangeType::from_cursor(&mut updated),
            ChangeType::Updated);

        let mut dropped: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 7, 68, 82, 79, 80, 80, 69, 68]);
        assert_eq!(ChangeType::from_cursor(&mut dropped),
            ChangeType::Dropped);
    }

    #[test]
    #[should_panic]
    fn from_cursor_wrong() {
        let mut wrong: Cursor<Vec<u8>> =
            Cursor::new(vec![0, 1, 78]);
        let _ = ChangeType::from_cursor(&mut wrong);
    }
}
<|endoftext|>


<commit_before>use std::io::Cursor;

use kuchiki::NodeRef;
use kuchiki::NodeDataRef;
use kuchiki::NodeData;
use kuchiki::ElementData;

use model::ListTopicItem;
use model::ListTopicTitleItem;
use model::ListTopicAuthorItem;
use model::ShowItem;
use model::ShowReplyItem;
use model::UrlQueryItem;
use reply_model::*;

use regex::Regex;
use url::Url;
use std::collections::HashMap;

#[derive(Clone)]
pub struct Builder { }

impl Builder {
    pub fn new() -> Self {
        Builder {}
    }
    pub fn url_query_item(&mut self, url: &str) -> UrlQueryItem {
        parse_url_query_item(&url)
    }
}

impl Builder {
    pub fn show_item(&mut self, document: &NodeRef,  url: &str) -> Result<ShowItem, &'static str> {

        let url_query = parse_url_query_item(&url);

        let (title, reply_count) = match parse_title_and_reply_count(&document, &url) {
            Ok((title,reply_count)) => (title, reply_count),
            Err(e) =>  { return Err(e) }
        };

        let (page, max_page) = {

            let page_select = document.select("select[name='page']").ok().expect("fail to build page and max_page, reason: 'page_select' not found").last().expect("fail to build page and max_page, reason: 'page_select' not found");
            let page_str = page_select.as_node()
                                      .select("option[selected='selected']")
                                      .ok().expect("fail to build page and max_page, reason: 'page_str' not found")
                                      .next()
                                      .expect("fail to build page and max_page, reason: 'page_str' not found");
            let max_page_str = page_select.as_node().select("option").ok().expect("fail to build page and max_page, reason: 'max_page_str' not found").last().expect("fail to build page and max_page, reason: 'max_page_str' not found");

            let page = page_str.text_contents().trim().to_string().parse::<usize>().unwrap_or(0);
            let max_page = max_page_str.text_contents()
                                       .trim()
                                       .to_string()
                                       .parse::<usize>()
                                       .unwrap_or(0);

            (page, max_page)
        };

        let replies = parse_show_reply_items(&document);

        let show_item = ShowItem {
            url_query: url_query,
            replies: replies,
            page: page,
            max_page: max_page,
            reply_count: String::from(reply_count),
            title: String::from(title),
        };

        Ok(show_item)
    }
    pub fn default_show_item(&self) -> ShowItem {
        default_show_item()
    }
}

impl Builder {
    pub fn list_topic_items(&mut self, document: &NodeRef) -> Vec<ListTopicItem> {
        parse_list_topic_items(&document)
    }
    pub fn default_list_item(&self) -> ListTopicItem {
        default_list_item()
    }
}

fn default_show_item() -> ShowItem {
    ShowItem {
        url_query: UrlQueryItem { channel: "".to_string(), message: String::from("") },
        replies: vec![],
        page: 0,
        max_page: 0,
        reply_count: String::from(""),
        title: String::from(""),
    }
}

fn default_list_item() -> ListTopicItem {
    ListTopicItem {
        title: ListTopicTitleItem {
            url: "".to_string(),
            url_query: UrlQueryItem {
                channel: "".to_string(),
                message: "".to_string()
            },
            text: "".to_string(),
            num_of_pages: 0
        },
        author: ListTopicAuthorItem {
            url: "".to_string(),
            name: "".to_string()
        },
        last_replied_date: "".to_string(),
        last_replied_time: "".to_string(),
        reply_count: "".to_string(),
        rating: "".to_string()
    }
}

fn parse_list_topic_items(document: &NodeRef) -> Vec<ListTopicItem>{

    let trs = match document.select(".Topic_ListPanel tr[id]") {
            Ok(trs) => trs,
            Err(e) => panic!("{:?}", e)
    };

    trs.enumerate().map(|(i, tr)| {
        let items = match tr.as_node().select("td") {
            Ok(items) => items,
            Err(e) => panic!("{:?}", e)
        };
        let mut result = default_list_item();

        for (j, item) in items.enumerate().filter(|&(j, _)| j > 0 && j < 6) {
            match j {
                1 => { result.title = parse_list_topic_title_item(&item) },
                2 => { result.author = parse_list_topic_author_item(&item) },
                3 => {
                    let (date, time) = {
                        let text = item.text_contents().trim().to_string();
                        let map = text.split("\n").map(|x| x.trim().to_string()).collect::<Vec<_>>();
                        if map.len() < 2 {
                            panic!("length of map is invalid.");
                        }
                        (map[0].clone(), map[1].clone())
                    };
                    result.last_replied_date = date;
                    result.last_replied_time = time;
                },
                4 => {
                    let text = item.text_contents().trim().to_string();
                    result.reply_count = text
                },
                5 => {
                    let text = item.text_contents().trim().to_string();
                    result.rating = text
                },
                _ => {}
            }
        }
        result
    }).collect::<Vec<_>>()
}

fn parse_list_topic_title_item(item: &NodeDataRef<ElementData>) -> ListTopicTitleItem {
    let (first_link, links_count) = {
        let mut links = match item.as_node().select("a") {
            Ok(links) => links,
            Err(e) => panic!("ERR: {:?}", e)
        };

        let first_link_option = links.next();
        let last_link_option = links.last();

        let first_link = match first_link_option {
            Some(first_link) => first_link,
            None => { panic!("ERR: Can't find 'first_link'.") }
        };

        let max_page = match last_link_option {
            Some(last_link) =>
                last_link.text_contents().trim().to_string().parse::<usize>()
                .unwrap_or(0)
            ,
            None => { 1 }
        };

        (first_link, max_page)
    };

    let (url_str, url_query_item) = {
        let attrs = &(first_link.attributes).borrow();
        let href = attrs.get("href").unwrap_or("");

        let base_url = match Url::parse("http://forum1.hkgolden.com/view.aspx") {
            Ok(url) => url,
            Err(e) => { panic!(format!("fail to parse Base URL. Reason: {}", e)) }
        };
        let url = match base_url.join(&href) {
            Ok(url) => url,
            Err(e) => { panic!(format!("fail to build URL. Reason: {}", e)) }
        };
        let url_str = url.into_string();
        let url_query_item = parse_url_query_item(&url_str);
        (url_str, url_query_item)
    };

    let text = first_link.text_contents().trim().to_string();

     ListTopicTitleItem {
        url: url_str,
        url_query: url_query_item,
        text: text,
        num_of_pages: links_count
    }
}

fn parse_list_topic_author_item(item: &NodeDataRef<ElementData>) -> ListTopicAuthorItem {
    let (url, name) = {
        let mut links = match item.as_node().select("a") {
            Ok(links) => links,
            Err(e) => panic!("ERR: {:?}", e)
        };

        let first_link_option = links.next();

        let first_link = match first_link_option {
            Some(first_link) => first_link,
            None => { panic!("ERR: Can't find 'first_link'.") }
        };

        let attrs = &(first_link.attributes).borrow();
        let href = attrs.get("href").unwrap_or("").to_string();
        let text = first_link.text_contents().trim().to_string();
        (href, text)
    };

    ListTopicAuthorItem {
       url: url,
       name: name
    }
}

// Show Item
fn parse_title_and_reply_count (document: &NodeRef,  url: &str) -> Result<(String, String), &'static str> {

    return match document.select(".repliers tr") {
        Ok(mut trs) => {
            return match trs.next()  {
                Some(repliers_tr) => {
                    let repliers_header = repliers_tr.as_node()
                                                     .select(".repliers_header")
                                                     .ok().expect("fail to build title and reply_count, reason: 'repliers_header' not found")
                                                     .last()
                                                     .expect("fail to build title and reply_count, reason: 'repliers_header' not found");
                    let divs = repliers_header.as_node().select("div").ok().expect("fail to build title and reply_count, reason: 'divs' not found").collect::<Vec<_>>();

                    let topic_data = divs.iter()
                                         .enumerate()
                                         .map(|(index, div)| {
                                             let s_trimmed = div.text_contents().trim().to_string();
                                             if index == 1 {
                                                 let re = Regex::new(r"^(?P<count>\d+)個回應$").expect("fail to build title and reply_count, reason: invalid regex");
                                                 let cap = re.captures(&s_trimmed).expect("fail to build title and reply_count, reason: 'cap' not found");
                                                 // String::from(cap.name("count").unwrap_or("0"))
                                                 cap.name("count").unwrap_or("0").to_string()
                                             } else {
                                                 s_trimmed
                                             }
                                         })
                                         .collect::<Vec<_>>();

                    if topic_data.len() < 2 {
                        Err(&"length of topic_data is invalid.")
                    } else {
                        Ok(
                            (topic_data.get(0).expect("fail to build title and reply_count, reason: 'topic_data' not found").to_string(), // return as title
                            topic_data.get(1).expect("fail to build title and reply_count, reason: 'topic_data' not found").to_string() /* return as reply_count */)
                        )
                    }
                },
                None => Err(&"fail to build title and reply_count, reason: 'repliers_tr' not found")
            }
        },
        Err(e) =>  Err("fail to build title and reply_count, reason: 'repliers_tr' not found")
    };

}

fn parse_show_reply_items(document: &NodeRef) -> Vec<ShowReplyItem> {

    let replies_data = document.select(".repliers tr[userid][username]")
                               .ok().expect("fail to parse show reply items, reason: 'replies_data' not found")
                               .collect::<Vec<_>>();

    replies_data.iter()
                .enumerate()
                .map(|(index, tr)| {

                    let tr_attrs = (&tr.attributes).borrow();
                    let userid = tr_attrs.get("userid").expect("fail to parse show reply item, reason: 'userid' not found");
                    let username = tr_attrs.get("username").expect("fail to parse show reply item, reason: 'userame' not found");

                    let content_elm = tr.as_node()
                                        .select(".repliers_right .ContentGrid")
                                        .ok().expect("fail to parse show reply item, reason: 'content_elm' not found")
                                        .next()
                                        .expect("fail to parse show reply item, reason: 'content_elm' not found"); // first

                    let mut buff = Cursor::new(Vec::new());
                    let serialize_result = content_elm.as_node().serialize(&mut buff);
                    let vec = buff.into_inner();
                    let content = String::from_utf8(vec).expect("fail to parse show reply item, reason: 'content' invalid");

                    let datatime = tr.as_node()
                                     .select(".repliers_right span")
                                     .ok().expect("fail to parse show reply item, reason: 'datatime' not found")
                                     .last()
                                     .expect("fail to parse show reply item, reason: 'datatime' not found")
                                     .text_contents();

                    let mut vec: Vec<NodeType> = Vec::new();

                    vec = recursive(content_elm.as_node());

                    ShowReplyItem {
                        userid: String::from(userid),
                        username: String::from(username),
                        content: String::from(content),
                        body: vec,
                        published_at: String::from(datatime),
                    }
                })
                .collect::<Vec<_>>()
}

fn parse_url_query_item(url_str: &str) -> UrlQueryItem {
    let url = Url::parse(&url_str).expect("fail to parse url query item, reason: invalid url");
    let query = url.query().expect("fail to parse url query item, reason: invalid url query");
    let re = Regex::new(r"(\\?|&)(?P<key>[^&=]+)=(?P<value>[^&]+)").expect("fail to parse url query item, reason: invalid regex");

    let (channel, message) = {

        let mut map = HashMap::new();

        for cap in re.captures_iter(query) {
            let key = cap.name("key").unwrap_or("").to_string();
            let value = cap.name("value").unwrap_or("").to_string();
            map.entry(key).or_insert(value);
        }

        if map.len() < 2 {
            panic!("length of map is invalid.")
        }

        (
            map.get("type").expect("fail to parse url query item, reason: can not get value of 'type' attribute").to_string(),
            map.get("message").expect("fail to parse url query item, reason: can not get value of 'message' attribute").to_string()
        )
    };

    UrlQueryItem {
        channel: String::from(channel),
        message: String::from(message)
    }

}

fn recursive(elm: &NodeRef) -> Vec<NodeType> {

    let mut vec: Vec<NodeType> = Vec::new();

    for (index, child) in elm.children().enumerate() {
        // println!("[{}] => {:?}", index, child);
        let node_data = child.data().clone();

        match node_data {
            NodeData::Element(element_data) => {
                // println!("[{}] => [ELEMENT] => {:?}", index, element_data);
                // println!("[{}] => [ELEMENT] => {:?}", index, child);

                if element_data.name.local.trim().eq("blockquote") {

                    // println!("[{}] => [ELEMENT] => {:?}", index, child.children());
                    let subvec = recursive(&child);
                    let node = NodeType::BlockQuote(BlockQuoteNode { data: subvec });
                    vec.push(node);
                } else if element_data.name.local.trim().eq("br") {
                    let node = NodeType::Br(BrNode {});
                    vec.push(node);
                } else if element_data.name.local.trim().eq("img") {

                    let attrs = (&element_data.attributes).borrow();
                    let url = attrs.get("src").unwrap_or("");
                    let alt = attrs.get("alt").unwrap_or("");
                    let node = NodeType::Image(ImageNode { data: url.to_string(), alt: alt.to_string() });
                    vec.push(node);

                } else {
                    // println!("[{}] => [ELEMENT] => {:?}", index, child);
                    let mut subvec = recursive(&child);
                    vec.append(&mut subvec);
                }
            }
            NodeData::Text(rc) => {
                // println!("[{}] => [TEXT] => {:?}", index, rc);
                let d = rc.clone();
                let b = d.borrow();

                let s = b.trim().to_string();

                if s == "\n" {
                    // s = "\\n".to_string()
                    continue;
                }

                let node = NodeType::Text(TextNode { data: s });
                vec.push(node);
            }
            _ => {}
        }
    }
    vec
}
<commit_msg>avoid panic if show item parse failed<commit_after>use std::io::Cursor;

use kuchiki::NodeRef;
use kuchiki::NodeDataRef;
use kuchiki::NodeData;
use kuchiki::ElementData;

use model::ListTopicItem;
use model::ListTopicTitleItem;
use model::ListTopicAuthorItem;
use model::ShowItem;
use model::ShowReplyItem;
use model::UrlQueryItem;
use reply_model::*;

use regex::Regex;
use url::Url;
use std::collections::HashMap;

#[derive(Clone)]
pub struct Builder { }

impl Builder {
    pub fn new() -> Self {
        Builder {}
    }
    pub fn url_query_item(&mut self, url: &str) -> UrlQueryItem {
        parse_url_query_item(&url)
    }
}

impl Builder {
    pub fn show_item(&mut self, document: &NodeRef,  url: &str) -> Result<ShowItem, &'static str> {

        let url_query = parse_url_query_item(&url);

        let (title, reply_count) = match parse_title_and_reply_count(&document, &url) {
            Ok((title,reply_count)) => (title, reply_count),
            Err(e) =>  { return Err(e) }
        };

        let (page, max_page) = {

            let page_select = document.select("select[name='page']").ok().expect("fail to build page and max_page, reason: 'page_select' not found").last().expect("fail to build page and max_page, reason: 'page_select' not found");
            let page_str = page_select.as_node()
                                      .select("option[selected='selected']")
                                      .ok().expect("fail to build page and max_page, reason: 'page_str' not found")
                                      .next()
                                      .expect("fail to build page and max_page, reason: 'page_str' not found");
            let max_page_str = page_select.as_node().select("option").ok().expect("fail to build page and max_page, reason: 'max_page_str' not found").last().expect("fail to build page and max_page, reason: 'max_page_str' not found");

            let page = page_str.text_contents().trim().to_string().parse::<usize>().unwrap_or(0);
            let max_page = max_page_str.text_contents()
                                       .trim()
                                       .to_string()
                                       .parse::<usize>()
                                       .unwrap_or(0);

            (page, max_page)
        };

        let replies = parse_show_reply_items(&document);

        let show_item = ShowItem {
            url_query: url_query,
            replies: replies,
            page: page,
            max_page: max_page,
            reply_count: String::from(reply_count),
            title: String::from(title),
        };

        Ok(show_item)
    }
    pub fn default_show_item(&self) -> ShowItem {
        default_show_item()
    }
}

impl Builder {
    pub fn list_topic_items(&mut self, document: &NodeRef) -> Vec<ListTopicItem> {
        parse_list_topic_items(&document)
    }
    pub fn default_list_item(&self) -> ListTopicItem {
        default_list_item()
    }
}

fn default_show_item() -> ShowItem {
    ShowItem {
        url_query: UrlQueryItem { channel: "".to_string(), message: String::from("") },
        replies: vec![],
        page: 0,
        max_page: 0,
        reply_count: String::from(""),
        title: String::from(""),
    }
}

fn default_list_item() -> ListTopicItem {
    ListTopicItem {
        title: ListTopicTitleItem {
            url: "".to_string(),
            url_query: UrlQueryItem {
                channel: "".to_string(),
                message: "".to_string()
            },
            text: "".to_string(),
            num_of_pages: 0
        },
        author: ListTopicAuthorItem {
            url: "".to_string(),
            name: "".to_string()
        },
        last_replied_date: "".to_string(),
        last_replied_time: "".to_string(),
        reply_count: "".to_string(),
        rating: "".to_string()
    }
}

fn parse_list_topic_items(document: &NodeRef) -> Vec<ListTopicItem>{

    let trs = match document.select(".Topic_ListPanel tr[id]") {
            Ok(trs) => trs,
            Err(e) => panic!("{:?}", e)
    };

    trs.enumerate().map(|(i, tr)| {
        let items = match tr.as_node().select("td") {
            Ok(items) => items,
            Err(e) => panic!("{:?}", e)
        };
        let mut result = default_list_item();

        for (j, item) in items.enumerate().filter(|&(j, _)| j > 0 && j < 6) {
            match j {
                1 => { result.title = parse_list_topic_title_item(&item) },
                2 => { result.author = parse_list_topic_author_item(&item) },
                3 => {
                    let (date, time) = {
                        let text = item.text_contents().trim().to_string();
                        let map = text.split("\n").map(|x| x.trim().to_string()).collect::<Vec<_>>();
                        if map.len() < 2 {
                            panic!("length of map is invalid.");
                        }
                        (map[0].clone(), map[1].clone())
                    };
                    result.last_replied_date = date;
                    result.last_replied_time = time;
                },
                4 => {
                    let text = item.text_contents().trim().to_string();
                    result.reply_count = text
                },
                5 => {
                    let text = item.text_contents().trim().to_string();
                    result.rating = text
                },
                _ => {}
            }
        }
        result
    }).collect::<Vec<_>>()
}

fn parse_list_topic_title_item(item: &NodeDataRef<ElementData>) -> ListTopicTitleItem {
    let (first_link, links_count) = {
        let mut links = match item.as_node().select("a") {
            Ok(links) => links,
            Err(e) => panic!("ERR: {:?}", e)
        };

        let first_link_option = links.next();
        let last_link_option = links.last();

        let first_link = match first_link_option {
            Some(first_link) => first_link,
            None => { panic!("ERR: Can't find 'first_link'.") }
        };

        let max_page = match last_link_option {
            Some(last_link) =>
                last_link.text_contents().trim().to_string().parse::<usize>()
                .unwrap_or(0)
            ,
            None => { 1 }
        };

        (first_link, max_page)
    };

    let (url_str, url_query_item) = {
        let attrs = &(first_link.attributes).borrow();
        let href = attrs.get("href").unwrap_or("");

        let base_url = match Url::parse("http://forum1.hkgolden.com/view.aspx") {
            Ok(url) => url,
            Err(e) => { panic!(format!("fail to parse Base URL. Reason: {}", e)) }
        };
        let url = match base_url.join(&href) {
            Ok(url) => url,
            Err(e) => { panic!(format!("fail to build URL. Reason: {}", e)) }
        };
        let url_str = url.into_string();
        let url_query_item = parse_url_query_item(&url_str);
        (url_str, url_query_item)
    };

    let text = first_link.text_contents().trim().to_string();

     ListTopicTitleItem {
        url: url_str,
        url_query: url_query_item,
        text: text,
        num_of_pages: links_count
    }
}

fn parse_list_topic_author_item(item: &NodeDataRef<ElementData>) -> ListTopicAuthorItem {
    let (url, name) = {
        let mut links = match item.as_node().select("a") {
            Ok(links) => links,
            Err(e) => panic!("ERR: {:?}", e)
        };

        let first_link_option = links.next();

        let first_link = match first_link_option {
            Some(first_link) => first_link,
            None => { panic!("ERR: Can't find 'first_link'.") }
        };

        let attrs = &(first_link.attributes).borrow();
        let href = attrs.get("href").unwrap_or("").to_string();
        let text = first_link.text_contents().trim().to_string();
        (href, text)
    };

    ListTopicAuthorItem {
       url: url,
       name: name
    }
}

// Show Item
fn parse_title_and_reply_count (document: &NodeRef,  url: &str) -> Result<(String, String), &'static str> {

    return match document.select(".repliers tr") {
        Ok(mut trs) => {
            return match trs.next()  {
                Some(repliers_tr) => {

                    let repliers_header_option = repliers_tr.as_node().select(".repliers_header").ok().map_or(None, |x| x.last() );

                    let repliers_header = match repliers_header_option {
                        Some(x) => x,
                        None => { return Err("fail to build title and reply_count, reason: 'repliers_header' not found"); }
                    };

                    let divs = match repliers_header.as_node().select("div").ok() {
                        Some(x) => x.collect::<Vec<_>>(),
                        None => { return Err("fail to build title and reply_count, reason: 'divs' not found"); }
                    };

                    let topic_data = divs.iter().enumerate().map(|(index, div)| {
                                             let s_trimmed = div.text_contents().trim().to_string();
                                             if index == 1 {
                                                 let re = Regex::new(r"^(?P<count>\d+)個回應$").expect("fail to build title and reply_count, reason: invalid regex");
                                                 let cap = re.captures(&s_trimmed).expect("fail to build title and reply_count, reason: 'cap' not found");
                                                 // String::from(cap.name("count").unwrap_or("0"))
                                                 cap.name("count").unwrap_or("0").to_string()
                                             } else {
                                                 s_trimmed
                                             }
                                         })
                                         .collect::<Vec<_>>();

                    if topic_data.len() < 2 {
                        Err(&"length of topic_data is invalid.")
                    } else {
                        Ok(
                            (topic_data.get(0).expect("fail to build title and reply_count, reason: 'topic_data' not found").to_string(), // return as title
                            topic_data.get(1).expect("fail to build title and reply_count, reason: 'topic_data' not found").to_string() /* return as reply_count */)
                        )
                    }
                },
                None => Err(&"fail to build title and reply_count, reason: 'repliers_tr' not found")
            }
        },
        Err(e) =>  Err("fail to build title and reply_count, reason: 'repliers_tr' not found")
    };

}

fn parse_show_reply_items(document: &NodeRef) -> Vec<ShowReplyItem> {

    let replies_data = document.select(".repliers tr[userid][username]")
                               .ok().expect("fail to parse show reply items, reason: 'replies_data' not found")
                               .collect::<Vec<_>>();

    replies_data.iter()
                .enumerate()
                .map(|(index, tr)| {

                    let tr_attrs = (&tr.attributes).borrow();
                    let userid = tr_attrs.get("userid").expect("fail to parse show reply item, reason: 'userid' not found");
                    let username = tr_attrs.get("username").expect("fail to parse show reply item, reason: 'userame' not found");

                    let content_elm = tr.as_node()
                                        .select(".repliers_right .ContentGrid")
                                        .ok().expect("fail to parse show reply item, reason: 'content_elm' not found")
                                        .next()
                                        .expect("fail to parse show reply item, reason: 'content_elm' not found"); // first

                    let mut buff = Cursor::new(Vec::new());
                    let serialize_result = content_elm.as_node().serialize(&mut buff);
                    let vec = buff.into_inner();
                    let content = String::from_utf8(vec).expect("fail to parse show reply item, reason: 'content' invalid");

                    let datatime = tr.as_node()
                                     .select(".repliers_right span")
                                     .ok().expect("fail to parse show reply item, reason: 'datatime' not found")
                                     .last()
                                     .expect("fail to parse show reply item, reason: 'datatime' not found")
                                     .text_contents();

                    let mut vec: Vec<NodeType> = Vec::new();

                    vec = recursive(content_elm.as_node());

                    ShowReplyItem {
                        userid: String::from(userid),
                        username: String::from(username),
                        content: String::from(content),
                        body: vec,
                        published_at: String::from(datatime),
                    }
                })
                .collect::<Vec<_>>()
}

fn parse_url_query_item(url_str: &str) -> UrlQueryItem {
    let url = Url::parse(&url_str).expect("fail to parse url query item, reason: invalid url");
    let query = url.query().expect("fail to parse url query item, reason: invalid url query");
    let re = Regex::new(r"(\\?|&)(?P<key>[^&=]+)=(?P<value>[^&]+)").expect("fail to parse url query item, reason: invalid regex");

    let (channel, message) = {

        let mut map = HashMap::new();

        for cap in re.captures_iter(query) {
            let key = cap.name("key").unwrap_or("").to_string();
            let value = cap.name("value").unwrap_or("").to_string();
            map.entry(key).or_insert(value);
        }

        if map.len() < 2 {
            panic!("length of map is invalid.")
        }

        (
            map.get("type").expect("fail to parse url query item, reason: can not get value of 'type' attribute").to_string(),
            map.get("message").expect("fail to parse url query item, reason: can not get value of 'message' attribute").to_string()
        )
    };

    UrlQueryItem {
        channel: String::from(channel),
        message: String::from(message)
    }

}

fn recursive(elm: &NodeRef) -> Vec<NodeType> {

    let mut vec: Vec<NodeType> = Vec::new();

    for (index, child) in elm.children().enumerate() {
        // println!("[{}] => {:?}", index, child);
        let node_data = child.data().clone();

        match node_data {
            NodeData::Element(element_data) => {
                // println!("[{}] => [ELEMENT] => {:?}", index, element_data);
                // println!("[{}] => [ELEMENT] => {:?}", index, child);

                if element_data.name.local.trim().eq("blockquote") {

                    // println!("[{}] => [ELEMENT] => {:?}", index, child.children());
                    let subvec = recursive(&child);
                    let node = NodeType::BlockQuote(BlockQuoteNode { data: subvec });
                    vec.push(node);
                } else if element_data.name.local.trim().eq("br") {
                    let node = NodeType::Br(BrNode {});
                    vec.push(node);
                } else if element_data.name.local.trim().eq("img") {

                    let attrs = (&element_data.attributes).borrow();
                    let url = attrs.get("src").unwrap_or("");
                    let alt = attrs.get("alt").unwrap_or("");
                    let node = NodeType::Image(ImageNode { data: url.to_string(), alt: alt.to_string() });
                    vec.push(node);

                } else {
                    // println!("[{}] => [ELEMENT] => {:?}", index, child);
                    let mut subvec = recursive(&child);
                    vec.append(&mut subvec);
                }
            }
            NodeData::Text(rc) => {
                // println!("[{}] => [TEXT] => {:?}", index, rc);
                let d = rc.clone();
                let b = d.borrow();

                let s = b.trim().to_string();

                if s == "\n" {
                    // s = "\\n".to_string()
                    continue;
                }

                let node = NodeType::Text(TextNode { data: s });
                vec.push(node);
            }
            _ => {}
        }
    }
    vec
}
<|endoftext|>


<commit_before>extern crate chrono;
extern crate toml;

use self::chrono::*;
use constants::*;
use self::toml::Table;
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

pub fn initialize() {
    println!("Initialize ...");

    let dir_name    = WORKING_DIR_NAME;
    let path_to_dir = Path::new(dir_name);
    if path_to_dir.exists() && path_to_dir.is_dir() {
        println!("  OK: \"{}\" directory has already exist.", dir_name);
    } else {
        match fs::create_dir(path_to_dir) {
            Ok(_)    => println!("  OK: Created \"{}\" directory.", dir_name),
            Err(why) => panic!("{:?}", why),
        }
    }

    let path_to_config = path_to_dir.clone().join(CONFIG_FILE_NAME);
    create_setting_file(path_to_config, DEFAULT_CONFIG);

    let path_to_ignore = path_to_dir.clone().join(IGNORE_FILE_NAME);
    let ignore_contents = walk_dir(".").iter()
        .fold(String::new(), |c, f| c + &f + "\n");
    create_setting_file(path_to_ignore, ignore_contents);
}

fn create_setting_file<S: AsRef<str>>(path_to_file: PathBuf, contents: S) {
    let file_name = path_to_file.file_name().and_then(|f| f.to_str()).unwrap();
    if path_to_file.exists() && path_to_file.is_file() {
        println!("  OK: \"{}\" file has already exist.", file_name);
    } else {
        match File::create(&path_to_file) {
            Ok(mut f) => {
                println!("  OK: Created \"{}\" file.", file_name);
                match f.write(contents.as_ref().as_bytes()) {
                    Ok(_)    => {},
                    Err(why) => panic!("{:?}", why),
                }
            },
            Err(why) => panic!("{:?}", why),
        }
    }
}

fn walk_dir<P: AsRef<Path>>(path: P) -> BTreeSet<String> {
    fn walk_dir(path: PathBuf) -> Vec<OsString> {
        let paths = match fs::read_dir(path) {
            Ok(rd)   => rd,
            Err(why) => panic!("{:?}", why),
        };
        paths
            .filter(|de| de.is_ok())
            .flat_map(|de| {
                let path = de.unwrap().path();
                if is_hidden(&path) {
                    Vec::new()
                } else if path.is_file() {
                    vec![path.into_os_string()]
                } else {
                    walk_dir(path)
                }
            })
            .collect::<Vec<OsString>>()
    }

    walk_dir(path.as_ref().to_path_buf()).iter()
        .map(|f| f.clone().into_string())
        .filter(|f| f.is_ok())
        .map(|f| f.unwrap())
        .collect::<BTreeSet<String>>()
}

fn is_hidden(path: &PathBuf) -> bool {
    let file_name = path.file_name().and_then(|f| f.to_str());
    file_name.map_or(false, |f| f.starts_with("."))
}

pub fn set_params() {
    println!("set parameters");
}

pub fn sweep() {
    let now           = Local::now();
    let trash_name    = format!("trash_{}", now.format("%Y-%m-%d"));
    let path_to_trash = Path::new(WORKING_DIR_NAME).join(trash_name);
    if !(path_to_trash.exists() && path_to_trash.is_dir()) {
        match fs::create_dir(path_to_trash) {
            Ok(_)    => {},
            Err(why) => panic!("{:?}", why),
        }
    }

    println!("Sweep ...");

    let config = read_config_file();
    let ignore = read_ignore_file();

    let target_files = walk_dir(".")
        .difference(&ignore)
        .cloned()
        .collect::<Vec<String>>();

    println!("{:?}", config);
    println!("{:?}", target_files);
}

fn read_config_file() -> Table {
    let config_file = Path::new(WORKING_DIR_NAME).join(CONFIG_FILE_NAME);
    let mut f       = match File::open(config_file) {
        Ok(f)    => f,
        Err(why) => panic!("{:?}", why),
    };
    let mut config = String::new();
    match f.read_to_string(&mut config) {
        Ok(_)    => {},
        Err(why) => panic!("{:?}", why),
    };

    match toml::Parser::new(&config).parse() {
        Some(toml) => toml,
        None       => panic!("Error occurs on parsing \"{}\".", CONFIG_FILE_NAME),
    }
}

fn read_ignore_file() -> BTreeSet<String> {
    let ignore_file = Path::new(WORKING_DIR_NAME).join(IGNORE_FILE_NAME);
    let mut f       = match File::open(ignore_file) {
        Ok(f)    => f,
        Err(why) => panic!("{:?}", why),
    };
    let mut contents = String::new();
    match f.read_to_string(&mut contents) {
        Ok(_)    => {},
        Err(why) => panic!("{:?}", why),
    }
    contents
        .lines()
        .map(|l| l.trim().to_string())
        .collect::<BTreeSet<String>>()
}

pub fn burn() {
    println!("burn");
}

pub fn register_with_cron() {
    println!("register");
}

pub fn unregister_cron() {
    println!("unregister");
}

pub fn destroy() {
    println!("Destroy ...");

    match fs::remove_dir_all(WORKING_DIR_NAME) {
        Ok(_)    => println!("  OK: Removed \"{}\" directory.", WORKING_DIR_NAME),
        Err(why) => panic!("{:?}", why),
    }
}
<commit_msg>Remove read_config_file()<commit_after>extern crate chrono;
extern crate toml;

use self::chrono::*;
use constants::*;
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

pub fn initialize() {
    println!("Initialize ...");

    let dir_name    = WORKING_DIR_NAME;
    let path_to_dir = Path::new(dir_name);
    if path_to_dir.exists() && path_to_dir.is_dir() {
        println!("  OK: \"{}\" directory has already exist.", dir_name);
    } else {
        match fs::create_dir(path_to_dir) {
            Ok(_)    => println!("  OK: Created \"{}\" directory.", dir_name),
            Err(why) => panic!("{:?}", why),
        }
    }

    let path_to_config = path_to_dir.clone().join(CONFIG_FILE_NAME);
    create_setting_file(path_to_config, DEFAULT_CONFIG);

    let path_to_ignore = path_to_dir.clone().join(IGNORE_FILE_NAME);
    let ignore_contents = walk_dir(".").iter()
        .fold(String::new(), |c, f| c + &f + "\n");
    create_setting_file(path_to_ignore, ignore_contents);
}

fn create_setting_file<S: AsRef<str>>(path_to_file: PathBuf, contents: S) {
    let file_name = path_to_file.file_name().and_then(|f| f.to_str()).unwrap();
    if path_to_file.exists() && path_to_file.is_file() {
        println!("  OK: \"{}\" file has already exist.", file_name);
    } else {
        match File::create(&path_to_file) {
            Ok(mut f) => {
                println!("  OK: Created \"{}\" file.", file_name);
                match f.write(contents.as_ref().as_bytes()) {
                    Ok(_)    => {},
                    Err(why) => panic!("{:?}", why),
                }
            },
            Err(why) => panic!("{:?}", why),
        }
    }
}

fn walk_dir<P: AsRef<Path>>(path: P) -> BTreeSet<String> {
    fn walk_dir(path: PathBuf) -> Vec<OsString> {
        let paths = match fs::read_dir(path) {
            Ok(rd)   => rd,
            Err(why) => panic!("{:?}", why),
        };
        paths
            .filter(|de| de.is_ok())
            .flat_map(|de| {
                let path = de.unwrap().path();
                if is_hidden(&path) {
                    Vec::new()
                } else if path.is_file() {
                    vec![path.into_os_string()]
                } else {
                    walk_dir(path)
                }
            })
            .collect::<Vec<OsString>>()
    }

    walk_dir(path.as_ref().to_path_buf()).iter()
        .map(|f| f.clone().into_string())
        .filter(|f| f.is_ok())
        .map(|f| f.unwrap())
        .collect::<BTreeSet<String>>()
}

fn is_hidden(path: &PathBuf) -> bool {
    let file_name = path.file_name().and_then(|f| f.to_str());
    file_name.map_or(false, |f| f.starts_with("."))
}

pub fn set_params() {
    println!("set parameters");
}

pub fn sweep() {
    let now           = Local::now();
    let trash_name    = format!("trash_{}", now.format("%Y-%m-%d"));
    let path_to_trash = Path::new(WORKING_DIR_NAME).join(trash_name);
    if !(path_to_trash.exists() && path_to_trash.is_dir()) {
        match fs::create_dir(path_to_trash) {
            Ok(_)    => {},
            Err(why) => panic!("{:?}", why),
        }
    }

    println!("Sweep ...");

    let ignore = read_ignore_file();

    let target_files = walk_dir(".")
        .difference(&ignore)
        .cloned()
        .collect::<Vec<String>>();

    println!("{:?}", target_files);
}

fn read_ignore_file() -> BTreeSet<String> {
    let ignore_file = Path::new(WORKING_DIR_NAME).join(IGNORE_FILE_NAME);
    let mut f       = match File::open(ignore_file) {
        Ok(f)    => f,
        Err(why) => panic!("{:?}", why),
    };
    let mut contents = String::new();
    match f.read_to_string(&mut contents) {
        Ok(_)    => {},
        Err(why) => panic!("{:?}", why),
    }
    contents
        .lines()
        .map(|l| l.trim().to_string())
        .collect::<BTreeSet<String>>()
}

pub fn burn() {
    println!("burn");
}

pub fn register_with_cron() {
    println!("register");
}

pub fn unregister_cron() {
    println!("unregister");
}

pub fn destroy() {
    println!("Destroy ...");

    match fs::remove_dir_all(WORKING_DIR_NAME) {
        Ok(_)    => println!("  OK: Removed \"{}\" directory.", WORKING_DIR_NAME),
        Err(why) => panic!("{:?}", why),
    }
}
<|endoftext|>