<commit_before>extern crate argparse;
extern crate libc;
extern crate libmount;
extern crate lithos;
extern crate nix;
extern crate quire;
extern crate signal;
extern crate syslog;
extern crate time;
extern crate unshare;
#[macro_use] extern crate log;

use std::env;
use std::str::FromStr;
use std::io::{stderr, Write};
use std::fs::OpenOptions;
use std::path::{Path};
use std::time::{Instant, Duration};
use std::thread::sleep;
use std::process::exit;

use libmount::BindMount;
use quire::{parse_config, Options as COptions};
use signal::trap::Trap;
use unshare::{Command, Stdio, reap_zombies};
use nix::sys::signal::Signal;
use nix::sys::signal::{SIGINT, SIGTERM, SIGCHLD};

use lithos::cgroup;
use lithos::utils::{check_mapping, in_mapping, change_root};
use lithos::range::in_range;
use lithos::master_config::MasterConfig;
use lithos::sandbox_config::SandboxConfig;
use lithos::container_config::{ContainerConfig, Variables};
use lithos::container_config::ContainerKind::Daemon;
use lithos::setup::{setup_filesystem, read_local_config, prepare_state_dir};
use lithos::setup::{init_logging};
use lithos::mount::{unmount, mount_private, mount_ro_recursive};
use lithos::limits::{set_fileno_limit};
use lithos_knot_options::Options;

mod lithos_knot_options;

struct SignalIter<'a> {
    trap: &'a mut Trap,
    interrupt: bool,
}

impl<'a> SignalIter<'a> {
    fn new(trap: &mut Trap) -> SignalIter {
        SignalIter {
            trap: trap,
            interrupt: false,
        }
    }
    fn interrupt(&mut self) {
        self.interrupt = true;
    }
}

impl<'a> Iterator for SignalIter<'a> {
    type Item = Signal;
    fn next(&mut self) -> Option<Signal> {
        if self.interrupt {
            return self.trap.wait(Instant::now());
        } else {
            return self.trap.next();
        }
    }
}

fn run(options: Options) -> Result<i32, String>
{
    let master: MasterConfig = try!(parse_config(&options.master_config,
        &MasterConfig::validator(), &COptions::default())
        .map_err(|e| format!("Error reading master config: {}", e)));
    let sandbox_name = options.name[..].splitn(2, '/').next().unwrap();
    let sandbox: SandboxConfig = try!(parse_config(
        &options.master_config.parent().unwrap()
         .join(&master.sandboxes_dir).join(sandbox_name.to_string() + ".yaml"),
        &SandboxConfig::validator(), &COptions::default())
        .map_err(|e| format!("Error reading sandbox config: {}", e)));

    let log_file;
    if let Some(ref fname) = sandbox.log_file {
        log_file = master.default_log_dir.join(fname);
    } else {
        log_file = master.default_log_dir.join(format!("{}.log", sandbox_name));
    }
    try!(init_logging(&master, &log_file,
        &format!("{}-{}", master.syslog_app_name, sandbox_name),
        options.log_stderr,
        options.log_level
            .or(sandbox.log_level.as_ref()
                .and_then(|x| FromStr::from_str(&x).ok()))
            .or_else(|| FromStr::from_str(&master.log_level).ok())
            .unwrap_or(log::LogLevel::Warn)));

    let stderr_path = master.stdio_log_dir
        .join(format!("{}.log", sandbox_name));
    let stderr_file = try!(OpenOptions::new()
                .create(true).append(true).write(true).open(&stderr_path)
                .map_err(|e| format!(
                    "Error opening stderr file {:?}: {}", stderr_path, e)));

    try!(mount_private(&Path::new("/")));
    let image_path = sandbox.image_dir.join(&options.config.image);
    let mount_dir = master.runtime_dir.join(&master.mount_dir);
    try!(BindMount::new(&image_path, &mount_dir).mount()
        .map_err(|e| e.to_string()));
    try!(mount_ro_recursive(&mount_dir));

    let container: ContainerConfig;
    container = try!(read_local_config(&mount_dir, &options.config));
    if !container.kind.matches(options.config.kind) {
        return Err(format!("Container type mismatch {:?} != {:?}",
              container.kind, options.config.kind));
    }
    let local = container.instantiate(&Variables {
        user_vars: &options.config.variables,
        lithos_name: &options.name,
        lithos_config_filename: &options.config.config,
    }).map_err(|e| format!("Variable substitution error: {}", e.join("; ")))?;

    if local.uid_map.len() > 0 {
        if !in_mapping(&local.uid_map, local.user_id) {
            return Err(format!("User is not in mapped range (uid: {})",
                local.user_id));
        }
    } else {
        if !in_range(&sandbox.allow_users, local.user_id) {
            return Err(format!("User is not in allowed range (uid: {})",
                local.user_id));
        }
    }
    if local.gid_map.len() > 0 {
        if !in_mapping(&local.gid_map, local.group_id) {
            return Err(format!("Group is not in mapped range (gid: {})",
                local.user_id));
        }
    } else {
        if !in_range(&sandbox.allow_groups, local.group_id) {
            return Err(format!("Group is not in allowed range (gid: {})",
                local.group_id));
        }
    }
    if !check_mapping(&sandbox.allow_users, &local.uid_map) {
        return Err("Bad uid mapping (probably doesn't match allow_users)"
            .to_string());
    }
    if !check_mapping(&sandbox.allow_groups, &local.gid_map) {
        return Err("Bad gid mapping (probably doesn't match allow_groups)"
            .to_string());
    }

    info!("[{}] Starting container", options.name);

    let state_dir = &master.runtime_dir.join(&master.state_dir)
        .join(&options.name);
    try!(prepare_state_dir(state_dir, &local, &sandbox));
    try!(setup_filesystem(&master, &sandbox, &local, state_dir));
    if let Some(cgroup_parent) = master.cgroup_name {
        // Warning setting cgroup relative to it's own cgroup may not work
        // if we ever want to restart lithos_knot in-place
        let cgroups = try!(cgroup::ensure_in_group(
            &(cgroup_parent + "/" +
              &options.name.replace("/", ":") + ".scope"),
            &master.cgroup_controllers));
        cgroups.set_value(cgroup::Controller::Memory,
            "memory.limit_in_bytes",
            &format!("{}", local.memory_limit))
            .map_err(|e| error!("Error setting cgroup limit: {}", e)).ok();
        cgroups.set_value(cgroup::Controller::Cpu,
                "cpu.shares",
                &format!("{}", local.cpu_shares))
            .map_err(|e| error!("Error setting cgroup limit: {}", e)).ok();
    }

    let mount_dir = master.runtime_dir.join(&master.mount_dir);
    try!(change_root(&mount_dir, &mount_dir.join("tmp")));
    try!(unmount(Path::new("/tmp")));

    try!(set_fileno_limit(local.fileno_limit)
        .map_err(|e| format!("Error setting file limit: {}", e)));

    let mut cmd = Command::new(&local.executable);
    cmd.uid(local.user_id);
    cmd.gid(local.group_id);
    cmd.current_dir(&local.workdir);

    // Should we propagate TERM?
    cmd.env_clear();
    cmd.env("TERM", env::var("TERM").unwrap_or("dumb".to_string()));
    for (k, v) in local.environ.iter() {
        cmd.env(k, v);
    }
    cmd.env("LITHOS_NAME", &options.name);
    cmd.env("LITHOS_CONFIG", &options.config.config);

    cmd.args(&local.arguments);
    cmd.args(&options.args);
    if sandbox.uid_map.len() > 0 || sandbox.gid_map.len() > 0 {
        cmd.set_id_maps(
            sandbox.uid_map.iter().map(|u| unshare::UidMap {
                inside_uid: u.inside,
                outside_uid: u.outside,
                count: u.count,
            }).collect(),
            sandbox.gid_map.iter().map(|g| unshare::GidMap {
                inside_gid: g.inside,
                outside_gid: g.outside,
                count: g.count,
            }).collect());
    } else if local.uid_map.len() > 0 || local.gid_map.len() > 0 {
        cmd.set_id_maps(
            local.uid_map.iter().map(|u| unshare::UidMap {
                inside_uid: u.inside,
                outside_uid: u.outside,
                count: u.count,
            }).collect(),
            local.gid_map.iter().map(|g| unshare::GidMap {
                inside_gid: g.inside,
                outside_gid: g.outside,
                count: g.count,
            }).collect());
    }
    let rtimeo = Duration::from_millis((local.restart_timeout*1000.0) as u64);

    let mut trap = Trap::trap(&[SIGINT, SIGTERM, SIGCHLD]);
    let mut should_exit = local.kind != Daemon || !local.restart_process_only;
    // only successful code on SIGTERM
    let mut exit_code = 2;
    loop {
        let start = Instant::now();

        if !local.interactive {
            if let Some(ref path) = local.stdout_stderr_file {
                // Reopen file at each start
                let f = try!(OpenOptions::new()
                    .create(true).append(true).write(true).open(path)
                    .map_err(|e| format!(
                        "Error opening output file {:?}: {}", path, e)));
                cmd.stdout(try!(Stdio::dup_file(&f)
                    .map_err(|e| format!(
                        "Duplicating file descriptor: {}", e))));
                cmd.stderr(Stdio::from_file(f));
            } else {
                // Can't reopen, because file is outside of container
                cmd.stdout(try!(Stdio::dup_file(&stderr_file)
                    .map_err(|e| format!(
                        "Duplicating file descriptor: {}", e))));
                cmd.stderr(try!(Stdio::dup_file(&stderr_file)
                    .map_err(|e| format!(
                        "Duplicating file descriptor: {}", e))));
            };
        }

        let child = try!(cmd.spawn().map_err(|e|
            format!("Error running {:?}: {}", options.name, e)));

        let mut iter = SignalIter::new(&mut trap);
        while let Some(signal) = iter.next() {
            match signal {
                SIGINT => {
                    // SIGINT is usually a Ctrl+C so it's sent to whole
                    // process group, so we don't need to do anything special
                    debug!("Received SIGINT. Waiting process to stop..");
                    should_exit = true;
                }
                SIGTERM => {
                    // SIGTERM is usually sent to a specific process so we
                    // forward it to children
                    debug!("Received SIGTERM signal, propagating");
                    should_exit = true;
                    exit_code = 0;
                    child.signal(SIGTERM).ok();
                }
                SIGCHLD => {
                    for (pid, status) in reap_zombies() {
                        if pid == child.pid() {
                            if status.signal() == Some(SIGTERM as i32) {
                                exit_code = 0;
                            }
                            let uptime = Instant::now() - start;
                            error!("Process {:?} {}, uptime {}s",
                                options.name, status, uptime.as_secs());
                            iter.interrupt();
                        }
                    }
                }
                _ => unreachable!(),
            }
        }

        if should_exit {
            break;
        }
        let left = rtimeo - (Instant::now() - start);
        if left > Duration::new(0, 0) {
            sleep(left);
        }
    }

    Ok(exit_code)
}

