<commit_before>extern crate cards;
extern crate libthyme;
extern crate ncurses;

use super::{Action,UI};
use libthyme::board::{Position,HPosition,VPosition};
use libthyme::game::Game;
use libthyme::score::Scorer;
use std::cmp;

const CARD_WIDTH: i32 = 7;
const CARD_HEIGHT: i32 = 5;
const CARD_MARGIN: i32 = 2;
const BOARD_MARGIN: i32 = 2;
const STATUS_HEIGHT: i32 = 2;

const CARD_COLOR_BLACK: i16 = 1; // black on white
const CARD_COLOR_RED: i16 = 2; // red on white
const CARD_COLOR_EMPTY: i16 = 4; // white on black
const SELECTED_COLOR: i16 = 5; // yellow on black
const CURSOR_INFO_COLOR: i16 = 6; // cyan on black
const GAME_INFO_COLOR: i16 = 7; // green on black
const BG_COLOR: i16 = ncurses::COLOR_BLACK;
const CARD_BG_COLOR: i16 = ncurses::COLOR_WHITE;

/// Set up the UI
pub fn initialize_screen() {
    ncurses::setlocale(ncurses::LcCategory::ctype, "");
    ncurses::initscr();
    ncurses::noecho();
    ncurses::start_color();
    ncurses::keypad(ncurses::stdscr, true);
    ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE);
    ncurses::init_pair(CARD_COLOR_BLACK, ncurses::COLOR_BLACK, CARD_BG_COLOR);
    ncurses::init_pair(CARD_COLOR_RED, ncurses::COLOR_RED, CARD_BG_COLOR);
    ncurses::init_pair(CARD_COLOR_EMPTY, ncurses::COLOR_WHITE, BG_COLOR);
    ncurses::init_pair(SELECTED_COLOR, ncurses::COLOR_YELLOW, BG_COLOR);
    ncurses::init_pair(CURSOR_INFO_COLOR, ncurses::COLOR_CYAN, BG_COLOR);
    ncurses::init_pair(GAME_INFO_COLOR, ncurses::COLOR_GREEN, BG_COLOR);
}

/// Redraw a UI in the current screen
pub fn redraw<T: Scorer>(ui: &UI, game: &mut Game<T>, refresh: bool) {
    if refresh {
        ncurses::clear();
        ncurses::bkgd(ncurses::COLOR_PAIR(CARD_COLOR_EMPTY));
    }
    if validate_screen_size() {
        write_title(game);
        draw_cards(ui, game);
        write_cursor_message(ui, game);
        write_message(&ui.message);
    }
    ncurses::refresh();
}

/// Tear down the UI
pub fn cleanup() {
    ncurses::endwin();
}

/// Process input from the user
///
/// Known inputs:
/// - Arrow keys/WASD: Move cursor between various positions on the board
/// - Q: Quit game
/// - H: Hint (unimplemented)
/// - ?: Help (unimplemented)
/// - Space: Toggle position selection
/// - Return: Play move, clear selection
pub fn get_action() -> Action {
    return match ncurses::getch() {
        ncurses::KEY_LEFT | 97 => Action::CursorLeft,
        ncurses::KEY_RIGHT | 100 => Action::CursorRight,
        ncurses::KEY_UP | 119 => Action::CursorUp,
        ncurses::KEY_DOWN | 115 => Action::CursorDown,
        ncurses::KEY_RESIZE => Action::Resize,
        ncurses::KEY_ENTER | 13 | 10 => Action::Play,
        32 =>  Action::ToggleSelection, // Space
        113 => Action::Quit, // Q
        104 => Action::Hint, // H
        63 =>  Action::Help, // ?
        _ =>   Action::Unknown
    }
}

/// Check that the content can fit
fn validate_screen_size() -> bool {
    let min_height = BOARD_MARGIN*2 + CARD_MARGIN*4 + STATUS_HEIGHT + CARD_HEIGHT*3;
    if ncurses::LINES < min_height || ncurses::COLS < 50 {
        write_message(&format!(
                "Please resize your terminal to be at least 50x{}",
                min_height));
        return false
    }
    return true
}

/// Print the game title and status info
fn write_title<T: Scorer>(game: &mut Game<T>) {
    printw_margin(0, 0);
    ncurses::attron(ncurses::A_BOLD());
    ncurses::printw("Thyme");
    ncurses::attroff(ncurses::A_BOLD());
    ncurses::printw(&format!(" - Score: {}", game.score()));
    printw_margin(0, 1);
    let (_, suit) = layout_suit(game.board.lucky_card);
    let info = ncurses::COLOR_PAIR(GAME_INFO_COLOR);
    ncurses::attron(info);
    ncurses::printw(&format!("Lucky Suit: {}  Discards Left: {}/{}", suit,
                             game.discards_allowed, game.discards_allowed_max));
    ncurses::clrtoeol();
    ncurses::attroff(info);
}

/// Print the message at the bottom of the window
fn write_message(message: &str) {
    printw_margin(0, ncurses::LINES - 1);
    ncurses::printw(message);
    ncurses::clrtoeol();
}

/// Print a message describing the stack above the cursor
fn write_cursor_message<T: Scorer>(ui: &UI, game: &Game<T>) {
    let stacked_cards = game.board.count_cards(ui.cursor_position);
    let all_cards = game.board.count_all_cards();
    let message = format!("*{}/{} cards in the stack", stacked_cards, all_cards);
    let color = ncurses::COLOR_PAIR(CURSOR_INFO_COLOR);
    printw_margin(0, ncurses::LINES - 2);
    ncurses::attron(color);
    ncurses::printw(&message);
    ncurses::clrtoeol();
    ncurses::attroff(color);
}

/// Print spaces for the width of the board margin
fn printw_margin(x: i32, y: i32) {
    ncurses::mv(y, x);
    printw_repeat(" ", BOARD_MARGIN, ncurses::COLOR_PAIR(CARD_COLOR_EMPTY));
}

/// Print the card values and empty stacks
fn draw_cards<T: Scorer>(ui: &UI, game: &mut Game<T>) {
    for position in game.board.positions() {
        let card = game.board.top(position);
        if card.is_some() {
            draw_card(position, card.unwrap());
        } else {
            draw_empty(game, position);
        }
        let (x, y) = card_location(position);
        toggle_highlight_card(x, y, ui.selection.contains(&position));
        if position == ui.cursor_position {
            let offset = (CARD_WIDTH as f32 /2.0).floor() as i32;
            ncurses::mvprintw(y + CARD_HEIGHT, x + offset, "*");
        }
    }
}

/// Draw a card on the board at a position
fn draw_card(position: Position, card: cards::card::Card) {
    let (x, y) = card_location(position);
    let (color, suit) = layout_suit(card);
    let value = layout_value(card);
    let black = ncurses::COLOR_PAIR(CARD_COLOR_BLACK);
    ncurses::attron(black);
    ncurses::mvprintw(y, x, &value);
    ncurses::attroff(black);
    ncurses::attron(color);
    ncurses::mvprintw(y, x + value.len() as i32, &suit);
    ncurses::attroff(color);
    let spacing = CARD_WIDTH - value.len() as i32 - 1;
    printw_repeat(" ", spacing, black);
    for i in 1..CARD_HEIGHT - 1 {
        ncurses::mv(y + i, x);
        printw_repeat(" ", CARD_WIDTH, black);
    }
    ncurses::mv(y + CARD_HEIGHT - 1, x);
    printw_repeat(" ", spacing, black);
    ncurses::attron(black);
    ncurses::printw(&value);
    ncurses::attroff(black);
    ncurses::attron(color);
    ncurses::printw(&suit);
    ncurses::attroff(color);
}

/// Draw empty slot for a card
fn draw_empty<T: Scorer>(game: &Game<T>, position: Position) {
    let color = ncurses::COLOR_PAIR(CARD_COLOR_EMPTY);
    let (x, y) = card_location(position);
    ncurses::attron(color);
    ncurses::mvprintw(y, x, "┌");
    printw_repeat("─", CARD_WIDTH - 2, color);
    ncurses::mvprintw(y, x + CARD_WIDTH - 1, "┐");
    ncurses::attroff(color);
    let gap_height = CARD_HEIGHT - 1;
    let bonus_height = gap_height/2;
    for i in 1..gap_height {
        ncurses::attron(color);
        ncurses::mvprintw(y + i, x , "│");
        if i == bonus_height {
            let bonus = format!("+{}", game.scorer.bonus(position));
            let available_width = cmp::max(0, CARD_WIDTH - 2 - bonus.len() as i32);
            let lede = available_width/2;
            printw_repeat(" ", lede, color);
            ncurses::printw(&bonus);
            printw_repeat(" ", available_width - lede, color);
        } else {
            printw_repeat(" ", CARD_WIDTH - 2, color);
        }
        ncurses::attroff(color);
        ncurses::attron(color);
        ncurses::mvprintw(y + i, x + CARD_WIDTH - 1, "│");
        ncurses::attroff(color);
    }
    ncurses::attron(color);
    ncurses::mvprintw(y + CARD_HEIGHT - 1, x, "└");
    printw_repeat("─", CARD_WIDTH - 2, color);
    ncurses::mvprintw(y + CARD_HEIGHT - 1, x + CARD_WIDTH - 1, "┘");
    ncurses::attroff(color);
}

/// Print a string repeatedly to fill a length
fn printw_repeat(content: &str, len: i32, color: ncurses::chtype) {
    ncurses::attron(color);
    for _ in 0..len {
       ncurses::printw(content);
    }
    ncurses::attroff(color);
}

/// Print or clear the border around a card
fn toggle_highlight_card(x: i32, y: i32, on: bool) {
    let color = ncurses::COLOR_PAIR(SELECTED_COLOR);
    ncurses::attron(color);
    ncurses::mvprintw(y - 1, x - 1, if on {"┌"} else {" "});
    ncurses::attroff(color);
    ncurses::mv(y - 1, x);
    printw_repeat(if on {"─"} else {" "}, CARD_WIDTH, color);
    ncurses::mv(y + CARD_HEIGHT, x);
    printw_repeat(if on {"─"} else {" "}, CARD_WIDTH, color);
    ncurses::attron(color);
    ncurses::mvprintw(y - 1, x + CARD_WIDTH, if on {"┐"} else {" "});
    for i in 0..CARD_HEIGHT {
        ncurses::mvprintw(y + i, x - 1, if on {"│"} else {" "});
        ncurses::mvprintw(y + i, x + CARD_WIDTH, if on {"│"} else {" "});
    }
    ncurses::mvprintw(y + CARD_HEIGHT, x - 1, if on {"└"} else {" "});
    ncurses::mvprintw(y + CARD_HEIGHT, x + CARD_WIDTH, if on {"┘"} else {" "});
    ncurses::attroff(color);
}

