<commit_before>//! The **Details** output view displays each file as a row in a table.
//!
//! It's used in the following situations:
//!
//! - Most commonly, when using the `--long` command-line argument to display the
//!   details of each file, which requires using a table view to hold all the data;
//! - When using the `--tree` argument, which uses the same table view to display
//!   each file on its own line, with the table providing the tree characters;
//! - When using both the `--long` and `--grid` arguments, which constructs a
//!   series of tables to fit all the data on the screen.
//!
//! You will probably recognise it from the `ls --long` command. It looks like
//! this:
//!
//! ```text
//!     .rw-r--r--  9.6k ben 29 Jun 16:16 Cargo.lock
//!     .rw-r--r--   547 ben 23 Jun 10:54 Cargo.toml
//!     .rw-r--r--  1.1k ben 23 Nov  2014 LICENCE
//!     .rw-r--r--  2.5k ben 21 May 14:38 README.md
//!     .rw-r--r--  382k ben  8 Jun 21:00 screenshot.png
//!     drwxr-xr-x     - ben 29 Jun 14:50 src
//!     drwxr-xr-x     - ben 28 Jun 19:53 target
//! ```
//!
//! The table is constructed by creating a `Table` value, which produces a `Row`
//! value for each file. These rows can contain a vector of `Cell`s, or they can
//! contain depth information for the tree view, or both. These are described
//! below.
//!
//!
//! ## Constructing Detail Views
//!
//! When using the `--long` command-line argument, the details of each file are
//! displayed next to its name.
//!
//! The table holds a vector of all the column types. For each file and column, a
//! `Cell` value containing the ANSI-coloured text and Unicode width of each cell
//! is generated, with the row and column determined by indexing into both arrays.
//!
//! The column types vector does not actually include the filename. This is
//! because the filename is always the rightmost field, and as such, it does not
//! need to have its width queried or be padded with spaces.
//!
//! To illustrate the above:
//!
//! ```text
//!     ┌─────────────────────────────────────────────────────────────────────────┐
//!     │ columns: [ Permissions,  Size,   User,  Date(Modified) ]                │
//!     ├─────────────────────────────────────────────────────────────────────────┤
//!     │   rows:  cells:                                            filename:    │
//!     │   row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ]   Cargo.lock   │
//!     │   row 2: [ ".rw-r--r--",  "547", "ben", "23 Jun 10:54" ]   Cargo.toml   │
//!     │   row 3: [ "drwxr-xr-x",    "-", "ben", "29 Jun 14:50" ]   src          │
//!     │   row 4: [ "drwxr-xr-x",    "-", "ben", "28 Jun 19:53" ]   target       │
//!     └─────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! Each column in the table needs to be resized to fit its widest argument. This
//! means that we must wait until every row has been added to the table before it
//! can be displayed, in order to make sure that every column is wide enough.

use std::io::{Write, Error as IOError, Result as IOResult};
use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;

use fs::{Dir, File};
use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions};
use output::colours::Colours;
use output::column::Columns;
use output::cell::TextCell;
use output::tree::TreeTrunk;
use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};

/// With the **Details** view, the output gets formatted into columns, with
/// each `Column` object showing some piece of information about the file,
/// such as its size, or its permissions.
///
/// To do this, the results have to be written to a table, instead of
/// displaying each file immediately. Then, the width of each column can be
/// calculated based on the individual results, and the fields are padded
/// during output.
///
/// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row.
#[derive(PartialEq, Debug, Clone, Default)]
pub struct Options {

    /// A Columns object that says which columns should be included in the
    /// output in the general case. Directories themselves can pick which
    /// columns are *added* to this list, such as the Git column.
    pub columns: Option<Columns>,

    /// Whether to show a header line or not.
    pub header: bool,

    /// Whether to show each file's extended attributes.
    pub xattr: bool,
}

pub struct Render<'a> {
    pub dir: Option<&'a Dir>,
    pub files: Vec<File<'a>>,
    pub colours: &'a Colours,
    pub classify: Classify,
    pub opts: &'a Options,

    /// Whether to recurse through directories with a tree view, and if so,
    /// which options to use. This field is only relevant here if the `tree`
    /// field of the RecurseOptions is `true`.
    pub recurse: Option<RecurseOptions>,

    /// How to sort and filter the files after getting their details.
    pub filter: &'a FileFilter,
}

struct Egg<'a> {
    table_row: Option<TableRow>,
    xattrs:    Vec<Attribute>,
    errors:    Vec<(IOError, Option<PathBuf>)>,
    dir:       Option<Dir>,
    file:      &'a File<'a>,
}

impl<'a> AsRef<File<'a>> for Egg<'a> {
    fn as_ref(&self) -> &File<'a> {
        self.file
    }
}

impl<'a> Render<'a> {
    pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
        let mut rows = Vec::new();

        if let Some(columns) = self.opts.columns {
            let env = Environment::default();
            let colz = columns.for_dir(self.dir);
            let mut table = Table::new(&colz, &self.colours, &env);

            if self.opts.header {
                let header = table.header_row();
                table.add_widths(&header);
                rows.push(self.render_header(header));
            }

            let mut table = Some(table);
            self.add_files_to_table(&mut table, &mut rows, &self.files, 0);

            for row in self.iterate_with_table(table.unwrap(), rows) {
                writeln!(w, "{}", row.strings())?
            }
        }
        else {
            self.add_files_to_table(&mut None, &mut rows, &self.files, 0);

            for row in self.iterate(rows) {
                writeln!(w, "{}", row.strings())?
            }
        }