fn main() {

    let options = match Options::parse_args() {
        Ok(options) => options,
        Err(x) => {
            exit(x);
        }
    };
    match run(options)
    {
        Ok(code) => {
            exit(code);
        }
        Err(e) => {
            write!(&mut stderr(), "Fatal error: {}\n", e).ok();
            error!("Fatal error: {}", e);
            exit(1);
        }
    }
}
<commit_msg>Warn-level logging at start of command in container<commit_after>extern crate argparse;
extern crate libc;
extern crate libmount;
extern crate lithos;
extern crate nix;
extern crate quire;
extern crate signal;
extern crate syslog;
extern crate time;
extern crate unshare;
#[macro_use] extern crate log;

use std::env;
use std::str::FromStr;
use std::io::{stderr, Write};
use std::fs::OpenOptions;
use std::path::{Path};
use std::time::{Instant, Duration};
use std::thread::sleep;
use std::process::exit;

use libmount::BindMount;
use quire::{parse_config, Options as COptions};
use signal::trap::Trap;
use unshare::{Command, Stdio, reap_zombies};
use nix::sys::signal::Signal;
use nix::sys::signal::{SIGINT, SIGTERM, SIGCHLD};

use lithos::cgroup;
use lithos::utils::{check_mapping, in_mapping, change_root};
use lithos::range::in_range;
use lithos::master_config::MasterConfig;
use lithos::sandbox_config::SandboxConfig;
use lithos::container_config::{ContainerConfig, Variables};
use lithos::container_config::ContainerKind::Daemon;
use lithos::setup::{setup_filesystem, read_local_config, prepare_state_dir};
use lithos::setup::{init_logging};
use lithos::mount::{unmount, mount_private, mount_ro_recursive};
use lithos::limits::{set_fileno_limit};
use lithos_knot_options::Options;

mod lithos_knot_options;

struct SignalIter<'a> {
    trap: &'a mut Trap,
    interrupt: bool,
}

impl<'a> SignalIter<'a> {
    fn new(trap: &mut Trap) -> SignalIter {
        SignalIter {
            trap: trap,
            interrupt: false,
        }
    }
    fn interrupt(&mut self) {
        self.interrupt = true;
    }
}

impl<'a> Iterator for SignalIter<'a> {
    type Item = Signal;
    fn next(&mut self) -> Option<Signal> {
        if self.interrupt {
            return self.trap.wait(Instant::now());
        } else {
            return self.trap.next();
        }
    }
}

fn run(options: Options) -> Result<i32, String>
{
    let master: MasterConfig = try!(parse_config(&options.master_config,
        &MasterConfig::validator(), &COptions::default())
        .map_err(|e| format!("Error reading master config: {}", e)));
    let sandbox_name = options.name[..].splitn(2, '/').next().unwrap();
    let sandbox: SandboxConfig = try!(parse_config(
        &options.master_config.parent().unwrap()
         .join(&master.sandboxes_dir).join(sandbox_name.to_string() + ".yaml"),
        &SandboxConfig::validator(), &COptions::default())
        .map_err(|e| format!("Error reading sandbox config: {}", e)));

    let log_file;
    if let Some(ref fname) = sandbox.log_file {
        log_file = master.default_log_dir.join(fname);
    } else {
        log_file = master.default_log_dir.join(format!("{}.log", sandbox_name));
    }
    try!(init_logging(&master, &log_file,
        &format!("{}-{}", master.syslog_app_name, sandbox_name),
        options.log_stderr,
        options.log_level
            .or(sandbox.log_level.as_ref()
                .and_then(|x| FromStr::from_str(&x).ok()))
            .or_else(|| FromStr::from_str(&master.log_level).ok())
            .unwrap_or(log::LogLevel::Warn)));

    let stderr_path = master.stdio_log_dir
        .join(format!("{}.log", sandbox_name));
    let stderr_file = try!(OpenOptions::new()
                .create(true).append(true).write(true).open(&stderr_path)
                .map_err(|e| format!(
                    "Error opening stderr file {:?}: {}", stderr_path, e)));

    try!(mount_private(&Path::new("/")));
    let image_path = sandbox.image_dir.join(&options.config.image);
    let mount_dir = master.runtime_dir.join(&master.mount_dir);
    try!(BindMount::new(&image_path, &mount_dir).mount()
        .map_err(|e| e.to_string()));
    try!(mount_ro_recursive(&mount_dir));

    let container: ContainerConfig;
    container = try!(read_local_config(&mount_dir, &options.config));
    if !container.kind.matches(options.config.kind) {
        return Err(format!("Container type mismatch {:?} != {:?}",
              container.kind, options.config.kind));
    }
    let local = container.instantiate(&Variables {
        user_vars: &options.config.variables,
        lithos_name: &options.name,
        lithos_config_filename: &options.config.config,
    }).map_err(|e| format!("Variable substitution error: {}", e.join("; ")))?;

    if local.uid_map.len() > 0 {
        if !in_mapping(&local.uid_map, local.user_id) {
            return Err(format!("User is not in mapped range (uid: {})",
                local.user_id));
        }
    } else {
        if !in_range(&sandbox.allow_users, local.user_id) {
            return Err(format!("User is not in allowed range (uid: {})",
                local.user_id));
        }
    }
    if local.gid_map.len() > 0 {
        if !in_mapping(&local.gid_map, local.group_id) {
            return Err(format!("Group is not in mapped range (gid: {})",
                local.user_id));
        }
    } else {
        if !in_range(&sandbox.allow_groups, local.group_id) {
            return Err(format!("Group is not in allowed range (gid: {})",
                local.group_id));
        }
    }
    if !check_mapping(&sandbox.allow_users, &local.uid_map) {
        return Err("Bad uid mapping (probably doesn't match allow_users)"
            .to_string());
    }
    if !check_mapping(&sandbox.allow_groups, &local.gid_map) {
        return Err("Bad gid mapping (probably doesn't match allow_groups)"
            .to_string());
    }

    info!("[{}] Starting container", options.name);

    let state_dir = &master.runtime_dir.join(&master.state_dir)
        .join(&options.name);
    try!(prepare_state_dir(state_dir, &local, &sandbox));
    try!(setup_filesystem(&master, &sandbox, &local, state_dir));
    if let Some(cgroup_parent) = master.cgroup_name {
        // Warning setting cgroup relative to it's own cgroup may not work
        // if we ever want to restart lithos_knot in-place
        let cgroups = try!(cgroup::ensure_in_group(
            &(cgroup_parent + "/" +
              &options.name.replace("/", ":") + ".scope"),
            &master.cgroup_controllers));
        cgroups.set_value(cgroup::Controller::Memory,
            "memory.limit_in_bytes",
            &format!("{}", local.memory_limit))
            .map_err(|e| error!("Error setting cgroup limit: {}", e)).ok();
        cgroups.set_value(cgroup::Controller::Cpu,
                "cpu.shares",
                &format!("{}", local.cpu_shares))
            .map_err(|e| error!("Error setting cgroup limit: {}", e)).ok();
    }

    let mount_dir = master.runtime_dir.join(&master.mount_dir);
    try!(change_root(&mount_dir, &mount_dir.join("tmp")));
    try!(unmount(Path::new("/tmp")));

    try!(set_fileno_limit(local.fileno_limit)
        .map_err(|e| format!("Error setting file limit: {}", e)));

    let mut cmd = Command::new(&local.executable);
    cmd.uid(local.user_id);
    cmd.gid(local.group_id);
    cmd.current_dir(&local.workdir);

    // Should we propagate TERM?
    cmd.env_clear();
    cmd.env("TERM", env::var("TERM").unwrap_or("dumb".to_string()));
    for (k, v) in local.environ.iter() {
        cmd.env(k, v);
    }
    cmd.env("LITHOS_NAME", &options.name);
    cmd.env("LITHOS_CONFIG", &options.config.config);

    cmd.args(&local.arguments);
    cmd.args(&options.args);
    if sandbox.uid_map.len() > 0 || sandbox.gid_map.len() > 0 {
        cmd.set_id_maps(
            sandbox.uid_map.iter().map(|u| unshare::UidMap {
                inside_uid: u.inside,
                outside_uid: u.outside,
                count: u.count,
            }).collect(),
            sandbox.gid_map.iter().map(|g| unshare::GidMap {
                inside_gid: g.inside,
                outside_gid: g.outside,
                count: g.count,
            }).collect());
    } else if local.uid_map.len() > 0 || local.gid_map.len() > 0 {
        cmd.set_id_maps(
            local.uid_map.iter().map(|u| unshare::UidMap {
                inside_uid: u.inside,
                outside_uid: u.outside,
                count: u.count,
            }).collect(),
            local.gid_map.iter().map(|g| unshare::GidMap {
                inside_gid: g.inside,
                outside_gid: g.outside,
                count: g.count,
            }).collect());
    }
    let rtimeo = Duration::from_millis((local.restart_timeout*1000.0) as u64);

    let mut trap = Trap::trap(&[SIGINT, SIGTERM, SIGCHLD]);
    let mut should_exit = local.kind != Daemon || !local.restart_process_only;
    // only successful code on SIGTERM
    let mut exit_code = 2;
    loop {
        let start = Instant::now();

        if !local.interactive {
            if let Some(ref path) = local.stdout_stderr_file {
                // Reopen file at each start
                let f = try!(OpenOptions::new()
                    .create(true).append(true).write(true).open(path)
                    .map_err(|e| format!(
                        "Error opening output file {:?}: {}", path, e)));
                cmd.stdout(try!(Stdio::dup_file(&f)
                    .map_err(|e| format!(
                        "Duplicating file descriptor: {}", e))));
                cmd.stderr(Stdio::from_file(f));
            } else {
                // Can't reopen, because file is outside of container
                cmd.stdout(try!(Stdio::dup_file(&stderr_file)
                    .map_err(|e| format!(
                        "Duplicating file descriptor: {}", e))));
                cmd.stderr(try!(Stdio::dup_file(&stderr_file)
                    .map_err(|e| format!(
                        "Duplicating file descriptor: {}", e))));
            };
        }

        warn!("Starting {:?}: {:?}", options.name, cmd);
        let child = try!(cmd.spawn().map_err(|e|
            format!("Error running {:?}: {}", options.name, e)));

        let mut iter = SignalIter::new(&mut trap);
        while let Some(signal) = iter.next() {
            match signal {
                SIGINT => {
                    // SIGINT is usually a Ctrl+C so it's sent to whole
                    // process group, so we don't need to do anything special
                    debug!("Received SIGINT. Waiting process to stop..");
                    should_exit = true;
                }
                SIGTERM => {
                    // SIGTERM is usually sent to a specific process so we
                    // forward it to children
                    debug!("Received SIGTERM signal, propagating");
                    should_exit = true;
                    exit_code = 0;
                    child.signal(SIGTERM).ok();
                }
                SIGCHLD => {
                    for (pid, status) in reap_zombies() {
                        if pid == child.pid() {
                            if status.signal() == Some(SIGTERM as i32) {
                                exit_code = 0;
                            }
                            let uptime = Instant::now() - start;
                            error!("Process {:?} {}, uptime {}s",
                                options.name, status, uptime.as_secs());
                            iter.interrupt();
                        }
                    }
                }
                _ => unreachable!(),
            }
        }

        if should_exit {
            break;
        }
        let left = rtimeo - (Instant::now() - start);
        if left > Duration::new(0, 0) {
            sleep(left);
        }
    }

    Ok(exit_code)
}

