<commit_before>use std::ops::{Range, RangeFrom, RangeTo, RangeFull};
use std::path::{Path, PathBuf};
use regex::{CaptureMatches, Captures, Regex};
use utils::fs::file_to_string;
use utils::take_lines;
use errors::*;

use super::Preprocessor;
use book::{Book, BookItem};

const ESCAPE_CHAR: char = '\\';

pub struct ReplaceAllPreprocessor {
    pub src_dir: PathBuf
}

impl Preprocessor for ReplaceAllPreprocessor {
    fn run(&self, book: &mut Book) -> Result<()> {
        for section in &mut book.sections {
            match *section {
                BookItem::Chapter(ref mut ch) => {
                    let content = ::std::mem::replace(&mut ch.content, String::new());
                    let base = ch.path.parent()
                        .map(|dir| self.src_dir.join(dir))
                        .ok_or_else(|| String::from("Invalid bookitem path!"))?;
                    ch.content = replace_all(&content, base)?
                }
                _ => {}
            }
        }

        Ok(())
    }
}

fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> {
    // When replacing one thing in a string by something with a different length,
    // the indices after that will not correspond,
    // we therefore have to store the difference to correct this
    let mut previous_end_index = 0;
    let mut replaced = String::new();

    for playpen in find_links(s) {
        replaced.push_str(&s[previous_end_index..playpen.start_index]);
        replaced.push_str(&playpen.render_with_path(&path)?);
        previous_end_index = playpen.end_index;
    }

    replaced.push_str(&s[previous_end_index..]);
    Ok(replaced)
}

#[derive(PartialEq, Debug, Clone)]
enum LinkType<'a> {
    Escaped,
    IncludeRange(PathBuf, Range<usize>),
    IncludeRangeFrom(PathBuf, RangeFrom<usize>),
    IncludeRangeTo(PathBuf, RangeTo<usize>),
    IncludeRangeFull(PathBuf, RangeFull),
    Playpen(PathBuf, Vec<&'a str>),
}

fn parse_include_path(path: &str) -> LinkType<'static> {
    let mut parts = path.split(':');
    let path = parts.next().unwrap().into();
    let start = parts.next().and_then(|s| s.parse::<usize>().ok());
    let end = parts.next().and_then(|s| s.parse::<usize>().ok());
    match start {
        Some(start) => {
            match end {
                Some(end) => LinkType::IncludeRange(path, Range{ start: start, end: end}),
                None => LinkType::IncludeRangeFrom(path, RangeFrom{ start: start }),
            }
        }
        None => {
            match end {
                Some(end) => LinkType::IncludeRangeTo(path, RangeTo{ end: end }),
                None => LinkType::IncludeRangeFull(path, RangeFull),
            }
        }
    }
}

#[derive(PartialEq, Debug, Clone)]
struct Link<'a> {
    start_index: usize,
    end_index: usize,
    link: LinkType<'a>,
    link_text: &'a str,
}

impl<'a> Link<'a> {
    fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
        let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
            (_, Some(typ), Some(rest)) => {
                let mut path_props = rest.as_str().split_whitespace();
                let file_arg = path_props.next();
                let props: Vec<&str> = path_props.collect();

                match (typ.as_str(), file_arg) {
                    ("include", Some(pth)) => Some(parse_include_path(pth)),
                    ("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
                    _ => None,
                }
            }
            (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some(
                LinkType::Escaped,
            ),
            _ => None,
        };

        link_type.and_then(|lnk| {
            cap.get(0).map(|mat| {
                Link {
                    start_index: mat.start(),
                    end_index: mat.end(),
                    link: lnk,
                    link_text: mat.as_str(),
                }
            })
        })
    }

    fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
        let base = base.as_ref();
        match self.link {
            // omit the escape char
            LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
            LinkType::IncludeRange(ref pat, ref range) => {
                file_to_string(base.join(pat))
                    .map(|s| take_lines(&s, range.clone()))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::IncludeRangeFrom(ref pat, ref range) => {
                file_to_string(base.join(pat))
                    .map(|s| take_lines(&s, range.clone()))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::IncludeRangeTo(ref pat, ref range) => {
                file_to_string(base.join(pat))
                    .map(|s| take_lines(&s, range.clone()))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::IncludeRangeFull(ref pat, _) => {
                file_to_string(base.join(pat))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::Playpen(ref pat, ref attrs) => {
                let contents = file_to_string(base.join(pat)).chain_err(|| {
                    format!("Could not read file for link {}", self.link_text)
                })?;
                let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
                Ok(format!(
                    "```{}{}\n{}\n```\n",
                    ftype,
                    attrs.join(","),
                    contents
                ))
            }
        }
    }
}

struct LinkIter<'a>(CaptureMatches<'a, 'a>);

impl<'a> Iterator for LinkIter<'a> {
    type Item = Link<'a>;
    fn next(&mut self) -> Option<Link<'a>> {
        for cap in &mut self.0 {
            if let Some(inc) = Link::from_capture(cap) {
                return Some(inc);
            }
        }
        None
    }
}

fn find_links(contents: &str) -> LinkIter {
    // lazily compute following regex
    // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
    lazy_static! {
        static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
                    \\\{\{\#.*\}\}               # match escaped link
                    |                            # or
                    \{\{\s*                      # link opening parens and whitespace
                      \#([a-zA-Z0-9]+)           # link type
                      \s+                        # separating whitespace
                      ([a-zA-Z0-9\s_.\-:/\\]+)   # link target path and space separated properties
                    \s*\}\}                      # whitespace and link closing parens
                                 ").unwrap();
    }
    LinkIter(RE.captures_iter(contents))
}

// ---------------------------------------------------------------------------------
//      Tests
//

#[test]
fn test_find_links_no_link() {
    let s = "Some random text without link...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_partial_link() {
    let s = "Some random text with {{#playpen...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
    let s = "Some random text with {{#include...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
    let s = "Some random text with \\{{#include...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_empty_link() {
    let s = "Some random text with {{#playpen}} and {{#playpen   }} {{}} {{#}}...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_unknown_link_type() {
    let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_simple_link() {
    let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);

    assert_eq!(res,
               vec![Link {
                        start_index: 22,
                        end_index: 42,
                        link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
                        link_text: "{{#playpen file.rs}}",
                    },
                    Link {
                        start_index: 47,
                        end_index: 68,
                        link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
                        link_text: "{{#playpen test.rs }}",
                    }]);
}

#[test]
fn test_find_links_with_range() {
    let s = "Some random text with {{#include file.rs:10:20}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 48,
                link: LinkType::IncludeRange(PathBuf::from("file.rs"), 10..20),
                link_text: "{{#include file.rs:10:20}}",
            },
        ]
    );
}

#[test]
fn test_find_links_with_from_range() {
    let s = "Some random text with {{#include file.rs:10:}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 46,
                link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 10..),
                link_text: "{{#include file.rs:10:}}",
            },
        ]
    );
}

#[test]
fn test_find_links_with_to_range() {
    let s = "Some random text with {{#include file.rs::20}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 46,
                link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
                link_text: "{{#include file.rs::20}}",
            },
        ]
    );
}

#[test]
fn test_find_links_with_full_range() {
    let s = "Some random text with {{#include file.rs::}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 44,
                link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
                link_text: "{{#include file.rs::}}",
            },
        ]
    );
}

#[test]
fn test_find_links_escaped_link() {
    let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);

    assert_eq!(res,
               vec![Link {
                        start_index: 38,
                        end_index: 68,
                        link: LinkType::Escaped,
                        link_text: "\\{{#playpen file.rs editable}}",
                    }]);
}

#[test]
fn test_find_playpens_with_properties() {
    let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n \
             text {{#playpen my.rs editable no_run should_panic}} ...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(res,
               vec![Link {
                        start_index: 38,
                        end_index: 68,
                        link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
                        link_text: "{{#playpen file.rs editable }}",
                    },
                    Link {
                        start_index: 89,
                        end_index: 136,
                        link: LinkType::Playpen(PathBuf::from("my.rs"),
                                                vec!["editable", "no_run", "should_panic"]),
                        link_text: "{{#playpen my.rs editable no_run should_panic}}",
                    }]);
}

#[test]
fn test_find_all_link_types() {
    let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
             insignifficant in escaped link}} some more\n text  {{#playpen my.rs editable no_run \
             should_panic}} ...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(res.len(), 3);
    assert_eq!(res[0],
               Link {
                   start_index: 38,
                   end_index: 58,
                   link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
                   link_text: "{{#include file.rs}}",
               });
    assert_eq!(res[1],
               Link {
                   start_index: 63,
                   end_index: 112,
                   link: LinkType::Escaped,
                   link_text: "\\{{#contents are insignifficant in escaped link}}",
               });
    assert_eq!(res[2],
               Link {
                   start_index: 130,
                   end_index: 177,
                   link: LinkType::Playpen(PathBuf::from("my.rs"),
                                           vec!["editable", "no_run", "should_panic"]),
                   link_text: "{{#playpen my.rs editable no_run should_panic}}",
               });
}
<commit_msg>Remove unnecessary mem::replace<commit_after>use std::ops::{Range, RangeFrom, RangeTo, RangeFull};
use std::path::{Path, PathBuf};
use regex::{CaptureMatches, Captures, Regex};
use utils::fs::file_to_string;
use utils::take_lines;
use errors::*;