        Ok(())
    }

    /// Adds files to the table, possibly recursively. This is easily
    /// parallelisable, and uses a pool of threads.
    fn add_files_to_table<'dir>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: usize) {
        use num_cpus;
        use scoped_threadpool::Pool;
        use std::sync::{Arc, Mutex};
        use fs::feature::xattr;

        let mut pool = Pool::new(num_cpus::get() as u32);
        let mut file_eggs = Vec::new();

        pool.scoped(|scoped| {
            let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
            let table = table.as_ref();

            for file in src {
                let file_eggs = file_eggs.clone();

                scoped.execute(move || {
                    let mut errors = Vec::new();
                    let mut xattrs = Vec::new();

                    if xattr::ENABLED {
                        match file.path.attributes() {
                            Ok(xs) => xattrs.extend(xs),
                            Err(e) => errors.push((e, None)),
                        };
                    }

                    let table_row = table.as_ref().map(|t| t.row_for_file(&file, !xattrs.is_empty()));

                    if !self.opts.xattr {
                        xattrs.clear();
                    }

                    let mut dir = None;

                    if let Some(r) = self.recurse {
                        if file.is_directory() && r.tree && !r.is_too_deep(depth) {
                            if let Ok(d) = file.to_dir(false) {
                                dir = Some(d);
                            }
                        }
                    };

                    let egg = Egg { table_row, xattrs, errors, dir, file };
                    file_eggs.lock().unwrap().push(egg);
                });
            }
        });

        self.filter.sort_files(&mut file_eggs);

        let num_eggs = file_eggs.len();
        for (index, egg) in file_eggs.into_iter().enumerate() {
            let mut files = Vec::new();
            let mut errors = egg.errors;

            if let (Some(ref mut t), Some(ref row)) = (table.as_mut(), egg.table_row.as_ref()) {
                t.add_widths(row);
            }

            let row = Row {
                depth:    depth,
                cells:    egg.table_row,
                name:     FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, self.colours).paint().promote(),
                last:     index == num_eggs - 1,
            };

            rows.push(row);

            if let Some(ref dir) = egg.dir {
                for file_to_add in dir.files(self.filter.dot_filter) {
                    match file_to_add {
                        Ok(f)          => files.push(f),
                        Err((path, e)) => errors.push((e, Some(path)))
                    }
                }

                self.filter.filter_child_files(&mut files);

                if !files.is_empty() {
                    for xattr in egg.xattrs {
                        rows.push(self.render_xattr(xattr, depth + 1, false));
                    }

                    for (error, path) in errors {
                        rows.push(self.render_error(&error, depth + 1, false, path));
                    }

                    self.add_files_to_table(table, rows, &files, depth + 1);
                    continue;
                }
            }

            let count = egg.xattrs.len();
            for (index, xattr) in egg.xattrs.into_iter().enumerate() {
                rows.push(self.render_xattr(xattr, depth + 1, errors.is_empty() && index == count - 1));
            }

            let count = errors.len();
            for (index, (error, path)) in errors.into_iter().enumerate() {
                rows.push(self.render_error(&error, depth + 1, index == count - 1, path));
            }
        }
    }

    pub fn render_header(&self, header: TableRow) -> Row {
        Row {
            depth:    0,
            cells:    Some(header),
            name:     TextCell::paint_str(self.colours.header, "Name"),
            last:     false,
        }
    }

    fn render_error(&self, error: &IOError, depth: usize, last: bool, path: Option<PathBuf>) -> Row {
        let error_message = match path {
            Some(path) => format!("<{}: {}>", path.display(), error),
            None       => format!("<{}>", error),
        };

        Row {
            depth:    depth,
            cells:    None,
            name:     TextCell::paint(self.colours.broken_arrow, error_message),
            last:     last,
        }
    }

    fn render_xattr(&self, xattr: Attribute, depth: usize, last: bool) -> Row {
        Row {
            depth:    depth,
            cells:    None,
            name:     TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)),
            last:     last,
        }
    }

    pub fn render_file(&self, cells: TableRow, name_cell: TextCell, depth: usize, last: bool) -> Row {
        Row {
            depth:  depth,
            cells:  Some(cells),
            name:   name_cell,
            last:   last,
        }
    }

    pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
        TableIter {
            tree_trunk: TreeTrunk::default(),
            total_width: table.widths().total(),
            table: table,
            inner: rows.into_iter(),
            colours: self.colours,
        }
    }

    pub fn iterate(&'a self, rows: Vec<Row>) -> Iter<'a> {
        Iter {
            tree_trunk: TreeTrunk::default(),
            inner: rows.into_iter(),
            colours: self.colours,
        }
    }
}

pub struct TableIter<'a> {
    table: Table<'a>,
    tree_trunk: TreeTrunk,
    total_width: usize,
    colours: &'a Colours,
    inner: VecIntoIter<Row>,
}

impl<'a> Iterator for TableIter<'a> {
    type Item = TextCell;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|row| {
            let mut cell =
                if let Some(cells) = row.cells {
                    self.table.render(cells)
                }
                else {
                    let mut cell = TextCell::default();
                    cell.add_spaces(self.total_width);
                    cell
                };

            for tree_part in self.tree_trunk.new_row(row.depth, row.last) {
                cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
            }

            // If any tree characters have been printed, then add an extra
            // space, which makes the output look much better.
            if row.depth != 0 {
                cell.add_spaces(1);
            }

            cell.append(row.name);
            cell
        })
    }
}

pub struct Row {

    /// Vector of cells to display.
    ///
    /// Most of the rows will be used to display files' metadata, so this will
    /// almost always be `Some`, containing a vector of cells. It will only be
    /// `None` for a row displaying an attribute or error, neither of which
    /// have cells.
    pub cells: Option<TableRow>,

    /// This file's name, in coloured output. The name is treated separately
    /// from the other cells, as it never requires padding.
    pub name: TextCell,

    /// How many directories deep into the tree structure this is. Directories
    /// on top have depth 0.
    pub depth: usize,

    /// Whether this is the last entry in the directory. This flag is used
    /// when calculating the tree view.
    pub last: bool,
}

pub struct Iter<'a> {
    tree_trunk: TreeTrunk,
    colours: &'a Colours,
    inner: VecIntoIter<Row>,
}

impl<'a> Iterator for Iter<'a> {
    type Item = TextCell;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|row| {
            let mut cell = TextCell::default();

            for tree_part in self.tree_trunk.new_row(row.depth, row.last) {
                cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
            }

            // If any tree characters have been printed, then add an extra
            // space, which makes the output look much better.
            if row.depth != 0 {
                cell.add_spaces(1);
            }

            cell.append(row.name);
            cell
        })
    }
}
<commit_msg>I give up<commit_after>//! The **Details** output view displays each file as a row in a table.
//!
//! It's used in the following situations:
//!
//! - Most commonly, when using the `--long` command-line argument to display the
//!   details of each file, which requires using a table view to hold all the data;
//! - When using the `--tree` argument, which uses the same table view to display
//!   each file on its own line, with the table providing the tree characters;
//! - When using both the `--long` and `--grid` arguments, which constructs a
//!   series of tables to fit all the data on the screen.
//!
//! You will probably recognise it from the `ls --long` command. It looks like
//! this:
//!
//! ```text
//!     .rw-r--r--  9.6k ben 29 Jun 16:16 Cargo.lock
//!     .rw-r--r--   547 ben 23 Jun 10:54 Cargo.toml
//!     .rw-r--r--  1.1k ben 23 Nov  2014 LICENCE
//!     .rw-r--r--  2.5k ben 21 May 14:38 README.md
//!     .rw-r--r--  382k ben  8 Jun 21:00 screenshot.png
//!     drwxr-xr-x     - ben 29 Jun 14:50 src
//!     drwxr-xr-x     - ben 28 Jun 19:53 target
//! ```
//!
//! The table is constructed by creating a `Table` value, which produces a `Row`
//! value for each file. These rows can contain a vector of `Cell`s, or they can
//! contain depth information for the tree view, or both. These are described
//! below.
//!
//!
//! ## Constructing Detail Views
//!
//! When using the `--long` command-line argument, the details of each file are
//! displayed next to its name.
//!
//! The table holds a vector of all the column types. For each file and column, a
//! `Cell` value containing the ANSI-coloured text and Unicode width of each cell
//! is generated, with the row and column determined by indexing into both arrays.
//!
//! The column types vector does not actually include the filename. This is
//! because the filename is always the rightmost field, and as such, it does not
//! need to have its width queried or be padded with spaces.
//!
//! To illustrate the above:
//!
//! ```text
//!     ┌─────────────────────────────────────────────────────────────────────────┐
//!     │ columns: [ Permissions,  Size,   User,  Date(Modified) ]                │
//!     ├─────────────────────────────────────────────────────────────────────────┤
//!     │   rows:  cells:                                            filename:    │
//!     │   row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ]   Cargo.lock   │
//!     │   row 2: [ ".rw-r--r--",  "547", "ben", "23 Jun 10:54" ]   Cargo.toml   │
//!     │   row 3: [ "drwxr-xr-x",    "-", "ben", "29 Jun 14:50" ]   src          │
//!     │   row 4: [ "drwxr-xr-x",    "-", "ben", "28 Jun 19:53" ]   target       │
//!     └─────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! Each column in the table needs to be resized to fit its widest argument. This
//! means that we must wait until every row has been added to the table before it
//! can be displayed, in order to make sure that every column is wide enough.