fn main() {

    let options = match Options::parse_args() {
        Ok(options) => options,
        Err(x) => {
            exit(x);
        }
    };
    match run(options)
    {
        Ok(code) => {
            exit(code);
        }
        Err(e) => {
            write!(&mut stderr(), "Fatal error: {}\n", e).ok();
            error!("Fatal error: {}", e);
            exit(1);
        }
    }
}
<|endoftext|>


<commit_before>use entities::*;
use super::geo::Coordinate;

pub trait InBBox {
    fn in_bbox(&self, bb: &[Coordinate]) -> bool;
}

pub enum Combination {
    And,
    Or
}

impl InBBox for Entry {
    fn in_bbox(&self, bb: &[Coordinate]) -> bool {
        // TODO: either return a Result or create a bounding box struct
        if bb.len() != 2 {
            warn!("invalid bounding box: {:?}", bb);
            return false;
        }
        bb.len() == 2 && self.lat >= bb[0].lat && self.lng >= bb[0].lng && self.lat <= bb[1].lat &&
        self.lng <= bb[1].lng
    }
}

pub fn entries_by_category_ids<'a>(ids: &'a [String]) -> Box<Fn(&&Entry) -> bool + 'a> {
    Box::new(move |e| ids.iter().any(|c| e.categories.iter().any(|x| x == c)))
}

pub fn triple_by_subject<'a>(o_id : ObjectId) -> Box<Fn(&&Triple) -> bool + 'a> {
    Box::new(move |triple| o_id == triple.subject)
}

pub fn entries_by_tags<'a>(tags: &'a [String], triples: &'a [Triple], combination: Combination) -> Box<Fn(&&Entry) -> bool + 'a> {

    let triples : Vec<(&String, &String)> = triples
        .into_iter()
        .filter_map(|x| match *x {
            Triple {
                subject   : ObjectId::Entry(ref e_id),
                predicate : Relation::IsTaggedWith,
                object    : ObjectId::Tag(ref t_id)
            } => Some((e_id,t_id)),
            _ => None
        })
        .collect();

    match combination {
        Combination::Or => {
            Box::new(move |entry|
                tags.iter()
                    .any(|tag| triples.iter().any(|t| *t.0 == entry.id && t.1 == tag))
            )
        },
        Combination::And => {
            Box::new(move |entry| {
                let e_tags : Vec<&String> = triples
                    .iter()
                    .filter(|t| *t.0 == entry.id)
                    .map(|t|t.1)
                    .collect();
                    tags.iter().all(|tag|e_tags.iter().any(|t| *t == tag))
                }
            )
        }
    }
}

pub fn entries_by_search_text<'a>(text: &'a str) -> Box<Fn(&&Entry) -> bool + 'a> {
    let words = to_words(text);
    Box::new(move |e| {
        words.iter().any(|word| {
            e.title.to_lowercase().contains(word) || e.description.to_lowercase().contains(word)
        })
    })
}

fn to_words(txt: &str) -> Vec<String> {
    txt.to_lowercase().split(',').map(|x| x.to_string()).collect()
}

#[cfg(test)]
mod tests {

    use super::*;
    use business::builder::*;

    #[test]
    fn is_in_bounding_box() {
        let bb = vec![Coordinate {
                          lat: -10.0,
                          lng: -10.0,
                      },
                      Coordinate {
                          lat: 10.0,
                          lng: 10.0,
                      }];
        let e = Entry::build()
            .title("foo")
            .description("bar")
            .lat(5.0)
            .lng(5.0)
            .finish();
        assert_eq!(e.in_bbox(&bb), true);
        let e = Entry::build()
            .title("foo")
            .description("bar")
            .lat(10.1)
            .lng(10.0)
            .finish();
        assert_eq!(e.in_bbox(&bb), false);
    }

    #[test]
    fn is_in_invalid_bounding_box() {
        let bb = vec![Coordinate {
                          lat: 10.0,
                          lng: 10.0,
                      }];
        let e = Entry::build()
            .lat(5.0)
            .lng(5.0)
            .finish();
        assert_eq!(e.in_bbox(&bb), false);
    }

    #[test]
    fn filter_by_bounding_box() {
        let bb = vec![Coordinate {
                          lat: -10.0,
                          lng: -10.0,
                      },
                      Coordinate {
                          lat: 10.0,
                          lng: 10.0,
                      }];
        let entries = vec![
            Entry::build().lat(5.0).lng(5.0).finish(),
            Entry::build().lat(-5.0).lng(5.0).finish(),
            Entry::build().lat(10.0).lng(10.1).finish(),
        ];
        assert_eq!(entries.iter().filter(|&x| x.in_bbox(&bb)).collect::<Vec<&Entry>>().len(),
                   2);
    }

    #[test]
    fn filter_by_category() {
        let entries = vec![
            Entry::build().categories(vec!["a"]).finish(),
            Entry::build().categories(vec!["c"]).finish(),
            Entry::build().categories(vec!["b","a"]).finish(),
        ];
        let ab = vec!["a".into(), "b".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_category_ids(&ab)).collect();
        assert_eq!(x.len(), 2);
        let b = vec!["b".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_category_ids(&b)).collect();
        assert_eq!(x.len(), 1);
        let c = vec!["c".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_category_ids(&c)).collect();
        assert_eq!(x.len(), 1);
    }

    #[test]
    fn search_entries_by_title() {
        let entries = vec![
            Entry::build().title("a title").description("x").finish(),
            Entry::build().title("not so interesting").description("y").finish(),
        ];
        let filter = entries_by_search_text("a");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].title, "a title");
        let filter = entries_by_search_text("ti");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
    }

    #[test]
    fn search_entries_by_description() {
        let entries = vec![
            Entry::build().title("a").description("x").finish(),
            Entry::build().title("b").description("y").finish(),
            Entry::build().title("c").description("x").finish(),
        ];
        let filter = entries_by_search_text("x");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
    }

    #[test]
    fn search_with_multiple_words() {
        let entries = vec![
            Entry::build().title("SoLaWi").description("mit gemüse").finish(),
            Entry::build().title("csa").description("This is a great csa").finish(),
            Entry::build().title("solawi").description("Das ist eine tolle solawi").finish(),
        ];
        let filter = entries_by_search_text("csa,toll");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
        let filter = entries_by_search_text("great,to,gemü");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 3);
    }

    #[test]
    fn search_and_ignore_capitalisation() {
        let e0 = Entry::build().title("Eintrag").description("Hallo! Ein EinTrag").finish();
        let e1 = Entry::build().title("Ein trag").description("foo").finish();
        let e2 = Entry::build().title("CSA").description("cool vegetables").finish();
        let entries = vec![&e0, &e1, &e2];

        let filter = entries_by_search_text("Foo");
        let x: Vec<&Entry> = entries.iter().cloned().filter(&*filter).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id, e1.id);

        let filter = entries_by_search_text("trag");
        let x: Vec<&Entry> = entries.iter().cloned().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
        assert_eq!(x[0].id, e0.id);
        assert_eq!(x[1].id, e1.id);

        let filter = entries_by_search_text("csa");
        let x: Vec<&Entry> = entries.iter().cloned().filter(&*filter).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id, e2.id);
    }

    #[test]
    fn filter_by_tags() {
        let entries = vec![
            Entry::build().id("a").finish(),
            Entry::build().id("b").finish(),
            Entry::build().id("c").finish(),
        ];
        let tags = vec!["csa".into()];
        let no_triples = vec![];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&no_triples, Combination::Or)).collect();
        assert_eq!(x.len(), 0);
        let triples = vec![
            Triple{ subject: ObjectId::Entry("b".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("csa".into())},
            Triple{ subject: ObjectId::Entry("c".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("foo".into())}
        ];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&triples, Combination::Or)).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id,"b");
        let tags = vec!["csa".into(),"foo".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&triples, Combination::Or)).collect();
        assert_eq!(x.len(), 2);
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&triples, Combination::And)).collect();
        assert_eq!(x.len(), 0);
    }
}
<commit_msg>started to change search to search key or tag<commit_after>use entities::*;
use super::geo::Coordinate;

pub trait InBBox {
    fn in_bbox(&self, bb: &[Coordinate]) -> bool;
}

pub enum Combination {
    And,
    Or
}

impl InBBox for Entry {
    fn in_bbox(&self, bb: &[Coordinate]) -> bool {
        // TODO: either return a Result or create a bounding box struct
        if bb.len() != 2 {
            warn!("invalid bounding box: {:?}", bb);
            return false;
        }
        bb.len() == 2 && self.lat >= bb[0].lat && self.lng >= bb[0].lng && self.lat <= bb[1].lat &&
        self.lng <= bb[1].lng
    }
}

pub fn entries_by_category_ids<'a>(ids: &'a [String]) -> Box<Fn(&&Entry) -> bool + 'a> {
    Box::new(move |e| ids.iter().any(|c| e.categories.iter().any(|x| x == c)))
}

pub fn triple_by_subject<'a>(o_id : ObjectId) -> Box<Fn(&&Triple) -> bool + 'a> {
    Box::new(move |triple| o_id == triple.subject)
}