/// Printed representation of a card's suit
fn layout_suit(card: cards::card::Card) -> (ncurses::chtype, String) {
    let black = ncurses::COLOR_PAIR(CARD_COLOR_BLACK);
    let red = ncurses::COLOR_PAIR(CARD_COLOR_RED);
    return match card.suit {
        cards::card::Suit::Diamonds => (red, "\u{2666}".to_string()),
        cards::card::Suit::Clubs => (black, "\u{2663}".to_string()),
        cards::card::Suit::Spades => (black, "\u{2660}".to_string()),
        cards::card::Suit::Hearts => (red, "\u{2665}".to_string()),
    }
}

/// Printed representation of a card's value
fn layout_value(card: cards::card::Card) -> String {
    return match card.value {
        cards::card::Value::Ace => "A",
        cards::card::Value::Two => "2",
        cards::card::Value::Three => "3",
        cards::card::Value::Four => "4",
        cards::card::Value::Five => "5",
        cards::card::Value::Six => "6",
        cards::card::Value::Seven => "7",
        cards::card::Value::Eight => "8",
        cards::card::Value::Nine => "9",
        cards::card::Value::Ten => "10",
        cards::card::Value::Jack => "J",
        cards::card::Value::Queen => "Q",
        cards::card::Value::King => "K",
    }.to_string()
}

/// Location (x, y) for a card position
fn card_location(position: Position) -> (i32, i32) {
    let left = BOARD_MARGIN + CARD_MARGIN;
    let center = BOARD_MARGIN + CARD_MARGIN*2 + CARD_WIDTH;
    let right = BOARD_MARGIN + CARD_MARGIN*3 + CARD_WIDTH*2;
    let top = BOARD_MARGIN + CARD_MARGIN;
    let middle = BOARD_MARGIN + CARD_MARGIN*2 + CARD_HEIGHT;
    let bottom = BOARD_MARGIN + CARD_MARGIN*3 + CARD_HEIGHT*2;
    match (position.x, position.y) {
        (HPosition::Left, VPosition::Top) => (left, top),
        (HPosition::Left, VPosition::Middle) => (left, middle),
        (HPosition::Left, VPosition::Bottom) => (left, bottom),
        (HPosition::Center, VPosition::Top) => (center, top),
        (HPosition::Center, VPosition::Middle) => (center, middle),
        (HPosition::Center, VPosition::Bottom) => (center, bottom),
        (HPosition::Right, VPosition::Top) => (right, top),
        (HPosition::Right, VPosition::Middle) => (right, middle),
        (HPosition::Right, VPosition::Bottom) => (right, bottom),
    }
}
<commit_msg>Support hjkl for movement, show hint/tip on T instead<commit_after>extern crate cards;
extern crate libthyme;
extern crate ncurses;

use super::{Action,UI};
use libthyme::board::{Position,HPosition,VPosition};
use libthyme::game::Game;
use libthyme::score::Scorer;
use std::cmp;

const CARD_WIDTH: i32 = 7;
const CARD_HEIGHT: i32 = 5;
const CARD_MARGIN: i32 = 2;
const BOARD_MARGIN: i32 = 2;
const STATUS_HEIGHT: i32 = 2;

const CARD_COLOR_BLACK: i16 = 1; // black on white
const CARD_COLOR_RED: i16 = 2; // red on white
const CARD_COLOR_EMPTY: i16 = 4; // white on black
const SELECTED_COLOR: i16 = 5; // yellow on black
const CURSOR_INFO_COLOR: i16 = 6; // cyan on black
const GAME_INFO_COLOR: i16 = 7; // green on black
const BG_COLOR: i16 = ncurses::COLOR_BLACK;
const CARD_BG_COLOR: i16 = ncurses::COLOR_WHITE;

/// Set up the UI
pub fn initialize_screen() {
    ncurses::setlocale(ncurses::LcCategory::ctype, "");
    ncurses::initscr();
    ncurses::noecho();
    ncurses::start_color();
    ncurses::keypad(ncurses::stdscr, true);
    ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE);
    ncurses::init_pair(CARD_COLOR_BLACK, ncurses::COLOR_BLACK, CARD_BG_COLOR);
    ncurses::init_pair(CARD_COLOR_RED, ncurses::COLOR_RED, CARD_BG_COLOR);
    ncurses::init_pair(CARD_COLOR_EMPTY, ncurses::COLOR_WHITE, BG_COLOR);
    ncurses::init_pair(SELECTED_COLOR, ncurses::COLOR_YELLOW, BG_COLOR);
    ncurses::init_pair(CURSOR_INFO_COLOR, ncurses::COLOR_CYAN, BG_COLOR);
    ncurses::init_pair(GAME_INFO_COLOR, ncurses::COLOR_GREEN, BG_COLOR);
}

/// Redraw a UI in the current screen
pub fn redraw<T: Scorer>(ui: &UI, game: &mut Game<T>, refresh: bool) {
    if refresh {
        ncurses::clear();
        ncurses::bkgd(ncurses::COLOR_PAIR(CARD_COLOR_EMPTY));
    }
    if validate_screen_size() {
        write_title(game);
        draw_cards(ui, game);
        write_cursor_message(ui, game);
        write_message(&ui.message);
    }
    ncurses::refresh();
}

/// Tear down the UI
pub fn cleanup() {
    ncurses::endwin();
}

/// Process input from the user
///
/// Known inputs:
/// - Arrow keys/WASD: Move cursor between various positions on the board
/// - Q: Quit game
/// - H: Hint (unimplemented)
/// - ?: Help (unimplemented)
/// - Space: Toggle position selection
/// - Return: Play move, clear selection
pub fn get_action() -> Action {
    return match ncurses::getch() {
        ncurses::KEY_LEFT | 97 | 104 => Action::CursorLeft,
        ncurses::KEY_RIGHT | 100 | 108 => Action::CursorRight,
        ncurses::KEY_UP | 119 | 107 => Action::CursorUp,
        ncurses::KEY_DOWN | 115 | 106 => Action::CursorDown,
        ncurses::KEY_RESIZE => Action::Resize,
        ncurses::KEY_ENTER | 13 | 10 => Action::Play,
        32 =>  Action::ToggleSelection, // Space
        113 => Action::Quit, // Q
        116 => Action::Hint, // T
        63 =>  Action::Help, // ?
        _ =>   Action::Unknown
    }
}

/// Check that the content can fit
fn validate_screen_size() -> bool {
    let min_height = BOARD_MARGIN*2 + CARD_MARGIN*4 + STATUS_HEIGHT + CARD_HEIGHT*3;
    if ncurses::LINES < min_height || ncurses::COLS < 50 {
        write_message(&format!(
                "Please resize your terminal to be at least 50x{}",
                min_height));
        return false
    }
    return true
}

/// Print the game title and status info
fn write_title<T: Scorer>(game: &mut Game<T>) {
    printw_margin(0, 0);
    ncurses::attron(ncurses::A_BOLD());
    ncurses::printw("Thyme");
    ncurses::attroff(ncurses::A_BOLD());
    ncurses::printw(&format!(" - Score: {}", game.score()));
    printw_margin(0, 1);
    let (_, suit) = layout_suit(game.board.lucky_card);
    let info = ncurses::COLOR_PAIR(GAME_INFO_COLOR);
    ncurses::attron(info);
    ncurses::printw(&format!("Lucky Suit: {}  Discards Left: {}/{}", suit,
                             game.discards_allowed, game.discards_allowed_max));
    ncurses::clrtoeol();
    ncurses::attroff(info);
}

/// Print the message at the bottom of the window
fn write_message(message: &str) {
    printw_margin(0, ncurses::LINES - 1);
    ncurses::printw(message);
    ncurses::clrtoeol();
}

/// Print a message describing the stack above the cursor
fn write_cursor_message<T: Scorer>(ui: &UI, game: &Game<T>) {
    let stacked_cards = game.board.count_cards(ui.cursor_position);
    let all_cards = game.board.count_all_cards();
    let message = format!("*{}/{} cards in the stack", stacked_cards, all_cards);
    let color = ncurses::COLOR_PAIR(CURSOR_INFO_COLOR);
    printw_margin(0, ncurses::LINES - 2);
    ncurses::attron(color);
    ncurses::printw(&message);
    ncurses::clrtoeol();
    ncurses::attroff(color);
}

/// Print spaces for the width of the board margin
fn printw_margin(x: i32, y: i32) {
    ncurses::mv(y, x);
    printw_repeat(" ", BOARD_MARGIN, ncurses::COLOR_PAIR(CARD_COLOR_EMPTY));
}

/// Print the card values and empty stacks
fn draw_cards<T: Scorer>(ui: &UI, game: &mut Game<T>) {
    for position in game.board.positions() {
        let card = game.board.top(position);
        if card.is_some() {
            draw_card(position, card.unwrap());
        } else {
            draw_empty(game, position);
        }
        let (x, y) = card_location(position);
        toggle_highlight_card(x, y, ui.selection.contains(&position));
        if position == ui.cursor_position {
            let offset = (CARD_WIDTH as f32 /2.0).floor() as i32;
            ncurses::mvprintw(y + CARD_HEIGHT, x + offset, "*");
        }
    }
}

/// Draw a card on the board at a position
fn draw_card(position: Position, card: cards::card::Card) {
    let (x, y) = card_location(position);
    let (color, suit) = layout_suit(card);
    let value = layout_value(card);
    let black = ncurses::COLOR_PAIR(CARD_COLOR_BLACK);
    ncurses::attron(black);
    ncurses::mvprintw(y, x, &value);
    ncurses::attroff(black);
    ncurses::attron(color);
    ncurses::mvprintw(y, x + value.len() as i32, &suit);
    ncurses::attroff(color);
    let spacing = CARD_WIDTH - value.len() as i32 - 1;
    printw_repeat(" ", spacing, black);
    for i in 1..CARD_HEIGHT - 1 {
        ncurses::mv(y + i, x);
        printw_repeat(" ", CARD_WIDTH, black);
    }
    ncurses::mv(y + CARD_HEIGHT - 1, x);
    printw_repeat(" ", spacing, black);
    ncurses::attron(black);
    ncurses::printw(&value);
    ncurses::attroff(black);
    ncurses::attron(color);
    ncurses::printw(&suit);
    ncurses::attroff(color);
}