use std::io::{Write, Error as IOError, Result as IOResult};
use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;

use fs::{Dir, File};
use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions};
use output::colours::Colours;
use output::column::Columns;
use output::cell::TextCell;
use output::tree::TreeTrunk;
use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};

/// With the **Details** view, the output gets formatted into columns, with
/// each `Column` object showing some piece of information about the file,
/// such as its size, or its permissions.
///
/// To do this, the results have to be written to a table, instead of
/// displaying each file immediately. Then, the width of each column can be
/// calculated based on the individual results, and the fields are padded
/// during output.
///
/// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row.
#[derive(PartialEq, Debug, Clone, Default)]
pub struct Options {

    /// A Columns object that says which columns should be included in the
    /// output in the general case. Directories themselves can pick which
    /// columns are *added* to this list, such as the Git column.
    pub columns: Option<Columns>,

    /// Whether to show a header line or not.
    pub header: bool,

    /// Whether to show each file's extended attributes.
    pub xattr: bool,
}

pub struct Render<'a> {
    pub dir: Option<&'a Dir>,
    pub files: Vec<File<'a>>,
    pub colours: &'a Colours,
    pub classify: Classify,
    pub opts: &'a Options,

    /// Whether to recurse through directories with a tree view, and if so,
    /// which options to use. This field is only relevant here if the `tree`
    /// field of the RecurseOptions is `true`.
    pub recurse: Option<RecurseOptions>,

    /// How to sort and filter the files after getting their details.
    pub filter: &'a FileFilter,
}

struct Egg<'a> {
    table_row: Option<TableRow>,
    xattrs:    Vec<Attribute>,
    errors:    Vec<(IOError, Option<PathBuf>)>,
    dir:       Option<Dir>,
    file:      &'a File<'a>,
}

impl<'a> AsRef<File<'a>> for Egg<'a> {
    fn as_ref(&self) -> &File<'a> {
        self.file
    }
}

impl<'a> Render<'a> {
    pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
        let mut rows = Vec::new();

        if let Some(columns) = self.opts.columns {
            let env = Environment::default();
            let colz = columns.for_dir(self.dir);
            let mut table = Table::new(&colz, &self.colours, &env);

            if self.opts.header {
                let header = table.header_row();
                table.add_widths(&header);
                rows.push(self.render_header(header));
            }

            // This is weird, but I can't find a way around it:
            // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
            let mut table = Some(table);
            self.add_files_to_table(&mut table, &mut rows, &self.files, 0);

            for row in self.iterate_with_table(table.unwrap(), rows) {
                writeln!(w, "{}", row.strings())?
            }
        }
        else {
            self.add_files_to_table(&mut None, &mut rows, &self.files, 0);

            for row in self.iterate(rows) {
                writeln!(w, "{}", row.strings())?
            }
        }

        Ok(())
    }

    /// Adds files to the table, possibly recursively. This is easily
    /// parallelisable, and uses a pool of threads.
    fn add_files_to_table<'dir>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: usize) {
        use num_cpus;
        use scoped_threadpool::Pool;
        use std::sync::{Arc, Mutex};
        use fs::feature::xattr;

        let mut pool = Pool::new(num_cpus::get() as u32);
        let mut file_eggs = Vec::new();

        pool.scoped(|scoped| {
            let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
            let table = table.as_ref();

            for file in src {
                let file_eggs = file_eggs.clone();

                scoped.execute(move || {
                    let mut errors = Vec::new();
                    let mut xattrs = Vec::new();

                    if xattr::ENABLED {
                        match file.path.attributes() {
                            Ok(xs) => xattrs.extend(xs),
                            Err(e) => errors.push((e, None)),
                        };
                    }

                    let table_row = table.as_ref().map(|t| t.row_for_file(&file, !xattrs.is_empty()));

                    if !self.opts.xattr {
                        xattrs.clear();
                    }

                    let mut dir = None;

                    if let Some(r) = self.recurse {
                        if file.is_directory() && r.tree && !r.is_too_deep(depth) {
                            if let Ok(d) = file.to_dir(false) {
                                dir = Some(d);
                            }
                        }
                    };

                    let egg = Egg { table_row, xattrs, errors, dir, file };
                    file_eggs.lock().unwrap().push(egg);
                });
            }
        });

        self.filter.sort_files(&mut file_eggs);

        let num_eggs = file_eggs.len();
        for (index, egg) in file_eggs.into_iter().enumerate() {
            let mut files = Vec::new();
            let mut errors = egg.errors;

            if let (Some(ref mut t), Some(ref row)) = (table.as_mut(), egg.table_row.as_ref()) {
                t.add_widths(row);
            }

            let row = Row {
                depth:    depth,
                cells:    egg.table_row,
                name:     FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, self.colours).paint().promote(),
                last:     index == num_eggs - 1,
            };

            rows.push(row);

            if let Some(ref dir) = egg.dir {
                for file_to_add in dir.files(self.filter.dot_filter) {
                    match file_to_add {
                        Ok(f)          => files.push(f),
                        Err((path, e)) => errors.push((e, Some(path)))
                    }
                }

                self.filter.filter_child_files(&mut files);

                if !files.is_empty() {
                    for xattr in egg.xattrs {
                        rows.push(self.render_xattr(xattr, depth + 1, false));
                    }

                    for (error, path) in errors {
                        rows.push(self.render_error(&error, depth + 1, false, path));
                    }

                    self.add_files_to_table(table, rows, &files, depth + 1);
                    continue;
                }
            }

            let count = egg.xattrs.len();
            for (index, xattr) in egg.xattrs.into_iter().enumerate() {
                rows.push(self.render_xattr(xattr, depth + 1, errors.is_empty() && index == count - 1));
            }

            let count = errors.len();
            for (index, (error, path)) in errors.into_iter().enumerate() {
                rows.push(self.render_error(&error, depth + 1, index == count - 1, path));
            }
        }
    }

    pub fn render_header(&self, header: TableRow) -> Row {
        Row {
            depth:    0,
            cells:    Some(header),
            name:     TextCell::paint_str(self.colours.header, "Name"),
            last:     false,
        }
    }

    fn render_error(&self, error: &IOError, depth: usize, last: bool, path: Option<PathBuf>) -> Row {
        let error_message = match path {
            Some(path) => format!("<{}: {}>", path.display(), error),
            None       => format!("<{}>", error),
        };

        Row {
            depth:    depth,
            cells:    None,
            name:     TextCell::paint(self.colours.broken_arrow, error_message),
            last:     last,
        }
    }

    fn render_xattr(&self, xattr: Attribute, depth: usize, last: bool) -> Row {
        Row {
            depth:    depth,
            cells:    None,
            name:     TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)),
            last:     last,
        }
    }

    pub fn render_file(&self, cells: TableRow, name_cell: TextCell, depth: usize, last: bool) -> Row {
        Row {
            depth:  depth,
            cells:  Some(cells),
            name:   name_cell,
            last:   last,
        }
    }

    pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
        TableIter {
            tree_trunk: TreeTrunk::default(),
            total_width: table.widths().total(),
            table: table,
            inner: rows.into_iter(),
            colours: self.colours,
        }
    }

    pub fn iterate(&'a self, rows: Vec<Row>) -> Iter<'a> {
        Iter {
            tree_trunk: TreeTrunk::default(),
            inner: rows.into_iter(),
            colours: self.colours,
        }
    }
}