pub fn entries_by_tags<'a>(tags: &'a [String], triples: &'a [Triple], combination: Combination) -> Box<Fn(&&Entry) -> bool + 'a> {

    let triples : Vec<(&String, &String)> = triples
        .into_iter()
        .filter_map(|x| match *x {
            Triple {
                subject   : ObjectId::Entry(ref e_id),
                predicate : Relation::IsTaggedWith,
                object    : ObjectId::Tag(ref t_id)
            } => Some((e_id,t_id)),
            _ => None
        })
        .collect();

    match combination {
        Combination::Or => {
            Box::new(move |entry|
                tags.iter()
                    .any(|tag| triples.iter().any(|t| *t.0 == entry.id && t.1 == tag))
            )
        },
        Combination::And => {
            Box::new(move |entry| {
                let e_tags : Vec<&String> = triples
                    .iter()
                    .filter(|t| *t.0 == entry.id)
                    .map(|t|t.1)
                    .collect();
                    tags.iter().all(|tag|e_tags.iter().any(|t| *t == tag))
                }
            )
        }
    }
}

pub fn entries_by_search_text<'a>(text: &'a str) -> Box<Fn(&&Entry) -> bool + 'a> {
    let words = to_words(text);
    Box::new(move |e| {
        words.iter().any(|word| {
            e.title.to_lowercase().contains(word) || e.description.to_lowercase().contains(word)
        })
    })
}

pub fn entries_by_tags_or_search_text<'a>(text: &'a str, tags: &'a [String], triples: &'a [Triple]) -> Box<Fn(&&Entry) -> bool + 'a> {
    let tag_triples : Vec<(&String, &String)> = triples
        .into_iter()
        .filter_map(|x| match *x {
            Triple {
                subject   : ObjectId::Entry(ref e_id),
                predicate : Relation::IsTaggedWith,
                object    : ObjectId::Tag(ref t_id)
            } => Some((e_id,t_id)),
            _ => None
        })
        .collect();

    let words = to_words(text);

    Box::new(move |entry|
        tags.iter()
            .any(|tag| tag_triples.iter().any(|t| *t.0 == entry.id && t.1 == tag))
        || words.iter().any(|word| {
            entry.title.to_lowercase().contains(word) || entry.description.to_lowercase().contains(word)
        })
    )
}

fn to_words(txt: &str) -> Vec<String> {
    txt.to_lowercase().split(',').map(|x| x.to_string()).collect()
}

#[cfg(test)]
mod tests {

    use super::*;
    use business::builder::*;

    #[test]
    fn is_in_bounding_box() {
        let bb = vec![Coordinate {
                          lat: -10.0,
                          lng: -10.0,
                      },
                      Coordinate {
                          lat: 10.0,
                          lng: 10.0,
                      }];
        let e = Entry::build()
            .title("foo")
            .description("bar")
            .lat(5.0)
            .lng(5.0)
            .finish();
        assert_eq!(e.in_bbox(&bb), true);
        let e = Entry::build()
            .title("foo")
            .description("bar")
            .lat(10.1)
            .lng(10.0)
            .finish();
        assert_eq!(e.in_bbox(&bb), false);
    }

    #[test]
    fn is_in_invalid_bounding_box() {
        let bb = vec![Coordinate {
                          lat: 10.0,
                          lng: 10.0,
                      }];
        let e = Entry::build()
            .lat(5.0)
            .lng(5.0)
            .finish();
        assert_eq!(e.in_bbox(&bb), false);
    }

    #[test]
    fn filter_by_bounding_box() {
        let bb = vec![Coordinate {
                          lat: -10.0,
                          lng: -10.0,
                      },
                      Coordinate {
                          lat: 10.0,
                          lng: 10.0,
                      }];
        let entries = vec![
            Entry::build().lat(5.0).lng(5.0).finish(),
            Entry::build().lat(-5.0).lng(5.0).finish(),
            Entry::build().lat(10.0).lng(10.1).finish(),
        ];
        assert_eq!(entries.iter().filter(|&x| x.in_bbox(&bb)).collect::<Vec<&Entry>>().len(),
                   2);
    }

    #[test]
    fn filter_by_category() {
        let entries = vec![
            Entry::build().categories(vec!["a"]).finish(),
            Entry::build().categories(vec!["c"]).finish(),
            Entry::build().categories(vec!["b","a"]).finish(),
        ];
        let ab = vec!["a".into(), "b".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_category_ids(&ab)).collect();
        assert_eq!(x.len(), 2);
        let b = vec!["b".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_category_ids(&b)).collect();
        assert_eq!(x.len(), 1);
        let c = vec!["c".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_category_ids(&c)).collect();
        assert_eq!(x.len(), 1);
    }

    #[test]
    fn search_entries_by_title() {
        let entries = vec![
            Entry::build().title("a title").description("x").finish(),
            Entry::build().title("not so interesting").description("y").finish(),
        ];
        let filter = entries_by_search_text("a");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].title, "a title");
        let filter = entries_by_search_text("ti");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
    }

    #[test]
    fn search_entries_by_description() {
        let entries = vec![
            Entry::build().title("a").description("x").finish(),
            Entry::build().title("b").description("y").finish(),
            Entry::build().title("c").description("x").finish(),
        ];
        let filter = entries_by_search_text("x");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
    }

    #[test]
    fn search_with_multiple_words() {
        let entries = vec![
            Entry::build().title("SoLaWi").description("mit gemüse").finish(),
            Entry::build().title("csa").description("This is a great csa").finish(),
            Entry::build().title("solawi").description("Das ist eine tolle solawi").finish(),
        ];
        let filter = entries_by_search_text("csa,toll");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
        let filter = entries_by_search_text("great,to,gemü");
        let x: Vec<&Entry> = entries.iter().filter(&*filter).collect();
        assert_eq!(x.len(), 3);
    }

    #[test]
    fn search_and_ignore_capitalisation() {
        let e0 = Entry::build().title("Eintrag").description("Hallo! Ein EinTrag").finish();
        let e1 = Entry::build().title("Ein trag").description("foo").finish();
        let e2 = Entry::build().title("CSA").description("cool vegetables").finish();
        let entries = vec![&e0, &e1, &e2];

        let filter = entries_by_search_text("Foo");
        let x: Vec<&Entry> = entries.iter().cloned().filter(&*filter).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id, e1.id);

        let filter = entries_by_search_text("trag");
        let x: Vec<&Entry> = entries.iter().cloned().filter(&*filter).collect();
        assert_eq!(x.len(), 2);
        assert_eq!(x[0].id, e0.id);
        assert_eq!(x[1].id, e1.id);

        let filter = entries_by_search_text("csa");
        let x: Vec<&Entry> = entries.iter().cloned().filter(&*filter).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id, e2.id);
    }

    #[test]
    fn filter_by_tags() {
        let entries = vec![
            Entry::build().id("a").finish(),
            Entry::build().id("b").finish(),
            Entry::build().id("c").finish(),
        ];
        let tags = vec!["csa".into()];
        let no_triples = vec![];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&no_triples, Combination::Or)).collect();
        assert_eq!(x.len(), 0);
        let triples = vec![
            Triple{ subject: ObjectId::Entry("b".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("csa".into())},
            Triple{ subject: ObjectId::Entry("c".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("foo".into())}
        ];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&triples, Combination::Or)).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id,"b");
        let tags = vec!["csa".into(),"foo".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&triples, Combination::Or)).collect();
        assert_eq!(x.len(), 2);
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags(&tags,&triples, Combination::And)).collect();
        assert_eq!(x.len(), 0);
    }

    #[test]
    fn filter_by_tags_or_text() {
        let entries = vec![
            Entry::build().id("a").title("solawi").finish(),
            Entry::build().id("b").title("blabla").description("blubb").finish(),
            Entry::build().id("c").finish(),
        ];
        let tags = vec!["csa".into()];
        let solawi = "solawi";
        let blubb = "blubb";
        let slowtec = "slowtec";
        let no_triples = vec![];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&slowtec, &tags,&no_triples)).collect();
        assert_eq!(x.len(), 0);

        let triples = vec![
            Triple{ subject: ObjectId::Entry("b".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("csa".into())},
            Triple{ subject: ObjectId::Entry("c".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("foo".into())}
        ];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&search_text, &tags,&triples)).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id,"b");
        let tags = vec!["csa".into(),"foo".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&search_text, &tags,&triples)).collect();
        assert_eq!(x.len(), 2);
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&search_text, &tags,&triplesd)).collect();
        assert_eq!(x.len(), 0);

        let triples = vec![
            Triple{ subject: ObjectId::Entry("b".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("csa".into())},
            Triple{ subject: ObjectId::Entry("c".into()), predicate: Relation::IsTaggedWith, object: ObjectId::Tag("foo".into())}
        ];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&search_text, &tags,&triples)).collect();
        assert_eq!(x.len(), 1);
        assert_eq!(x[0].id,"b");
        let tags = vec!["csa".into(),"foo".into()];
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&search_text, &tags,&triples)).collect();
        assert_eq!(x.len(), 2);
        let x: Vec<&Entry> = entries.iter().filter(&*entries_by_tags_or_search_text(&search_text, &tags,&triplesd)).collect();
        assert_eq!(x.len(), 0);
    }
}
<|endoftext|>


<commit_before>use crate::clean;
use crate::core::DocContext;
use crate::fold::{self, DocFolder};
use crate::passes::Pass;

use syntax::attr;

pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
    name: "calculate-doc-coverage",
    pass: calculate_doc_coverage,
    description: "counts the number of items with and without documentation",
};

fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
    let mut calc = CoverageCalculator::default();
    let krate = calc.fold_crate(krate);

    let total_minus_traits = calc.total - calc.total_trait_impls;
    let docs_minus_traits = calc.with_docs - calc.trait_impls_with_docs;

    print!("Rustdoc found {}/{} items with documentation", calc.with_docs, calc.total);
    println!(" ({}/{} not counting trait impls)", docs_minus_traits, total_minus_traits);

    if calc.total > 0 {
        let percentage = (calc.with_docs as f64 * 100.0) / calc.total as f64;
        let percentage_minus_traits =
            (docs_minus_traits as f64 * 100.0) / total_minus_traits as f64;
        println!("    Score: {:.1}% ({:.1}% not counting trait impls)",
                 percentage, percentage_minus_traits);
    }

    krate
}

#[derive(Default)]
struct CoverageCalculator {
    total: usize,
    with_docs: usize,
    total_trait_impls: usize,
    trait_impls_with_docs: usize,
}