use super::Preprocessor;
use book::{Book, BookItem};

const ESCAPE_CHAR: char = '\\';

pub struct ReplaceAllPreprocessor {
    pub src_dir: PathBuf
}

impl Preprocessor for ReplaceAllPreprocessor {
    fn run(&self, book: &mut Book) -> Result<()> {
        for section in &mut book.sections {
            match *section {
                BookItem::Chapter(ref mut ch) => {
                    let base = ch.path.parent()
                        .map(|dir| self.src_dir.join(dir))
                        .ok_or_else(|| String::from("Invalid bookitem path!"))?;
                    let content = replace_all(&ch.content, base)?;
                    ch.content = content
                }
                _ => {}
            }
        }

        Ok(())
    }
}

fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> {
    // When replacing one thing in a string by something with a different length,
    // the indices after that will not correspond,
    // we therefore have to store the difference to correct this
    let mut previous_end_index = 0;
    let mut replaced = String::new();

    for playpen in find_links(s) {
        replaced.push_str(&s[previous_end_index..playpen.start_index]);
        replaced.push_str(&playpen.render_with_path(&path)?);
        previous_end_index = playpen.end_index;
    }

    replaced.push_str(&s[previous_end_index..]);
    Ok(replaced)
}

#[derive(PartialEq, Debug, Clone)]
enum LinkType<'a> {
    Escaped,
    IncludeRange(PathBuf, Range<usize>),
    IncludeRangeFrom(PathBuf, RangeFrom<usize>),
    IncludeRangeTo(PathBuf, RangeTo<usize>),
    IncludeRangeFull(PathBuf, RangeFull),
    Playpen(PathBuf, Vec<&'a str>),
}

fn parse_include_path(path: &str) -> LinkType<'static> {
    let mut parts = path.split(':');
    let path = parts.next().unwrap().into();
    let start = parts.next().and_then(|s| s.parse::<usize>().ok());
    let end = parts.next().and_then(|s| s.parse::<usize>().ok());
    match start {
        Some(start) => {
            match end {
                Some(end) => LinkType::IncludeRange(path, Range{ start: start, end: end}),
                None => LinkType::IncludeRangeFrom(path, RangeFrom{ start: start }),
            }
        }
        None => {
            match end {
                Some(end) => LinkType::IncludeRangeTo(path, RangeTo{ end: end }),
                None => LinkType::IncludeRangeFull(path, RangeFull),
            }
        }
    }
}

#[derive(PartialEq, Debug, Clone)]
struct Link<'a> {
    start_index: usize,
    end_index: usize,
    link: LinkType<'a>,
    link_text: &'a str,
}

impl<'a> Link<'a> {
    fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
        let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
            (_, Some(typ), Some(rest)) => {
                let mut path_props = rest.as_str().split_whitespace();
                let file_arg = path_props.next();
                let props: Vec<&str> = path_props.collect();

                match (typ.as_str(), file_arg) {
                    ("include", Some(pth)) => Some(parse_include_path(pth)),
                    ("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
                    _ => None,
                }
            }
            (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some(
                LinkType::Escaped,
            ),
            _ => None,
        };

        link_type.and_then(|lnk| {
            cap.get(0).map(|mat| {
                Link {
                    start_index: mat.start(),
                    end_index: mat.end(),
                    link: lnk,
                    link_text: mat.as_str(),
                }
            })
        })
    }

    fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
        let base = base.as_ref();
        match self.link {
            // omit the escape char
            LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
            LinkType::IncludeRange(ref pat, ref range) => {
                file_to_string(base.join(pat))
                    .map(|s| take_lines(&s, range.clone()))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::IncludeRangeFrom(ref pat, ref range) => {
                file_to_string(base.join(pat))
                    .map(|s| take_lines(&s, range.clone()))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::IncludeRangeTo(ref pat, ref range) => {
                file_to_string(base.join(pat))
                    .map(|s| take_lines(&s, range.clone()))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::IncludeRangeFull(ref pat, _) => {
                file_to_string(base.join(pat))
                    .chain_err(|| {
                        format!("Could not read file for link {}", self.link_text)
                    })
            }
            LinkType::Playpen(ref pat, ref attrs) => {
                let contents = file_to_string(base.join(pat)).chain_err(|| {
                    format!("Could not read file for link {}", self.link_text)
                })?;
                let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
                Ok(format!(
                    "```{}{}\n{}\n```\n",
                    ftype,
                    attrs.join(","),
                    contents
                ))
            }
        }
    }
}

struct LinkIter<'a>(CaptureMatches<'a, 'a>);

impl<'a> Iterator for LinkIter<'a> {
    type Item = Link<'a>;
    fn next(&mut self) -> Option<Link<'a>> {
        for cap in &mut self.0 {
            if let Some(inc) = Link::from_capture(cap) {
                return Some(inc);
            }
        }
        None
    }
}

fn find_links(contents: &str) -> LinkIter {
    // lazily compute following regex
    // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
    lazy_static! {
        static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
                    \\\{\{\#.*\}\}               # match escaped link
                    |                            # or
                    \{\{\s*                      # link opening parens and whitespace
                      \#([a-zA-Z0-9]+)           # link type
                      \s+                        # separating whitespace
                      ([a-zA-Z0-9\s_.\-:/\\]+)   # link target path and space separated properties
                    \s*\}\}                      # whitespace and link closing parens
                                 ").unwrap();
    }
    LinkIter(RE.captures_iter(contents))
}

// ---------------------------------------------------------------------------------
//      Tests
//

#[test]
fn test_find_links_no_link() {
    let s = "Some random text without link...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_partial_link() {
    let s = "Some random text with {{#playpen...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
    let s = "Some random text with {{#include...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
    let s = "Some random text with \\{{#include...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_empty_link() {
    let s = "Some random text with {{#playpen}} and {{#playpen   }} {{}} {{#}}...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_unknown_link_type() {
    let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
    assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}

#[test]
fn test_find_links_simple_link() {
    let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);

    assert_eq!(res,
               vec![Link {
                        start_index: 22,
                        end_index: 42,
                        link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
                        link_text: "{{#playpen file.rs}}",
                    },
                    Link {
                        start_index: 47,
                        end_index: 68,
                        link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
                        link_text: "{{#playpen test.rs }}",
                    }]);
}

#[test]
fn test_find_links_with_range() {
    let s = "Some random text with {{#include file.rs:10:20}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 48,
                link: LinkType::IncludeRange(PathBuf::from("file.rs"), 10..20),
                link_text: "{{#include file.rs:10:20}}",
            },
        ]
    );
}

#[test]
fn test_find_links_with_from_range() {
    let s = "Some random text with {{#include file.rs:10:}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 46,
                link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 10..),
                link_text: "{{#include file.rs:10:}}",
            },
        ]
    );
}

#[test]
fn test_find_links_with_to_range() {
    let s = "Some random text with {{#include file.rs::20}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 46,
                link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
                link_text: "{{#include file.rs::20}}",
            },
        ]
    );
}

#[test]
fn test_find_links_with_full_range() {
    let s = "Some random text with {{#include file.rs::}}...";
    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(
        res,
        vec![
            Link {
                start_index: 22,
                end_index: 44,
                link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
                link_text: "{{#include file.rs::}}",
            },
        ]
    );
}

#[test]
fn test_find_links_escaped_link() {
    let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);

    assert_eq!(res,
               vec![Link {
                        start_index: 38,
                        end_index: 68,
                        link: LinkType::Escaped,
                        link_text: "\\{{#playpen file.rs editable}}",
                    }]);
}

#[test]
fn test_find_playpens_with_properties() {
    let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n \
             text {{#playpen my.rs editable no_run should_panic}} ...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(res,
               vec![Link {
                        start_index: 38,
                        end_index: 68,
                        link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
                        link_text: "{{#playpen file.rs editable }}",
                    },
                    Link {
                        start_index: 89,
                        end_index: 136,
                        link: LinkType::Playpen(PathBuf::from("my.rs"),
                                                vec!["editable", "no_run", "should_panic"]),
                        link_text: "{{#playpen my.rs editable no_run should_panic}}",
                    }]);
}

#[test]
fn test_find_all_link_types() {
    let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
             insignifficant in escaped link}} some more\n text  {{#playpen my.rs editable no_run \
             should_panic}} ...";

    let res = find_links(s).collect::<Vec<_>>();
    println!("\nOUTPUT: {:?}\n", res);
    assert_eq!(res.len(), 3);
    assert_eq!(res[0],
               Link {
                   start_index: 38,
                   end_index: 58,
                   link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
                   link_text: "{{#include file.rs}}",
               });
    assert_eq!(res[1],
               Link {
                   start_index: 63,
                   end_index: 112,
                   link: LinkType::Escaped,
                   link_text: "\\{{#contents are insignifficant in escaped link}}",
               });
    assert_eq!(res[2],
               Link {
                   start_index: 130,
                   end_index: 177,
                   link: LinkType::Playpen(PathBuf::from("my.rs"),
                                           vec!["editable", "no_run", "should_panic"]),
                   link_text: "{{#playpen my.rs editable no_run should_panic}}",
               });
}
<|endoftext|>


<commit_before>use chrono::UTC;
use rustc_serialize::json::{self, Json, ToJson};

use params::*;