/// Draw empty slot for a card
fn draw_empty<T: Scorer>(game: &Game<T>, position: Position) {
    let color = ncurses::COLOR_PAIR(CARD_COLOR_EMPTY);
    let (x, y) = card_location(position);
    ncurses::attron(color);
    ncurses::mvprintw(y, x, "┌");
    printw_repeat("─", CARD_WIDTH - 2, color);
    ncurses::mvprintw(y, x + CARD_WIDTH - 1, "┐");
    ncurses::attroff(color);
    let gap_height = CARD_HEIGHT - 1;
    let bonus_height = gap_height/2;
    for i in 1..gap_height {
        ncurses::attron(color);
        ncurses::mvprintw(y + i, x , "│");
        if i == bonus_height {
            let bonus = format!("+{}", game.scorer.bonus(position));
            let available_width = cmp::max(0, CARD_WIDTH - 2 - bonus.len() as i32);
            let lede = available_width/2;
            printw_repeat(" ", lede, color);
            ncurses::printw(&bonus);
            printw_repeat(" ", available_width - lede, color);
        } else {
            printw_repeat(" ", CARD_WIDTH - 2, color);
        }
        ncurses::attroff(color);
        ncurses::attron(color);
        ncurses::mvprintw(y + i, x + CARD_WIDTH - 1, "│");
        ncurses::attroff(color);
    }
    ncurses::attron(color);
    ncurses::mvprintw(y + CARD_HEIGHT - 1, x, "└");
    printw_repeat("─", CARD_WIDTH - 2, color);
    ncurses::mvprintw(y + CARD_HEIGHT - 1, x + CARD_WIDTH - 1, "┘");
    ncurses::attroff(color);
}

/// Print a string repeatedly to fill a length
fn printw_repeat(content: &str, len: i32, color: ncurses::chtype) {
    ncurses::attron(color);
    for _ in 0..len {
       ncurses::printw(content);
    }
    ncurses::attroff(color);
}

/// Print or clear the border around a card
fn toggle_highlight_card(x: i32, y: i32, on: bool) {
    let color = ncurses::COLOR_PAIR(SELECTED_COLOR);
    ncurses::attron(color);
    ncurses::mvprintw(y - 1, x - 1, if on {"┌"} else {" "});
    ncurses::attroff(color);
    ncurses::mv(y - 1, x);
    printw_repeat(if on {"─"} else {" "}, CARD_WIDTH, color);
    ncurses::mv(y + CARD_HEIGHT, x);
    printw_repeat(if on {"─"} else {" "}, CARD_WIDTH, color);
    ncurses::attron(color);
    ncurses::mvprintw(y - 1, x + CARD_WIDTH, if on {"┐"} else {" "});
    for i in 0..CARD_HEIGHT {
        ncurses::mvprintw(y + i, x - 1, if on {"│"} else {" "});
        ncurses::mvprintw(y + i, x + CARD_WIDTH, if on {"│"} else {" "});
    }
    ncurses::mvprintw(y + CARD_HEIGHT, x - 1, if on {"└"} else {" "});
    ncurses::mvprintw(y + CARD_HEIGHT, x + CARD_WIDTH, if on {"┘"} else {" "});
    ncurses::attroff(color);
}

/// Printed representation of a card's suit
fn layout_suit(card: cards::card::Card) -> (ncurses::chtype, String) {
    let black = ncurses::COLOR_PAIR(CARD_COLOR_BLACK);
    let red = ncurses::COLOR_PAIR(CARD_COLOR_RED);
    return match card.suit {
        cards::card::Suit::Diamonds => (red, "\u{2666}".to_string()),
        cards::card::Suit::Clubs => (black, "\u{2663}".to_string()),
        cards::card::Suit::Spades => (black, "\u{2660}".to_string()),
        cards::card::Suit::Hearts => (red, "\u{2665}".to_string()),
    }
}

/// Printed representation of a card's value
fn layout_value(card: cards::card::Card) -> String {
    return match card.value {
        cards::card::Value::Ace => "A",
        cards::card::Value::Two => "2",
        cards::card::Value::Three => "3",
        cards::card::Value::Four => "4",
        cards::card::Value::Five => "5",
        cards::card::Value::Six => "6",
        cards::card::Value::Seven => "7",
        cards::card::Value::Eight => "8",
        cards::card::Value::Nine => "9",
        cards::card::Value::Ten => "10",
        cards::card::Value::Jack => "J",
        cards::card::Value::Queen => "Q",
        cards::card::Value::King => "K",
    }.to_string()
}

/// Location (x, y) for a card position
fn card_location(position: Position) -> (i32, i32) {
    let left = BOARD_MARGIN + CARD_MARGIN;
    let center = BOARD_MARGIN + CARD_MARGIN*2 + CARD_WIDTH;
    let right = BOARD_MARGIN + CARD_MARGIN*3 + CARD_WIDTH*2;
    let top = BOARD_MARGIN + CARD_MARGIN;
    let middle = BOARD_MARGIN + CARD_MARGIN*2 + CARD_HEIGHT;
    let bottom = BOARD_MARGIN + CARD_MARGIN*3 + CARD_HEIGHT*2;
    match (position.x, position.y) {
        (HPosition::Left, VPosition::Top) => (left, top),
        (HPosition::Left, VPosition::Middle) => (left, middle),
        (HPosition::Left, VPosition::Bottom) => (left, bottom),
        (HPosition::Center, VPosition::Top) => (center, top),
        (HPosition::Center, VPosition::Middle) => (center, middle),
        (HPosition::Center, VPosition::Bottom) => (center, bottom),
        (HPosition::Right, VPosition::Top) => (right, top),
        (HPosition::Right, VPosition::Middle) => (right, middle),
        (HPosition::Right, VPosition::Bottom) => (right, bottom),
    }
}
<|endoftext|>


<commit_before>// Copyright 2015-2016 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use std::fmt::{self, Display};
use std::net::SocketAddr;

use futures::{Async, Future, Poll, Stream};

use error::ProtoError;
use udp::UdpStream;
use xfer::{DnsClientStream, SerialMessage};
use BufDnsStreamHandle;
use DnsStreamHandle;

/// A UDP client stream of DNS binary packets
#[must_use = "futures do nothing unless polled"]
pub struct UdpClientStream {
    name_server: SocketAddr,
    udp_stream: UdpStream,
}

impl UdpClientStream {
    /// it is expected that the resolver wrapper will be responsible for creating and managing
    ///  new UdpClients such that each new client would have a random port (reduce chance of cache
    ///  poisoning)
    ///
    /// # Return
    ///
    /// a tuple of a Future Stream which will handle sending and receiving messsages, and a
    ///  handle which can be used to send messages into the stream.
    pub fn new(name_server: SocketAddr) -> (UdpClientConnect, Box<DnsStreamHandle + Send>) {
        let (stream_future, sender) = UdpStream::new(name_server);

        let new_future = Box::new(
            stream_future
                .map(move |udp_stream| UdpClientStream {
                    name_server,
                    udp_stream,
                }).map_err(ProtoError::from),
        );
        let new_future = UdpClientConnect(new_future);

        let sender = Box::new(BufDnsStreamHandle::new(name_server, sender));

        (new_future, sender)
    }
}

impl Display for UdpClientStream {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(formatter, "UDP({})", self.name_server)
    }
}

impl DnsClientStream for UdpClientStream {
    fn name_server_addr(&self) -> SocketAddr {
        self.name_server
    }
}

impl Stream for UdpClientStream {
    type Item = SerialMessage;
    type Error = ProtoError;

    fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
        match try_ready!(self.udp_stream.poll().map_err(ProtoError::from)) {
            Some(message) => {
                if message.addr() != self.name_server {
                    debug!(
                        "{} does not match name_server: {}",
                        message.addr(),
                        self.name_server
                    )
                }

                Ok(Async::Ready(Some(message)))
            }
            None => Ok(Async::Ready(None)),
        }
    }
}

/// A future that resolves to an UdpClientStream
pub struct UdpClientConnect(Box<Future<Item = UdpClientStream, Error = ProtoError> + Send>);

impl Future for UdpClientConnect {
    type Item = UdpClientStream;
    type Error = ProtoError;

    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
        self.0.poll()
    }
}

#[cfg(not(target_os = "linux"))]
#[cfg(test)]
use std::net::Ipv6Addr;
#[cfg(test)]
use std::net::{IpAddr, Ipv4Addr};

#[test]
fn test_udp_client_stream_ipv4() {
    udp_client_stream_test(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))
}

#[test]
#[cfg(not(target_os = "linux"))] // ignored until Travis-CI fixes IPv6
fn test_udp_client_stream_ipv6() {
    udp_client_stream_test(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
}

#[cfg(test)]
fn udp_client_stream_test(server_addr: IpAddr) {
    use tokio::runtime::current_thread::Runtime;

    use std;
    let succeeded = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
    let succeeded_clone = succeeded.clone();
    std::thread::Builder::new()
        .name("thread_killer".to_string())
        .spawn(move || {
            let succeeded = succeeded_clone.clone();
            for _ in 0..15 {
                std::thread::sleep(std::time::Duration::from_secs(1));
                if succeeded.load(std::sync::atomic::Ordering::Relaxed) {
                    return;
                }
            }

            panic!("timeout");
        }).unwrap();

    let server = std::net::UdpSocket::bind(SocketAddr::new(server_addr, 0)).unwrap();
    server
        .set_read_timeout(Some(std::time::Duration::from_secs(5)))
        .unwrap(); // should recieve something within 5 seconds...
    server
        .set_write_timeout(Some(std::time::Duration::from_secs(5)))
        .unwrap(); // should recieve something within 5 seconds...
    let server_addr = server.local_addr().unwrap();

    let test_bytes: &'static [u8; 8] = b"DEADBEEF";
    let send_recv_times = 4;

    // an in and out server
    let server_handle = std::thread::Builder::new()
        .name("test_udp_client_stream_ipv4:server".to_string())
        .spawn(move || {
            let mut buffer = [0_u8; 512];

            for _ in 0..send_recv_times {
                // wait for some bytes...
                let (len, addr) = server.recv_from(&mut buffer).expect("receive failed");

                assert_eq!(&buffer[0..len], test_bytes);

                // bounce them right back...
                assert_eq!(
                    server.send_to(&buffer[0..len], addr).expect("send failed"),
                    len
                );
            }
        }).unwrap();

    // setup the client, which is going to run on the testing thread...
    let mut io_loop = Runtime::new().unwrap();

    // the tests should run within 5 seconds... right?
    // TODO: add timeout here, so that test never hangs...
    // let timeout = Timeout::new(Duration::from_secs(5));
    let (stream, mut sender) = UdpClientStream::new(server_addr);
    let mut stream: UdpClientStream = io_loop.block_on(stream).ok().unwrap();

    for _ in 0..send_recv_times {
        // test once
        sender
            .send(SerialMessage::new(test_bytes.to_vec(), server_addr))
            .unwrap();
        let (buffer, stream_tmp) = io_loop.block_on(stream.into_future()).ok().unwrap();
        stream = stream_tmp;
        assert_eq!(buffer.expect("no buffer received").bytes(), test_bytes);
    }

    succeeded.store(true, std::sync::atomic::Ordering::Relaxed);
    server_handle.join().expect("server thread failed");
}
<commit_msg>restrict udp responses to known hosts<commit_after>// Copyright 2015-2016 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use std::fmt::{self, Display};
use std::net::SocketAddr;

use futures::{Async, Future, Poll, Stream, task};

use error::ProtoError;
use udp::UdpStream;
use xfer::{DnsClientStream, SerialMessage};
use BufDnsStreamHandle;
use DnsStreamHandle;

/// A UDP client stream of DNS binary packets
#[must_use = "futures do nothing unless polled"]
pub struct UdpClientStream {
    name_server: SocketAddr,
    udp_stream: UdpStream,
}

impl UdpClientStream {
    /// it is expected that the resolver wrapper will be responsible for creating and managing
    ///  new UdpClients such that each new client would have a random port (reduce chance of cache
    ///  poisoning)
    ///
    /// # Return
    ///
    /// a tuple of a Future Stream which will handle sending and receiving messsages, and a
    ///  handle which can be used to send messages into the stream.
    pub fn new(name_server: SocketAddr) -> (UdpClientConnect, Box<DnsStreamHandle + Send>) {
        let (stream_future, sender) = UdpStream::new(name_server);

        let new_future = Box::new(
            stream_future
                .map(move |udp_stream| UdpClientStream {
                    name_server,
                    udp_stream,
                }).map_err(ProtoError::from),
        );
        let new_future = UdpClientConnect(new_future);

        let sender = Box::new(BufDnsStreamHandle::new(name_server, sender));

        (new_future, sender)
    }
}

impl Display for UdpClientStream {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(formatter, "UDP({})", self.name_server)
    }
}