impl fold::DocFolder for CoverageCalculator {
    fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
        match i.inner {
            clean::StrippedItem(..) => {
                // don't count items in stripped modules
                return Some(i);
            }
            clean::ImportItem(..) | clean::ExternCrateItem(..) => {}
            clean::ImplItem(ref impl_)
                if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
                    || impl_.synthetic || impl_.blanket_impl.is_some() =>
            {
                // skip counting anything inside these impl blocks
                // FIXME(misdreavus): need to also find items that came out of a derive macro
                return Some(i);
            }
            // non-local items are skipped because they can be out of the users control, especially
            // in the case of trait impls, which rustdoc eagerly inlines
            _ => if i.def_id.is_local() {
                let has_docs = !i.attrs.doc_strings.is_empty();

                if let clean::ImplItem(ref i) = i.inner {
                    if let Some(ref tr) = i.trait_ {
                        debug!("counting impl {:#} for {:#}", tr, i.for_);

                        self.total += 1;
                        if has_docs {
                            self.with_docs += 1;
                        }

                        // trait impls inherit their docs from the trait definition, so documenting
                        // them can be considered optional

                        self.total_trait_impls += 1;
                        if has_docs {
                            self.trait_impls_with_docs += 1;
                        }

                        for it in &i.items {
                            self.total_trait_impls += 1;
                            if !it.attrs.doc_strings.is_empty() {
                                self.trait_impls_with_docs += 1;
                            }
                        }
                    } else {
                        // inherent impls *can* be documented, and those docs show up, but in most
                        // cases it doesn't make sense, as all methods on a type are in one single
                        // impl block
                        debug!("not counting impl {:#}", i.for_);
                    }
                } else {
                    debug!("counting {} {:?}", i.type_(), i.name);
                    self.total += 1;
                    if has_docs {
                        self.with_docs += 1;
                    }
                }
            }
        }

        self.fold_item_recur(i)
    }
}
<commit_msg>refactor: combine item count numbers into a new struct<commit_after>use crate::clean;
use crate::core::DocContext;
use crate::fold::{self, DocFolder};
use crate::passes::Pass;

use syntax::attr;

use std::ops::Sub;
use std::fmt;

pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
    name: "calculate-doc-coverage",
    pass: calculate_doc_coverage,
    description: "counts the number of items with and without documentation",
};

fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
    let mut calc = CoverageCalculator::default();
    let krate = calc.fold_crate(krate);

    let non_traits = calc.items - calc.trait_impl_items;

    print!("Rustdoc found {} items with documentation", calc.items);
    println!(" ({} not counting trait impls)", non_traits);

    if let (Some(percentage), Some(percentage_non_traits)) =
        (calc.items.percentage(), non_traits.percentage())
    {
        println!("    Score: {:.1}% ({:.1}% not counting trait impls)",
                 percentage, percentage_non_traits);
    }

    krate
}

#[derive(Default, Copy, Clone)]
struct ItemCount {
    total: u64,
    with_docs: u64,
}

impl ItemCount {
    fn count_item(&mut self, has_docs: bool) {
        self.total += 1;

        if has_docs {
            self.with_docs += 1;
        }
    }

    fn percentage(&self) -> Option<f64> {
        if self.total > 0 {
            Some((self.with_docs as f64 * 100.0) / self.total as f64)
        } else {
            None
        }
    }
}

impl Sub for ItemCount {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self {
        ItemCount {
            total: self.total - rhs.total,
            with_docs: self.with_docs - rhs.with_docs,
        }
    }
}

impl fmt::Display for ItemCount {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}/{}", self.with_docs, self.total)
    }
}

#[derive(Default)]
struct CoverageCalculator {
    items: ItemCount,
    trait_impl_items: ItemCount,
}

impl fold::DocFolder for CoverageCalculator {
    fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
        match i.inner {
            clean::StrippedItem(..) => {
                // don't count items in stripped modules
                return Some(i);
            }
            clean::ImportItem(..) | clean::ExternCrateItem(..) => {}
            clean::ImplItem(ref impl_)
                if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
                    || impl_.synthetic || impl_.blanket_impl.is_some() =>
            {
                // skip counting anything inside these impl blocks
                // FIXME(misdreavus): need to also find items that came out of a derive macro
                return Some(i);
            }
            // non-local items are skipped because they can be out of the users control, especially
            // in the case of trait impls, which rustdoc eagerly inlines
            _ => if i.def_id.is_local() {
                let has_docs = !i.attrs.doc_strings.is_empty();

                if let clean::ImplItem(ref i) = i.inner {
                    if let Some(ref tr) = i.trait_ {
                        debug!("counting impl {:#} for {:#}", tr, i.for_);

                        self.items.count_item(has_docs);

                        // trait impls inherit their docs from the trait definition, so documenting
                        // them can be considered optional

                        self.trait_impl_items.count_item(has_docs);

                        for it in &i.items {
                            self.trait_impl_items.count_item(!it.attrs.doc_strings.is_empty());
                        }
                    } else {
                        // inherent impls *can* be documented, and those docs show up, but in most
                        // cases it doesn't make sense, as all methods on a type are in one single
                        // impl block
                        debug!("not counting impl {:#}", i.for_);
                    }
                } else {
                    debug!("counting {} {:?}", i.type_(), i.name);
                    self.items.count_item(has_docs);
                }
            }
        }

        self.fold_item_recur(i)
    }
}
<|endoftext|>


<commit_before>use crate::clean;
use crate::core::DocContext;
use crate::fold::{self, DocFolder};
use crate::passes::Pass;

use syntax::attr;
use syntax_pos::FileName;

use std::collections::BTreeMap;
use std::fmt;
use std::ops;

pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
    name: "calculate-doc-coverage",
    pass: calculate_doc_coverage,
    description: "counts the number of items with and without documentation",
};

fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
    let mut calc = CoverageCalculator::default();
    let krate = calc.fold_crate(krate);

    calc.print_results();

    krate
}

#[derive(Default, Copy, Clone)]
struct ItemCount {
    total: u64,
    with_docs: u64,
}

impl ItemCount {
    fn count_item(&mut self, has_docs: bool) {
        self.total += 1;

        if has_docs {
            self.with_docs += 1;
        }
    }

    fn percentage(&self) -> Option<f64> {
        if self.total > 0 {
            Some((self.with_docs as f64 * 100.0) / self.total as f64)
        } else {
            None
        }
    }
}

impl ops::Sub for ItemCount {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self {
        ItemCount {
            total: self.total - rhs.total,
            with_docs: self.with_docs - rhs.with_docs,
        }
    }
}

impl ops::AddAssign for ItemCount {
    fn add_assign(&mut self, rhs: Self) {
        self.total += rhs.total;
        self.with_docs += rhs.with_docs;
    }
}

impl fmt::Display for ItemCount {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}/{}", self.with_docs, self.total)
    }
}

#[derive(Default)]
struct CoverageCalculator {
    items: BTreeMap<FileName, ItemCount>,
}

impl CoverageCalculator {
    fn print_results(&self) {
        let mut total = ItemCount::default();

        fn print_table_line() {
            println!("+-{0:->35}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+", "");
        }

        fn print_table_record(name: &str, count: ItemCount, percentage: f64) {
            println!("| {:<35} | {:>10} | {:>10} | {:>9.1}% |",
                     name, count.with_docs, count.total, percentage);
        }

        print_table_line();
        println!("| {:<35} | {:>10} | {:>10} | {:>10} |",
                 "File", "Documented", "Total", "Percentage");
        print_table_line();

        for (file, &count) in &self.items {
            if let Some(percentage) = count.percentage() {
                let mut name = file.to_string();
                // if a filename is too long, shorten it so we don't blow out the table
                // FIXME(misdreavus): this needs to count graphemes, and probably also track
                // double-wide characters...
                if name.len() > 35 {
                    name = "...".to_string() + &name[name.len()-32..];
                }

                print_table_record(&name, count, percentage);

                total += count;
            }
        }

        print_table_line();
        print_table_record("Total", total, total.percentage().unwrap_or(0.0));
        print_table_line();
    }
}

impl fold::DocFolder for CoverageCalculator {
    fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
        let has_docs = !i.attrs.doc_strings.is_empty();

        match i.inner {
            _ if !i.def_id.is_local() => {
                // non-local items are skipped because they can be out of the users control,
                // especially in the case of trait impls, which rustdoc eagerly inlines
                return Some(i);
            }
            clean::StrippedItem(..) => {
                // don't count items in stripped modules
                return Some(i);
            }
            clean::ImportItem(..) | clean::ExternCrateItem(..) => {
                // docs on `use` and `extern crate` statements are not displayed, so they're not
                // worth counting
                return Some(i);
            }
            clean::ImplItem(ref impl_)
                if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
                    || impl_.synthetic || impl_.blanket_impl.is_some() =>
            {
                // built-in derives get the `#[automatically_derived]` attribute, and
                // synthetic/blanket impls are made up by rustdoc and can't be documented
                // FIXME(misdreavus): need to also find items that came out of a derive macro
                return Some(i);
            }
            clean::ImplItem(ref impl_) => {
                if let Some(ref tr) = impl_.trait_ {
                    debug!("impl {:#} for {:#} in {}", tr, impl_.for_, i.source.filename);

                    // don't count trait impls, the missing-docs lint doesn't so we shouldn't
                    // either
                    return Some(i);
                } else {
                    // inherent impls *can* be documented, and those docs show up, but in most
                    // cases it doesn't make sense, as all methods on a type are in one single
                    // impl block
                    debug!("impl {:#} in {}", impl_.for_, i.source.filename);
                }
            }
            _ => {
                debug!("counting {} {:?} in {}", i.type_(), i.name, i.source.filename);
                self.items.entry(i.source.filename.clone())
                          .or_default()
                          .count_item(has_docs);
            }
        }

        self.fold_item_recur(i)
    }
}
<commit_msg>remove unused Display impl<commit_after>use crate::clean;
use crate::core::DocContext;
use crate::fold::{self, DocFolder};
use crate::passes::Pass;

use syntax::attr;
use syntax_pos::FileName;

use std::collections::BTreeMap;
use std::ops;

pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
    name: "calculate-doc-coverage",
    pass: calculate_doc_coverage,
    description: "counts the number of items with and without documentation",
};

fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
    let mut calc = CoverageCalculator::default();
    let krate = calc.fold_crate(krate);

    calc.print_results();

    krate
}

#[derive(Default, Copy, Clone)]
struct ItemCount {
    total: u64,
    with_docs: u64,
}

impl ItemCount {
    fn count_item(&mut self, has_docs: bool) {
        self.total += 1;

        if has_docs {
            self.with_docs += 1;
        }
    }