use rs_es::Client;
use rs_es::query::{Filter, Query};
use rs_es::units::JsonVal;
use rs_es::operations::search::{Sort, SortField, Order};
use rs_es::operations::index::IndexResult;
use rs_es::operations::bulk::{Action, BulkResult};
use rs_es::operations::mapping::*;
use rs_es::error::EsError;

extern crate searchspot;
use searchspot::terms::VectorOfTerms;

#[derive(RustcEncodable, RustcDecodable, Debug)]
pub struct Talent {
  pub id:                 u32,
  pub accepted:           bool,
  pub work_roles:         Vec<String>,
  pub work_languages:     Vec<String>,
  pub work_experience:    String,
  pub work_locations:     Vec<String>,
  pub work_authorization: String,
  pub company_ids:        Vec<u32>,
  pub batch_starts_at:    String,
  pub batch_ends_at:      String,
  pub added_to_batch_at:  String,
  pub weight:             i32,
  pub blocked_companies:  Vec<u32>
}

impl ToJson for Talent {
  fn to_json(&self) -> Json {
    json::encode(&self).unwrap()
                       .to_json()
  }
}

impl Talent {
  /// Populate the ElasticSearch index with `self`.
  pub fn index(&self, mut es: &mut Client, index: &str) -> Result<IndexResult, EsError> {
    es.index(index, "talent")
      .with_doc(&self)
      .with_id(&*self.id.to_string())
      .send()
  }

  /// Populate the ElasticSearch index with `Vec<Talent>`.
  pub fn index_many(talents: Vec<Talent>, mut es: &mut Client, index: &str) -> Result<BulkResult, EsError> {
    let actions: Vec<Action> = talents.into_iter().map(|talent| {
      let id   = &*talent.id.to_string();
      let json = json::encode(&talent).unwrap()
                                      .to_json();

      Action::index(json).with_id(id)
                         .with_index(index)
    }).collect();

    es.bulk(&actions).send()
  }

  /// Query ElasticSearch on given `indexes` and `params` and return the IDs of
  /// the found talents.
  pub fn search(mut es: &mut Client, default_index: &str, params: &Map) -> Vec<u32> {
    let now   = UTC::now().to_rfc3339();
    let epoch = match params.find(&["epoch"]) {
      Some(epoch) => String::from_value(&epoch).unwrap_or(now),
      _           => now
    };

    let index: Vec<&str> = match params.find(&["index"]) {
      Some(&Value::String(ref index)) => vec![&index[..]],
      _ => vec![default_index]
    };

    let result = es.search_query()
                   .with_indexes(&*index)
                   .with_query(&Talent::search_filters(params, &*epoch))
                   .with_sort(&Talent::sorting_criteria())
                   .with_size(1000) // TODO
                   .send();

    match result {
      Ok(result) => {
        let mut results = result.hits.hits.into_iter()
                                          .map(|hit| hit.source.unwrap()["id"]
                                                               .as_u64()
                                                               .unwrap() as u32)
                                          .collect::<Vec<u32>>();
        results.dedup();
        results
      },
      Err(err) => {
        println!("{:?}", err);
        vec![]
      }
    }
  }

  /// Reset the given index. All the data will be destroyed and then the index
  /// will be created again. The map that will be used is hardcoded.
  pub fn reset_index(mut es: &mut Client, index: &str) -> Result<MappingResult, EsError> {
    let mapping = hashmap! {
      "talent" => hashmap! {
        "id" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        },

        "work_roles" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_languages" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_experience" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_locations" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_authorization" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "company_ids" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        },

        "accepted" => hashmap! {
          "type" => "boolean",
          "index" => "not_analyzed"
        },

        "batch_starts_at" => hashmap! {
          "type" => "date",
          "format" => "strict_date_optional_time",
          "index" => "not_analyzed"
        },

        "batch_ends_at" => hashmap! {
          "type" => "date",
          "format" => "strict_date_optional_time",
          "index" => "not_analyzed"
        },

        "added_to_batch_at" => hashmap! {
          "type" => "date",
          "format" => "strict_date_optional_time",
          "index" => "not_analyzed"
        },

        "weight" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        },