pub struct TableIter<'a> {
    table: Table<'a>,
    tree_trunk: TreeTrunk,
    total_width: usize,
    colours: &'a Colours,
    inner: VecIntoIter<Row>,
}

impl<'a> Iterator for TableIter<'a> {
    type Item = TextCell;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|row| {
            let mut cell =
                if let Some(cells) = row.cells {
                    self.table.render(cells)
                }
                else {
                    let mut cell = TextCell::default();
                    cell.add_spaces(self.total_width);
                    cell
                };

            for tree_part in self.tree_trunk.new_row(row.depth, row.last) {
                cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
            }

            // If any tree characters have been printed, then add an extra
            // space, which makes the output look much better.
            if row.depth != 0 {
                cell.add_spaces(1);
            }

            cell.append(row.name);
            cell
        })
    }
}

pub struct Row {

    /// Vector of cells to display.
    ///
    /// Most of the rows will be used to display files' metadata, so this will
    /// almost always be `Some`, containing a vector of cells. It will only be
    /// `None` for a row displaying an attribute or error, neither of which
    /// have cells.
    pub cells: Option<TableRow>,

    /// This file's name, in coloured output. The name is treated separately
    /// from the other cells, as it never requires padding.
    pub name: TextCell,

    /// How many directories deep into the tree structure this is. Directories
    /// on top have depth 0.
    pub depth: usize,

    /// Whether this is the last entry in the directory. This flag is used
    /// when calculating the tree view.
    pub last: bool,
}

pub struct Iter<'a> {
    tree_trunk: TreeTrunk,
    colours: &'a Colours,
    inner: VecIntoIter<Row>,
}

impl<'a> Iterator for Iter<'a> {
    type Item = TextCell;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|row| {
            let mut cell = TextCell::default();

            for tree_part in self.tree_trunk.new_row(row.depth, row.last) {
                cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
            }

            // If any tree characters have been printed, then add an extra
            // space, which makes the output look much better.
            if row.depth != 0 {
                cell.add_spaces(1);
            }

            cell.append(row.name);
            cell
        })
    }
}
<|endoftext|>


<commit_before>//! Module for parsing query text pieces into DICOM queries.

use std::str::FromStr;

use dicom_core::header::HasLength;
use dicom_core::DataDictionary;
use dicom_core::DataElement;
use dicom_core::PrimitiveValue;
use dicom_core::Tag;
use dicom_core::VR;
use dicom_dictionary_std::StandardDataDictionary;
use dicom_object::InMemDicomObject;
use snafu::whatever;
use snafu::{OptionExt, ResultExt, Whatever};

#[derive(Debug, Clone, Eq, Hash, PartialEq)]
struct TermQuery {
    field: Tag,
    match_value: String,
}

/// Term queries can be parsed with the syntax `«tag»=«value»`.
///
impl FromStr for TermQuery {
    type Err = Whatever;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut parts = s.split('=');

        let tag_part = parts.next().whatever_context("empty query")?;
        let value_part = parts.next().unwrap_or_default();

        let field: Tag = tag_part.parse().or_else(|_| {
            // look for tag in standard data dictionary
            let data_entry = StandardDataDictionary
                .by_name(tag_part)
                .whatever_context("could not resolve query field name")?;
            Ok(data_entry.tag.inner())
        })?;

        Ok(TermQuery {
            field,
            match_value: value_part.to_owned(),
        })
    }
}

pub fn parse_queries<T>(qs: &[T]) -> Result<InMemDicomObject, Whatever>
where
    T: AsRef<str>,
{
    let mut obj = InMemDicomObject::new_empty();

    for q in qs {
        let term_query: TermQuery = q.as_ref().parse()?;
        obj.put(term_to_element(term_query.field, &term_query.match_value)?);
    }
    Ok(obj)
}

fn term_to_element<I, O>(tag: Tag, txt_value: &str) -> Result<DataElement<I, O>, Whatever>
where
    I: HasLength,
{
    let vr = {
        StandardDataDictionary
            .by_tag(tag)
            .map(|e| e.vr)
            .unwrap_or(VR::LO)
    };
    let value = match vr {
        VR::AE
        | VR::AS
        | VR::CS
        | VR::DA
        | VR::DS
        | VR::IS
        | VR::LO
        | VR::LT
        | VR::SH
        | VR::PN
        | VR::ST
        | VR::TM
        | VR::UI
        | VR::UC
        | VR::UR
        | VR::UT
        | VR::DT => PrimitiveValue::from(txt_value),
        VR::AT => whatever!("Unsupported VR AT"),
        VR::OB => whatever!("Unsupported VR OB"),
        VR::OD => whatever!("Unsupported VR OD"),
        VR::OF => whatever!("Unsupported VR OF"),
        VR::OL => whatever!("Unsupported VR OL"),
        VR::OV => whatever!("Unsupported VR OV"),
        VR::OW => whatever!("Unsupported VR OW"),
        VR::UN => whatever!("Unsupported VR UN"),
        VR::SQ => whatever!("Unsuppoted sequence-based query"),
        VR::SS => {
            let ss: i16 = txt_value
                .parse()
                .whatever_context("Failed to parse value as SS")?;
            PrimitiveValue::from(ss)
        }
        VR::SL => {
            let sl: i32 = txt_value
                .parse()
                .whatever_context("Failed to parse value as SL")?;
            PrimitiveValue::from(sl)
        }
        VR::SV => {
            let sv: i64 = txt_value
                .parse()
                .whatever_context("Failed to parse value as SV")?;
            PrimitiveValue::from(sv)
        }
        VR::US => {
            let us: u16 = txt_value
                .parse()
                .whatever_context("Failed to parse value as US")?;
            PrimitiveValue::from(us)
        }
        VR::UL => {
            let ul: u32 = txt_value
                .parse()
                .whatever_context("Failed to parse value as UL")?;
            PrimitiveValue::from(ul)
        }
        VR::UV => {
            let uv: u64 = txt_value
                .parse()
                .whatever_context("Failed to parse value as UV")?;
            PrimitiveValue::from(uv)
        }
        VR::FL => {
            let fl: f32 = txt_value
                .parse()
                .whatever_context("Failed to parse value as FL")?;
            PrimitiveValue::from(fl)
        }
        VR::FD => {
            let fd: f64 = txt_value
                .parse()
                .whatever_context("Failed to parse value as FD")?;
            PrimitiveValue::from(fd)
        }
    };
    Ok(DataElement::new(tag, vr, value))
}
<commit_msg>[findscu] Update documentation of query.rs<commit_after>//! Module for parsing query text pieces into DICOM queries.

use std::str::FromStr;

use dicom_core::header::HasLength;
use dicom_core::DataDictionary;
use dicom_core::DataElement;
use dicom_core::PrimitiveValue;
use dicom_core::Tag;
use dicom_core::VR;
use dicom_dictionary_std::StandardDataDictionary;
use dicom_object::InMemDicomObject;
use snafu::whatever;
use snafu::{OptionExt, ResultExt, Whatever};