    fn percentage(&self) -> Option<f64> {
        if self.total > 0 {
            Some((self.with_docs as f64 * 100.0) / self.total as f64)
        } else {
            None
        }
    }
}

impl ops::Sub for ItemCount {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self {
        ItemCount {
            total: self.total - rhs.total,
            with_docs: self.with_docs - rhs.with_docs,
        }
    }
}

impl ops::AddAssign for ItemCount {
    fn add_assign(&mut self, rhs: Self) {
        self.total += rhs.total;
        self.with_docs += rhs.with_docs;
    }
}

#[derive(Default)]
struct CoverageCalculator {
    items: BTreeMap<FileName, ItemCount>,
}

impl CoverageCalculator {
    fn print_results(&self) {
        let mut total = ItemCount::default();

        fn print_table_line() {
            println!("+-{0:->35}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+", "");
        }

        fn print_table_record(name: &str, count: ItemCount, percentage: f64) {
            println!("| {:<35} | {:>10} | {:>10} | {:>9.1}% |",
                     name, count.with_docs, count.total, percentage);
        }

        print_table_line();
        println!("| {:<35} | {:>10} | {:>10} | {:>10} |",
                 "File", "Documented", "Total", "Percentage");
        print_table_line();

        for (file, &count) in &self.items {
            if let Some(percentage) = count.percentage() {
                let mut name = file.to_string();
                // if a filename is too long, shorten it so we don't blow out the table
                // FIXME(misdreavus): this needs to count graphemes, and probably also track
                // double-wide characters...
                if name.len() > 35 {
                    name = "...".to_string() + &name[name.len()-32..];
                }

                print_table_record(&name, count, percentage);

                total += count;
            }
        }

        print_table_line();
        print_table_record("Total", total, total.percentage().unwrap_or(0.0));
        print_table_line();
    }
}

impl fold::DocFolder for CoverageCalculator {
    fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
        let has_docs = !i.attrs.doc_strings.is_empty();

        match i.inner {
            _ if !i.def_id.is_local() => {
                // non-local items are skipped because they can be out of the users control,
                // especially in the case of trait impls, which rustdoc eagerly inlines
                return Some(i);
            }
            clean::StrippedItem(..) => {
                // don't count items in stripped modules
                return Some(i);
            }
            clean::ImportItem(..) | clean::ExternCrateItem(..) => {
                // docs on `use` and `extern crate` statements are not displayed, so they're not
                // worth counting
                return Some(i);
            }
            clean::ImplItem(ref impl_)
                if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
                    || impl_.synthetic || impl_.blanket_impl.is_some() =>
            {
                // built-in derives get the `#[automatically_derived]` attribute, and
                // synthetic/blanket impls are made up by rustdoc and can't be documented
                // FIXME(misdreavus): need to also find items that came out of a derive macro
                return Some(i);
            }
            clean::ImplItem(ref impl_) => {
                if let Some(ref tr) = impl_.trait_ {
                    debug!("impl {:#} for {:#} in {}", tr, impl_.for_, i.source.filename);

                    // don't count trait impls, the missing-docs lint doesn't so we shouldn't
                    // either
                    return Some(i);
                } else {
                    // inherent impls *can* be documented, and those docs show up, but in most
                    // cases it doesn't make sense, as all methods on a type are in one single
                    // impl block
                    debug!("impl {:#} in {}", impl_.for_, i.source.filename);
                }
            }
            _ => {
                debug!("counting {} {:?} in {}", i.type_(), i.name, i.source.filename);
                self.items.entry(i.source.filename.clone())
                          .or_default()
                          .count_item(has_docs);
            }
        }

        self.fold_item_recur(i)
    }
}
<|endoftext|>


<commit_before>#![feature(phase)]

extern crate regex;
#[phase(plugin)] extern crate regex_macros;

use std::io::{File, IoResult, IoError, OtherIoError};
use std::collections::HashMap;
use std::default::Default;
use std::os;

pub struct Dotenv {
	env: HashMap<Vec<u8>, Vec<u8>>
}

fn merge_env_hashes(
	a: HashMap<Vec<u8>, Vec<u8>>,
	b: HashMap<Vec<u8>, Vec<u8>>,
) -> HashMap<Vec<u8>, Vec<u8>> {
	let mut result = a.clone();
	result.extend(b.into_iter());
	result
}

fn parse_env(data: Vec<u8>) -> HashMap<Vec<u8>, Vec<u8>> {
	let line_splitter = regex!(r"[\r\n]+");
	let env_format = regex!(
		r"^(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<value>.+)$"
	);

	let data_string = String::from_utf8_lossy(data.as_slice());
	let data_slice = data_string.as_slice();

	line_splitter.split(data_slice).filter_map(|line| {
		env_format.captures(line).and_then(|captures| {
			let key = captures.name("key");
			let value = captures.name("value");

			// regex captures return empty string on failure
			if key == "" || value == "" {
				None
			} else {
				Some((
					String::from_str(key).into_bytes(),
					String::from_str(value).into_bytes()
				))
			}
		})
	}).collect()
}

impl Dotenv {
	pub fn dotenv() -> Dotenv {
		Dotenv::from_filename(".env").ok().unwrap_or_default()
	}

	pub fn from_str(s: &str) -> Dotenv {
		Dotenv::from_bytes(String::from_str(s).into_bytes())
	}

	pub fn from_bytes(bytes: Vec<u8>) -> Dotenv {
		let dotenv_hash = parse_env(bytes);
		let env_hash = os::env_as_bytes().into_iter().collect();
		Dotenv{env: merge_env_hashes(env_hash, dotenv_hash)}
	}

	pub fn from_file(mut file: File) -> IoResult<Dotenv> {
		file.read_to_end().map(|file_contents|
			Dotenv::from_bytes(file_contents)
		)
	}

	pub fn from_filename(n: &str) -> IoResult<Dotenv> {
		os::self_exe_path().as_mut().map(|path| {
			path.push(n);
			Dotenv::from_path(path)
		}).unwrap_or(Err(IoError{
			kind: OtherIoError,
			desc: "Could not fetch the path of this executable",
			detail: None
		}))
	}

	pub fn from_path(path: &Path) -> IoResult<Dotenv> {
		File::open(path).and_then(|file| {
			Dotenv::from_file(file)
		})
	}

	pub fn getenv(&self, n: &str) -> Option<String> {
		let n_bytes = &String::from_str(n).into_bytes();
		self.env.find(n_bytes).and_then(|bytes| {
			String::from_utf8(bytes.clone()).ok()
		})
	}

	pub fn getenv_as_bytes(&self, n: &str) -> Option<Vec<u8>> {
		let n_bytes = &String::from_str(n).into_bytes();
		self.env.find(n_bytes).map(|bytes| {
			bytes.clone()})
		}

	pub fn env(&self) -> Vec<(String, String)> {
		self.env.iter().filter_map(|(key, value)| {
			String::from_utf8(key.clone()).ok().and_then(|key_string| {
				String::from_utf8(value.clone()).ok().map(|value_string| {
					(key_string.clone(), value_string)
				})
			})
		}).collect()
	}

	pub fn env_as_bytes(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
		self.env.iter().map(|(key, value)| {
			(key.clone(), value.clone())
		}).collect()
	}
}

impl Default for Dotenv {
	fn default() -> Dotenv {
		Dotenv::from_bytes(Default::default())
	}
}

#[test]
fn merge_env_hashes_test() {
	let hash_a: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("bad").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("neutral").into_bytes()
		)
	].into_iter().collect();

	let hash_b: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		)
	].into_iter().collect();

	let expected_hash: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("neutral").into_bytes()
		)
	].into_iter().collect();

	let actual_hash = merge_env_hashes(hash_a, hash_b);

	assert_eq!(expected_hash, actual_hash)
}

#[test]
fn parse_env_test_basic() {
	let input = String::from_str(concat!(
		"bacon=good", "\n",
		"cabbage=neutral", "\n"
	)).into_bytes();

	let expected_hash: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("neutral").into_bytes()
		)
	].into_iter().collect();

	let actual_hash = parse_env(input);

	assert_eq!(expected_hash, actual_hash);
}

#[test]
fn parse_env_test_edges() {
	let input = String::from_str(concat!(
		"chili=spicy=EVIL", "\n",       // equals on value, uppercase on value
		"to_MAH_to=okay", "\n",         // uppercase and underscores on key
		"bacon   =  good", "\n\r\n\r",  // spaces around equal, newlines
		"cabbage=not evil"              // space on value, no final newline
	)).into_bytes();

	let expected_hash: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("chili").into_bytes(),
			String::from_str("spicy=EVIL").into_bytes()
		), (
			String::from_str("to_MAH_to").into_bytes(),
			String::from_str("okay").into_bytes()
		), (
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("not evil").into_bytes()
		)
	].into_iter().collect();

	let actual_hash = parse_env(input);

	assert_eq!(expected_hash, actual_hash);
}
<commit_msg>Change `find` to `get`, avoid deprecation warning<commit_after>#![feature(phase)]

extern crate regex;
#[phase(plugin)] extern crate regex_macros;

use std::io::{File, IoResult, IoError, OtherIoError};
use std::collections::HashMap;
use std::default::Default;
use std::os;

pub struct Dotenv {
	env: HashMap<Vec<u8>, Vec<u8>>
}

fn merge_env_hashes(
	a: HashMap<Vec<u8>, Vec<u8>>,
	b: HashMap<Vec<u8>, Vec<u8>>,
) -> HashMap<Vec<u8>, Vec<u8>> {
	let mut result = a.clone();
	result.extend(b.into_iter());
	result
}

fn parse_env(data: Vec<u8>) -> HashMap<Vec<u8>, Vec<u8>> {
	let line_splitter = regex!(r"[\r\n]+");
	let env_format = regex!(
		r"^(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<value>.+)$"
	);

	let data_string = String::from_utf8_lossy(data.as_slice());
	let data_slice = data_string.as_slice();

	line_splitter.split(data_slice).filter_map(|line| {
		env_format.captures(line).and_then(|captures| {
			let key = captures.name("key");
			let value = captures.name("value");

			// regex captures return empty string on failure
			if key == "" || value == "" {
				None
			} else {
				Some((
					String::from_str(key).into_bytes(),
					String::from_str(value).into_bytes()
				))
			}
		})
	}).collect()
}

impl Dotenv {
	pub fn dotenv() -> Dotenv {
		Dotenv::from_filename(".env").ok().unwrap_or_default()
	}

	pub fn from_str(s: &str) -> Dotenv {
		Dotenv::from_bytes(String::from_str(s).into_bytes())
	}