impl DnsClientStream for UdpClientStream {
    fn name_server_addr(&self) -> SocketAddr {
        self.name_server
    }
}

impl Stream for UdpClientStream {
    type Item = SerialMessage;
    type Error = ProtoError;

    fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
        match try_ready!(self.udp_stream.poll().map_err(ProtoError::from)) {
            Some(message) => {
                if message.addr() != self.name_server {
                    warn!(
                        "ignoring response from {} because it does not match name_server: {}.",
                        message.addr(),
                        self.name_server
                    );
                    task::current().notify();
                    return Ok(Async::NotReady);
                }

                Ok(Async::Ready(Some(message)))
            }
            None => Ok(Async::Ready(None)),
        }
    }
}

/// A future that resolves to an UdpClientStream
pub struct UdpClientConnect(Box<Future<Item = UdpClientStream, Error = ProtoError> + Send>);

impl Future for UdpClientConnect {
    type Item = UdpClientStream;
    type Error = ProtoError;

    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
        self.0.poll()
    }
}

#[cfg(not(target_os = "linux"))]
#[cfg(test)]
use std::net::Ipv6Addr;
#[cfg(test)]
use std::net::{IpAddr, Ipv4Addr};

#[test]
fn test_udp_client_stream_ipv4() {
    udp_client_stream_test(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))
}