        "blocked_companies" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        }
      }
    };

    es.delete_index(index)
      .unwrap();

    MappingOperation::new(&mut es, index, &mapping).send()
  }

  /// Return a `Vec<Filter>` with visibility criteria for the talents.
  /// The `epoch` must be given as `I64` (UNIX time in seconds) and is
  /// the range in which batches are searched.
  /// If `presented_talents` is provided, talents who match the IDs
  /// contained there skip the standard visibility criteria.
  ///
  /// Basically, the talents must be accepted into the platform and must be
  /// inside a living batch to match the visibility criteria.
  fn visibility_filters(epoch: &str, presented_talents: Vec<i32>) -> Vec<Filter> {
    let visibility_rules = Filter::build_bool()
                                  .with_must(
                                    vec![
                                      Filter::build_term("accepted", true)
                                             .build(),
                                      Filter::build_range("batch_starts_at")
                                             .with_lte(JsonVal::from(epoch))
                                             .with_format("strict_date_optional_time")
                                             .build(),
                                      Filter::build_range("batch_ends_at")
                                             .with_gte(JsonVal::from(epoch))
                                             .with_format("strict_date_optional_time")
                                             .build()
                                    ])
                                  .build();

    if presented_talents.len() > 0 { // preferred over !_.is_empty()
      let presented_talents_filters = Filter::build_bool()
                                             .with_must(
                                               vec![
                                                 <Filter as VectorOfTerms<i32>>::build_terms(
                                                   "ids", &presented_talents)
                                               ].into_iter()
                                                .flat_map(|x| x)
                                                .collect::<Vec<Filter>>())
                                             .build();
      vec![
        Filter::build_bool()
               .with_should(vec![visibility_rules, presented_talents_filters])
               .build()
      ]
    }
    else {
      vec![visibility_rules]
    }
  }

  /// Given parameters inside the query string mapped inside a `Map`,
  /// and the `epoch` (defined as UNIX time in seconds) for batches,
  /// return a `Query` for ElasticSearch.
  ///
  /// `VectorOfTerms` are ORred, while `Filter`s are ANDed.
  /// I.e.: given ["Fullstack", "DevOps"] as `work_roles`, found talents
  /// will present at least one of these roles), but both `work_roles`
  /// and `work_languages`, if provided, must not be empty.
  fn search_filters(params: &Map, epoch: &str) -> Query {
    let company_id = i32_vec_from_params!(params, "company_id");

    Query::build_filtered(Filter::build_bool()
                                 .with_must(
                                   vec![
                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_roles", &vec_from_params!(params, "work_roles")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_languages", &vec_from_params!(params, "work_languages")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_experience", &string_vec_from_params!(params, "work_experience")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                      "work_authorization", &string_vec_from_params!(params, "work_authorization")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_locations", &vec_from_params!(params, "work_locations")),

                                     Talent::visibility_filters(epoch,
                                       i32_vec_from_params!(params, "presented_talents"))
                                   ].into_iter()
                                    .flat_map(|x| x)
                                    .collect::<Vec<Filter>>())
                                 .with_must_not(
                                   vec![
                                     <Filter as VectorOfTerms<i32>>::build_terms(
                                       "company_ids", &company_id),

                                     <Filter as VectorOfTerms<i32>>::build_terms(
                                       "blocked_companies", &company_id)
                                   ].into_iter()
                                    .flat_map(|x| x)
                                    .collect::<Vec<Filter>>())
                                 .build())
          .build()
  }

  /// Return a `Sort` that makes values be sorted for given fields, descendently.
  fn sorting_criteria() -> Sort {
    Sort::new(
      vec![
        SortField::new("batch_starts_at",   Some(Order::Desc)).build(),
        SortField::new("weight",            Some(Order::Desc)).build(),
        SortField::new("added_to_batch_at", Some(Order::Desc)).build()
      ])
  }
}

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

  use rs_es::Client;

  use params::*;

  extern crate searchspot;
  use searchspot::config::*;

  use resources::user::Talent;

  const CONFIG_FILE: &'static str = "examples/tests.toml";

  lazy_static! {
    static ref config: Config = Config::from_file(CONFIG_FILE.to_owned());
  }

  pub fn make_client() -> Client {
    Client::new(&*config.es.host, config.es.port)
  }

  macro_rules! epoch_from_year {
    ($year:expr) => {
      UTC.datetime_from_str(&format!("{}-01-01 12:00:00", $year),
        "%Y-%m-%d %H:%M:%S").unwrap().to_rfc3339()
    }
  }

  pub fn populate_index(mut client: &mut Client) -> bool {
    vec![
      Talent {
        id:                 1,
        accepted:           true,
        work_roles:         vec![],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization: "yes".to_owned(),
        company_ids:        vec![],
        batch_starts_at:    epoch_from_year!("2006"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2006"),
        weight:             -5,
        blocked_companies:  vec![]
      },

      Talent {
        id:                 2,
        accepted:           true,
        work_roles:         vec![],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization: "yes".to_owned(),
        company_ids:        vec![],
        batch_starts_at:    epoch_from_year!("2006"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2006"),
        weight:             6,
        blocked_companies:  vec![]
      },

      Talent {
        id:                 3,
        accepted:           false,
        work_roles:         vec![],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization: "yes".to_owned(),
        company_ids:        vec![],
        batch_starts_at:    epoch_from_year!("2007"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2011"),
        weight:             6,
        blocked_companies:  vec![]
      },

      Talent {
        id:                 4,
        accepted:           true,
        work_roles:         vec!["Fullstack".to_owned(), "DevOps".to_owned()],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization:  "yes".to_owned(),
        company_ids:        vec![6],
        batch_starts_at:    epoch_from_year!("2008"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2011"),
        weight:             0,
        blocked_companies:  vec![]
      }
    ].iter()
     .map(|talent| talent.index(&mut client, &config.es.index)
                         .is_ok())
     .collect::<Vec<bool>>()
     .into_iter()
     .all(|result| result)
  }

  fn refresh_index(mut client: &mut Client) {
    client.refresh()
          .with_indexes(&[&config.es.index])
          .send()
          .unwrap();
  }

  #[test]
  fn test_search() {
    let mut client = make_client();
    Talent::reset_index(&mut client, &*config.es.index).unwrap();
    refresh_index(&mut client);
    assert!(populate_index(&mut client));
    refresh_index(&mut client);

    // no parameters are given
    {
      let results = Talent::search(&mut client, &*config.es.index, &Map::new());
      assert_eq!(vec![4, 2, 1], results);
    }

    // a non existing index is given
    {
      let mut map = Map::new();
      map.assign("index", Value::String("lololol".to_owned())).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert!(results.is_empty());
    }

    // a date that doesn't match given indexes is given
    {
      let mut map = Map::new();
      map.assign("epoch", Value::String(epoch_from_year!("2040"))).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert!(results.is_empty());
    }

    {
      let mut map = Map::new();
      map.assign("work_roles[]", Value::String("Fullstack".to_owned())).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert_eq!(vec![4], results);
    }

    // filtering for given company_id
    {
      let mut map = Map::new();
      map.assign("company_id", Value::String("6".into())).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert_eq!(vec![2, 1], results);
    }
  }
}
<commit_msg>resources/user: remove index_many() and move to dateOptionalTime<commit_after>use chrono::UTC;
use rustc_serialize::json::{self, Json, ToJson};

use params::*;

use rs_es::Client;
use rs_es::query::{Filter, Query};
use rs_es::units::JsonVal;
use rs_es::operations::search::{Sort, SortField, Order};
use rs_es::operations::index::IndexResult;
use rs_es::operations::mapping::*;
use rs_es::error::EsError;

extern crate searchspot;
use searchspot::terms::VectorOfTerms;

#[derive(RustcEncodable, RustcDecodable, Debug)]
pub struct Talent {
  pub id:                 u32,
  pub accepted:           bool,
  pub work_roles:         Vec<String>,
  pub work_languages:     Vec<String>,
  pub work_experience:    String,
  pub work_locations:     Vec<String>,
  pub work_authorization: String,
  pub company_ids:        Vec<u32>,
  pub batch_starts_at:    String,
  pub batch_ends_at:      String,
  pub added_to_batch_at:  String,
  pub weight:             i32,
  pub blocked_companies:  Vec<u32>
}

impl ToJson for Talent {
  fn to_json(&self) -> Json {
    json::encode(&self).unwrap()
                       .to_json()
  }
}

impl Talent {
  /// Populate the ElasticSearch index with `self`.
  // I'm having problems with bulk actions. Let's wait for the next iteration.
  pub fn index(&self, mut es: &mut Client, index: &str) -> Result<IndexResult, EsError> {
    es.index(index, "talent")
      .with_doc(&self)
      .with_id(&*self.id.to_string())
      .send()
  }

  /// Query ElasticSearch on given `indexes` and `params` and return the IDs of
  /// the found talents.
  pub fn search(mut es: &mut Client, default_index: &str, params: &Map) -> Vec<u32> {
    let now   = UTC::now().to_rfc3339();
    let epoch = match params.find(&["epoch"]) {
      Some(epoch) => String::from_value(&epoch).unwrap_or(now),
      _           => now
    };

    let index: Vec<&str> = match params.find(&["index"]) {
      Some(&Value::String(ref index)) => vec![&index[..]],
      _ => vec![default_index]
    };

    let result = es.search_query()
                   .with_indexes(&*index)
                   .with_query(&Talent::search_filters(params, &*epoch))
                   .with_sort(&Talent::sorting_criteria())
                   .with_size(1000) // TODO
                   .send();

    match result {
      Ok(result) => {
        let mut results = result.hits.hits.into_iter()
                                          .map(|hit| hit.source.unwrap()["id"]
                                                               .as_u64()
                                                               .unwrap() as u32)
                                          .collect::<Vec<u32>>();
        results.dedup();
        results
      },
      Err(err) => {
        println!("{:?}", err);
        vec![]
      }
    }
  }

  /// Reset the given index. All the data will be destroyed and then the index
  /// will be created again. The map that will be used is hardcoded.
  pub fn reset_index(mut es: &mut Client, index: &str) -> Result<MappingResult, EsError> {
    let mapping = hashmap! {
      "talent" => hashmap! {
        "id" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        },

        "work_roles" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_languages" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_experience" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_locations" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "work_authorization" => hashmap! {
          "type" => "string",
          "index" => "not_analyzed"
        },

        "company_ids" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        },

        "accepted" => hashmap! {
          "type" => "boolean",
          "index" => "not_analyzed"
        },

        "batch_starts_at" => hashmap! {
          "type" => "date",
          "format" => "dateOptionalTime",
          "index" => "not_analyzed"
        },

        "batch_ends_at" => hashmap! {
          "type" => "date",
          "format" => "dateOptionalTime",
          "index" => "not_analyzed"
        },

        "added_to_batch_at" => hashmap! {
          "type" => "date",
          "format" => "dateOptionalTime",
          "index" => "not_analyzed"
        },

        "weight" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        },

        "blocked_companies" => hashmap! {
          "type" => "integer",
          "index" => "not_analyzed"
        }
      }
    };

    es.delete_index(index);

    MappingOperation::new(&mut es, index, &mapping).send()
  }

  /// Return a `Vec<Filter>` with visibility criteria for the talents.
  /// The `epoch` must be given as `I64` (UNIX time in seconds) and is
  /// the range in which batches are searched.
  /// If `presented_talents` is provided, talents who match the IDs
  /// contained there skip the standard visibility criteria.
  ///
  /// Basically, the talents must be accepted into the platform and must be
  /// inside a living batch to match the visibility criteria.
  fn visibility_filters(epoch: &str, presented_talents: Vec<i32>) -> Vec<Filter> {
    let visibility_rules = Filter::build_bool()
                                  .with_must(
                                    vec![
                                      Filter::build_term("accepted", true)
                                             .build(),
                                      Filter::build_range("batch_starts_at")
                                             .with_lte(JsonVal::from(epoch))
                                             .with_format("dateOptionalTime")
                                             .build(),
                                      Filter::build_range("batch_ends_at")
                                             .with_gte(JsonVal::from(epoch))
                                             .with_format("dateOptionalTime")
                                             .build()
                                    ])
                                  .build();

    if presented_talents.len() > 0 { // preferred over !_.is_empty()
      let presented_talents_filters = Filter::build_bool()
                                             .with_must(
                                               vec![
                                                 <Filter as VectorOfTerms<i32>>::build_terms(
                                                   "ids", &presented_talents)
                                               ].into_iter()
                                                .flat_map(|x| x)
                                                .collect::<Vec<Filter>>())
                                             .build();
      vec![
        Filter::build_bool()
               .with_should(vec![visibility_rules, presented_talents_filters])
               .build()
      ]
    }
    else {
      vec![visibility_rules]
    }
  }

  /// Given parameters inside the query string mapped inside a `Map`,
  /// and the `epoch` (defined as UNIX time in seconds) for batches,
  /// return a `Query` for ElasticSearch.
  ///
  /// `VectorOfTerms` are ORred, while `Filter`s are ANDed.
  /// I.e.: given ["Fullstack", "DevOps"] as `work_roles`, found talents
  /// will present at least one of these roles), but both `work_roles`
  /// and `work_languages`, if provided, must not be empty.
  fn search_filters(params: &Map, epoch: &str) -> Query {
    let company_id = i32_vec_from_params!(params, "company_id");

    Query::build_filtered(Filter::build_bool()
                                 .with_must(
                                   vec![
                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_roles", &vec_from_params!(params, "work_roles")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_languages", &vec_from_params!(params, "work_languages")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_experience", &string_vec_from_params!(params, "work_experience")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                      "work_authorization", &string_vec_from_params!(params, "work_authorization")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                       "work_locations", &vec_from_params!(params, "work_locations")),

                                     Talent::visibility_filters(epoch,
                                       i32_vec_from_params!(params, "presented_talents"))
                                   ].into_iter()
                                    .flat_map(|x| x)
                                    .collect::<Vec<Filter>>())
                                 .with_must_not(
                                   vec![
                                     <Filter as VectorOfTerms<i32>>::build_terms(
                                       "company_ids", &company_id),

                                     <Filter as VectorOfTerms<i32>>::build_terms(
                                       "blocked_companies", &company_id)
                                   ].into_iter()
                                    .flat_map(|x| x)
                                    .collect::<Vec<Filter>>())
                                 .build())
          .build()
  }

  /// Return a `Sort` that makes values be sorted for given fields, descendently.
  fn sorting_criteria() -> Sort {
    Sort::new(
      vec![
        SortField::new("batch_starts_at",   Some(Order::Desc)).build(),
        SortField::new("weight",            Some(Order::Desc)).build(),
        SortField::new("added_to_batch_at", Some(Order::Desc)).build()
      ])
  }
}

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

  use rs_es::Client;

  use params::*;

  extern crate searchspot;
  use searchspot::config::*;

  use resources::user::Talent;

  const CONFIG_FILE: &'static str = "examples/tests.toml";

  lazy_static! {
    static ref config: Config = Config::from_file(CONFIG_FILE.to_owned());
  }

  pub fn make_client() -> Client {
    Client::new(&*config.es.host, config.es.port)
  }

  macro_rules! epoch_from_year {
    ($year:expr) => {
      UTC.datetime_from_str(&format!("{}-01-01 12:00:00", $year),
        "%Y-%m-%d %H:%M:%S").unwrap().to_rfc3339()
    }
  }

  pub fn populate_index(mut client: &mut Client) -> bool {
    vec![
      Talent {
        id:                 1,
        accepted:           true,
        work_roles:         vec![],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization: "yes".to_owned(),
        company_ids:        vec![],
        batch_starts_at:    epoch_from_year!("2006"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2006"),
        weight:             -5,
        blocked_companies:  vec![]
      },

      Talent {
        id:                 2,
        accepted:           true,
        work_roles:         vec![],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization: "yes".to_owned(),
        company_ids:        vec![],
        batch_starts_at:    epoch_from_year!("2006"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2006"),
        weight:             6,
        blocked_companies:  vec![]
      },

      Talent {
        id:                 3,
        accepted:           false,
        work_roles:         vec![],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization: "yes".to_owned(),
        company_ids:        vec![],
        batch_starts_at:    epoch_from_year!("2007"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2011"),
        weight:             6,
        blocked_companies:  vec![]
      },

      Talent {
        id:                 4,
        accepted:           true,
        work_roles:         vec!["Fullstack".to_owned(), "DevOps".to_owned()],
        work_languages:     vec![],
        work_experience:    "1..2".to_owned(),
        work_locations:     vec!["Berlin".to_owned()],
        work_authorization:  "yes".to_owned(),
        company_ids:        vec![6],
        batch_starts_at:    epoch_from_year!("2008"),
        batch_ends_at:      epoch_from_year!("2020"),
        added_to_batch_at:  epoch_from_year!("2011"),
        weight:             0,
        blocked_companies:  vec![]
      }
    ].iter()
     .map(|talent| talent.index(&mut client, &config.es.index)
                         .is_ok())
     .collect::<Vec<bool>>()
     .into_iter()
     .all(|result| result)
  }

  fn refresh_index(mut client: &mut Client) {
    client.refresh()
          .with_indexes(&[&config.es.index])
          .send()
          .unwrap();
  }

  #[test]
  fn test_search() {
    let mut client = make_client();
    assert!(Talent::reset_index(&mut client, &*config.es.index).is_ok());
    refresh_index(&mut client);
    assert!(populate_index(&mut client));
    refresh_index(&mut client);

    // no parameters are given
    {
      let results = Talent::search(&mut client, &*config.es.index, &Map::new());
      assert_eq!(vec![4, 2, 1], results);
    }

    // a non existing index is given
    {
      let mut map = Map::new();
      map.assign("index", Value::String("lololol".to_owned())).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert!(results.is_empty());
    }

    // a date that doesn't match given indexes is given
    {
      let mut map = Map::new();
      map.assign("epoch", Value::String(epoch_from_year!("2040"))).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert!(results.is_empty());
    }

    {
      let mut map = Map::new();
      map.assign("work_roles[]", Value::String("Fullstack".to_owned())).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert_eq!(vec![4], results);
    }

    // filtering for given company_id
    {
      let mut map = Map::new();
      map.assign("company_id", Value::String("6".into())).unwrap();

      let results = Talent::search(&mut client, &*config.es.index, &map);
      assert_eq!(vec![2, 1], results);
    }
  }
}
<|endoftext|>


<commit_before>// Copyright (c) 2017 Jason White
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

use std::fmt;
use std::path::PathBuf;

use task::Task;

/// A task that executes a single command. A command is simply a process to be
/// spawned.
#[derive(Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct Command {
    /// Process to spawn.
    args: Vec<String>,

    /// Optional working directory to spawn the process in. If `None`, uses the
    /// working directory of the parent process.
    cwd: Option<PathBuf>,

    /// String to display when executing the task. If `None`, the command
    /// arguments are displayed in full instead.
    display: Option<String>,
}

impl Command {
    #[cfg(test)]
    pub fn new(args: Vec<String>,
               cwd: Option<PathBuf>,
               display: Option<String>)
               -> Command {
        Command {
            args: args,
            cwd: cwd,
            display: display,
        }
    }
}

impl fmt::Display for Command {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if let Some(ref display) = self.display {
            write!(f, "{}", display)
        } else {
            // TODO: Display as a bash command.
            write!(f, "{:?}", self.args)
        }
    }
}