#[derive(Debug, Clone, Eq, Hash, PartialEq)]
struct TermQuery {
    field: Tag,
    match_value: String,
}

/// Term queries can be parsed with the syntax `«tag»=«value»`,
/// where `«tag»` is either a DICOM tag group-element pair
/// or the respective tag keyword,
/// and `=«value»` is optional.
impl FromStr for TermQuery {
    type Err = Whatever;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut parts = s.split('=');

        let tag_part = parts.next().whatever_context("empty query")?;
        let value_part = parts.next().unwrap_or_default();

        let field: Tag = tag_part.parse().or_else(|_| {
            // look for tag in standard data dictionary
            let data_entry = StandardDataDictionary
                .by_name(tag_part)
                .whatever_context("could not resolve query field name")?;
            Ok(data_entry.tag.inner())
        })?;

        Ok(TermQuery {
            field,
            match_value: value_part.to_owned(),
        })
    }
}

pub fn parse_queries<T>(qs: &[T]) -> Result<InMemDicomObject, Whatever>
where
    T: AsRef<str>,
{
    let mut obj = InMemDicomObject::new_empty();

    for q in qs {
        let term_query: TermQuery = q.as_ref().parse()?;
        obj.put(term_to_element(term_query.field, &term_query.match_value)?);
    }
    Ok(obj)
}

fn term_to_element<I, O>(tag: Tag, txt_value: &str) -> Result<DataElement<I, O>, Whatever>
where
    I: HasLength,
{
    let vr = {
        StandardDataDictionary
            .by_tag(tag)
            .map(|e| e.vr)
            .unwrap_or(VR::LO)
    };
    let value = match vr {
        VR::AE
        | VR::AS
        | VR::CS
        | VR::DA
        | VR::DS
        | VR::IS
        | VR::LO
        | VR::LT
        | VR::SH
        | VR::PN
        | VR::ST
        | VR::TM
        | VR::UI
        | VR::UC
        | VR::UR
        | VR::UT
        | VR::DT => PrimitiveValue::from(txt_value),
        VR::AT => whatever!("Unsupported VR AT"),
        VR::OB => whatever!("Unsupported VR OB"),
        VR::OD => whatever!("Unsupported VR OD"),
        VR::OF => whatever!("Unsupported VR OF"),
        VR::OL => whatever!("Unsupported VR OL"),
        VR::OV => whatever!("Unsupported VR OV"),
        VR::OW => whatever!("Unsupported VR OW"),
        VR::UN => whatever!("Unsupported VR UN"),
        VR::SQ => whatever!("Unsuppoted sequence-based query"),
        VR::SS => {
            let ss: i16 = txt_value
                .parse()
                .whatever_context("Failed to parse value as SS")?;
            PrimitiveValue::from(ss)
        }
        VR::SL => {
            let sl: i32 = txt_value
                .parse()
                .whatever_context("Failed to parse value as SL")?;
            PrimitiveValue::from(sl)
        }
        VR::SV => {
            let sv: i64 = txt_value
                .parse()
                .whatever_context("Failed to parse value as SV")?;
            PrimitiveValue::from(sv)
        }
        VR::US => {
            let us: u16 = txt_value
                .parse()
                .whatever_context("Failed to parse value as US")?;
            PrimitiveValue::from(us)
        }
        VR::UL => {
            let ul: u32 = txt_value
                .parse()
                .whatever_context("Failed to parse value as UL")?;
            PrimitiveValue::from(ul)
        }
        VR::UV => {
            let uv: u64 = txt_value
                .parse()
                .whatever_context("Failed to parse value as UV")?;
            PrimitiveValue::from(uv)
        }
        VR::FL => {
            let fl: f32 = txt_value
                .parse()
                .whatever_context("Failed to parse value as FL")?;
            PrimitiveValue::from(fl)
        }
        VR::FD => {
            let fd: f64 = txt_value
                .parse()
                .whatever_context("Failed to parse value as FD")?;
            PrimitiveValue::from(fd)
        }
    };
    Ok(DataElement::new(tag, vr, value))
}
<|endoftext|>


<commit_before>use std::slice;
use std::str;
use libc::c_char;

use objc::{Id, ShareId};

use INSObject;

pub trait INSCopying : INSObject {
    type Output: INSObject;

    fn copy(&self) -> ShareId<Self::Output> {
        unsafe {
            let obj: *mut Self::Output = msg_send![self, copy];
            Id::from_retained_ptr(obj)
        }
    }
}

pub trait INSMutableCopying : INSObject {
    type Output: INSObject;

    fn mutable_copy(&self) -> Id<Self::Output> {
        unsafe {
            let obj: *mut Self::Output = msg_send![self, mutableCopy];
            Id::from_retained_ptr(obj)
        }
    }
}

static UTF8_ENCODING: usize = 4;

pub trait INSString : INSObject {
    fn len(&self) -> usize {
        unsafe {
            msg_send![self, lengthOfBytesUsingEncoding:UTF8_ENCODING]
        }
    }

    fn as_str(&self) -> &str {
        let bytes = unsafe {
            let bytes: *const c_char = msg_send![self, UTF8String];
            bytes as *const u8
        };
        let len = self.len();
        unsafe {
            let bytes = slice::from_raw_parts(bytes, len);
            str::from_utf8(bytes).unwrap()
        }
    }

    fn from_str(string: &str) -> Id<Self> {
        let cls = <Self as INSObject>::class();
        unsafe {
            let obj: *mut Self = msg_send![cls, alloc];
            let obj: *mut Self = msg_send![obj, initWithBytes:string.as_ptr()
                                                       length:string.len()
                                                     encoding:UTF8_ENCODING];
            Id::from_retained_ptr(obj)
        }
    }
}

object_struct!(NSString);

impl INSString for NSString { }

impl INSCopying for NSString {
    type Output = NSString;
}

impl Str for NSString {
    fn as_slice(&self) -> &str {
        self.as_str()
    }
}

#[cfg(test)]
mod tests {
    use objc::Id;
    use super::{INSCopying, INSString, NSString};

    #[test]
    fn test_utf8() {
        let expected = "ประเทศไทย中华Việt Nam";
        let s: Id<NSString> = INSString::from_str(expected);
        assert!(s.len() == expected.len());
        assert!(s.as_str() == expected);
    }

    #[test]
    fn test_interior_nul() {
        let expected = "Hello\0World";
        let s: Id<NSString> = INSString::from_str(expected);
        assert!(s.len() == expected.len());
        assert!(s.as_str() == expected);
    }

    #[test]
    fn test_copy() {
        let s: Id<NSString> = INSString::from_str("Hello!");
        let copied = s.copy();
        assert!(copied.as_str() == s.as_str());
    }
}
<commit_msg>Convert a static to a const.<commit_after>use std::slice;
use std::str;
use libc::c_char;

use objc::{Id, ShareId};

use INSObject;

pub trait INSCopying : INSObject {
    type Output: INSObject;

    fn copy(&self) -> ShareId<Self::Output> {
        unsafe {
            let obj: *mut Self::Output = msg_send![self, copy];
            Id::from_retained_ptr(obj)
        }
    }
}

pub trait INSMutableCopying : INSObject {
    type Output: INSObject;

    fn mutable_copy(&self) -> Id<Self::Output> {
        unsafe {
            let obj: *mut Self::Output = msg_send![self, mutableCopy];
            Id::from_retained_ptr(obj)
        }
    }
}