#[test]
#[cfg(not(target_os = "linux"))] // ignored until Travis-CI fixes IPv6
fn test_udp_client_stream_ipv6() {
    udp_client_stream_test(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
}

#[cfg(test)]
fn udp_client_stream_test(server_addr: IpAddr) {
    use tokio::runtime::current_thread::Runtime;

    use std;
    let succeeded = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
    let succeeded_clone = succeeded.clone();
    std::thread::Builder::new()
        .name("thread_killer".to_string())
        .spawn(move || {
            let succeeded = succeeded_clone.clone();
            for _ in 0..15 {
                std::thread::sleep(std::time::Duration::from_secs(1));
                if succeeded.load(std::sync::atomic::Ordering::Relaxed) {
                    return;
                }
            }

            panic!("timeout");
        }).unwrap();

    let server = std::net::UdpSocket::bind(SocketAddr::new(server_addr, 0)).unwrap();
    server
        .set_read_timeout(Some(std::time::Duration::from_secs(5)))
        .unwrap(); // should recieve something within 5 seconds...
    server
        .set_write_timeout(Some(std::time::Duration::from_secs(5)))
        .unwrap(); // should recieve something within 5 seconds...
    let server_addr = server.local_addr().unwrap();

    let test_bytes: &'static [u8; 8] = b"DEADBEEF";
    let send_recv_times = 4;

    // an in and out server
    let server_handle = std::thread::Builder::new()
        .name("test_udp_client_stream_ipv4:server".to_string())
        .spawn(move || {
            let mut buffer = [0_u8; 512];

            for _ in 0..send_recv_times {
                // wait for some bytes...
                let (len, addr) = server.recv_from(&mut buffer).expect("receive failed");

                assert_eq!(&buffer[0..len], test_bytes);

                // bounce them right back...
                assert_eq!(
                    server.send_to(&buffer[0..len], addr).expect("send failed"),
                    len
                );
            }
        }).unwrap();

    // setup the client, which is going to run on the testing thread...
    let mut io_loop = Runtime::new().unwrap();

    // the tests should run within 5 seconds... right?
    // TODO: add timeout here, so that test never hangs...
    // let timeout = Timeout::new(Duration::from_secs(5));
    let (stream, mut sender) = UdpClientStream::new(server_addr);
    let mut stream: UdpClientStream = io_loop.block_on(stream).ok().unwrap();

    for _ in 0..send_recv_times {
        // test once
        sender
            .send(SerialMessage::new(test_bytes.to_vec(), server_addr))
            .unwrap();
        let (buffer, stream_tmp) = io_loop.block_on(stream.into_future()).ok().unwrap();
        stream = stream_tmp;
        assert_eq!(buffer.expect("no buffer received").bytes(), test_bytes);
    }

    succeeded.store(true, std::sync::atomic::Ordering::Relaxed);
    server_handle.join().expect("server thread failed");
}
<|endoftext|>


<commit_before>use std::collections::HashMap;

use hyper::Client as HyperClient;
use hyper::client::{IntoUrl, Response};
use hyper::header::ContentType;
use hyper::status::StatusCode;
use rustc_serialize::json::{self, Json, ToJson};
use url::{ParseError, Url};

use error::FleetError;
use schema::{MachinePage, Unit, UnitOption, UnitPage, UnitStatePage, UnitStates};
use serialize::{self, CreateUnit, ModifyUnit};

/// An API client for fleet.
///
/// # Pagination
///
/// TODO: Document the pagination API.
///
/// # Failures
///
/// Each method involves making an HTTP request to the fleet API and can possibly result in an
/// error. Errors may occur either due to problems with the HTTP request itself (such as network
/// errors) or because the fleet API returned an explicit error code and message. In the *failures*
/// sections for the methods below, the error conditions that the fleet API itself can return are
/// detailed, but this is not an exhaustive list of reasons a request might fail.
///
/// # Examples
///
/// ```
/// extern crate fleet;
///
/// use fleet::Client;
///
/// let client = Client::new("http://localhost:2999");
///
/// match client.list_units() {
///     Ok(unit_page) => {
///         for unit in unit_page.units {
///             println!("{}", unit.name);
///         }
///     },
///     None => println!("No units in fleet!"),
/// };
pub struct Client {
    root_url: String,
}

impl Client {
    /// Constructs a new `Client`.
    ///
    /// `root_url` is a network scheme, hostname or IP address, and optional port where fleetd is
    /// running. This value should not include a path.
    ///
    /// Due to limitations in underlying libraries, Unix domain sockets are not yet supported. On
    /// CoreOS, fleet runs only on a Unix domain socket by default. It can be exposed on a TCP port
    /// by including a systemd drop-in for the `fleet.socket` unit. This can be achieved via
    /// cloud-config by overriding the default `fleet.socket` unit.
    ///
    /// ```yaml
    /// #cloud-config
    ///
    /// ---
    /// coreos:
    ///   units:
    ///     -  name: fleet.socket
    ///        command: start
    ///        drop_ins:
    ///        -  name: 30-ListenStream.conf
    ///           content: |
    ///             [Socket]
    ///             ListenStream=2999
    /// ```
    ///
    /// # Failures
    ///
    /// If the value provided for `root_url` cannot be parsed, a `url::ParseError` will be
    /// returned.
    pub fn new(root_url: &str) -> Result<Client, ParseError> {
        let url = try!(Url::parse(root_url));
        let client = Client {
            root_url: format!("{}{}", url.serialize(), "fleet/v1"),
        };

        Ok(client)
    }

    /// Creates a fleet unit.
    ///
    /// A unit consists of a name, the desired runtime state, and a set of unit options which
    /// comprise the unit's unit file.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern create fleet;
    /// # use fleet::{Client, UnitOption};
    /// # let client = Client::new("http://localhost:2999").unwrap();
    /// let options = vec![
    ///     UnitOption {
    ///         name: "ExecStart".to_string(),
    ///         section: "Service".to_string(),
    ///         value: "/usr/bin/sleep 3000".to_string(),
    ///     },
    /// ];
    ///
    /// client.create_unit("test.service", UnitStates::Launched, options).unwrap();
    /// ```
    pub fn create_unit(
        &self,
        name: &str,
        desired_state: UnitStates,
        options: Vec<UnitOption>
    ) -> Result<(), FleetError> {
        let serializer = CreateUnit {
            desiredState: desired_state.to_json(),
            options: options,
        };

        let url = self.build_url(&format!("/units/{}", name));
        let body = json::encode(&serializer).unwrap();
        let mut response = try!(self.put(url, body));

        match response.status {
            StatusCode::Created | StatusCode::NoContent => Ok(()),
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Destroys the unit with the given name.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// client.destroy_unit("test.service").ok().unwrap();
    pub fn destroy_unit(&self, name: &str) -> Result<(), FleetError> {
        let url = self.build_url(&format!("/units/{}", name));
        let mut response = try!(self.delete(&url[..]));

        match response.status {
            StatusCode::NoContent => Ok(()),
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Gets a single unit by name.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// client.get_unit("test.service").ok().unwrap();
    /// ```
    pub fn get_unit(&self, name: &str) -> Result<Unit, FleetError> {
        let url = self.build_url(&format!("/units/{}", name));
        let mut response = try!(self.get(&url[..]));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();

                Ok(serialize::unit_from_json(&json))
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Lists machines in the fleet cluster. This is a paginated resource.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// match client.list_machines("test.service") {
    ///     Ok(machine_page) {
    ///         for machine in machine_page.machines {
    ///             println!("Machine {}", machine.id);
    ///         }
    ///     },
    ///     None => println!("No machines found"),
    /// };
    /// ```
    pub fn list_machines(&self) -> Result<MachinePage, FleetError> {
        let url = self.build_url(&format!("/machines"));
        let mut response = try!(self.get(&url[..]));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();

                let machines = match json.find("machines") {
                    Some(machines_json) => {
                        let machines = machines_json.as_array().unwrap();

                        machines.iter().map(|json| {
                            serialize::machine_from_json(json)
                        }).collect()
                    },
                    None => vec![],
                };

                Ok(MachinePage {
                    machines: machines,
                    next_page_token: self.get_next_page_token(&json),
                })
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Lists the states of units in the fleet cluster. This is a paginated resource.
    ///
    /// If `Some` values are provided for `machine_id` or `unit_name`, they will be used to filter
    /// the unit states to only those matching the supplied values.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// match client.list_unit_states(None, None) {
    ///     Ok(unit_state_page) {
    ///         for state in unit_state_page.states {
    ///             println!("{}: {}", state.name, state.systemd_load_state);
    ///         }
    ///     },
    ///     None => println!("No units found"),
    /// };
    /// ```
    pub fn list_unit_states(
        &self,
        machine_id: Option<&str>,
        unit_name: Option<&str>
    ) -> Result<UnitStatePage, FleetError> {
        let mut query_pairs = HashMap::new();

        if machine_id.is_some() {
            query_pairs.insert("machineID", machine_id.unwrap());
        }

        if unit_name.is_some() {
            query_pairs.insert("unitName", unit_name.unwrap());
        }

        let base_url = self.build_url("/state");
        let mut url = Url::parse(&base_url[..]).unwrap();
        url.set_query_from_pairs(query_pairs.iter().map(|(k, v)| (*k, *v)));
        let mut response = try!(self.get(url));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();

                let unit_states = match json.find("states") {
                    Some(unit_states_json) => {
                        let unit_states = unit_states_json.as_array().unwrap();

                        unit_states.iter().map(|json| {
                            serialize::unit_state_from_json(json)
                        }).collect()
                    },
                    None => vec![],
                };

                Ok(UnitStatePage {
                    states: unit_states,
                    next_page_token: self.get_next_page_token(&json),
                })
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Lists the units in the fleet cluster. This is a paginated resource.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// match client.list_units() {
    ///     Ok(unit_page) {
    ///         for unit in unit_page.units {
    ///             println!("{}: {}", state.name, state.current_state);
    ///         }
    ///     },
    ///     None => println!("No units found"),
    /// };
    /// ```
    pub fn list_units(&self) -> Result<UnitPage, FleetError> {
        let url = self.build_url("/units");
        let mut response = try!(self.get(&url[..]));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();
                let units_json = json.find("units").unwrap().as_array().unwrap();
                let units = units_json.iter().map(|json| serialize::unit_from_json(json)).collect();

                Ok(UnitPage {
                    units: units,
                    next_page_token: self.get_next_page_token(&json),
                })
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Modifies a unit, instructing fleetd to move the unit to a new state.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::{Client, UnitStates};
    /// # let client = Client::new("http://localhost:2999");
    /// client.modify_unit("test.service", UnitStates::Loaded).ok().unwrap();
    /// ```
    pub fn modify_unit(
        &self,
        name: &str,
        desired_state: UnitStates
    ) -> Result<(), FleetError> {
        let serializer = ModifyUnit {
            desiredState: desired_state.to_json(),
        };

        let url = self.build_url(&format!("/units/{}", name));
        let body = json::encode(&serializer).unwrap();
        let mut response = try!(self.put(url, body));

        match response.status {
            StatusCode::Created | StatusCode::NoContent => Ok(()),
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    // Private

    fn build_url(&self, path: &str) -> String {
        format!("{}{}", self.root_url, path)
    }

    fn delete(&self, url: &str) -> Result<Response, FleetError> {
        let mut client = HyperClient::new();
        let content_type: ContentType = ContentType("application/json".parse().unwrap());

        match client.delete(url).header(content_type).send() {
            Ok(response) => Ok(response),
            Err(error) => Err(FleetError::from_hyper_error(&error)),
        }
    }

    fn get<U: IntoUrl>(&self, url: U) -> Result<Response, FleetError> {
        let mut client = HyperClient::new();
        let content_type: ContentType = ContentType("application/json".parse().unwrap());

        match client.get(url).header(content_type).send() {
            Ok(response) => Ok(response),
            Err(error) => Err(FleetError::from_hyper_error(&error)),
        }
    }

    fn get_next_page_token(&self, json: &Json) -> Option<String> {
        match json.find("nextPageToken") {
            Some(next_page_token_json) => match next_page_token_json.as_string() {
                Some(next_page_token) => Some(next_page_token.to_string()),
                None => None,
            },
            None => None,
        }
    }

    fn put(&self, url: String, body: String) -> Result<Response, FleetError> {
        let mut client = HyperClient::new();
        let content_type: ContentType = ContentType("application/json".parse().unwrap());

        match client.put(&url[..]).header(content_type).body(&body[..]).send() {
            Ok(response) => Ok(response),
            Err(error) => Err(FleetError::from_hyper_error(&error)),
        }
    }
}

#[cfg(test)]
mod client_tests {
    use super::Client;

    #[test]
    fn it_can_be_constructed() {
       Client::new("http://localhost").unwrap();
    }

    #[test]
    fn it_returns_an_error_for_invalid_root_urls() {
        assert!(Client::new("asdf").is_err());
    }
}
<commit_msg>Update method signatures for pagination.<commit_after>use std::collections::HashMap;

use hyper::Client as HyperClient;
use hyper::client::{IntoUrl, Response};
use hyper::header::ContentType;
use hyper::status::StatusCode;
use rustc_serialize::json::{self, Json, ToJson};
use url::{ParseError, Url};

use error::FleetError;
use schema::{MachinePage, Unit, UnitOption, UnitPage, UnitStatePage, UnitStates};
use serialize::{self, CreateUnit, ModifyUnit};

/// An API client for fleet.
///
/// # Pagination
///
/// Methods that return a collection of resources are paginated. On a successful API call, a
/// special "page" type will be returned. This type contains a vector of values of that resource
/// type and an optional "next page token." If this token is present, it indicates that at least
/// one additional page of resources is available. To make another call for the next page, pass
/// this token in to the API method. When the token value is `None`, the last page has been
/// reached.
///
/// # Failures
///
/// Each method involves making an HTTP request to the fleet API and can possibly result in an
/// error. Errors may occur either due to problems with the HTTP request itself (such as network
/// errors) or because the fleet API returned an explicit error code and message. In the *failures*
/// sections for the methods below, the error conditions that the fleet API itself can return are
/// detailed, but this is not an exhaustive list of reasons a request might fail.
///
/// # Examples
///
/// ```
/// extern crate fleet;
///
/// use fleet::Client;
///
/// let client = Client::new("http://localhost:2999");
///
/// match client.list_units(None) {
///     Ok(unit_page) => {
///         for unit in unit_page.units {
///             println!("{}", unit.name);
///         }
///     },
///     None => println!("No units in fleet!"),
/// };
pub struct Client {
    root_url: String,
}

impl Client {
    /// Constructs a new `Client`.
    ///
    /// `root_url` is a network scheme, hostname or IP address, and optional port where fleetd is
    /// running. This value should not include a path.
    ///
    /// Due to limitations in underlying libraries, Unix domain sockets are not yet supported. On
    /// CoreOS, fleet runs only on a Unix domain socket by default. It can be exposed on a TCP port
    /// by including a systemd drop-in for the `fleet.socket` unit. This can be achieved via
    /// cloud-config by overriding the default `fleet.socket` unit.
    ///
    /// ```yaml
    /// #cloud-config
    ///
    /// ---
    /// coreos:
    ///   units:
    ///     -  name: fleet.socket
    ///        command: start
    ///        drop_ins:
    ///        -  name: 30-ListenStream.conf
    ///           content: |
    ///             [Socket]
    ///             ListenStream=2999
    /// ```
    ///
    /// # Failures
    ///
    /// If the value provided for `root_url` cannot be parsed, a `url::ParseError` will be
    /// returned.
    pub fn new(root_url: &str) -> Result<Client, ParseError> {
        let url = try!(Url::parse(root_url));
        let client = Client {
            root_url: format!("{}{}", url.serialize(), "fleet/v1"),
        };

        Ok(client)
    }

    /// Creates a fleet unit.
    ///
    /// A unit consists of a name, the desired runtime state, and a set of unit options which
    /// comprise the unit's unit file.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern create fleet;
    /// # use fleet::{Client, UnitOption};
    /// # let client = Client::new("http://localhost:2999").unwrap();
    /// let options = vec![
    ///     UnitOption {
    ///         name: "ExecStart".to_string(),
    ///         section: "Service".to_string(),
    ///         value: "/usr/bin/sleep 3000".to_string(),
    ///     },
    /// ];
    ///
    /// client.create_unit("test.service", UnitStates::Launched, options).unwrap();
    /// ```
    pub fn create_unit(
        &self,
        name: &str,
        desired_state: UnitStates,
        options: Vec<UnitOption>
    ) -> Result<(), FleetError> {
        let serializer = CreateUnit {
            desiredState: desired_state.to_json(),
            options: options,
        };

        let url = self.build_url(&format!("/units/{}", name));
        let body = json::encode(&serializer).unwrap();
        let mut response = try!(self.put(url, body));

        match response.status {
            StatusCode::Created | StatusCode::NoContent => Ok(()),
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Destroys the unit with the given name.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// client.destroy_unit("test.service").ok().unwrap();
    pub fn destroy_unit(&self, name: &str) -> Result<(), FleetError> {
        let url = self.build_url(&format!("/units/{}", name));
        let mut response = try!(self.delete(&url[..]));

        match response.status {
            StatusCode::NoContent => Ok(()),
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Gets a single unit by name.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// client.get_unit("test.service").ok().unwrap();
    /// ```
    pub fn get_unit(&self, name: &str) -> Result<Unit, FleetError> {
        let url = self.build_url(&format!("/units/{}", name));
        let mut response = try!(self.get(&url[..]));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();

                Ok(serialize::unit_from_json(&json))
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Lists machines in the fleet cluster. This is a paginated resource.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// match client.list_machines(None) {
    ///     Ok(machine_page) {
    ///         for machine in machine_page.machines {
    ///             println!("Machine {}", machine.id);
    ///         }
    ///     },
    ///     None => println!("No machines found"),
    /// };
    /// ```
    pub fn list_machines(
        &self,
        next_page_token: Option<String>,
    ) -> Result<MachinePage, FleetError> {
        let url = self.build_url(&format!("/machines"));
        let mut response = try!(self.get(&url[..]));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();

                let machines = match json.find("machines") {
                    Some(machines_json) => {
                        let machines = machines_json.as_array().unwrap();

                        machines.iter().map(|json| {
                            serialize::machine_from_json(json)
                        }).collect()
                    },
                    None => vec![],
                };

                Ok(MachinePage {
                    machines: machines,
                    next_page_token: self.get_next_page_token(&json),
                })
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Lists the states of units in the fleet cluster. This is a paginated resource.
    ///
    /// If `Some` values are provided for `machine_id` or `unit_name`, they will be used to filter
    /// the unit states to only those matching the supplied values.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// match client.list_unit_states(None, None, None) {
    ///     Ok(unit_state_page) {
    ///         for state in unit_state_page.states {
    ///             println!("{}: {}", state.name, state.systemd_load_state);
    ///         }
    ///     },
    ///     None => println!("No units found"),
    /// };
    /// ```
    pub fn list_unit_states(
        &self,
        machine_id: Option<&str>,
        unit_name: Option<&str>,
        next_page_token: Option<String>,
    ) -> Result<UnitStatePage, FleetError> {
        let mut query_pairs = HashMap::new();

        if machine_id.is_some() {
            query_pairs.insert("machineID", machine_id.unwrap());
        }

        if unit_name.is_some() {
            query_pairs.insert("unitName", unit_name.unwrap());
        }

        let base_url = self.build_url("/state");
        let mut url = Url::parse(&base_url[..]).unwrap();
        url.set_query_from_pairs(query_pairs.iter().map(|(k, v)| (*k, *v)));
        let mut response = try!(self.get(url));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();

                let unit_states = match json.find("states") {
                    Some(unit_states_json) => {
                        let unit_states = unit_states_json.as_array().unwrap();

                        unit_states.iter().map(|json| {
                            serialize::unit_state_from_json(json)
                        }).collect()
                    },
                    None => vec![],
                };

                Ok(UnitStatePage {
                    states: unit_states,
                    next_page_token: self.get_next_page_token(&json),
                })
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Lists the units in the fleet cluster. This is a paginated resource.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::Client;
    /// # let client = Client::new("http://localhost:2999");
    /// match client.list_units(None) {
    ///     Ok(unit_page) {
    ///         for unit in unit_page.units {
    ///             println!("{}: {}", state.name, state.current_state);
    ///         }
    ///     },
    ///     None => println!("No units found"),
    /// };
    /// ```
    pub fn list_units(&self, next_page_token: Option<String>) -> Result<UnitPage, FleetError> {
        let url = self.build_url("/units");
        let mut response = try!(self.get(&url[..]));

        match response.status {
            StatusCode::Ok => {
                let json = Json::from_reader(&mut response).unwrap();
                let units_json = json.find("units").unwrap().as_array().unwrap();
                let units = units_json.iter().map(|json| serialize::unit_from_json(json)).collect();

                Ok(UnitPage {
                    units: units,
                    next_page_token: self.get_next_page_token(&json),
                })
            },
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    /// Modifies a unit, instructing fleetd to move the unit to a new state.
    ///
    /// # Failures
    ///
    /// TODO: Document possible API error responses.
    ///
    /// # Examples
    ///
    /// ```
    /// # extern crate fleet;
    /// # use fleet::{Client, UnitStates};
    /// # let client = Client::new("http://localhost:2999");
    /// client.modify_unit("test.service", UnitStates::Loaded).ok().unwrap();
    /// ```
    pub fn modify_unit(
        &self,
        name: &str,
        desired_state: UnitStates
    ) -> Result<(), FleetError> {
        let serializer = ModifyUnit {
            desiredState: desired_state.to_json(),
        };

        let url = self.build_url(&format!("/units/{}", name));
        let body = json::encode(&serializer).unwrap();
        let mut response = try!(self.put(url, body));

        match response.status {
            StatusCode::Created | StatusCode::NoContent => Ok(()),
            _ => Err(FleetError::from_hyper_response(&mut response)),
        }
    }

    // Private

    fn build_url(&self, path: &str) -> String {
        format!("{}{}", self.root_url, path)
    }

    fn delete(&self, url: &str) -> Result<Response, FleetError> {
        let mut client = HyperClient::new();
        let content_type: ContentType = ContentType("application/json".parse().unwrap());

        match client.delete(url).header(content_type).send() {
            Ok(response) => Ok(response),
            Err(error) => Err(FleetError::from_hyper_error(&error)),
        }
    }

    fn get<U: IntoUrl>(&self, url: U) -> Result<Response, FleetError> {
        let mut client = HyperClient::new();
        let content_type: ContentType = ContentType("application/json".parse().unwrap());

        match client.get(url).header(content_type).send() {
            Ok(response) => Ok(response),
            Err(error) => Err(FleetError::from_hyper_error(&error)),
        }
    }

    fn get_next_page_token(&self, json: &Json) -> Option<String> {
        match json.find("nextPageToken") {
            Some(next_page_token_json) => match next_page_token_json.as_string() {
                Some(next_page_token) => Some(next_page_token.to_string()),
                None => None,
            },
            None => None,
        }
    }

    fn put(&self, url: String, body: String) -> Result<Response, FleetError> {
        let mut client = HyperClient::new();
        let content_type: ContentType = ContentType("application/json".parse().unwrap());

        match client.put(&url[..]).header(content_type).body(&body[..]).send() {
            Ok(response) => Ok(response),
            Err(error) => Err(FleetError::from_hyper_error(&error)),
        }
    }
}

#[cfg(test)]
mod client_tests {
    use super::Client;

    #[test]
    fn it_can_be_constructed() {
       Client::new("http://localhost").unwrap();
    }

    #[test]
    fn it_returns_an_error_for_invalid_root_urls() {
        assert!(Client::new("asdf").is_err());
    }
}
<|endoftext|>


<commit_before>#![allow(dead_code)]
#![allow(non_upper_case_globals)]
//#![allow(unused_assignments)]
#![allow(unused_imports)]
//#![allow(unused_variables)]

use std::env;
use std::ffi::OsStr;
use std::fmt::Display;
use std::fs;
use std::fs::File;
use std::io::{ BufReader, BufWriter, };
use std::io::prelude::*;
use std::os::unix::net::UnixStream;
use std::os::unix::process::CommandExt;
use std::path::{ Path, PathBuf, };
use std::process::{ Command, exit, };

use jsonrpc_lite;
use jsonrpc_lite::JsonRpc;

use serde_json;
use serde_json::Value;

type JsonRpcId = i64;

fn make_lsp_json_str(id: JsonRpcId, method: &str, params: &Value) -> Result<String, serde_json::error::Error> {
    let msg = json!({
        "jsonrpc": "2.0",
        "id": id,
        "method": method,
        "params": params,
    });
    let request = serde_json::to_string(&msg)?;
    Ok(format!("Content-Length: {}\r\n\r\n{}", request.len(), request))
}

#[derive(Debug, PartialEq)]
/// A message header, as described in the Language Server Protocol specification.
enum LspHeader {
    ContentType,
    ContentLength(usize),
}

/// Given a reference to a reader, attempts to read a Language Server Protocol message,
/// blocking until a message is received.
fn read_message<B: BufRead>(reader: &mut B) -> Value {
    let mut buffer = String::new();
    let mut content_length: Option<usize> = None;

    // read in headers.
    loop {
            buffer.clear();
            reader.read_line(&mut buffer).unwrap();
            match &buffer {
                s if s.trim().is_empty() => { break }, // empty line is end of headers
                s => {
                    match parse_header(s) {
                        LspHeader::ContentLength(len) => content_length = Some(len),
                        LspHeader::ContentType => (), // utf-8 only currently allowed value
                    };
                }
            };
        }

    let content_length = content_length.ok_or_else(|| format!("missing content-length header: {}", buffer)).unwrap();
    // message body isn't newline terminated, so we read content_length bytes
    let mut body_buffer = vec![0; content_length];
    reader.read_exact(&mut body_buffer).unwrap();
    let body = String::from_utf8(body_buffer).unwrap();
    serde_json::from_str::<Value>(&body).unwrap()
}

const HEADER_CONTENT_LENGTH: &str = "content-length";
const HEADER_CONTENT_TYPE: &str = "content-type";

/// Given a header string, attempts to extract and validate the name and value parts.
fn parse_header(s: &str) -> LspHeader {
    let split: Vec<String> = s.split(": ").map(|s| s.trim().to_lowercase()).collect();
    if split.len() != 2 { panic!(format!("malformed header: {}", s)) }
    match split[0].as_ref() {
        HEADER_CONTENT_TYPE   => LspHeader::ContentType,
        HEADER_CONTENT_LENGTH => LspHeader::ContentLength(usize::from_str_radix(&split[1], 10).unwrap()),
        _ => panic!(format!("Unknown header: {}", s)),
    }
}

fn handle_msg_quietly<B: BufRead>(mut reader: B) {
    match serde_json::from_value(read_message(&mut reader)).unwrap() {
        JsonRpc::Request(obj)    => eprintln!("client received unexpected request: {:?}", obj),
        JsonRpc::Notification(_) => (),
        JsonRpc::Success(_)      => (),
        JsonRpc::Error(obj)      => println!("recv error: {:?}", obj),
    }
}

fn handle_msg<B: BufRead>(mut reader: B) {
    match serde_json::from_value(read_message(&mut reader)).unwrap() {
        JsonRpc::Request(obj)      => eprintln!("client received unexpected request: {:?}", obj),
        JsonRpc::Notification(obj) => println!("recv notification: {:?}", obj),
        JsonRpc::Success(obj)      => println!("recv success: {:?}", obj),
        JsonRpc::Error(obj)        => println!("recv error: {:?}", obj),
    }
}

enum ExitCode { Success, Failure }

fn handle_msg_to_exit_code<B: BufRead>(mut reader: B, id: JsonRpcId) -> ExitCode {
    let mut done = false;
    let mut success = false;
    let mut failure = false;

    loop {
        let json_rpc: JsonRpc = serde_json::from_value(read_message(&mut reader)).unwrap();
        match json_rpc {
            JsonRpc::Request(ref obj)      => eprintln!("client received unexpected request: {:?}", obj),
            JsonRpc::Success(ref obj)      => {
                let json_id = json_rpc.get_id().unwrap();
                let id_str = match json_id {
                    jsonrpc_lite::Id::Num(n)   => n.to_string(),
                    jsonrpc_lite::Id::Str(s)   => s,
                    jsonrpc_lite::Id::None(()) => "".to_string(),
                };
                if id_str == id.to_string() {
                    return ExitCode::Success
                } else {
                    println!("recv success: {:?}", obj)
                }
            },
            JsonRpc::Error(ref obj)        => {
                let json_id = json_rpc.get_id().unwrap();
                let id_str = match json_id {
                    jsonrpc_lite::Id::Num(n)   => n.to_string(),
                    jsonrpc_lite::Id::Str(s)   => s,
                    jsonrpc_lite::Id::None(()) => "".to_string(),
                };
                if id_str == id.to_string() {
                    let error = json_rpc.get_error().unwrap();
                    eprintln!("[error] {}", error);
                    return ExitCode::Failure
                } else {
                    println!("recv error: {:?}", obj)
                }
            },
            JsonRpc::Notification(ref obj) => {
                match json_rpc.get_method() {
                    Some("window/logMessage") => {
                        let params0 = json_rpc.get_params().unwrap();
                        let params = match params0 {
                            jsonrpc_lite::Params::Array(_) => panic!("not expecting array"),
                            jsonrpc_lite::Params::None(()) => panic!("not expecting none"),
                            jsonrpc_lite::Params::Map(kvs) => kvs,
                        };
                        let lvl = params["type"].as_i64().unwrap();
                        let msg = params["message"].as_str().unwrap();
                        println!("{}", msg);
                        if msg == "Exited with code 0" { success = true }
                        if msg == "Done" { done = true }
                        if lvl == 1 { failure = true }
                    },
                    Some("textDocument/publishDiagnostics") => {
                        let params0 = json_rpc.get_params().unwrap();
                        let params = match params0 {
                            jsonrpc_lite::Params::Array(_) => panic!("not expecting array"),
                            jsonrpc_lite::Params::None(()) => panic!("not expecting none"),
                            jsonrpc_lite::Params::Map(kvs) => kvs,
                        };
                        let uri = params["uri"].as_str().unwrap();
                        let diagnostics = params["diagnostics"].as_array().unwrap();
                        for diagnostic in diagnostics {
                            println!("{}: {}", uri, diagnostic);
                        }
                    },
                    Some(_) | _ => println!("recv notification: {:?}", obj),
                }
            },
        }

        // val Error = 1L
        // val Warning = 2L
        // val Info = 3L
        // val Log = 4L

        // 'runMain t.Main'     reports "[log] Exited with code 0" and then "[log] Done"
        // 'runMain t.BadMain'  reports "[log] Done"               and then "[error] Nonzero exit code: 1"
        // 'compile' w/ error   reports "[error] Compilation failed"
        // 'compile' w/o errors reports "[log] Done", but we can't distinguish that from BadMain's "[log] Done"...

        if success && done { return ExitCode::Success }
        if failure { return ExitCode::Failure }
    }
}

pub fn talk_to_client(port_file: File) {
    let json: serde_json::Value = serde_json::from_reader(port_file).unwrap();
    let uri = json["uri"].as_str().unwrap();
    // TODO: Use a less idiotic way to get the path from the URI
    let socket_file_path = &uri[8..];

    match UnixStream::connect(socket_file_path) {
        Ok(stream) => talk_to_client_impl(socket_file_path, stream),
        Err(_)     => {
            fs::remove_file("project/target/active.json").expect("Failed to delete port file");
            ::main()
        },
    }
}

fn talk_to_client_impl<P: AsRef<Path>>(socket_file_path: P, mut stream: UnixStream) {
    let json_str = make_lsp_json_str(1, "initialize", &json!({})).unwrap();
    stream.write_all(json_str.as_bytes()).unwrap();
    stream.flush().unwrap();

    let mut reader = BufReader::new(stream);
    handle_msg_quietly(&mut reader);

    let mut args = env::args().skip(1); // skip the path of the executable
    let command_line = args.nth(0).expect("at least one argument to sbt when server already running");
    let json_str2 = make_lsp_json_str(2, "sbt/exec", &json!({"commandLine": command_line})).unwrap();
    let mut stream2 = UnixStream::connect(socket_file_path).unwrap();
    stream2.write_all(json_str2.as_bytes()).unwrap();
    stream2.flush().unwrap();

    let reader2 = BufReader::new(stream2);
    match handle_msg_to_exit_code(reader2, 2) {
        ExitCode::Failure => exit(1),
        ExitCode::Success => exit(0),
    }
}

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

    #[test]
    fn decode_ok() {
        let msg = r#"{"jsonrpc":"2.0","result":{"commandQueue":["collectAnalyses","shell"],"exitCode":0,"status":"Done"},"id":null}"#;
        let json = serde_json::from_str::<serde_json::Value>(&msg).unwrap();
        let json_rpc: JsonRpc = serde_json::from_value(json).expect("is JSON RPC");
    }

    #[test]
    #[should_panic(expected = "data did not match any variant of untagged enum JsonRpc")]
    fn decode_ko() {
        let msg = r#"{"jsonrpc":"2.0","result":{"commandQueue":["collectAnalyses","shell"],"exitCode":0,"status":"Done"}}"#;
        let json = serde_json::from_str::<serde_json::Value>(&msg).unwrap();
        let json_rpc: JsonRpc = serde_json::from_value(json).expect("is JSON RPC");
    }
}
<commit_msg>Use deserialise JsonRpc in test<commit_after>#![allow(dead_code)]
#![allow(non_upper_case_globals)]
//#![allow(unused_assignments)]
#![allow(unused_imports)]
//#![allow(unused_variables)]

use std::env;
use std::ffi::OsStr;
use std::fmt::Display;
use std::fs;
use std::fs::File;
use std::io::{ BufReader, BufWriter, };
use std::io::prelude::*;
use std::os::unix::net::UnixStream;
use std::os::unix::process::CommandExt;
use std::path::{ Path, PathBuf, };
use std::process::{ Command, exit, };

use jsonrpc_lite;
use jsonrpc_lite::JsonRpc;

use serde_json;
use serde_json::Value;

type JsonRpcId = i64;

fn make_lsp_json_str(id: JsonRpcId, method: &str, params: &Value) -> Result<String, serde_json::error::Error> {
    let msg = json!({
        "jsonrpc": "2.0",
        "id": id,
        "method": method,
        "params": params,
    });
    let request = serde_json::to_string(&msg)?;
    Ok(format!("Content-Length: {}\r\n\r\n{}", request.len(), request))
}

#[derive(Debug, PartialEq)]
/// A message header, as described in the Language Server Protocol specification.
enum LspHeader {
    ContentType,
    ContentLength(usize),
}

/// Given a reference to a reader, attempts to read a Language Server Protocol message,
/// blocking until a message is received.
fn read_message<B: BufRead>(reader: &mut B) -> Value {
    let mut buffer = String::new();
    let mut content_length: Option<usize> = None;

    // read in headers.
    loop {
            buffer.clear();
            reader.read_line(&mut buffer).unwrap();
            match &buffer {
                s if s.trim().is_empty() => { break }, // empty line is end of headers
                s => {
                    match parse_header(s) {
                        LspHeader::ContentLength(len) => content_length = Some(len),
                        LspHeader::ContentType => (), // utf-8 only currently allowed value
                    };
                }
            };
        }

    let content_length = content_length.ok_or_else(|| format!("missing content-length header: {}", buffer)).unwrap();
    // message body isn't newline terminated, so we read content_length bytes
    let mut body_buffer = vec![0; content_length];
    reader.read_exact(&mut body_buffer).unwrap();
    let body = String::from_utf8(body_buffer).unwrap();
    serde_json::from_str::<Value>(&body).unwrap()
}

const HEADER_CONTENT_LENGTH: &str = "content-length";
const HEADER_CONTENT_TYPE: &str = "content-type";

/// Given a header string, attempts to extract and validate the name and value parts.
fn parse_header(s: &str) -> LspHeader {
    let split: Vec<String> = s.split(": ").map(|s| s.trim().to_lowercase()).collect();
    if split.len() != 2 { panic!(format!("malformed header: {}", s)) }
    match split[0].as_ref() {
        HEADER_CONTENT_TYPE   => LspHeader::ContentType,
        HEADER_CONTENT_LENGTH => LspHeader::ContentLength(usize::from_str_radix(&split[1], 10).unwrap()),
        _ => panic!(format!("Unknown header: {}", s)),
    }
}

fn handle_msg_quietly<B: BufRead>(mut reader: B) {
    match serde_json::from_value(read_message(&mut reader)).unwrap() {
        JsonRpc::Request(obj)    => eprintln!("client received unexpected request: {:?}", obj),
        JsonRpc::Notification(_) => (),
        JsonRpc::Success(_)      => (),
        JsonRpc::Error(obj)      => println!("recv error: {:?}", obj),
    }
}

fn handle_msg<B: BufRead>(mut reader: B) {
    match serde_json::from_value(read_message(&mut reader)).unwrap() {
        JsonRpc::Request(obj)      => eprintln!("client received unexpected request: {:?}", obj),
        JsonRpc::Notification(obj) => println!("recv notification: {:?}", obj),
        JsonRpc::Success(obj)      => println!("recv success: {:?}", obj),
        JsonRpc::Error(obj)        => println!("recv error: {:?}", obj),
    }
}

enum ExitCode { Success, Failure }

fn handle_msg_to_exit_code<B: BufRead>(mut reader: B, id: JsonRpcId) -> ExitCode {
    let mut done = false;
    let mut success = false;
    let mut failure = false;

    loop {
        let json_rpc: JsonRpc = serde_json::from_value(read_message(&mut reader)).unwrap();
        match json_rpc {
            JsonRpc::Request(ref obj)      => eprintln!("client received unexpected request: {:?}", obj),
            JsonRpc::Success(ref obj)      => {
                let json_id = json_rpc.get_id().unwrap();
                let id_str = match json_id {
                    jsonrpc_lite::Id::Num(n)   => n.to_string(),
                    jsonrpc_lite::Id::Str(s)   => s,
                    jsonrpc_lite::Id::None(()) => "".to_string(),
                };
                if id_str == id.to_string() {
                    return ExitCode::Success
                } else {
                    println!("recv success: {:?}", obj)
                }
            },
            JsonRpc::Error(ref obj)        => {
                let json_id = json_rpc.get_id().unwrap();
                let id_str = match json_id {
                    jsonrpc_lite::Id::Num(n)   => n.to_string(),
                    jsonrpc_lite::Id::Str(s)   => s,
                    jsonrpc_lite::Id::None(()) => "".to_string(),
                };
                if id_str == id.to_string() {
                    let error = json_rpc.get_error().unwrap();
                    eprintln!("[error] {}", error);
                    return ExitCode::Failure
                } else {
                    println!("recv error: {:?}", obj)
                }
            },
            JsonRpc::Notification(ref obj) => {
                match json_rpc.get_method() {
                    Some("window/logMessage") => {
                        let params0 = json_rpc.get_params().unwrap();
                        let params = match params0 {
                            jsonrpc_lite::Params::Array(_) => panic!("not expecting array"),
                            jsonrpc_lite::Params::None(()) => panic!("not expecting none"),
                            jsonrpc_lite::Params::Map(kvs) => kvs,
                        };
                        let lvl = params["type"].as_i64().unwrap();
                        let msg = params["message"].as_str().unwrap();
                        println!("{}", msg);
                        if msg == "Exited with code 0" { success = true }
                        if msg == "Done" { done = true }
                        if lvl == 1 { failure = true }
                    },
                    Some("textDocument/publishDiagnostics") => {
                        let params0 = json_rpc.get_params().unwrap();
                        let params = match params0 {
                            jsonrpc_lite::Params::Array(_) => panic!("not expecting array"),
                            jsonrpc_lite::Params::None(()) => panic!("not expecting none"),
                            jsonrpc_lite::Params::Map(kvs) => kvs,
                        };
                        let uri = params["uri"].as_str().unwrap();
                        let diagnostics = params["diagnostics"].as_array().unwrap();
                        for diagnostic in diagnostics {
                            println!("{}: {}", uri, diagnostic);
                        }
                    },
                    Some(_) | _ => println!("recv notification: {:?}", obj),
                }
            },
        }

        // val Error = 1L
        // val Warning = 2L
        // val Info = 3L
        // val Log = 4L

        // 'runMain t.Main'     reports "[log] Exited with code 0" and then "[log] Done"
        // 'runMain t.BadMain'  reports "[log] Done"               and then "[error] Nonzero exit code: 1"
        // 'compile' w/ error   reports "[error] Compilation failed"
        // 'compile' w/o errors reports "[log] Done", but we can't distinguish that from BadMain's "[log] Done"...

        if success && done { return ExitCode::Success }
        if failure { return ExitCode::Failure }
    }
}

pub fn talk_to_client(port_file: File) {
    let json: serde_json::Value = serde_json::from_reader(port_file).unwrap();
    let uri = json["uri"].as_str().unwrap();
    // TODO: Use a less idiotic way to get the path from the URI
    let socket_file_path = &uri[8..];

    match UnixStream::connect(socket_file_path) {
        Ok(stream) => talk_to_client_impl(socket_file_path, stream),
        Err(_)     => {
            fs::remove_file("project/target/active.json").expect("Failed to delete port file");
            ::main()
        },
    }
}

fn talk_to_client_impl<P: AsRef<Path>>(socket_file_path: P, mut stream: UnixStream) {
    let json_str = make_lsp_json_str(1, "initialize", &json!({})).unwrap();
    stream.write_all(json_str.as_bytes()).unwrap();
    stream.flush().unwrap();

    let mut reader = BufReader::new(stream);
    handle_msg_quietly(&mut reader);

    let mut args = env::args().skip(1); // skip the path of the executable
    let command_line = args.nth(0).expect("at least one argument to sbt when server already running");
    let json_str2 = make_lsp_json_str(2, "sbt/exec", &json!({"commandLine": command_line})).unwrap();
    let mut stream2 = UnixStream::connect(socket_file_path).unwrap();
    stream2.write_all(json_str2.as_bytes()).unwrap();
    stream2.flush().unwrap();

    let reader2 = BufReader::new(stream2);
    match handle_msg_to_exit_code(reader2, 2) {
        ExitCode::Failure => exit(1),
        ExitCode::Success => exit(0),
    }
}

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

    #[test]
    fn decode_ok() {
        let msg = r#"{"jsonrpc":"2.0","result":{"commandQueue":["collectAnalyses","shell"],"exitCode":0,"status":"Done"},"id":null}"#;
        let json = serde_json::from_str::<serde_json::Value>(&msg).unwrap();
        let json_rpc: JsonRpc = serde_json::from_value(json).expect("is JSON RPC");
        drop(json_rpc)
    }

    #[test]
    #[should_panic(expected = "data did not match any variant of untagged enum JsonRpc")]
    fn decode_ko() {
        let msg = r#"{"jsonrpc":"2.0","result":{"commandQueue":["collectAnalyses","shell"],"exitCode":0,"status":"Done"}}"#;
        let json = serde_json::from_str::<serde_json::Value>(&msg).unwrap();
        let json_rpc: JsonRpc = serde_json::from_value(json).expect("is JSON RPC");
        drop(json_rpc)
    }
}
<|endoftext|>


<commit_before>//! The `Client` allows users of the `raft` library to connect to remote `Server` instances and
//! issue commands to be applied to the `StateMachine`.

use std::collections::HashSet;
use std::io::Write;
use std::net::SocketAddr;
use std::net::TcpStream;

use bufstream::BufStream;
use capnp::{serialize, MessageReader, ReaderOptions};

use messages_capnp::{client_response, proposal_response};
use messages;
use Result;

/// The representation of a Client connection to the cluster.
pub struct Client {
    /// The current connection to the current leader.
    /// If it is none it may mean that there is no estabished leader or that there has been
    /// a disconnection.
    leader_connection: Option<BufStream<TcpStream>>,
    /// A lookup for the cluster's nodes.
    cluster: HashSet<SocketAddr>,
}

impl Client {

    /// Creates a new client.
    pub fn new(cluster: HashSet<SocketAddr>) -> Client {
        Client {
            leader_connection: None,
            cluster: cluster,
        }
    }

    /// Proposes an entry to be appended to the replicated log. This will only
    /// return once the entry has been durably committed.
    pub fn propose(&mut self, entry: &[u8]) -> Result<()> {
        let mut message = messages::proposal_request(entry);

        let mut members = self.cluster.iter().cloned().cycle();

        loop {
            let mut connection = match self.leader_connection.take() {
                Some(cxn) => cxn,
                None => {
                    let leader = members.next().unwrap();
                    debug!("connecting to potential leader {}", leader);
                    BufStream::new(try!(TcpStream::connect(leader)))
                }
            };

            try!(serialize::write_message(&mut connection, &mut message));
            try!(connection.flush());
            let response = try!(serialize::read_message(&mut connection, ReaderOptions::new()));

            match try!(response.get_root::<client_response::Reader>()).which().unwrap() {
                client_response::Which::Proposal(Ok(response)) => {
                    match response.which().unwrap() {
                        proposal_response::Which::Success(()) => {
                            self.leader_connection = Some(connection);
                            return Ok(())
                        },
                        proposal_response::Which::UnknownLeader(()) => (),
                        proposal_response::Which::NotLeader(leader) => {
                            let connection: TcpStream = try!(TcpStream::connect(try!(leader)));
                            self.leader_connection = Some(BufStream::new(connection));
                        }
                    }
                },
                _ => panic!("Unexpected message type"), // TODO: return a proper error
            }
        }
    }
}
<commit_msg>Auto merge of #58 - Hoverbear:client, r=Hoverbear<commit_after>//! The `Client` allows users of the `raft` library to connect to remote `Server` instances and
//! issue commands to be applied to the `StateMachine`.

use std::collections::HashSet;
use std::io::Write;
use std::net::SocketAddr;
use std::net::TcpStream;

use bufstream::BufStream;
use capnp::{serialize, MessageReader, ReaderOptions};

use messages_capnp::{client_response, proposal_response};
use messages;
use ClientId;
use Result;

/// The representation of a Client connection to the cluster.
pub struct Client {
    /// The `Uuid` of the client, should be unique in the cluster.
    pub id: ClientId,
    /// The current connection to the current leader.
    /// If it is none it may mean that there is no estabished leader or that there has been
    /// a disconnection.
    leader_connection: Option<BufStream<TcpStream>>,
    /// A lookup for the cluster's nodes.
    cluster: HashSet<SocketAddr>,
}

impl Client {

    /// Creates a new client.
    pub fn new(cluster: HashSet<SocketAddr>) -> Client {
        Client {
            id: ClientId::new(),
            leader_connection: None,
            cluster: cluster,
        }
    }

    /// Proposes an entry to be appended to the replicated log. This will only
    /// return once the entry has been durably committed.
    pub fn propose(&mut self, entry: &[u8]) -> Result<()> {
        let mut message = messages::proposal_request(entry);

        let mut members = self.cluster.iter().cloned().cycle();

        loop {
            let mut connection = match self.leader_connection.take() {
                Some(cxn) => cxn,
                None => {
                    let leader = members.next().unwrap();
                    debug!("connecting to potential leader {}", leader);
                    // Send the preamble.
                    let preamble = messages::client_connection_preamble(self.id);
                    let mut stream = BufStream::new(try!(TcpStream::connect(leader)));
                    try!(serialize::write_message(&mut stream, &*preamble));
                    stream
                }
            };
            try!(serialize::write_message(&mut connection, &mut message));
            try!(connection.flush());
            let response = try!(serialize::read_message(&mut connection, ReaderOptions::new()));
            match try!(response.get_root::<client_response::Reader>()).which().unwrap() {
                client_response::Which::Proposal(Ok(response)) => {
                    match response.which().unwrap() {
                        proposal_response::Which::Success(()) => {
                            self.leader_connection = Some(connection);
                            return Ok(())
                        },
                        proposal_response::Which::UnknownLeader(()) => (),
                        proposal_response::Which::NotLeader(leader) => {
                            let connection: TcpStream = try!(TcpStream::connect(try!(leader)));
                            self.leader_connection = Some(BufStream::new(connection));
                        }
                    }
                },
                _ => panic!("Unexpected message type"), // TODO: return a proper error
            }
        }
    }
}

#[cfg(test)]
mod test {
    use Client;
    use uuid::Uuid;
    use std::net::{SocketAddr, TcpListener};
    use std::collections::HashSet;
    use std::str::FromStr;
    use std::thread;
    use std::io::Read;
    use capnp::{serialize, ReaderOptions};
    use capnp::message::MessageReader;
    use messages;
    use messages_capnp::{connection_preamble, client_request};

    #[test]
    fn test_proposal_standalone() {
        let mut cluster = HashSet::new();
        let test_server = TcpListener::bind(SocketAddr::from_str("127.0.0.1:0").unwrap()).unwrap();
        let test_addr = test_server.local_addr().unwrap();
        cluster.insert(test_addr);

        let mut client = Client::new(cluster);
        let client_id = client.id.0.clone();
        let to_propose = b"Bears";

        // The client connects on the first proposal.
        // Wait for it.
        let child = thread::spawn(move || {
            let (mut connection, _)  = test_server.accept().unwrap();

            // Expect Preamble.
            let message = serialize::read_message(&mut connection, ReaderOptions::new()).unwrap();
            let preamble = message.get_root::<connection_preamble::Reader>().unwrap();
            // Test to make sure preamble has the right id.
            if let connection_preamble::id::Which::Client(Ok(id)) = preamble.get_id().which().unwrap() {
                assert_eq!(Uuid::from_bytes(id).unwrap(), client_id);
            } else { panic!("Invalid preamble."); }

            // Expect proposal.
            let message = serialize::read_message(&mut connection, ReaderOptions::new()).unwrap();
            let request = message.get_root::<client_request::Reader>().unwrap();
            // Test to make sure request has the right value.
            if let client_request::Which::Proposal(Ok(proposal)) = request.which().unwrap() {
                assert_eq!(proposal.get_entry().unwrap(), to_propose);
            } else { panic!("Invalid request."); }

            // Send a response.
            let response = messages::proposal_response_success();
            serialize::write_message(&mut connection, &*response).unwrap();
        });
        // Propose. It's a marriage made in heaven! :)
        client.propose(to_propose).unwrap();
        child.join().unwrap();
    }
}
<|endoftext|>