	pub fn from_bytes(bytes: Vec<u8>) -> Dotenv {
		let dotenv_hash = parse_env(bytes);
		let env_hash = os::env_as_bytes().into_iter().collect();
		Dotenv{env: merge_env_hashes(env_hash, dotenv_hash)}
	}

	pub fn from_file(mut file: File) -> IoResult<Dotenv> {
		file.read_to_end().map(|file_contents|
			Dotenv::from_bytes(file_contents)
		)
	}

	pub fn from_filename(n: &str) -> IoResult<Dotenv> {
		os::self_exe_path().as_mut().map(|path| {
			path.push(n);
			Dotenv::from_path(path)
		}).unwrap_or(Err(IoError{
			kind: OtherIoError,
			desc: "Could not fetch the path of this executable",
			detail: None
		}))
	}

	pub fn from_path(path: &Path) -> IoResult<Dotenv> {
		File::open(path).and_then(|file| {
			Dotenv::from_file(file)
		})
	}

	pub fn getenv(&self, n: &str) -> Option<String> {
		let n_bytes = &String::from_str(n).into_bytes();
		self.env.get(n_bytes).and_then(|bytes| {
			String::from_utf8(bytes.clone()).ok()
		})
	}

	pub fn getenv_as_bytes(&self, n: &str) -> Option<Vec<u8>> {
		let n_bytes = &String::from_str(n).into_bytes();
		self.env.get(n_bytes).map(|bytes| {
			bytes.clone()})
		}

	pub fn env(&self) -> Vec<(String, String)> {
		self.env.iter().filter_map(|(key, value)| {
			String::from_utf8(key.clone()).ok().and_then(|key_string| {
				String::from_utf8(value.clone()).ok().map(|value_string| {
					(key_string.clone(), value_string)
				})
			})
		}).collect()
	}

	pub fn env_as_bytes(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
		self.env.iter().map(|(key, value)| {
			(key.clone(), value.clone())
		}).collect()
	}
}

impl Default for Dotenv {
	fn default() -> Dotenv {
		Dotenv::from_bytes(Default::default())
	}
}

#[test]
fn merge_env_hashes_test() {
	let hash_a: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("bad").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("neutral").into_bytes()
		)
	].into_iter().collect();

	let hash_b: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		)
	].into_iter().collect();

	let expected_hash: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("neutral").into_bytes()
		)
	].into_iter().collect();

	let actual_hash = merge_env_hashes(hash_a, hash_b);

	assert_eq!(expected_hash, actual_hash)
}

#[test]
fn parse_env_test_basic() {
	let input = String::from_str(concat!(
		"bacon=good", "\n",
		"cabbage=neutral", "\n"
	)).into_bytes();

	let expected_hash: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("neutral").into_bytes()
		)
	].into_iter().collect();

	let actual_hash = parse_env(input);

	assert_eq!(expected_hash, actual_hash);
}

#[test]
fn parse_env_test_edges() {
	let input = String::from_str(concat!(
		"chili=spicy=EVIL", "\n",       // equals on value, uppercase on value
		"to_MAH_to=okay", "\n",         // uppercase and underscores on key
		"bacon   =  good", "\n\r\n\r",  // spaces around equal, newlines
		"cabbage=not evil"              // space on value, no final newline
	)).into_bytes();

	let expected_hash: HashMap<Vec<u8>, Vec<u8>> = vec![
		(
			String::from_str("chili").into_bytes(),
			String::from_str("spicy=EVIL").into_bytes()
		), (
			String::from_str("to_MAH_to").into_bytes(),
			String::from_str("okay").into_bytes()
		), (
			String::from_str("bacon").into_bytes(),
			String::from_str("good").into_bytes()
		), (
			String::from_str("cabbage").into_bytes(),
			String::from_str("not evil").into_bytes()
		)
	].into_iter().collect();

	let actual_hash = parse_env(input);

	assert_eq!(expected_hash, actual_hash);
}
<|endoftext|>


<commit_before>//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//

use std::fmt::{Display, Formatter, Error};

/// Utility type to specify which kind of store action is running
#[derive(Clone, Debug)]
pub enum StoreAction {
    Create,
    Retrieve,
    Update,
    Delete,
}

impl StoreAction {

    pub fn uppercase(&self) -> &str {
        match *self {
            StoreAction::Create   => "CREATE",
            StoreAction::Retrieve => "RETRIEVE",
            StoreAction::Update   => "UPDATE",
            StoreAction::Delete   => "DELETE",
        }
    }

    pub fn as_commit_message(&self) -> &str {
        match *self {
            StoreAction::Create   => "Create",
            StoreAction::Retrieve => "Retrieve",
            StoreAction::Update   => "Update",
            StoreAction::Delete   => "Delete",
        }
    }
}

impl Display for StoreAction {

    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        write!(fmt, "StoreAction: {}",
                match *self {
                    StoreAction::Create   => "create",
                    StoreAction::Retrieve => "retrieve",
                    StoreAction::Update   => "update",
                    StoreAction::Delete   => "delete",
                })
    }

}

<commit_msg>Add StoreAction enum type for store unload hook<commit_after>//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//

use std::fmt::{Display, Formatter, Error};

/// Utility type to specify which kind of store action is running
#[derive(Clone, Debug)]
pub enum StoreAction {
    Create,
    Retrieve,
    Update,
    Delete,
    StoreUnload,
}

impl StoreAction {

    pub fn uppercase(&self) -> &str {
        match *self {
            StoreAction::Create      => "CREATE",
            StoreAction::Retrieve    => "RETRIEVE",
            StoreAction::Update      => "UPDATE",
            StoreAction::Delete      => "DELETE",
            StoreAction::StoreUnload => "STORE UNLOAD",
        }
    }

    pub fn as_commit_message(&self) -> &str {
        match *self {
            StoreAction::Create      => "Create",
            StoreAction::Retrieve    => "Retrieve",
            StoreAction::Update      => "Update",
            StoreAction::Delete      => "Delete",
            StoreAction::StoreUnload => "Store Unload",
        }
    }
}

impl Display for StoreAction {

    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        write!(fmt, "StoreAction: {}",
                match *self {
                    StoreAction::Create      => "create",
                    StoreAction::Retrieve    => "retrieve",
                    StoreAction::Update      => "update",
                    StoreAction::Delete      => "delete",
                    StoreAction::StoreUnload => "store unload",
                })
    }

}

<|endoftext|>


<commit_before>use lalafell::commands::prelude::*;

use lalafell::error::*;

use fflogs::{self, FfLogs};
use fflogs::net::{ServerRegion, Metric};

use std::cmp::Ordering;
use std::str::FromStr;

pub struct FfLogsCommand {
  fflogs: FfLogs
}

impl ::commands::BotCommand for FfLogsCommand {
  fn new(env: Arc<::bot::BotEnv>) -> Self {
    FfLogsCommand {
      fflogs: FfLogs::new(&env.environment.fflogs_api_key)
    }
  }
}

#[derive(Debug, StructOpt)]
#[structopt(about = "Display information about the character on FF Logs")]
pub struct Params {
  #[structopt(help = "The server the character is on")]
  server: String,
  #[structopt(help = "The character's first name")]
  first_name: String,
  #[structopt(help = "The character's last name")]
  last_name: String,
  #[structopt(
    short = "j",
    long = "job",
    help = "The job to look at",
    parse(try_from_str)
  )]
  job: Option<Job>
}

impl HasParams for FfLogsCommand {
  type Params = Params;
}

impl<'a> Command<'a> for FfLogsCommand {
  fn run(&self, _: &Context, _: &Message, params: &[&str]) -> CommandResult<'a> {
    let params = self.params_then("fflogs", params, |a| a.setting(::structopt::clap::AppSettings::ArgRequiredElseHelp))?;
    let server = params.server;
    let region = match FfLogsCommand::region(&server) {
      Some(r) => r,
      None => return Err("Invalid server.".into())
    };
    let name = format!("{} {}", params.first_name, params.last_name);

    let parses = self.fflogs.parses(
      &name,
      &server,
      region,
      |x| x.metric(Metric::Dps)
    );

    let parses = match parses {
      Ok(r) => r,
      Err(fflogs::errors::Error::FfLogs(fflogs::net::FfLogsError { status, error })) => {
        return Err(format!("FF Logs didn't like that ({}): {}", status, error).into());
      },
      Err(e) => return Err(e).chain_err(|| "could not query fflogs")?
    };

    if parses.is_empty() {
      return Ok("No parses found.".into());
    }

    let first_spec = match parses[0].specs.get(0) {
      Some(s) => s,
      None => return Err("Somehow there was no first spec.".into())
    };
    let first_data = match first_spec.data.get(0) {
      Some(d) => d,
      None => return Err("Somehow there was no first data.".into())
    };

    let job = params.job.or_else(|| Job::from_str(&first_spec.spec).ok()).unwrap().to_string();
    let lower_job = job.to_lowercase().replace(" ", "");
    let name = &first_data.character_name;
    let id = first_data.character_id;

    let mut embed = CreateEmbed::default();

    embed = embed
      .title(&name)
      .url(format!("https://www.fflogs.com/character/id/{}", id))
      .field("Job", &job, true)
      .field("Server", &server, true);

    for parse in &parses {
      let spec = match parse.specs.iter().filter(|s| s.spec.to_lowercase() == lower_job).next() {
        Some(s) => s,
        None => continue
      };
      let data = match spec.data.iter().max_by(|a, b| a.historical_percent.partial_cmp(&b.historical_percent).unwrap_or(Ordering::Less)) {
        Some(d) => d,
        None => continue
      };

      let url = format!("https://www.fflogs.com/reports/{}#fight={}", data.report_code, data.report_fight);

      let string = format!("[Link]({url}) – {dps:.2} DPS – {perc:.2} percentile out of {total_parses} parses",
        url = url,
        dps = data.persecondamount,
        perc = data.historical_percent,
        total_parses = data.historical_count
      );

      embed = embed.field(&parse.name, string, false);
    }
    Ok(CommandSuccess::default().message(|_| embed))
  }
}