const UTF8_ENCODING: usize = 4;

pub trait INSString : INSObject {
    fn len(&self) -> usize {
        unsafe {
            msg_send![self, lengthOfBytesUsingEncoding:UTF8_ENCODING]
        }
    }

    fn as_str(&self) -> &str {
        let bytes = unsafe {
            let bytes: *const c_char = msg_send![self, UTF8String];
            bytes as *const u8
        };
        let len = self.len();
        unsafe {
            let bytes = slice::from_raw_parts(bytes, len);
            str::from_utf8(bytes).unwrap()
        }
    }

    fn from_str(string: &str) -> Id<Self> {
        let cls = <Self as INSObject>::class();
        unsafe {
            let obj: *mut Self = msg_send![cls, alloc];
            let obj: *mut Self = msg_send![obj, initWithBytes:string.as_ptr()
                                                       length:string.len()
                                                     encoding:UTF8_ENCODING];
            Id::from_retained_ptr(obj)
        }
    }
}

object_struct!(NSString);

impl INSString for NSString { }

impl INSCopying for NSString {
    type Output = NSString;
}

impl Str for NSString {
    fn as_slice(&self) -> &str {
        self.as_str()
    }
}

#[cfg(test)]
mod tests {
    use objc::Id;
    use super::{INSCopying, INSString, NSString};

    #[test]
    fn test_utf8() {
        let expected = "ประเทศไทย中华Việt Nam";
        let s: Id<NSString> = INSString::from_str(expected);
        assert!(s.len() == expected.len());
        assert!(s.as_str() == expected);
    }

    #[test]
    fn test_interior_nul() {
        let expected = "Hello\0World";
        let s: Id<NSString> = INSString::from_str(expected);
        assert!(s.len() == expected.len());
        assert!(s.as_str() == expected);
    }

    #[test]
    fn test_copy() {
        let s: Id<NSString> = INSString::from_str("Hello!");
        let copied = s.copy();
        assert!(copied.as_str() == s.as_str());
    }
}
<|endoftext|>


<commit_before>//! Flycheck provides the functionality needed to run `cargo check` or
//! another compatible command (f.x. clippy) in a background thread and provide
//! LSP diagnostics based on the output of the command.

use std::{fmt, io, process::Command, time::Duration};

use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
use paths::AbsPathBuf;
use serde::Deserialize;
use stdx::process::streaming_output;

pub use cargo_metadata::diagnostic::{
    Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
    DiagnosticSpanMacroExpansion,
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FlycheckConfig {
    CargoCommand {
        command: String,
        target_triple: Option<String>,
        all_targets: bool,
        no_default_features: bool,
        all_features: bool,
        features: Vec<String>,
        extra_args: Vec<String>,
    },
    CustomCommand {
        command: String,
        args: Vec<String>,
    },
}

impl fmt::Display for FlycheckConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
            FlycheckConfig::CustomCommand { command, args } => {
                write!(f, "{} {}", command, args.join(" "))
            }
        }
    }
}

/// Flycheck wraps the shared state and communication machinery used for
/// running `cargo check` (or other compatible command) and providing
/// diagnostics based on the output.
/// The spawned thread is shut down when this struct is dropped.
#[derive(Debug)]
pub struct FlycheckHandle {
    // XXX: drop order is significant
    sender: Sender<Restart>,
    _thread: jod_thread::JoinHandle,
}

impl FlycheckHandle {
    pub fn spawn(
        id: usize,
        sender: Box<dyn Fn(Message) + Send>,
        config: FlycheckConfig,
        workspace_root: AbsPathBuf,
    ) -> FlycheckHandle {
        let actor = FlycheckActor::new(id, sender, config, workspace_root);
        let (sender, receiver) = unbounded::<Restart>();
        let thread = jod_thread::Builder::new()
            .name("Flycheck".to_owned())
            .spawn(move || actor.run(receiver))
            .expect("failed to spawn thread");
        FlycheckHandle { sender, _thread: thread }
    }

    /// Schedule a re-start of the cargo check worker.
    pub fn update(&self) {
        self.sender.send(Restart).unwrap();
    }
}

pub enum Message {
    /// Request adding a diagnostic with fixes included to a file
    AddDiagnostic { workspace_root: AbsPathBuf, diagnostic: Diagnostic },

    /// Request check progress notification to client
    Progress {
        /// Flycheck instance ID
        id: usize,
        progress: Progress,
    },
}

impl fmt::Debug for Message {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Message::AddDiagnostic { workspace_root, diagnostic } => f
                .debug_struct("AddDiagnostic")
                .field("workspace_root", workspace_root)
                .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
                .finish(),
            Message::Progress { id, progress } => {
                f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
            }
        }
    }
}

#[derive(Debug)]
pub enum Progress {
    DidStart,
    DidCheckCrate(String),
    DidFinish(io::Result<()>),
    DidCancel,
}

struct Restart;

struct FlycheckActor {
    id: usize,
    sender: Box<dyn Fn(Message) + Send>,
    config: FlycheckConfig,
    workspace_root: AbsPathBuf,
    /// WatchThread exists to wrap around the communication needed to be able to
    /// run `cargo check` without blocking. Currently the Rust standard library
    /// doesn't provide a way to read sub-process output without blocking, so we
    /// have to wrap sub-processes output handling in a thread and pass messages
    /// back over a channel.
    cargo_handle: Option<CargoHandle>,
}

enum Event {
    Restart(Restart),
    CheckEvent(Option<CargoMessage>),
}

impl FlycheckActor {
    fn new(
        id: usize,
        sender: Box<dyn Fn(Message) + Send>,
        config: FlycheckConfig,
        workspace_root: AbsPathBuf,
    ) -> FlycheckActor {
        FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
    }
    fn progress(&self, progress: Progress) {
        self.send(Message::Progress { id: self.id, progress });
    }
    fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
        let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
        select! {
            recv(inbox) -> msg => msg.ok().map(Event::Restart),
            recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
        }
    }
    fn run(mut self, inbox: Receiver<Restart>) {
        while let Some(event) = self.next_event(&inbox) {
            match event {
                Event::Restart(Restart) => {
                    while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}

                    self.cancel_check_process();

                    let command = self.check_command();
                    tracing::info!("restart flycheck {:?}", command);
                    self.cargo_handle = Some(CargoHandle::spawn(command));
                    self.progress(Progress::DidStart);
                }
                Event::CheckEvent(None) => {
                    // Watcher finished, replace it with a never channel to
                    // avoid busy-waiting.
                    let cargo_handle = self.cargo_handle.take().unwrap();
                    let res = cargo_handle.join();
                    if res.is_err() {
                        tracing::error!(
                            "Flycheck failed to run the following command: {:?}",
                            self.check_command()
                        );
                    }
                    self.progress(Progress::DidFinish(res));
                }
                Event::CheckEvent(Some(message)) => match message {
                    CargoMessage::CompilerArtifact(msg) => {
                        self.progress(Progress::DidCheckCrate(msg.target.name));
                    }

                    CargoMessage::Diagnostic(msg) => {
                        self.send(Message::AddDiagnostic {
                            workspace_root: self.workspace_root.clone(),
                            diagnostic: msg,
                        });
                    }
                },
            }
        }
        // If we rerun the thread, we need to discard the previous check results first
        self.cancel_check_process();
    }
    fn cancel_check_process(&mut self) {
        if self.cargo_handle.take().is_some() {
            self.progress(Progress::DidCancel);
        }
    }
    fn check_command(&self) -> Command {
        let mut cmd = match &self.config {
            FlycheckConfig::CargoCommand {
                command,
                target_triple,
                no_default_features,
                all_targets,
                all_features,
                extra_args,
                features,
            } => {
                let mut cmd = Command::new(toolchain::cargo());
                cmd.arg(command);
                cmd.current_dir(&self.workspace_root);
                cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
                    .arg(self.workspace_root.join("Cargo.toml").as_os_str());

                if let Some(target) = target_triple {
                    cmd.args(&["--target", target.as_str()]);
                }
                if *all_targets {
                    cmd.arg("--all-targets");
                }
                if *all_features {
                    cmd.arg("--all-features");
                } else {
                    if *no_default_features {
                        cmd.arg("--no-default-features");
                    }
                    if !features.is_empty() {
                        cmd.arg("--features");
                        cmd.arg(features.join(" "));
                    }
                }
                cmd.args(extra_args);
                cmd
            }
            FlycheckConfig::CustomCommand { command, args } => {
                let mut cmd = Command::new(command);
                cmd.args(args);
                cmd
            }
        };
        cmd.current_dir(&self.workspace_root);
        cmd
    }

    fn send(&self, check_task: Message) {
        (self.sender)(check_task);
    }
}