impl fmt::Debug for Command {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self.args)
    }
}

impl Task for Command {
    fn execute(&self) {
        println!("Executing `{:?}` in directory {:?}", self.args, self.cwd);
    }
}
<commit_msg>Add timeout and retries to command<commit_after>// Copyright (c) 2017 Jason White
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

use std::fmt;
use std::time;
use std::path::PathBuf;

use task::Task;

/// A task that executes a single command. A command is simply a process to be
/// spawned.
#[derive(Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct Command {
    /// Process to spawn.
    args: Vec<String>,

    /// Optional working directory to spawn the process in. If `None`, uses the
    /// working directory of the parent process (i.e., the build process).
    cwd: Option<PathBuf>,

    /// String to display when executing the task. If `None`, the command
    /// arguments are displayed in full instead.
    display: Option<String>,

    /// How much time to give the command to execute. If `None`, there is no
    /// time limit.
    timeout: Option<time::Duration>,

    /// How many times to retry the command before giving up. This is useful for
    /// flaky tests that may need to be run several times before succeeding.
    /// Between each execution, we wait a period of time. By default, an
    /// exponential backoff function is used to determine the wait duration.
    #[serde(default)]
    retries: u32,
}

impl Command {
    #[cfg(test)]
    pub fn new(args: Vec<String>,
               cwd: Option<PathBuf>,
               display: Option<String>)
               -> Command {
        Command {
            args: args,
            cwd: cwd,
            display: display,
            timeout: None,
            retries: 0,
        }
    }
}

impl fmt::Display for Command {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if let Some(ref display) = self.display {
            write!(f, "{}", display)
        } else {
            // TODO: Display as a bash command.
            write!(f, "{:?}", self.args)
        }
    }
}

impl fmt::Debug for Command {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self.args)
    }
}

impl Task for Command {
    fn execute(&self) {
        println!("Executing `{:?}` in directory {:?}", self.args, self.cwd);
    }
}
<|endoftext|>


<commit_before>#![deny(warnings)]
#![feature(asm)]
#![feature(const_fn)]

extern crate orbclient;
extern crate syscall;

use orbclient::event;

use std::{env, str};
use std::error::Error;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::process::{Command, Stdio};

use console::Console;
use getpty::getpty;

mod console;
mod getpty;

fn main() {
    let shell = env::args().nth(1).unwrap_or("sh".to_string());

    let (master_fd, tty_path) = getpty();

    let slave_stdin = File::open(&tty_path).unwrap();
    let slave_stdout = File::create(&tty_path).unwrap();
    let slave_stderr = File::create(&tty_path).unwrap();

    let width = 800;
    let height = 600;

    env::set_var("COLUMNS", format!("{}", width / 8));
    env::set_var("LINES", format!("{}", height / 16));
    env::set_var("TTY", format!("{}", tty_path.display()));

    match unsafe {
        Command::new(&shell)
            .stdin(Stdio::from_raw_fd(slave_stdin.into_raw_fd()))
            .stdout(Stdio::from_raw_fd(slave_stdout.into_raw_fd()))
            .stderr(Stdio::from_raw_fd(slave_stderr.into_raw_fd()))
            .spawn()
    } {
        Ok(mut process) => {
            {
                let mut console = Console::new(width, height);

                let mut event_file = File::open("event:").expect("terminal: failed to open event file");

                let window_fd = console.window.as_raw_fd();
                syscall::fevent(window_fd, syscall::flag::EVENT_READ).expect("terminal: failed to fevent console window");

                let mut master = unsafe { File::from_raw_fd(master_fd) };
                syscall::fevent(master_fd, syscall::flag::EVENT_READ).expect("terminal: failed to fevent master PTY");

                let mut handle_event = |event_id: usize, event_count: usize| -> bool {
                    if event_id == window_fd {
                        for event in console.window.events() {
                            if event.code == event::EVENT_QUIT {
                                return false;
                            }

                            console.input(&event);
                        }

                        if ! console.input.is_empty()  {
                            if let Err(err) = master.write(&console.input) {
                                let term_stderr = io::stderr();
                                let mut term_stderr = term_stderr.lock();

                                let _ = term_stderr.write(b"failed to write stdin: ");
                                let _ = term_stderr.write(err.description().as_bytes());
                                let _ = term_stderr.write(b"\n");
                                return false;
                            }
                            console.input.clear();
                        }
                    } else if event_id == master_fd {
                        let mut packet = [0; 4096];
                        let count = master.read(&mut packet).expect("terminal: failed to read master PTY");
                        if count == 0 {
                            if event_count == 0 {
                                return false;
                            }
                        } else {
                            console.write(&packet[1..count], true).expect("terminal: failed to write to console");

                            if packet[0] & 1 == 1 {
                                console.redraw();
                            }
                        }
                    } else {
                        println!("Unknown event {}", event_id);
                    }

                    true
                };

                handle_event(window_fd, 0);
                handle_event(master_fd, 0);

                'events: loop {
                    let mut sys_event = syscall::Event::default();
                    event_file.read(&mut sys_event).expect("terminal: failed to read event file");
                    if ! handle_event(sys_event.id, sys_event.data) {
                        break 'events;
                    }
                }
            }

            process.wait().expect("terminal: failed to wait on shell");
        },
        Err(err) => {
            let term_stderr = io::stderr();
            let mut term_stderr = term_stderr.lock();
            let _ = term_stderr.write(b"failed to execute '");
            let _ = term_stderr.write(shell.as_bytes());
            let _ = term_stderr.write(b"': ");
            let _ = term_stderr.write(err.description().as_bytes());
            let _ = term_stderr.write(b"\n");
        }
    }
}
<commit_msg>Add file_path feature<commit_after>#![deny(warnings)]
#![feature(asm)]
#![feature(const_fn)]
#![feature(file_path)]