impl FfLogsCommand {
  fn region(server: &str) -> Option<ServerRegion> {
    match server.to_lowercase().as_str() {
      "adamantoise" | "balmung" | "cactuar" | "coeurl" | "faerie" | "gilgamesh" | "goblin" | "jenova" | "mateus" | "midgardsormr" | "sargatanas" | "siren" | "zalera"
      | "behemoth" | "brynhildr" | "diabolos" | "excalibur" | "exodus" | "famfrit" | "hyperion" | "lamia" | "leviathan" | "malboro" | "ultros"
        => Some(ServerRegion::NorthAmerica),
      "aegis" | "atomos" | "carbuncle" | "garuda" | "gungnir" | "kujata" | "ramuh" | "tonberry" | "typhon" | "unicorn"
      | "alexander" | "bahamut" | "durandal" | "fenrir" | "ifrit" | "ridill" | "tiamat" | "ultima" | "valefor" | "yojimbo" | "zeromus"
      | "anima" | "asura" | "belias" | "chocobo" | "hades" | "ixion" | "mandragora" | "masamune" | "pandaemonium" | "shinryu" | "titan"
        => Some(ServerRegion::Japan),
      "cerberus" | "lich" | "louisoix" | "moogle" | "odin" | "omega" | "phoenix" | "ragnarok" | "shiva" | "zodiark"
        => Some(ServerRegion::Europe),
      _ => None
    }
  }
}

#[derive(Debug)]
enum Job {
  Astrologian,
  Bard,
  BlackMage,
  DarkKnight,
  Dragoon,
  Machinist,
  Monk,
  Ninja,
  Paladin,
  RedMage,
  Samurai,
  Scholar,
  Summoner,
  Warrior,
  WhiteMage
}

impl FromStr for Job {
  type Err = String;

  fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
    let job = match s.to_lowercase().as_str() {
      "astrologian" | "astro" | "ast" => Job::Astrologian,
      "bard" | "brd" => Job::Bard,
      "blackmage" | "black mage" | "blm" => Job::BlackMage,
      "darkknight" | "dark knight" | "drk" => Job::DarkKnight,
      "dragoon" | "lolgoon" | "loldrg" | "drg" => Job::Dragoon,
      "machinist" | "mch" => Job::Machinist,
      "monk" | "mnk" => Job::Monk,
      "ninja" | "nin" => Job::Ninja,
      "paladin" | "pld" => Job::Paladin,
      "redmage" | "red mage" | "rdm" => Job::RedMage,
      "samurai" | "sam" => Job::Samurai,
      "scholar" | "sch" => Job::Scholar,
      "summoner" | "smn" => Job::Summoner,
      "warrior" | "war" | "best" => Job::Warrior,
      "whitemage" | "white mage" | "whm" => Job::WhiteMage,
      x => return Err(format!("Unknown job: {}", x))
    };
    Ok(job)
  }
}

impl ToString for Job {
  fn to_string(&self) -> String {
    match *self {
      Job::Astrologian => "Astrologian",
      Job::Bard => "Bard",
      Job::BlackMage => "Black Mage",
      Job::DarkKnight => "Dark Knight",
      Job::Dragoon => "Dragoon",
      Job::Machinist => "Machinist",
      Job::Monk => "Monk",
      Job::Ninja => "Ninja",
      Job::Paladin => "Paladin",
      Job::RedMage => "Red Mage",
      Job::Samurai => "Samurai",
      Job::Scholar => "Scholar",
      Job::Summoner => "Summoner",
      Job::Warrior => "Warrior",
      Job::WhiteMage => "White Mage",
    }.to_string()
  }
}
<commit_msg>fix: show message for 0 parses<commit_after>use lalafell::commands::prelude::*;

use lalafell::error::*;

use fflogs::{self, FfLogs};
use fflogs::net::{ServerRegion, Metric};

use std::cmp::Ordering;
use std::str::FromStr;

pub struct FfLogsCommand {
  fflogs: FfLogs
}

impl ::commands::BotCommand for FfLogsCommand {
  fn new(env: Arc<::bot::BotEnv>) -> Self {
    FfLogsCommand {
      fflogs: FfLogs::new(&env.environment.fflogs_api_key)
    }
  }
}

#[derive(Debug, StructOpt)]
#[structopt(about = "Display information about the character on FF Logs")]
pub struct Params {
  #[structopt(help = "The server the character is on")]
  server: String,
  #[structopt(help = "The character's first name")]
  first_name: String,
  #[structopt(help = "The character's last name")]
  last_name: String,
  #[structopt(
    short = "j",
    long = "job",
    help = "The job to look at",
    parse(try_from_str)
  )]
  job: Option<Job>
}

impl HasParams for FfLogsCommand {
  type Params = Params;
}

impl<'a> Command<'a> for FfLogsCommand {
  fn run(&self, _: &Context, _: &Message, params: &[&str]) -> CommandResult<'a> {
    let params = self.params_then("fflogs", params, |a| a.setting(::structopt::clap::AppSettings::ArgRequiredElseHelp))?;
    let server = params.server;
    let region = match FfLogsCommand::region(&server) {
      Some(r) => r,
      None => return Err("Invalid server.".into())
    };
    let name = format!("{} {}", params.first_name, params.last_name);

    let parses = self.fflogs.parses(
      &name,
      &server,
      region,
      |x| x.metric(Metric::Dps)
    );

    let parses = match parses {
      Ok(r) => r,
      Err(fflogs::errors::Error::FfLogs(fflogs::net::FfLogsError { status, error })) => {
        return Err(format!("FF Logs didn't like that ({}): {}", status, error).into());
      },
      Err(e) => return Err(e).chain_err(|| "could not query fflogs")?
    };

    if parses.is_empty() {
      return Ok("No parses found.".into());
    }

    let first_spec = match parses[0].specs.get(0) {
      Some(s) => s,
      None => return Err("Somehow there was no first spec.".into())
    };
    let first_data = match first_spec.data.get(0) {
      Some(d) => d,
      None => return Err("Somehow there was no first data.".into())
    };

    let job = params.job.or_else(|| Job::from_str(&first_spec.spec).ok()).unwrap().to_string();
    let lower_job = job.to_lowercase().replace(" ", "");
    let name = &first_data.character_name;
    let id = first_data.character_id;

    let mut embed = CreateEmbed::default();

    embed = embed
      .title(&name)
      .url(format!("https://www.fflogs.com/character/id/{}", id))
      .field("Job", &job, true)
      .field("Server", &server, true);

    let mut count = 0;

    for parse in &parses {
      let spec = match parse.specs.iter().filter(|s| s.spec.to_lowercase() == lower_job).next() {
        Some(s) => s,
        None => continue
      };
      let data = match spec.data.iter().max_by(|a, b| a.historical_percent.partial_cmp(&b.historical_percent).unwrap_or(Ordering::Less)) {
        Some(d) => d,
        None => continue
      };

      count += 1;

      let url = format!("https://www.fflogs.com/reports/{}#fight={}", data.report_code, data.report_fight);

      let string = format!("[Link]({url}) – {dps:.2} DPS – {perc:.2} percentile out of {total_parses} parses",
        url = url,
        dps = data.persecondamount,
        perc = data.historical_percent,
        total_parses = data.historical_count
      );

      embed = embed.field(&parse.name, string, false);
    }

    if count == 0 {
      return Ok("No parses for that job.".into());
    }

    Ok(CommandSuccess::default().message(|_| embed))
  }
}

impl FfLogsCommand {
  fn region(server: &str) -> Option<ServerRegion> {
    match server.to_lowercase().as_str() {
      "adamantoise" | "balmung" | "cactuar" | "coeurl" | "faerie" | "gilgamesh" | "goblin" | "jenova" | "mateus" | "midgardsormr" | "sargatanas" | "siren" | "zalera"
      | "behemoth" | "brynhildr" | "diabolos" | "excalibur" | "exodus" | "famfrit" | "hyperion" | "lamia" | "leviathan" | "malboro" | "ultros"
        => Some(ServerRegion::NorthAmerica),
      "aegis" | "atomos" | "carbuncle" | "garuda" | "gungnir" | "kujata" | "ramuh" | "tonberry" | "typhon" | "unicorn"
      | "alexander" | "bahamut" | "durandal" | "fenrir" | "ifrit" | "ridill" | "tiamat" | "ultima" | "valefor" | "yojimbo" | "zeromus"
      | "anima" | "asura" | "belias" | "chocobo" | "hades" | "ixion" | "mandragora" | "masamune" | "pandaemonium" | "shinryu" | "titan"
        => Some(ServerRegion::Japan),
      "cerberus" | "lich" | "louisoix" | "moogle" | "odin" | "omega" | "phoenix" | "ragnarok" | "shiva" | "zodiark"
        => Some(ServerRegion::Europe),
      _ => None
    }
  }
}

#[derive(Debug)]
enum Job {
  Astrologian,
  Bard,
  BlackMage,
  DarkKnight,
  Dragoon,
  Machinist,
  Monk,
  Ninja,
  Paladin,
  RedMage,
  Samurai,
  Scholar,
  Summoner,
  Warrior,
  WhiteMage
}

impl FromStr for Job {
  type Err = String;

  fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
    let job = match s.to_lowercase().as_str() {
      "astrologian" | "astro" | "ast" => Job::Astrologian,
      "bard" | "brd" => Job::Bard,
      "blackmage" | "black mage" | "blm" => Job::BlackMage,
      "darkknight" | "dark knight" | "drk" => Job::DarkKnight,
      "dragoon" | "lolgoon" | "loldrg" | "drg" => Job::Dragoon,
      "machinist" | "mch" => Job::Machinist,
      "monk" | "mnk" => Job::Monk,
      "ninja" | "nin" => Job::Ninja,
      "paladin" | "pld" => Job::Paladin,
      "redmage" | "red mage" | "rdm" => Job::RedMage,
      "samurai" | "sam" => Job::Samurai,
      "scholar" | "sch" => Job::Scholar,
      "summoner" | "smn" => Job::Summoner,
      "warrior" | "war" | "best" => Job::Warrior,
      "whitemage" | "white mage" | "whm" => Job::WhiteMage,
      x => return Err(format!("Unknown job: {}", x))
    };
    Ok(job)
  }
}

impl ToString for Job {
  fn to_string(&self) -> String {
    match *self {
      Job::Astrologian => "Astrologian",
      Job::Bard => "Bard",
      Job::BlackMage => "Black Mage",
      Job::DarkKnight => "Dark Knight",
      Job::Dragoon => "Dragoon",
      Job::Machinist => "Machinist",
      Job::Monk => "Monk",
      Job::Ninja => "Ninja",
      Job::Paladin => "Paladin",
      Job::RedMage => "Red Mage",
      Job::Samurai => "Samurai",
      Job::Scholar => "Scholar",
      Job::Summoner => "Summoner",
      Job::Warrior => "Warrior",
      Job::WhiteMage => "White Mage",
    }.to_string()
  }
}
<|endoftext|>