struct CargoHandle {
    #[allow(unused)]
    thread: jod_thread::JoinHandle<io::Result<()>>,
    receiver: Receiver<CargoMessage>,
}

impl CargoHandle {
    fn spawn(command: Command) -> CargoHandle {
        let (sender, receiver) = unbounded();
        let actor = CargoActor::new(sender);
        let thread = jod_thread::Builder::new()
            .name("CargoHandle".to_owned())
            .spawn(move || actor.run(command))
            .expect("failed to spawn thread");
        CargoHandle { thread, receiver }
    }

    fn join(self) -> io::Result<()> {
        self.thread.join()
    }
}

struct CargoActor {
    sender: Sender<CargoMessage>,
}

impl CargoActor {
    fn new(sender: Sender<CargoMessage>) -> CargoActor {
        CargoActor { sender }
    }

    fn run(self, command: Command) -> io::Result<()> {
        // We manually read a line at a time, instead of using serde's
        // stream deserializers, because the deserializer cannot recover
        // from an error, resulting in it getting stuck, because we try to
        // be resilient against failures.
        //
        // Because cargo only outputs one JSON object per line, we can
        // simply skip a line if it doesn't parse, which just ignores any
        // erroneus output.

        let mut error = String::new();
        let mut read_at_least_one_message = false;
        let output = streaming_output(
            command,
            &mut |line| {
                read_at_least_one_message = true;

                // Try to deserialize a message from Cargo or Rustc.
                let mut deserializer = serde_json::Deserializer::from_str(&line);
                deserializer.disable_recursion_limit();
                if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
                    match message {
                        // Skip certain kinds of messages to only spend time on what's useful
                        JsonMessage::Cargo(message) => match message {
                            cargo_metadata::Message::CompilerArtifact(artifact)
                                if !artifact.fresh =>
                            {
                                self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
                            }
                            cargo_metadata::Message::CompilerMessage(msg) => {
                                self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
                            }
                            _ => (),
                        },
                        JsonMessage::Rustc(message) => {
                            self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
                        }
                    }
                }
            },
            &mut |line| {
                error.push_str(line);
                error.push('\n');
            },
        );
        match output {
            Ok(_) if read_at_least_one_message => Ok(()),
            Ok(output) if output.status.success() => {
                Err(io::Error::new(io::ErrorKind::Other, format!(
                    "Cargo watcher failed, the command produced no valid metadata (exit code: {:?})",
                    output.status
                )))
            }
            Ok(_) => Err(io::Error::new(io::ErrorKind::Other, error)),
            Err(e) => Err(e),
        }
    }
}

enum CargoMessage {
    CompilerArtifact(cargo_metadata::Artifact),
    Diagnostic(Diagnostic),
}

#[derive(Deserialize)]
#[serde(untagged)]
enum JsonMessage {
    Cargo(cargo_metadata::Message),
    Rustc(Diagnostic),
}
<commit_msg>Remove unnecessary unused attribute<commit_after>//! Flycheck provides the functionality needed to run `cargo check` or
//! another compatible command (f.x. clippy) in a background thread and provide
//! LSP diagnostics based on the output of the command.

use std::{fmt, io, process::Command, time::Duration};

use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
use paths::AbsPathBuf;
use serde::Deserialize;
use stdx::process::streaming_output;

pub use cargo_metadata::diagnostic::{
    Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
    DiagnosticSpanMacroExpansion,
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FlycheckConfig {
    CargoCommand {
        command: String,
        target_triple: Option<String>,
        all_targets: bool,
        no_default_features: bool,
        all_features: bool,
        features: Vec<String>,
        extra_args: Vec<String>,
    },
    CustomCommand {
        command: String,
        args: Vec<String>,
    },
}

impl fmt::Display for FlycheckConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
            FlycheckConfig::CustomCommand { command, args } => {
                write!(f, "{} {}", command, args.join(" "))
            }
        }
    }
}

/// Flycheck wraps the shared state and communication machinery used for
/// running `cargo check` (or other compatible command) and providing
/// diagnostics based on the output.
/// The spawned thread is shut down when this struct is dropped.
#[derive(Debug)]
pub struct FlycheckHandle {
    // XXX: drop order is significant
    sender: Sender<Restart>,
    _thread: jod_thread::JoinHandle,
}

impl FlycheckHandle {
    pub fn spawn(
        id: usize,
        sender: Box<dyn Fn(Message) + Send>,
        config: FlycheckConfig,
        workspace_root: AbsPathBuf,
    ) -> FlycheckHandle {
        let actor = FlycheckActor::new(id, sender, config, workspace_root);
        let (sender, receiver) = unbounded::<Restart>();
        let thread = jod_thread::Builder::new()
            .name("Flycheck".to_owned())
            .spawn(move || actor.run(receiver))
            .expect("failed to spawn thread");
        FlycheckHandle { sender, _thread: thread }
    }

    /// Schedule a re-start of the cargo check worker.
    pub fn update(&self) {
        self.sender.send(Restart).unwrap();
    }
}

pub enum Message {
    /// Request adding a diagnostic with fixes included to a file
    AddDiagnostic { workspace_root: AbsPathBuf, diagnostic: Diagnostic },

    /// Request check progress notification to client
    Progress {
        /// Flycheck instance ID
        id: usize,
        progress: Progress,
    },
}

impl fmt::Debug for Message {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Message::AddDiagnostic { workspace_root, diagnostic } => f
                .debug_struct("AddDiagnostic")
                .field("workspace_root", workspace_root)
                .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
                .finish(),
            Message::Progress { id, progress } => {
                f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
            }
        }
    }
}

#[derive(Debug)]
pub enum Progress {
    DidStart,
    DidCheckCrate(String),
    DidFinish(io::Result<()>),
    DidCancel,
}

struct Restart;