extern crate orbclient;
extern crate syscall;

use orbclient::event;

use std::{env, str};
use std::error::Error;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::process::{Command, Stdio};

use console::Console;
use getpty::getpty;

mod console;
mod getpty;

fn main() {
    let shell = env::args().nth(1).unwrap_or("sh".to_string());

    let (master_fd, tty_path) = getpty();

    let slave_stdin = File::open(&tty_path).unwrap();
    let slave_stdout = File::create(&tty_path).unwrap();
    let slave_stderr = File::create(&tty_path).unwrap();

    let width = 800;
    let height = 600;

    env::set_var("COLUMNS", format!("{}", width / 8));
    env::set_var("LINES", format!("{}", height / 16));
    env::set_var("TTY", format!("{}", tty_path.display()));

    match unsafe {
        Command::new(&shell)
            .stdin(Stdio::from_raw_fd(slave_stdin.into_raw_fd()))
            .stdout(Stdio::from_raw_fd(slave_stdout.into_raw_fd()))
            .stderr(Stdio::from_raw_fd(slave_stderr.into_raw_fd()))
            .spawn()
    } {
        Ok(mut process) => {
            {
                let mut console = Console::new(width, height);

                let mut event_file = File::open("event:").expect("terminal: failed to open event file");

                let window_fd = console.window.as_raw_fd();
                syscall::fevent(window_fd, syscall::flag::EVENT_READ).expect("terminal: failed to fevent console window");

                let mut master = unsafe { File::from_raw_fd(master_fd) };
                syscall::fevent(master_fd, syscall::flag::EVENT_READ).expect("terminal: failed to fevent master PTY");

                let mut handle_event = |event_id: usize, event_count: usize| -> bool {
                    if event_id == window_fd {
                        for event in console.window.events() {
                            if event.code == event::EVENT_QUIT {
                                return false;
                            }

                            console.input(&event);
                        }

                        if ! console.input.is_empty()  {
                            if let Err(err) = master.write(&console.input) {
                                let term_stderr = io::stderr();
                                let mut term_stderr = term_stderr.lock();

                                let _ = term_stderr.write(b"failed to write stdin: ");
                                let _ = term_stderr.write(err.description().as_bytes());
                                let _ = term_stderr.write(b"\n");
                                return false;
                            }
                            console.input.clear();
                        }
                    } else if event_id == master_fd {
                        let mut packet = [0; 4096];
                        let count = master.read(&mut packet).expect("terminal: failed to read master PTY");
                        if count == 0 {
                            if event_count == 0 {
                                return false;
                            }
                        } else {
                            console.write(&packet[1..count], true).expect("terminal: failed to write to console");

                            if packet[0] & 1 == 1 {
                                console.redraw();
                            }
                        }
                    } else {
                        println!("Unknown event {}", event_id);
                    }

                    true
                };

                handle_event(window_fd, 0);
                handle_event(master_fd, 0);

                'events: loop {
                    let mut sys_event = syscall::Event::default();
                    event_file.read(&mut sys_event).expect("terminal: failed to read event file");
                    if ! handle_event(sys_event.id, sys_event.data) {
                        break 'events;
                    }
                }
            }

            process.wait().expect("terminal: failed to wait on shell");
        },
        Err(err) => {
            let term_stderr = io::stderr();
            let mut term_stderr = term_stderr.lock();
            let _ = term_stderr.write(b"failed to execute '");
            let _ = term_stderr.write(shell.as_bytes());
            let _ = term_stderr.write(b"': ");
            let _ = term_stderr.write(err.description().as_bytes());
            let _ = term_stderr.write(b"\n");
        }
    }
}
<|endoftext|>


<commit_before>#![cfg(windows)]

extern crate winapi;
extern crate kernel32;

use self::kernel32::*;
use self::winapi::*;
use std::ptr;
use std::iter::repeat;
use std::cmp::min;
use screen::Key;
use screen::Key::*;

use std::thread;
use std::sync::mpsc::Receiver;
use std::sync::mpsc;

macro_rules! win32 {
    ($funcall:expr) => (
        if unsafe { $funcall } == 0 {
            panic!("Win32 call failed");
        }
    );
}

pub struct Screen {
    pub height: u16,
    pub width: u16,
    pub visible_choices: u16,
    pub start_line: u16,
    original_console_mode: DWORD,
    original_colors: WORD,
    original_cp: DWORD,
    input: Receiver<u16>,
    conout: HANDLE,
}

impl Drop for Screen {
    fn drop(&mut self) {
        win32!(SetConsoleMode(self.conout, self.original_console_mode));
        win32!(SetConsoleOutputCP(self.original_cp));
    }
}

impl Screen {
    pub fn open_screen() -> Screen {
        let mut orig_mode;
        let conin: HANDLE;
        let conout: HANDLE;
        let cp: DWORD;
        unsafe {
            const OPEN_EXISTING: DWORD = 3;
            let rw_access = GENERIC_READ | GENERIC_WRITE;
            conin = CreateFileA("CONIN$\0".as_ptr() as *const i8, rw_access, FILE_SHARE_READ, ptr::null_mut(), OPEN_EXISTING, 0, ptr::null_mut());
            conout = CreateFileA("CONOUT$\0".as_ptr() as *const i8, rw_access, FILE_SHARE_READ, ptr::null_mut(), OPEN_EXISTING, 0, ptr::null_mut());
            orig_mode = ::std::mem::uninitialized();
            cp = GetConsoleOutputCP();
        }

        if conin == INVALID_HANDLE_VALUE || conout == INVALID_HANDLE_VALUE {
            panic!("Unable to open console");
        }

        let (cols, rows) = Screen::winsize(conout).unwrap();

        win32!(GetConsoleMode(conout, &mut orig_mode));
        let new_mode = orig_mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
        win32!(SetConsoleMode(conin, new_mode));
        win32!(SetConsoleOutputCP(437 as DWORD));

        let rx = Screen::spawn_input_thread(conin as usize);
        let visible_choices = min(20, rows - 1);
        let start_line = rows - visible_choices - 1;
        let original_colors = Screen::get_original_colors(conout);
        Screen {
            height: rows,
            width: cols,
            visible_choices: visible_choices,
            start_line: start_line,
            original_console_mode: orig_mode,
            original_colors: original_colors,
            original_cp: cp,
            input: rx,
            conout: conout,
        }
    }

    // We have to take the conin handle as a usize instead of a *mut c_void in order to avoid a
    // lecture from the compiler about how the latter type cannot be safely sent between threads.
    // I'm not sure if a better solution exists at this time.
    fn spawn_input_thread(conin: usize) -> Receiver<u16> {
        let (tx, rx) = mpsc::channel();

        thread::spawn(move || {
            loop {
                let conin = conin as *mut c_void;
                let mut buf: Vec<u16> = repeat(0u16).take(0x1000).collect();
                let mut chars_read: DWORD = 0;
                win32!(ReadFile(conin, buf.as_mut_ptr() as LPVOID, 1, &mut chars_read as LPDWORD, ptr::null_mut()));
                for i in 0..chars_read as usize {
                    tx.send(buf[i]).unwrap();
                }
            }
        });

        rx
    }

    pub fn move_cursor(&mut self, line: u16, column: u16) {
        win32!(SetConsoleCursorPosition(self.conout, COORD { X: column as i16, Y: line as i16}));
    }

    pub fn move_cursor_to_bottom(&mut self) {
        let end_line = self.start_line + self.visible_choices;
        self.move_cursor(end_line, 0);
        self.write("\n");
    }

    pub fn blank_screen(&mut self) {
        let start_line = self.start_line;
        self.move_cursor(start_line, 0);
        let blank_line = repeat(' ').take(self.width as usize).collect::<String>();
        for _ in 0..self.height {
            self.write(&blank_line);
        }
        self.move_cursor(start_line, 0);
    }