struct FlycheckActor {
    id: usize,
    sender: Box<dyn Fn(Message) + Send>,
    config: FlycheckConfig,
    workspace_root: AbsPathBuf,
    /// WatchThread exists to wrap around the communication needed to be able to
    /// run `cargo check` without blocking. Currently the Rust standard library
    /// doesn't provide a way to read sub-process output without blocking, so we
    /// have to wrap sub-processes output handling in a thread and pass messages
    /// back over a channel.
    cargo_handle: Option<CargoHandle>,
}

enum Event {
    Restart(Restart),
    CheckEvent(Option<CargoMessage>),
}

impl FlycheckActor {
    fn new(
        id: usize,
        sender: Box<dyn Fn(Message) + Send>,
        config: FlycheckConfig,
        workspace_root: AbsPathBuf,
    ) -> FlycheckActor {
        FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
    }
    fn progress(&self, progress: Progress) {
        self.send(Message::Progress { id: self.id, progress });
    }
    fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
        let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
        select! {
            recv(inbox) -> msg => msg.ok().map(Event::Restart),
            recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
        }
    }
    fn run(mut self, inbox: Receiver<Restart>) {
        while let Some(event) = self.next_event(&inbox) {
            match event {
                Event::Restart(Restart) => {
                    while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}

                    self.cancel_check_process();

                    let command = self.check_command();
                    tracing::info!("restart flycheck {:?}", command);
                    self.cargo_handle = Some(CargoHandle::spawn(command));
                    self.progress(Progress::DidStart);
                }
                Event::CheckEvent(None) => {
                    // Watcher finished, replace it with a never channel to
                    // avoid busy-waiting.
                    let cargo_handle = self.cargo_handle.take().unwrap();
                    let res = cargo_handle.join();
                    if res.is_err() {
                        tracing::error!(
                            "Flycheck failed to run the following command: {:?}",
                            self.check_command()
                        );
                    }
                    self.progress(Progress::DidFinish(res));
                }
                Event::CheckEvent(Some(message)) => match message {
                    CargoMessage::CompilerArtifact(msg) => {
                        self.progress(Progress::DidCheckCrate(msg.target.name));
                    }

                    CargoMessage::Diagnostic(msg) => {
                        self.send(Message::AddDiagnostic {
                            workspace_root: self.workspace_root.clone(),
                            diagnostic: msg,
                        });
                    }
                },
            }
        }
        // If we rerun the thread, we need to discard the previous check results first
        self.cancel_check_process();
    }
    fn cancel_check_process(&mut self) {
        if self.cargo_handle.take().is_some() {
            self.progress(Progress::DidCancel);
        }
    }
    fn check_command(&self) -> Command {
        let mut cmd = match &self.config {
            FlycheckConfig::CargoCommand {
                command,
                target_triple,
                no_default_features,
                all_targets,
                all_features,
                extra_args,
                features,
            } => {
                let mut cmd = Command::new(toolchain::cargo());
                cmd.arg(command);
                cmd.current_dir(&self.workspace_root);
                cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
                    .arg(self.workspace_root.join("Cargo.toml").as_os_str());

                if let Some(target) = target_triple {
                    cmd.args(&["--target", target.as_str()]);
                }
                if *all_targets {
                    cmd.arg("--all-targets");
                }
                if *all_features {
                    cmd.arg("--all-features");
                } else {
                    if *no_default_features {
                        cmd.arg("--no-default-features");
                    }
                    if !features.is_empty() {
                        cmd.arg("--features");
                        cmd.arg(features.join(" "));
                    }
                }
                cmd.args(extra_args);
                cmd
            }
            FlycheckConfig::CustomCommand { command, args } => {
                let mut cmd = Command::new(command);
                cmd.args(args);
                cmd
            }
        };
        cmd.current_dir(&self.workspace_root);
        cmd
    }

    fn send(&self, check_task: Message) {
        (self.sender)(check_task);
    }
}

struct CargoHandle {
    thread: jod_thread::JoinHandle<io::Result<()>>,
    receiver: Receiver<CargoMessage>,
}

impl CargoHandle {
    fn spawn(command: Command) -> CargoHandle {
        let (sender, receiver) = unbounded();
        let actor = CargoActor::new(sender);
        let thread = jod_thread::Builder::new()
            .name("CargoHandle".to_owned())
            .spawn(move || actor.run(command))
            .expect("failed to spawn thread");
        CargoHandle { thread, receiver }
    }

    fn join(self) -> io::Result<()> {
        self.thread.join()
    }
}

struct CargoActor {
    sender: Sender<CargoMessage>,
}

impl CargoActor {
    fn new(sender: Sender<CargoMessage>) -> CargoActor {
        CargoActor { sender }
    }

    fn run(self, command: Command) -> io::Result<()> {
        // We manually read a line at a time, instead of using serde's
        // stream deserializers, because the deserializer cannot recover
        // from an error, resulting in it getting stuck, because we try to
        // be resilient against failures.
        //
        // Because cargo only outputs one JSON object per line, we can
        // simply skip a line if it doesn't parse, which just ignores any
        // erroneus output.

        let mut error = String::new();
        let mut read_at_least_one_message = false;
        let output = streaming_output(
            command,
            &mut |line| {
                read_at_least_one_message = true;

                // Try to deserialize a message from Cargo or Rustc.
                let mut deserializer = serde_json::Deserializer::from_str(&line);
                deserializer.disable_recursion_limit();
                if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
                    match message {
                        // Skip certain kinds of messages to only spend time on what's useful
                        JsonMessage::Cargo(message) => match message {
                            cargo_metadata::Message::CompilerArtifact(artifact)
                                if !artifact.fresh =>
                            {
                                self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
                            }
                            cargo_metadata::Message::CompilerMessage(msg) => {
                                self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
                            }
                            _ => (),
                        },
                        JsonMessage::Rustc(message) => {
                            self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
                        }
                    }
                }
            },
            &mut |line| {
                error.push_str(line);
                error.push('\n');
            },
        );
        match output {
            Ok(_) if read_at_least_one_message => Ok(()),
            Ok(output) if output.status.success() => {
                Err(io::Error::new(io::ErrorKind::Other, format!(
                    "Cargo watcher failed, the command produced no valid metadata (exit code: {:?})",
                    output.status
                )))
            }
            Ok(_) => Err(io::Error::new(io::ErrorKind::Other, error)),
            Err(e) => Err(e),
        }
    }
}

enum CargoMessage {
    CompilerArtifact(cargo_metadata::Artifact),
    Diagnostic(Diagnostic),
}

#[derive(Deserialize)]
#[serde(untagged)]
enum JsonMessage {
    Cargo(cargo_metadata::Message),
    Rustc(Diagnostic),
}
<|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::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", &vec_from_params!(params, "work_experience")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                      "work_authorization", &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);
    }
  }
}
<commit_msg>resources/user: remove work_languages<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_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.
  #[allow(unused_must_use)]
  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_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.
  ///
  /// Considering a single row, the terms inside there are ORred,
  /// while through the rows there is an AND.
  /// I.e.: given ["Fullstack", "DevOps"] as `work_roles`, found talents
  /// will present at least one of these roles), but both `work_roles`
  /// and `work_location`, if provided, must be matched successfully.
  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_experience", &vec_from_params!(params, "work_experience")),

                                     <Filter as VectorOfTerms<String>>::build_terms(
                                      "work_authorization", &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_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_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_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_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|>