    pub fn show_cursor(&mut self) {
        let cursor_info = CONSOLE_CURSOR_INFO { dwSize: 100, bVisible: TRUE };
        win32!(SetConsoleCursorInfo(self.conout, &cursor_info));
    }

    pub fn hide_cursor(&mut self) {
        let cursor_info = CONSOLE_CURSOR_INFO { dwSize: 100, bVisible: FALSE };
        win32!(SetConsoleCursorInfo(self.conout, &cursor_info));
    }

    pub fn write(&mut self, s: &str) {
        let mut copy = s.to_string();
        let len: DWORD = copy.len() as DWORD;
        copy.push('\0');
        let mut bytes_written: DWORD = 0;
        win32!(WriteFile(self.conout, copy.as_ptr() as PVOID, len, &mut bytes_written as LPDWORD, ptr::null_mut()));
    }

    pub fn write_red_inverted(&mut self, s: &str) {
        let orig = self.original_colors;
        const WHITE_ON_RED: WORD = BACKGROUND_RED as WORD;
        self.set_colors(WHITE_ON_RED);
        self.write(s);
        self.set_colors(orig);
    }

    pub fn write_red(&mut self, s: &str) {
        let orig = self.original_colors;
        const RED_ON_BLACK: WORD = FOREGROUND_RED as WORD;
        self.set_colors(RED_ON_BLACK);
        self.write(s);
        self.set_colors(orig);
    }

    pub fn write_inverted(&mut self, s: &str) {
        let orig = self.original_colors;
        const BLACK_ON_WHITE: WORD = (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE) as WORD;
        self.set_colors(BLACK_ON_WHITE);
        self.write(s);
        self.set_colors(orig);
    }

    fn set_colors(&mut self, colors: WORD) {
        win32!(SetConsoleTextAttribute(self.conout, colors));
    }

    pub fn get_buffered_keys(&mut self) -> Vec<Key> {
        let mut ret = Vec::new();
        while let Ok(byte) = self.input.try_recv() {
            ret.push(Screen::translate_byte(byte));
        }
        if ret.is_empty() {
            let byte = self.input.recv().unwrap();
            ret.push(Screen::translate_byte(byte));
        }
        ret
    }

    fn translate_byte(byte: u16) -> Key {
        if byte == '\r' as u16 {
            Enter
        } else if byte == 9 {
            Tab
        } else if byte == 127 {
            Backspace
        } else if byte & 96 == 0 {
            Control(((byte + 96u16) as u8) as char)
        } else {
            Char((byte as u8) as char)
        }
    }

    fn get_original_colors(handle: HANDLE) -> WORD {
        let mut buffer_info = unsafe { ::std::mem::uninitialized() };
        win32!(GetConsoleScreenBufferInfo(handle, &mut buffer_info));
        buffer_info.wAttributes
    }

    fn winsize(conout: HANDLE) -> Option<(u16, u16)> {
        let mut buffer_info = unsafe { ::std::mem::uninitialized() };
        let result = unsafe { GetConsoleScreenBufferInfo(conout, &mut buffer_info) };
        if result != 0 {
            // This code specifically computes the size of the window,
            // *not* the size of the buffer (which is easily available
            // from dwSize). I got the algorithm from:
            //
            // http://stackoverflow.com/a/12642749
            let left = buffer_info.srWindow.Left;
            let top = buffer_info.srWindow.Top;
            let right = buffer_info.srWindow.Right;
            let bottom = buffer_info.srWindow.Bottom;
            let cols = right - left + 1;
            let rows = bottom - top + 1;
            Some((cols as u16, rows as u16))
        } else {
            None
        }
    }
}

#[test]
fn winsize_test() {
    // AppVeyor builds run without a console, making this test impossible.
    if option_env!("APPVEYOR").is_some() {
        // TODO: It should be made obvious from the output that this test was skipped
        return;
    }
    let conout = unsafe { kernel32::GetStdHandle(0xFFFFFFF5) };
    let (cols, rows) = Screen::winsize(conout).expect("Failed to get window size!");
    // We don't know the window size a priori, but we can at least
    // assert that it is within some kind of sensible range.
    assert!(cols > 40);
    assert!(rows > 40);
    assert!(cols < 1000);
    assert!(rows < 1000);
}
<commit_msg>Call WriteConsoleA instead of WriteFile<commit_after>#![cfg(windows)]

extern crate winapi;
extern crate kernel32;

use self::kernel32::*;
use self::winapi::*;
use std::ptr;
use std::iter::repeat;
use std::cmp::min;
use screen::Key;
use screen::Key::*;

use std::thread;
use std::sync::mpsc::Receiver;
use std::sync::mpsc;

macro_rules! win32 {
    ($funcall:expr) => (
        if unsafe { $funcall } == 0 {
            panic!("Win32 call failed");
        }
    );
}

pub struct Screen {
    pub height: u16,
    pub width: u16,
    pub visible_choices: u16,
    pub start_line: u16,
    original_console_mode: DWORD,
    original_colors: WORD,
    original_cp: DWORD,
    input: Receiver<u16>,
    conout: HANDLE,
}

impl Drop for Screen {
    fn drop(&mut self) {
        win32!(SetConsoleMode(self.conout, self.original_console_mode));
        win32!(SetConsoleOutputCP(self.original_cp));
    }
}

impl Screen {
    pub fn open_screen() -> Screen {
        let mut orig_mode;
        let conin: HANDLE;
        let conout: HANDLE;
        let cp: DWORD;
        unsafe {
            const OPEN_EXISTING: DWORD = 3;
            let rw_access = GENERIC_READ | GENERIC_WRITE;
            conin = CreateFileA("CONIN$\0".as_ptr() as *const i8, rw_access, FILE_SHARE_READ, ptr::null_mut(), OPEN_EXISTING, 0, ptr::null_mut());
            conout = CreateFileA("CONOUT$\0".as_ptr() as *const i8, rw_access, FILE_SHARE_READ, ptr::null_mut(), OPEN_EXISTING, 0, ptr::null_mut());
            orig_mode = ::std::mem::uninitialized();
            cp = GetConsoleOutputCP();
        }

        if conin == INVALID_HANDLE_VALUE || conout == INVALID_HANDLE_VALUE {
            panic!("Unable to open console");
        }

        let (cols, rows) = Screen::winsize(conout).unwrap();

        win32!(GetConsoleMode(conout, &mut orig_mode));
        let new_mode = orig_mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
        win32!(SetConsoleMode(conin, new_mode));
        win32!(SetConsoleOutputCP(437 as DWORD));

        let rx = Screen::spawn_input_thread(conin as usize);
        let visible_choices = min(20, rows - 1);
        let start_line = rows - visible_choices - 1;
        let original_colors = Screen::get_original_colors(conout);
        Screen {
            height: rows,
            width: cols,
            visible_choices: visible_choices,
            start_line: start_line,
            original_console_mode: orig_mode,
            original_colors: original_colors,
            original_cp: cp,
            input: rx,
            conout: conout,
        }
    }

    // We have to take the conin handle as a usize instead of a *mut c_void in order to avoid a
    // lecture from the compiler about how the latter type cannot be safely sent between threads.
    // I'm not sure if a better solution exists at this time.
    fn spawn_input_thread(conin: usize) -> Receiver<u16> {
        let (tx, rx) = mpsc::channel();

        thread::spawn(move || {
            loop {
                let conin = conin as *mut c_void;
                let mut buf: Vec<u16> = repeat(0u16).take(0x1000).collect();
                let mut chars_read: DWORD = 0;
                win32!(ReadFile(conin, buf.as_mut_ptr() as LPVOID, 1, &mut chars_read as LPDWORD, ptr::null_mut()));
                for i in 0..chars_read as usize {
                    tx.send(buf[i]).unwrap();
                }
            }
        });

        rx
    }

    pub fn move_cursor(&mut self, line: u16, column: u16) {
        win32!(SetConsoleCursorPosition(self.conout, COORD { X: column as i16, Y: line as i16}));
    }

    pub fn move_cursor_to_bottom(&mut self) {
        let end_line = self.start_line + self.visible_choices;
        self.move_cursor(end_line, 0);
        self.write("\n");
    }

    pub fn blank_screen(&mut self) {
        let start_line = self.start_line;
        self.move_cursor(start_line, 0);
        let blank_line = repeat(' ').take(self.width as usize).collect::<String>();
        for _ in 0..self.height {
            self.write(&blank_line);
        }
        self.move_cursor(start_line, 0);
    }

    pub fn show_cursor(&mut self) {
        let cursor_info = CONSOLE_CURSOR_INFO { dwSize: 100, bVisible: TRUE };
        win32!(SetConsoleCursorInfo(self.conout, &cursor_info));
    }

    pub fn hide_cursor(&mut self) {
        let cursor_info = CONSOLE_CURSOR_INFO { dwSize: 100, bVisible: FALSE };
        win32!(SetConsoleCursorInfo(self.conout, &cursor_info));
    }

    pub fn write(&mut self, s: &str) {
        let mut copy = s.to_string();
        let len: DWORD = copy.len() as DWORD;
        copy.push('\0');
        let mut bytes_written: DWORD = 0;
        win32!(WriteConsoleA(self.conout, copy.as_ptr() as PVOID, len, &mut bytes_written as LPDWORD, ptr::null_mut()));
    }

    pub fn write_red_inverted(&mut self, s: &str) {
        let orig = self.original_colors;
        const WHITE_ON_RED: WORD = BACKGROUND_RED as WORD;
        self.set_colors(WHITE_ON_RED);
        self.write(s);
        self.set_colors(orig);
    }

    pub fn write_red(&mut self, s: &str) {
        let orig = self.original_colors;
        const RED_ON_BLACK: WORD = FOREGROUND_RED as WORD;
        self.set_colors(RED_ON_BLACK);
        self.write(s);
        self.set_colors(orig);
    }

    pub fn write_inverted(&mut self, s: &str) {
        let orig = self.original_colors;
        const BLACK_ON_WHITE: WORD = (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE) as WORD;
        self.set_colors(BLACK_ON_WHITE);
        self.write(s);
        self.set_colors(orig);
    }

    fn set_colors(&mut self, colors: WORD) {
        win32!(SetConsoleTextAttribute(self.conout, colors));
    }

    pub fn get_buffered_keys(&mut self) -> Vec<Key> {
        let mut ret = Vec::new();
        while let Ok(byte) = self.input.try_recv() {
            ret.push(Screen::translate_byte(byte));
        }
        if ret.is_empty() {
            let byte = self.input.recv().unwrap();
            ret.push(Screen::translate_byte(byte));
        }
        ret
    }

    fn translate_byte(byte: u16) -> Key {
        if byte == '\r' as u16 {
            Enter
        } else if byte == 9 {
            Tab
        } else if byte == 127 {
            Backspace
        } else if byte & 96 == 0 {
            Control(((byte + 96u16) as u8) as char)
        } else {
            Char((byte as u8) as char)
        }
    }

    fn get_original_colors(handle: HANDLE) -> WORD {
        let mut buffer_info = unsafe { ::std::mem::uninitialized() };
        win32!(GetConsoleScreenBufferInfo(handle, &mut buffer_info));
        buffer_info.wAttributes
    }

    fn winsize(conout: HANDLE) -> Option<(u16, u16)> {
        let mut buffer_info = unsafe { ::std::mem::uninitialized() };
        let result = unsafe { GetConsoleScreenBufferInfo(conout, &mut buffer_info) };
        if result != 0 {
            // This code specifically computes the size of the window,
            // *not* the size of the buffer (which is easily available
            // from dwSize). I got the algorithm from:
            //
            // http://stackoverflow.com/a/12642749
            let left = buffer_info.srWindow.Left;
            let top = buffer_info.srWindow.Top;
            let right = buffer_info.srWindow.Right;
            let bottom = buffer_info.srWindow.Bottom;
            let cols = right - left + 1;
            let rows = bottom - top + 1;
            Some((cols as u16, rows as u16))
        } else {
            None
        }
    }
}

#[test]
fn winsize_test() {
    // AppVeyor builds run without a console, making this test impossible.
    if option_env!("APPVEYOR").is_some() {
        // TODO: It should be made obvious from the output that this test was skipped
        return;
    }
    let conout = unsafe { kernel32::GetStdHandle(0xFFFFFFF5) };
    let (cols, rows) = Screen::winsize(conout).expect("Failed to get window size!");
    // We don't know the window size a priori, but we can at least
    // assert that it is within some kind of sensible range.
    assert!(cols > 40);
    assert!(rows > 40);
    assert!(cols < 1000);
    assert!(rows < 1000);
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! Windows implementation of `tutil::screen`.
//!
//! Currently there are no tests written for this implementation.

use super::{Width, Height};

use winapi::{HANDLE, STD_OUTPUT_HANDLE, COORD, SMALL_RECT, CONSOLE_SCREEN_BUFFER_INFO};
use kernel32::{GetStdHandle, GetConsoleScreenBufferInfo};

/// Returns the terminal screen size.
///
/// Returns `None` if the screen size is `(0, 0)` or is not able to be
/// determined.
pub fn size() -> Option<(Width, Height)> {
    // Retrieve a handle to STDOUT.
    let handle: HANDLE = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };

    // An empty COORD struct for use with CONSOLE_SCREEN_BUFFER_INFO.
    let coord = COORD { X: 0, Y: 0 };

    // An empty CONSOLE_SCREEN_BUFFER_INFO struct for use with
    // GetConsoleScreenBufferInfo.
    let mut csbi = CONSOLE_SCREEN_BUFFER_INFO {
        // A COORD structure that contains the size of the console screen
        // buffer, in character columns and rows.
        dwSize: coord.clone(),

        // A COORD structure that contains the column and row coordinates of
        // the cursor in the console screen buffer.
        dwCursorPosition: coord.clone(),

        // The attributes of the characters written to a screen buffer by the
        // WriteFile and WriteConsole functions, or echoed to a screen buffer
        // by the ReadFile and ReadConsole functions.
        wAttributes: 0,

        // A SMALL_RECT structure that contains the console screen buffer
        // coordinates of the upper-left and lower-right corners of the
        // display window.
        srWindow: SMALL_RECT{ Left: 0, Top: 0, Right: 0, Bottom: 0 },

        // A COORD structure that contains the maximum size of the console
        // window, in character columns and rows, given the current screen
        // buffer size and font and the screen size.
        dwMaximumWindowSize: coord,
    };

    let success: bool = unsafe {
        GetConsoleScreenBufferInfo(handle, &mut csbi) != 0
    };

    if success {
        let width = Width((csbi.srWindow.Right - csbi.srWindow.Left + 1) as u16);
        let height = Height((csbi.srWindow.Bottom - csbi.srWindow.Top + 1) as u16);
        Some((width, height))
    } else {
        None
    }
}

/// Returns the terminal screen width.
///
/// Returns `None` if the terminal width is detected as being <= 0 columns or is
/// not able to be determined at all.
pub fn width() -> Option<Width> {
    let size = size();

    if let Some((Width(width), Height(_))) = size {
        Some(Width(width))
    } else {
        None
    }
}

/// Returns the terminal height.
///
/// Returns `None` if the terminal height is detected as being <= 0 rows or is
/// not able to be determined at all.
pub fn height() -> Option<Height> {
    let size = size();

    if let Some((Width(_), Height(height))) = size {
        Some(Height(height))
    } else {
        None
    }
}

#[cfg(test)]
mod test {
    // TODO: Write tests.
}
<commit_msg>Remove unneeded .clones()<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! Windows implementation of `tutil::screen`.
//!
//! Currently there are no tests written for this implementation.

use super::{Width, Height};

use winapi::{HANDLE, STD_OUTPUT_HANDLE, COORD, SMALL_RECT, CONSOLE_SCREEN_BUFFER_INFO};
use kernel32::{GetStdHandle, GetConsoleScreenBufferInfo};

/// Returns the terminal screen size.
///
/// Returns `None` if the screen size is `(0, 0)` or is not able to be
/// determined.
pub fn size() -> Option<(Width, Height)> {
    // Retrieve a handle to STDOUT.
    let handle: HANDLE = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };

    // An empty COORD struct for use with CONSOLE_SCREEN_BUFFER_INFO.
    let coord = COORD { X: 0, Y: 0 };

    // An empty CONSOLE_SCREEN_BUFFER_INFO struct for use with
    // GetConsoleScreenBufferInfo.
    let mut csbi = CONSOLE_SCREEN_BUFFER_INFO {
        // A COORD structure that contains the size of the console screen
        // buffer, in character columns and rows.
        dwSize: coord,

        // A COORD structure that contains the column and row coordinates of
        // the cursor in the console screen buffer.
        dwCursorPosition: coord,

        // The attributes of the characters written to a screen buffer by the
        // WriteFile and WriteConsole functions, or echoed to a screen buffer
        // by the ReadFile and ReadConsole functions.
        wAttributes: 0,

        // A SMALL_RECT structure that contains the console screen buffer
        // coordinates of the upper-left and lower-right corners of the
        // display window.
        srWindow: SMALL_RECT{ Left: 0, Top: 0, Right: 0, Bottom: 0 },

        // A COORD structure that contains the maximum size of the console
        // window, in character columns and rows, given the current screen
        // buffer size and font and the screen size.
        dwMaximumWindowSize: coord,
    };

    let success: bool = unsafe {
        GetConsoleScreenBufferInfo(handle, &mut csbi) != 0
    };

    if success {
        let width = Width((csbi.srWindow.Right - csbi.srWindow.Left + 1) as u16);
        let height = Height((csbi.srWindow.Bottom - csbi.srWindow.Top + 1) as u16);
        Some((width, height))
    } else {
        None
    }
}

/// Returns the terminal screen width.
///
/// Returns `None` if the terminal width is detected as being <= 0 columns or is
/// not able to be determined at all.
pub fn width() -> Option<Width> {
    let size = size();

    if let Some((Width(width), Height(_))) = size {
        Some(Width(width))
    } else {
        None
    }
}

/// Returns the terminal height.
///
/// Returns `None` if the terminal height is detected as being <= 0 rows or is
/// not able to be determined at all.
pub fn height() -> Option<Height> {
    let size = size();

    if let Some((Width(_), Height(height))) = size {
        Some(Height(height))
    } else {
        None
    }
}

#[cfg(test)]
mod test {
    // TODO: Write tests.
}
<|endoftext|>