<commit_before>pub mod proto;
mod reader;
mod writer;
mod errors;
mod client;
mod processor;
mod transfer;

use std::{io, str, result};
use std::net::{TcpListener, TcpStream, Ipv4Addr, SocketAddrV4};
use std::collections::HashMap;

use slog::Logger;
use serde_json;
use amy;

pub use self::proto::resource;
use self::proto::message::{self, SMessage};
pub use self::errors::{Result, ResultExt, ErrorKind, Error};
use self::proto::ws;
use self::client::{Incoming, IncomingStatus, Client};
use self::processor::{Processor, TransferKind};
use self::transfer::{Transfers, TransferResult};
use util;
use bencode;
use handle;
use torrent;
use CONFIG;

const POLL_INT_MS: usize = 1000;
const CLEANUP_INT_MS: usize = 2000;

#[derive(Debug)]
pub enum CtlMessage {
    Extant(Vec<resource::Resource>),
    Update(Vec<resource::SResourceUpdate<'static>>),
    Removed(Vec<String>),
    Shutdown,
}

#[derive(Debug)]
pub enum Message {
    UpdateTorrent(resource::CResourceUpdate),
    UpdateServer {
        id: String,
        throttle_up: Option<u32>,
        throttle_down: Option<u32>,
    },
    UpdateFile {
        id: String,
        torrent_id: String,
        priority: u8,
    },
    RemoveTorrent(String),
    RemovePeer { id: String, torrent_id: String },
    RemoveTracker { id: String, torrent_id: String },
    Torrent { info: torrent::Info, path: Option<String> },
}

#[allow(dead_code)]
pub struct RPC {
    poll: amy::Poller,
    reg: amy::Registrar,
    ch: handle::Handle<CtlMessage, Message>,
    listener: TcpListener,
    lid: usize,
    cleanup: usize,
    processor: Processor,
    transfers: Transfers,
    clients: HashMap<usize, Client>,
    incoming: HashMap<usize, Incoming>,
    l: Logger,
}

impl RPC {
    pub fn start(creg: &mut amy::Registrar) -> io::Result<handle::Handle<Message, CtlMessage>> {
        let poll = amy::Poller::new()?;
        let mut reg = poll.get_registrar()?;
        let cleanup = reg.set_interval(CLEANUP_INT_MS)?;
        let (ch, dh) = handle::Handle::new(creg, &mut reg)?;

        let ip = if CONFIG.rpc.local {
            Ipv4Addr::new(127, 0, 0, 1)
        } else {
            Ipv4Addr::new(0, 0, 0, 0)
        };
        let port = CONFIG.rpc.port;
        let listener = TcpListener::bind(SocketAddrV4::new(ip, port))?;
        listener.set_nonblocking(true)?;
        let lid = reg.register(&listener, amy::Event::Both)?;

        dh.run("rpc", move |ch, l| {
            RPC {
                ch,
                poll,
                reg,
                listener,
                lid,
                cleanup,
                clients: HashMap::new(),
                incoming: HashMap::new(),
                processor: Processor::new(),
                transfers: Transfers::new(),
                l,
            }.run()
        });
        Ok(ch)
    }

    pub fn run(&mut self) {
        debug!(self.l, "Running RPC!");
        'outer: while let Ok(res) = self.poll.wait(POLL_INT_MS) {
            for not in res {
                match not.id {
                    id if id == self.lid => self.handle_accept(),
                    id if id == self.ch.rx.get_id() => {
                        if self.handle_ctl() {
                            return;
                        }
                    }
                    id if self.incoming.contains_key(&id) => self.handle_incoming(id),
                    id if id == self.cleanup => self.cleanup(),
                    id if self.transfers.contains(id) => self.handle_transfer(id),
                    _ => self.handle_conn(not),
                }
            }
        }
    }

    fn handle_ctl(&mut self) -> bool {
        while let Ok(m) = self.ch.recv() {
            match m {
                CtlMessage::Shutdown => return true,
                m => {
                    let msgs: Vec<_> = {
                        self.processor
                            .handle_ctl(m)
                            .into_iter()
                            .map(|(c, m)| (c, serde_json::to_string(&m).unwrap()))
                            .collect()
                    };
                    for (c, m) in msgs {
                        let res = match self.clients.get_mut(&c) {
                            Some(client) => client.send(ws::Frame::Text(m)),
                            None => {
                                warn!(
                                    self.l,
                                    "Processor requested a message transfer to a nonexistent client!"
                                );
                                Ok(())
                            }
                        };
                        if res.is_err() {
                            let client = self.clients.remove(&c).unwrap();
                            self.remove_client(c, client);
                        }
                    }
                }
            }
        }
        false
    }

    fn handle_transfer(&mut self, id: usize) {
        match self.transfers.ready(id) {
            TransferResult::Incomplete => {}
            TransferResult::Torrent {
                conn,
                data,
                path,
                client,
                serial,
            } => {
                debug!(self.l, "Got torrent via HTTP transfer!");
                self.reg.deregister(&conn).unwrap();
                // TODO: Send this to the client in an error msg
                match bencode::decode_buf(&data) {
                    Ok(b) => {
                        if let Ok(i) = torrent::info::Info::from_bencode(b) {
                            let res = self.clients.get_mut(&client).map(|c| {
                                let tid = util::hash_to_id(&i.hash[..]);
                                c.send(ws::Frame::Text(
                                    serde_json::to_string(&SMessage::ResourcesExtant {
                                        serial,
                                        ids: vec![&tid],
                                    }).unwrap(),
                                ))
                            });
                            if res.unwrap_or(Ok(())).is_err() {
                                let client = self.clients.remove(&id).unwrap();
                                self.remove_client(id, client);
                            }
                            if self.ch.send(Message::Torrent { info: i, path: path }).is_err() {
                                crit!(self.l, "Failed to pass message to ctrl!");
                            }
                        } else {
                            warn!(self.l, "Failed to parse torrent!");
                        }
                    }
                    Err(e) => {
                        warn!(self.l, "Failed to decode BE data: {}!", e);
                    }
                }
            }
            TransferResult::Error {
                conn,
                err,
                client: id,
            } => {
                self.reg.deregister(&conn).unwrap();
                let res = self.clients
                    .get_mut(&id)
                    .map(|c| {
                        c.send(ws::Frame::Text(
                            serde_json::to_string(&SMessage::TransferFailed(err))
                                .unwrap(),
                        ))
                    })
                    .unwrap_or(Ok(()));
                if res.is_err() {
                    let client = self.clients.remove(&id).unwrap();
                    self.remove_client(id, client);
                }
            }
        }
    }

    fn handle_accept(&mut self) {
        loop {
            match self.listener.accept() {
                Ok((conn, ip)) => {
                    debug!(self.l, "Accepted new connection from {:?}!", ip);
                    let id = self.reg.register(&conn, amy::Event::Both).unwrap();
                    self.incoming.insert(id, Incoming::new(conn));
                }
                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                    break;
                }
                Err(e) => {
                    error!(self.l, "Failed to accept conn: {}", e);
                }
            }
        }
    }

    fn handle_incoming(&mut self, id: usize) {
        if let Some(mut i) = self.incoming.remove(&id) {
            match i.readable() {
                Ok(IncomingStatus::Upgrade) => {
                    debug!(self.l, "Succesfully upgraded conn");
                    self.clients.insert(id, i.into());
                }
                Ok(IncomingStatus::Incomplete) => {
                    self.incoming.insert(id, i);
                }
                Ok(IncomingStatus::Transfer { data, token }) => {
                    debug!(self.l, "File transfer requested, validating");
                    match self.processor.get_transfer(token) {
                        Some((client, serial, TransferKind::UploadTorrent { path, size })) => {
                            debug!(self.l, "Torrent transfer initiated");
                            self.transfers.add_torrent(
                                id,
                                client,
                                serial,
                                i.into(),
                                data,
                                path,
                                size,
                            );
                            // Since a succesful result means the buffer hasn't been flushed,
                            // immediatly attempt to handle the transfer as if it was ready
                            self.handle_transfer(id);
                        }
                        Some((_, _, TransferKind::DownloadFile { path })) => {
                            debug!(self.l, "File download requested, validating");
                            // The file transfer is going to be done in a new thread
                            // with blocking ups, so deregister and set blocking.
                            let conn: TcpStream = i.into();
                            conn.set_nonblocking(false).is_ok();
                            self.reg.deregister(&conn).is_ok();
                            self.transfers.add_download(conn, path);
                        }
                        Some(_) => warn!(self.l, "Unimplemented transfer type ignored"),
                        None => {
                            warn!(self.l, "Transfer used invalid token");
                            // TODO: Handle downloads and other uploads
                        }
                    }
                }
                Err(e) => {
                    debug!(self.l, "Incoming ws upgrade failed: {}", e);
                    self.reg.deregister::<TcpStream>(&i.into()).unwrap();
                }
            }
        }
    }

    fn handle_conn(&mut self, not: amy::Notification) {
        if let Some(mut c) = self.clients.remove(&not.id) {
            if not.event.readable() {
                let res = 'outer: loop {
                    match c.read() {
                        Ok(None) => break true,
                        Ok(Some(ws::Frame::Text(data))) => {
                            if self.process_frame(not.id, &mut c, &data).is_err() {
                                break false;
                            }
                        }
                        Ok(Some(_)) => break false,
                        Err(_) => break false,
                    }
                };
                if !res {
                    debug!(self.l, "Client error, disconnecting");
                    self.remove_client(not.id, c);
                    return;
                }
            }
            if not.event.writable() {
                if c.write().is_err() {
                    self.remove_client(not.id, c);
                    return;
                }
            }
            self.clients.insert(not.id, c);
        }
    }

    fn process_frame(&mut self, id: usize, c: &mut Client, data: &str) -> result::Result<(), ()> {
        match serde_json::from_str(data) {
            Ok(m) => {
                let (msgs, rm) = self.processor.handle_client(id, m);
                if let Some(m) = rm {
                    self.ch.send(m).unwrap();
                }
                for msg in msgs {
                    if c.send(ws::Frame::Text(serde_json::to_string(&msg).unwrap()))
                        .is_err()
                    {
                        return Err(());
                    }
                }
            }
            Err(e) => {
                if e.is_syntax() || e.is_eof() {
                    let msg = SMessage::InvalidSchema(message::Error {
                        serial: None,
                        reason: format!("JSON decode error: {}", e),
                    });
                    if c.send(ws::Frame::Text(serde_json::to_string(&msg).unwrap()))
                        .is_err()
                    {}
                    return Err(());
                }
                if e.is_data() {
                    let msg = SMessage::InvalidSchema(message::Error {
                        serial: None,
                        reason: format!("Invalid message format: {}", e),
                    });
                    if c.send(ws::Frame::Text(serde_json::to_string(&msg).unwrap()))
                        .is_err()
                    {
                        return Err(());
                    }
                }
            }
        }
        Ok(())
    }

    fn cleanup(&mut self) {
        self.processor.remove_expired_tokens();
        let reg = &self.reg;
        let l = &self.l;
        self.clients.retain(|id, client| {
            let res = client.timed_out();
            if res {
                info!(l, "client {} timed out", id);
                reg.deregister(&client.conn).unwrap();
            }
            !res
        });
        self.incoming.retain(|_, inc| {
            let res = inc.timed_out();
            if res {
                reg.deregister(&inc.conn).unwrap();
            }
            !res
        });
        for (conn, id, err) in self.transfers.cleanup() {
            reg.deregister(&conn).unwrap();
            self.clients.get_mut(&id).map(|c| {
                c.send(ws::Frame::Text(
                    serde_json::to_string(&SMessage::TransferFailed(err))
                        .unwrap(),
                ))
            });
        }
    }

    fn remove_client(&mut self, id: usize, client: Client) {
        self.processor.remove_client(id);
        self.reg.deregister::<TcpStream>(&client.into()).unwrap();
    }
}
<commit_msg>Attempt to respond to incorrectly encoded messages with serial in error<commit_after>pub mod proto;
mod reader;
mod writer;
mod errors;
mod client;
mod processor;
mod transfer;

use std::{io, str, result};
use std::net::{TcpListener, TcpStream, Ipv4Addr, SocketAddrV4};
use std::collections::HashMap;

use slog::Logger;
use serde_json;
use amy;

pub use self::proto::resource;
use self::proto::message::{self, SMessage};
pub use self::errors::{Result, ResultExt, ErrorKind, Error};
use self::proto::ws;
use self::client::{Incoming, IncomingStatus, Client};
use self::processor::{Processor, TransferKind};
use self::transfer::{Transfers, TransferResult};
use util;
use bencode;
use handle;
use torrent;
use CONFIG;

const POLL_INT_MS: usize = 1000;
const CLEANUP_INT_MS: usize = 2000;

#[derive(Debug)]
pub enum CtlMessage {
    Extant(Vec<resource::Resource>),
    Update(Vec<resource::SResourceUpdate<'static>>),
    Removed(Vec<String>),
    Shutdown,
}

#[derive(Debug)]
pub enum Message {
    UpdateTorrent(resource::CResourceUpdate),
    UpdateServer {
        id: String,
        throttle_up: Option<u32>,
        throttle_down: Option<u32>,
    },
    UpdateFile {
        id: String,
        torrent_id: String,
        priority: u8,
    },
    RemoveTorrent(String),
    RemovePeer { id: String, torrent_id: String },
    RemoveTracker { id: String, torrent_id: String },
    Torrent { info: torrent::Info, path: Option<String> },
}

#[allow(dead_code)]
pub struct RPC {
    poll: amy::Poller,
    reg: amy::Registrar,
    ch: handle::Handle<CtlMessage, Message>,
    listener: TcpListener,
    lid: usize,
    cleanup: usize,
    processor: Processor,
    transfers: Transfers,
    clients: HashMap<usize, Client>,
    incoming: HashMap<usize, Incoming>,
    l: Logger,
}

impl RPC {
    pub fn start(creg: &mut amy::Registrar) -> io::Result<handle::Handle<Message, CtlMessage>> {
        let poll = amy::Poller::new()?;
        let mut reg = poll.get_registrar()?;
        let cleanup = reg.set_interval(CLEANUP_INT_MS)?;
        let (ch, dh) = handle::Handle::new(creg, &mut reg)?;

        let ip = if CONFIG.rpc.local {
            Ipv4Addr::new(127, 0, 0, 1)
        } else {
            Ipv4Addr::new(0, 0, 0, 0)
        };
        let port = CONFIG.rpc.port;
        let listener = TcpListener::bind(SocketAddrV4::new(ip, port))?;
        listener.set_nonblocking(true)?;
        let lid = reg.register(&listener, amy::Event::Both)?;

        dh.run("rpc", move |ch, l| {
            RPC {
                ch,
                poll,
                reg,
                listener,
                lid,
                cleanup,
                clients: HashMap::new(),
                incoming: HashMap::new(),
                processor: Processor::new(),
                transfers: Transfers::new(),
                l,
            }.run()
        });
        Ok(ch)
    }

    pub fn run(&mut self) {
        debug!(self.l, "Running RPC!");
        'outer: while let Ok(res) = self.poll.wait(POLL_INT_MS) {
            for not in res {
                match not.id {
                    id if id == self.lid => self.handle_accept(),
                    id if id == self.ch.rx.get_id() => {
                        if self.handle_ctl() {
                            return;
                        }
                    }
                    id if self.incoming.contains_key(&id) => self.handle_incoming(id),
                    id if id == self.cleanup => self.cleanup(),
                    id if self.transfers.contains(id) => self.handle_transfer(id),
                    _ => self.handle_conn(not),
                }
            }
        }
    }

    fn handle_ctl(&mut self) -> bool {
        while let Ok(m) = self.ch.recv() {
            match m {
                CtlMessage::Shutdown => return true,
                m => {
                    let msgs: Vec<_> = {
                        self.processor
                            .handle_ctl(m)
                            .into_iter()
                            .map(|(c, m)| (c, serde_json::to_string(&m).unwrap()))
                            .collect()
                    };
                    for (c, m) in msgs {
                        let res = match self.clients.get_mut(&c) {
                            Some(client) => client.send(ws::Frame::Text(m)),
                            None => {
                                warn!(
                                    self.l,
                                    "Processor requested a message transfer to a nonexistent client!"
                                );
                                Ok(())
                            }
                        };
                        if res.is_err() {
                            let client = self.clients.remove(&c).unwrap();
                            self.remove_client(c, client);
                        }
                    }
                }
            }
        }
        false
    }

    fn handle_transfer(&mut self, id: usize) {
        match self.transfers.ready(id) {
            TransferResult::Incomplete => {}
            TransferResult::Torrent {
                conn,
                data,
                path,
                client,
                serial,
            } => {
                debug!(self.l, "Got torrent via HTTP transfer!");
                self.reg.deregister(&conn).unwrap();
                // TODO: Send this to the client in an error msg
                match bencode::decode_buf(&data) {
                    Ok(b) => {
                        if let Ok(i) = torrent::info::Info::from_bencode(b) {
                            let res = self.clients.get_mut(&client).map(|c| {
                                let tid = util::hash_to_id(&i.hash[..]);
                                c.send(ws::Frame::Text(
                                    serde_json::to_string(&SMessage::ResourcesExtant {
                                        serial,
                                        ids: vec![&tid],
                                    }).unwrap(),
                                ))
                            });
                            if res.unwrap_or(Ok(())).is_err() {
                                let client = self.clients.remove(&id).unwrap();
                                self.remove_client(id, client);
                            }
                            if self.ch.send(Message::Torrent { info: i, path: path }).is_err() {
                                crit!(self.l, "Failed to pass message to ctrl!");
                            }
                        } else {
                            warn!(self.l, "Failed to parse torrent!");
                        }
                    }
                    Err(e) => {
                        warn!(self.l, "Failed to decode BE data: {}!", e);
                    }
                }
            }
            TransferResult::Error {
                conn,
                err,
                client: id,
            } => {
                self.reg.deregister(&conn).unwrap();
                let res = self.clients
                    .get_mut(&id)
                    .map(|c| {
                        c.send(ws::Frame::Text(
                            serde_json::to_string(&SMessage::TransferFailed(err))
                                .unwrap(),
                        ))
                    })
                    .unwrap_or(Ok(()));
                if res.is_err() {
                    let client = self.clients.remove(&id).unwrap();
                    self.remove_client(id, client);
                }
            }
        }
    }

    fn handle_accept(&mut self) {
        loop {
            match self.listener.accept() {
                Ok((conn, ip)) => {
                    debug!(self.l, "Accepted new connection from {:?}!", ip);
                    let id = self.reg.register(&conn, amy::Event::Both).unwrap();
                    self.incoming.insert(id, Incoming::new(conn));
                }
                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                    break;
                }
                Err(e) => {
                    error!(self.l, "Failed to accept conn: {}", e);
                }
            }
        }
    }

    fn handle_incoming(&mut self, id: usize) {
        if let Some(mut i) = self.incoming.remove(&id) {
            match i.readable() {
                Ok(IncomingStatus::Upgrade) => {
                    debug!(self.l, "Succesfully upgraded conn");
                    self.clients.insert(id, i.into());
                }
                Ok(IncomingStatus::Incomplete) => {
                    self.incoming.insert(id, i);
                }
                Ok(IncomingStatus::Transfer { data, token }) => {
                    debug!(self.l, "File transfer requested, validating");
                    match self.processor.get_transfer(token) {
                        Some((client, serial, TransferKind::UploadTorrent { path, size })) => {
                            debug!(self.l, "Torrent transfer initiated");
                            self.transfers.add_torrent(
                                id,
                                client,
                                serial,
                                i.into(),
                                data,
                                path,
                                size,
                            );
                            // Since a succesful result means the buffer hasn't been flushed,
                            // immediatly attempt to handle the transfer as if it was ready
                            self.handle_transfer(id);
                        }
                        Some((_, _, TransferKind::DownloadFile { path })) => {
                            debug!(self.l, "File download requested, validating");
                            // The file transfer is going to be done in a new thread
                            // with blocking ups, so deregister and set blocking.
                            let conn: TcpStream = i.into();
                            conn.set_nonblocking(false).is_ok();
                            self.reg.deregister(&conn).is_ok();
                            self.transfers.add_download(conn, path);
                        }
                        Some(_) => warn!(self.l, "Unimplemented transfer type ignored"),
                        None => {
                            warn!(self.l, "Transfer used invalid token");
                            // TODO: Handle downloads and other uploads
                        }
                    }
                }
                Err(e) => {
                    debug!(self.l, "Incoming ws upgrade failed: {}", e);
                    self.reg.deregister::<TcpStream>(&i.into()).unwrap();
                }
            }
        }
    }

    fn handle_conn(&mut self, not: amy::Notification) {
        if let Some(mut c) = self.clients.remove(&not.id) {
            if not.event.readable() {
                let res = 'outer: loop {
                    match c.read() {
                        Ok(None) => break true,
                        Ok(Some(ws::Frame::Text(data))) => {
                            if self.process_frame(not.id, &mut c, &data).is_err() {
                                break false;
                            }
                        }
                        Ok(Some(_)) => break false,
                        Err(_) => break false,
                    }
                };
                if !res {
                    debug!(self.l, "Client error, disconnecting");
                    self.remove_client(not.id, c);
                    return;
                }
            }
            if not.event.writable() {
                if c.write().is_err() {
                    self.remove_client(not.id, c);
                    return;
                }
            }
            self.clients.insert(not.id, c);
        }
    }

    fn process_frame(&mut self, id: usize, c: &mut Client, data: &str) -> result::Result<(), ()> {
        match serde_json::from_str(data) {
            Ok(m) => {
                let (msgs, rm) = self.processor.handle_client(id, m);
                if let Some(m) = rm {
                    self.ch.send(m).unwrap();
                }
                for msg in msgs {
                    if c.send(ws::Frame::Text(serde_json::to_string(&msg).unwrap()))
                        .is_err()
                    {
                        return Err(());
                    }
                }
            }
            Err(e) => {
                if e.is_syntax() || e.is_eof() {
                    let msg = SMessage::InvalidSchema(message::Error {
                        serial: None,
                        reason: format!("JSON decode error: {}", e),
                    });
                    if c.send(ws::Frame::Text(serde_json::to_string(&msg).unwrap()))
                        .is_err()
                    {}
                    return Err(());
                }
                if e.is_data() {
                    #[derive(Deserialize)]
                    struct Serial {
                        serial: u64,
                    }

                    let serial = match serde_json::from_str::<Serial>(data) {
                        Ok(s) => Some(s.serial),
                        Err(_) => None,
                    };

                    let msg = SMessage::InvalidSchema(message::Error {
                        serial,
                        reason: format!("Invalid message format: {}", e),
                    });
                    if c.send(ws::Frame::Text(serde_json::to_string(&msg).unwrap()))
                        .is_err()
                    {
                        return Err(());
                    }
                }
            }
        }
        Ok(())
    }

    fn cleanup(&mut self) {
        self.processor.remove_expired_tokens();
        let reg = &self.reg;
        let l = &self.l;
        self.clients.retain(|id, client| {
            let res = client.timed_out();
            if res {
                info!(l, "client {} timed out", id);
                reg.deregister(&client.conn).unwrap();
            }
            !res
        });
        self.incoming.retain(|_, inc| {
            let res = inc.timed_out();
            if res {
                reg.deregister(&inc.conn).unwrap();
            }
            !res
        });
        for (conn, id, err) in self.transfers.cleanup() {
            reg.deregister(&conn).unwrap();
            self.clients.get_mut(&id).map(|c| {
                c.send(ws::Frame::Text(
                    serde_json::to_string(&SMessage::TransferFailed(err))
                        .unwrap(),
                ))
            });
        }
    }

    fn remove_client(&mut self, id: usize, client: Client) {
        self.processor.remove_client(id);
        self.reg.deregister::<TcpStream>(&client.into()).unwrap();
    }
}
<|endoftext|>


<commit_before>use crate::config::Config;
use crate::error::{Error, Result};
use crate::github_api::Client;
use crate::page::{DiffOp, Line, Page};
use crate::pull_request;
use std::borrow::Cow;
use std::mem;
use url::Url;

#[cfg(target_os = "windows")]
fn to_slash<S: AsRef<str>>(s: &S) -> String {
    use path_slash::PathExt;
    use std::path::Path;
    Path::new(s.as_ref()).to_slash_lossy()
}

// Do nothing on Windows
#[cfg(not(target_os = "windows"))]
fn to_slash<S: AsRef<str>>(s: &S) -> &str {
    s.as_ref()
}

// TODO: Omit fallback and return Result<String>
fn first_available_url<T: AsRef<str>>(
    candidates: &mut [String],
    fallback: String,
    https_proxy: &Option<T>,
) -> String {
    let mut builder = reqwest::Client::builder();
    if let Some(ref p) = https_proxy {
        if let Ok(p) = reqwest::Proxy::https(p.as_ref()) {
            builder = builder.proxy(p);
        } else {
            return fallback;
        }
    }
    if let Ok(client) = builder.build() {
        for mut candidate in candidates.iter_mut() {
            let req = client.head(candidate.as_str());
            if let Ok(res) = req.send() {
                let status = res.status();
                if status == reqwest::StatusCode::OK {
                    return mem::replace(&mut candidate, String::new());
                }
            }
        }
    }
    fallback
}

fn build_github_like_url<S: AsRef<str>>(
    host: &str,
    user: &str,
    repo: &str,
    api_endpoint: Option<S>,
    cfg: &Config,
    page: &Page,
) -> Result<String> {
    match page {
        Page::Open { website: true, .. } => {
            match host {
                "github.com" => {
                    if let Some(endpoint) = api_endpoint {
                        if let Ok(client) = Client::build(
                            endpoint.as_ref(),
                            cfg.env.github_token.as_ref(),
                            &cfg.env.https_proxy,
                        ) {
                            if let Ok(Some(homepage)) = client.repo_homepage(user, repo) {
                                return Ok(homepage);
                            }
                        }
                    }
                    let host = &host[0..host.len() - 4];
                    Ok(format!("https://{}.{}.io/{}", user, host, repo))
                }
                "gitlab.com" => Ok(format!("https://{}.gitlab.io/{}", user, repo)),
                host if host.starts_with("gitlab.") => {
                    Ok(format!("https://{}.{}/{}", user, host, repo))
                }
                // For GitHub Enterprise:
                //   https://help.github.com/enterprise/user/articles/user-organization-and-project-pages/
                host => {
                    // Token is always required for GitHub Enterprise
                    if let (Some(ref endpoint), Some(ref token)) =
                        (api_endpoint, &cfg.env.ghe_token)
                    {
                        if let Ok(client) =
                            Client::build(endpoint.as_ref(), Some(token), &cfg.env.https_proxy)
                        {
                            if let Ok(Some(homepage)) = client.repo_homepage(user, repo) {
                                return Ok(homepage);
                            }
                        }
                    }
                    let with_subdomain = format!("https://pages.{}/{}/{}", host, user, repo);
                    let without_subdomain = format!("https://{}/pages/{}/{}", host, user, repo);
                    Ok(first_available_url(
                        &mut [with_subdomain],
                        without_subdomain,
                        &cfg.env.https_proxy,
                    ))
                }
            }
        }
        Page::Open {
            pull_request: true, ..
        } => {
            if let Some(endpoint) = api_endpoint {
                match pull_request::find_page(endpoint.as_ref(), user, repo, cfg)? {
                    pull_request::Page::Existing { url } => Ok(url),
                    pull_request::Page::New {
                        author,
                        repo,
                        default_branch,
                        branch,
                    } => Ok(format!(
                        "https://{}/{}/{}/compare/{}...{}",
                        host, author, repo, default_branch, branch,
                    )),
                    pull_request::Page::NewAtParent {
                        author,
                        repo,
                        fork_author,
                        default_branch,
                        branch,
                    } => Ok(format!(
                        "https://{}/{}/{}/compare/{}...{}:{}",
                        host, author, repo, default_branch, fork_author, branch,
                    )),
                }
            } else {
                Err(Error::PullReqNotSupported {
                    service: host.to_string(),
                })
            }
        }
        Page::Open { .. } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!("https://{}/{}/{}/tree/{}", host, user, repo, b))
            } else {
                Ok(format!("https://{}/{}/{}", host, user, repo))
            }
        }
        Page::Tag { ref tagname, .. } => Ok(format!(
            "https://{}/{}/{}/tree/{}",
            host, user, repo, tagname,
        )),
        Page::Diff {
            ref lhs,
            ref rhs,
            ref op,
        } => Ok(format!(
            "https://{}/{}/{}/compare/{}{}{}",
            host, user, repo, lhs, op, rhs,
        )),
        Page::Commit { ref hash } => Ok(format!(
            "https://{}/{}/{}/commit/{}",
            host, user, repo, hash
        )),
        Page::FileOrDir {
            ref relative_path,
            ref hash,
            line,
            blame,
        } => Ok(format!(
            "https://{host}/{user}/{repo}/{feat}/{hash}/{path}{anchor}",
            host = host,
            user = user,
            repo = repo,
            feat = if *blame { "blame" } else { "blob" },
            hash = hash,
            path = to_slash(relative_path),
            anchor = match line {
                None => "".to_string(),
                Some(Line::At(line)) => format!("#L{}", line),
                Some(Line::Range(start, end)) => format!("#L{}-L{}", start, end),
            },
        )),
        Page::Issue { number } => Ok(format!(
            "https://{}/{}/{}/issues/{}",
            host, user, repo, number
        )),
    }
}

fn build_gitlab_url(
    host: &str,
    user: &str,
    repo: &str,
    cfg: &Config,
    page: &Page,
) -> Result<String> {
    if let Page::Diff { op, .. } = page {
        if *op == DiffOp::TwoDots {
            return Err(Error::GitLabDiffNotSupported);
        }
    }
    build_github_like_url::<&str>(host, user, repo, None, cfg, page)
}

fn build_bitbucket_url(user: &str, repo: &str, cfg: &Config, page: &Page) -> Result<String> {
    match page {
        Page::Open { website: true, .. } => {
            // Build bitbucket cloud URL:
            //   https://confluence.atlassian.com/bitbucket/publishing-a-website-on-bitbucket-cloud-221449776.html
            let with_user = format!("https://{}.bitbucket.io/{}", user, repo);
            let without_user = format!("https://{}.bitbucket.io", user);
            Ok(first_available_url(
                &mut [with_user],
                without_user,
                &cfg.env.https_proxy,
            ))
        }
        Page::Open {
            pull_request: true, ..
        } => Err(Error::PullReqNotSupported {
            service: "bitbucket.org".to_string(),
        }),
        Page::Open { .. } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!(
                    "https://bitbucket.org/{}/{}/branch/{}",
                    user, repo, b,
                ))
            } else {
                Ok(format!("https://bitbucket.org/{}/{}", user, repo))
            }
        }
        Page::Diff { .. } => Err(Error::BitbucketDiffNotSupported),
        Page::Commit { ref hash } => Ok(format!(
            "https://bitbucket.org/{}/{}/commits/{}",
            user, repo, hash,
        )),
        // On Bitbucket, there is no tag-specific page. However, unlike GitHub, bitbucket supports
        // tag commit. Open the tag commit page instead.
        Page::Tag { ref commit, .. } => Ok(format!(
            "https://bitbucket.org/{}/{}/commits/{}",
            user, repo, commit,
        )),
        Page::FileOrDir {
            ref relative_path,
            ref hash,
            line,
            blame,
        } => Ok(format!(
            "https://bitbucket.org/{user}/{repo}/{feat}/{hash}/{path}{anchor}",
            user = user,
            repo = repo,
            feat = if *blame { "annotate" } else { "src" },
            hash = hash,
            path = to_slash(relative_path),
            anchor = match line {
                None => "".to_string(),
                Some(Line::At(line)) => format!("#lines-{}", line),
                Some(Line::Range(start, end)) => format!("#lines-{}:{}", start, end),
            },
        )),
        Page::Issue { number } => Ok(format!(
            "https://bitbucket.org/{}/{}/issues/{}",
            user, repo, number,
        )),
    }
}

fn build_azdevops_url(
    team: &str,
    repo: &str,
    cfg: &Config,
    page: &Page,
) -> Result<String> {
    match page {
        Page::Open { pull_request: true, ..  } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!("https://dev.azure.com/{}/_git/{}/pullrequestcreate?sourceRef={}&targetRef=master", team, repo, b))
            } else {
                Err(Error::NoLocalRepoFound { operation: "opening a pull request without specifying branch".to_string(), })
            }
        },

        Page::Open { .. } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!("https://dev.azure.com/{}/_git/{}?version=GB{}", team, repo, b))
            } else {
                Ok(format!("https://dev.azure.com/{}/{}", team, repo))
            }
        },

        Page::Commit { ref hash } => {
            Ok(format!("https://dev.azure.com/{}/_git/{}/commit/{}", team, repo, hash))
        },

        Page::Tag { ref tagname, .. } => {
            Ok(format!("https://dev.azure.com/{}/_git/{}?version=GT{}", team, repo, tagname))
        },

        Page::FileOrDir {
            ref relative_path,
            ref hash,
            line: None,
            blame,
        } => Ok(format!(
            "https://dev.azure.com/{}/_git/{}/commit/{}?path={}{}",
            team,
            repo,
            hash,
            to_slash(relative_path),
            if *blame { "?_a=annotate" } else { "" },
        )),

        Page::Issue { number } => {
            Ok(format!("https://dev.azure.com/{}/{}/_workitems/edit/{}", team, repo, number))
        },

       _ => Err(Error::AzureDevOpsNotSupported)
    }
}

// Note: Parse '/user/repo.git' or '/user/repo' or 'user/repo' into 'user' and 'repo'
pub fn slug_from_path<'a>(path: &'a str) -> Result<(&'a str, &'a str)> {
    let mut split = path.split('/').skip_while(|s| s.is_empty());
    let mut user = split.next().ok_or_else(|| Error::NoUserInPath {
        path: path.to_string(),
    })?;

    if user == "v3-special-az-devops-case" {
        // Skip v3 in azure devops ssh urls.
        user = split.next().ok_or_else(|| Error::NoUserInPath {
        path: path.to_string(),})?;
    }

    let mut repo = split.next().ok_or_else(|| Error::NoRepoInPath {
        path: path.to_string(),
    })?;
    if repo.ends_with(".git") {
        // Slice '.git' from 'repo.git'
        repo = &repo[0..repo.len() - 4];
    } else if repo.ends_with("_git") {
        // Handle special case for weird azure urls.
        repo = split.next().ok_or_else(|| Error::NoRepoInPath {
        path: path.to_string(),
        })?;
    }
    Ok((user, repo))
}

fn preprocess_repo_to_url(repo: &str) -> Result<Url> {
    let processed_repo: String;

    // Workaround Url::parse not being able to parse the SSH urls for AzureDevOps,
    // it seems like the URL's don't adhere to the RFC?
    if repo.contains("visualstudio.com:v3") || repo.contains("azure.com:v3") {
        // Hack: It should be nice to find a cleaner solution than having
        // this special marker that slug_from_path knows about.
        processed_repo = repo.replace(":v3/", ":22/v3-special-az-devops-case/");
    } else {
        processed_repo = repo.to_string();
    }

    Url::parse(&processed_repo).map_err(|e| Error::BrokenUrl {
        url: processed_repo,
        msg: format!("{}", e),
    })
}

// Known URL formats
//  1. https://hosting_service.com/user/repo.git
//  2. git@hosting_service.com:user/repo.git (-> ssh://git@hosting_service.com:22/user/repo.git)
pub fn build_page_url(page: &Page, cfg: &Config) -> Result<String> {
    let repo_url = &cfg.repo;
    let url = preprocess_repo_to_url(&repo_url)?;
    let env = &cfg.env;

    let path = url.path();
    let (user, repo_name) = slug_from_path(path)?;
    let host = url.host_str().ok_or_else(|| Error::BrokenUrl {
        url: repo_url.to_string(),
        msg: "No host in URL".to_string(),
    })?;

    match host {
        "github.com" => {
            build_github_like_url(host, user, repo_name, Some("api.github.com"), cfg, page)
        }
        "gitlab.com" => build_gitlab_url(host, user, repo_name, cfg, page),
        "bitbucket.org" => build_bitbucket_url(user, repo_name, cfg, page),
        "visualstudio.com" => build_azdevops_url(user, repo_name, cfg, page),
        "vs-ssh.visualstudio.com" => build_azdevops_url(user, repo_name, cfg, page),
        "dev.azure.com" => build_azdevops_url(user, repo_name, cfg, page),
        "ssh.dev.azure.com" => build_azdevops_url(user, repo_name, cfg, page),
        _ => {
            let is_gitlab = host.starts_with("gitlab.");
            let port = if host.starts_with("github.") {
                env.ghe_ssh_port
            } else if is_gitlab {
                env.gitlab_ssh_port
            } else {
                match env.ghe_url_host {
                    Some(ref v) if v == host => env.ghe_ssh_port,
                    _ => {
                        return Err(Error::UnknownHostingService {
                            url: repo_url.to_string(),
                        });
                    }
                }
            };

            let host = match port {
                Some(port) => Cow::Owned(format!("{}:{}", host, port)),
                None => Cow::Borrowed(host),
            };

            if is_gitlab {
                build_gitlab_url(&host, user, repo_name, cfg, page)
            } else {
                build_github_like_url(
                    &host,
                    user,
                    repo_name,
                    Some(format!("{}/api/v3", host)),
                    cfg,
                    page,
                )
            }
        }
    }
}
<commit_msg>Fix clippy warning<commit_after>use crate::config::Config;
use crate::error::{Error, Result};
use crate::github_api::Client;
use crate::page::{DiffOp, Line, Page};
use crate::pull_request;
use std::borrow::Cow;
use std::mem;
use url::Url;

#[cfg(target_os = "windows")]
fn to_slash<S: AsRef<str>>(s: &S) -> String {
    use path_slash::PathExt;
    use std::path::Path;
    Path::new(s.as_ref()).to_slash_lossy()
}

// Do nothing on Windows
#[cfg(not(target_os = "windows"))]
fn to_slash<S: AsRef<str>>(s: &S) -> &str {
    s.as_ref()
}

// TODO: Omit fallback and return Result<String>
fn first_available_url<T: AsRef<str>>(
    candidates: &mut [String],
    fallback: String,
    https_proxy: &Option<T>,
) -> String {
    let mut builder = reqwest::Client::builder();
    if let Some(ref p) = https_proxy {
        if let Ok(p) = reqwest::Proxy::https(p.as_ref()) {
            builder = builder.proxy(p);
        } else {
            return fallback;
        }
    }
    if let Ok(client) = builder.build() {
        for mut candidate in candidates.iter_mut() {
            let req = client.head(candidate.as_str());
            if let Ok(res) = req.send() {
                let status = res.status();
                if status == reqwest::StatusCode::OK {
                    return mem::replace(&mut candidate, String::new());
                }
            }
        }
    }
    fallback
}

fn build_github_like_url<S: AsRef<str>>(
    host: &str,
    user: &str,
    repo: &str,
    api_endpoint: Option<S>,
    cfg: &Config,
    page: &Page,
) -> Result<String> {
    match page {
        Page::Open { website: true, .. } => {
            match host {
                "github.com" => {
                    if let Some(endpoint) = api_endpoint {
                        if let Ok(client) = Client::build(
                            endpoint.as_ref(),
                            cfg.env.github_token.as_ref(),
                            &cfg.env.https_proxy,
                        ) {
                            if let Ok(Some(homepage)) = client.repo_homepage(user, repo) {
                                return Ok(homepage);
                            }
                        }
                    }
                    let host = &host[0..host.len() - 4];
                    Ok(format!("https://{}.{}.io/{}", user, host, repo))
                }
                "gitlab.com" => Ok(format!("https://{}.gitlab.io/{}", user, repo)),
                host if host.starts_with("gitlab.") => {
                    Ok(format!("https://{}.{}/{}", user, host, repo))
                }
                // For GitHub Enterprise:
                //   https://help.github.com/enterprise/user/articles/user-organization-and-project-pages/
                host => {
                    // Token is always required for GitHub Enterprise
                    if let (Some(ref endpoint), Some(ref token)) =
                        (api_endpoint, &cfg.env.ghe_token)
                    {
                        if let Ok(client) =
                            Client::build(endpoint.as_ref(), Some(token), &cfg.env.https_proxy)
                        {
                            if let Ok(Some(homepage)) = client.repo_homepage(user, repo) {
                                return Ok(homepage);
                            }
                        }
                    }
                    let with_subdomain = format!("https://pages.{}/{}/{}", host, user, repo);
                    let without_subdomain = format!("https://{}/pages/{}/{}", host, user, repo);
                    Ok(first_available_url(
                        &mut [with_subdomain],
                        without_subdomain,
                        &cfg.env.https_proxy,
                    ))
                }
            }
        }
        Page::Open {
            pull_request: true, ..
        } => {
            if let Some(endpoint) = api_endpoint {
                match pull_request::find_page(endpoint.as_ref(), user, repo, cfg)? {
                    pull_request::Page::Existing { url } => Ok(url),
                    pull_request::Page::New {
                        author,
                        repo,
                        default_branch,
                        branch,
                    } => Ok(format!(
                        "https://{}/{}/{}/compare/{}...{}",
                        host, author, repo, default_branch, branch,
                    )),
                    pull_request::Page::NewAtParent {
                        author,
                        repo,
                        fork_author,
                        default_branch,
                        branch,
                    } => Ok(format!(
                        "https://{}/{}/{}/compare/{}...{}:{}",
                        host, author, repo, default_branch, fork_author, branch,
                    )),
                }
            } else {
                Err(Error::PullReqNotSupported {
                    service: host.to_string(),
                })
            }
        }
        Page::Open { .. } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!("https://{}/{}/{}/tree/{}", host, user, repo, b))
            } else {
                Ok(format!("https://{}/{}/{}", host, user, repo))
            }
        }
        Page::Tag { ref tagname, .. } => Ok(format!(
            "https://{}/{}/{}/tree/{}",
            host, user, repo, tagname,
        )),
        Page::Diff {
            ref lhs,
            ref rhs,
            ref op,
        } => Ok(format!(
            "https://{}/{}/{}/compare/{}{}{}",
            host, user, repo, lhs, op, rhs,
        )),
        Page::Commit { ref hash } => Ok(format!(
            "https://{}/{}/{}/commit/{}",
            host, user, repo, hash
        )),
        Page::FileOrDir {
            ref relative_path,
            ref hash,
            line,
            blame,
        } => Ok(format!(
            "https://{host}/{user}/{repo}/{feat}/{hash}/{path}{anchor}",
            host = host,
            user = user,
            repo = repo,
            feat = if *blame { "blame" } else { "blob" },
            hash = hash,
            path = to_slash(relative_path),
            anchor = match line {
                None => "".to_string(),
                Some(Line::At(line)) => format!("#L{}", line),
                Some(Line::Range(start, end)) => format!("#L{}-L{}", start, end),
            },
        )),
        Page::Issue { number } => Ok(format!(
            "https://{}/{}/{}/issues/{}",
            host, user, repo, number
        )),
    }
}

fn build_gitlab_url(
    host: &str,
    user: &str,
    repo: &str,
    cfg: &Config,
    page: &Page,
) -> Result<String> {
    if let Page::Diff { op, .. } = page {
        if *op == DiffOp::TwoDots {
            return Err(Error::GitLabDiffNotSupported);
        }
    }
    build_github_like_url::<&str>(host, user, repo, None, cfg, page)
}

fn build_bitbucket_url(user: &str, repo: &str, cfg: &Config, page: &Page) -> Result<String> {
    match page {
        Page::Open { website: true, .. } => {
            // Build bitbucket cloud URL:
            //   https://confluence.atlassian.com/bitbucket/publishing-a-website-on-bitbucket-cloud-221449776.html
            let with_user = format!("https://{}.bitbucket.io/{}", user, repo);
            let without_user = format!("https://{}.bitbucket.io", user);
            Ok(first_available_url(
                &mut [with_user],
                without_user,
                &cfg.env.https_proxy,
            ))
        }
        Page::Open {
            pull_request: true, ..
        } => Err(Error::PullReqNotSupported {
            service: "bitbucket.org".to_string(),
        }),
        Page::Open { .. } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!(
                    "https://bitbucket.org/{}/{}/branch/{}",
                    user, repo, b,
                ))
            } else {
                Ok(format!("https://bitbucket.org/{}/{}", user, repo))
            }
        }
        Page::Diff { .. } => Err(Error::BitbucketDiffNotSupported),
        Page::Commit { ref hash } => Ok(format!(
            "https://bitbucket.org/{}/{}/commits/{}",
            user, repo, hash,
        )),
        // On Bitbucket, there is no tag-specific page. However, unlike GitHub, bitbucket supports
        // tag commit. Open the tag commit page instead.
        Page::Tag { ref commit, .. } => Ok(format!(
            "https://bitbucket.org/{}/{}/commits/{}",
            user, repo, commit,
        )),
        Page::FileOrDir {
            ref relative_path,
            ref hash,
            line,
            blame,
        } => Ok(format!(
            "https://bitbucket.org/{user}/{repo}/{feat}/{hash}/{path}{anchor}",
            user = user,
            repo = repo,
            feat = if *blame { "annotate" } else { "src" },
            hash = hash,
            path = to_slash(relative_path),
            anchor = match line {
                None => "".to_string(),
                Some(Line::At(line)) => format!("#lines-{}", line),
                Some(Line::Range(start, end)) => format!("#lines-{}:{}", start, end),
            },
        )),
        Page::Issue { number } => Ok(format!(
            "https://bitbucket.org/{}/{}/issues/{}",
            user, repo, number,
        )),
    }
}

fn build_azdevops_url(
    team: &str,
    repo: &str,
    cfg: &Config,
    page: &Page,
) -> Result<String> {
    match page {
        Page::Open { pull_request: true, ..  } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!("https://dev.azure.com/{}/_git/{}/pullrequestcreate?sourceRef={}&targetRef=master", team, repo, b))
            } else {
                Err(Error::NoLocalRepoFound { operation: "opening a pull request without specifying branch".to_string(), })
            }
        },

        Page::Open { .. } => {
            if let Some(ref b) = cfg.branch {
                Ok(format!("https://dev.azure.com/{}/_git/{}?version=GB{}", team, repo, b))
            } else {
                Ok(format!("https://dev.azure.com/{}/{}", team, repo))
            }
        },

        Page::Commit { ref hash } => {
            Ok(format!("https://dev.azure.com/{}/_git/{}/commit/{}", team, repo, hash))
        },

        Page::Tag { ref tagname, .. } => {
            Ok(format!("https://dev.azure.com/{}/_git/{}?version=GT{}", team, repo, tagname))
        },

        Page::FileOrDir {
            ref relative_path,
            ref hash,
            line: None,
            blame,
        } => Ok(format!(
            "https://dev.azure.com/{}/_git/{}/commit/{}?path={}{}",
            team,
            repo,
            hash,
            to_slash(relative_path),
            if *blame { "?_a=annotate" } else { "" },
        )),

        Page::Issue { number } => {
            Ok(format!("https://dev.azure.com/{}/{}/_workitems/edit/{}", team, repo, number))
        },

       _ => Err(Error::AzureDevOpsNotSupported)
    }
}

// Note: Parse '/user/repo.git' or '/user/repo' or 'user/repo' into 'user' and 'repo'
pub fn slug_from_path<'a>(path: &'a str) -> Result<(&'a str, &'a str)> {
    let mut split = path.split('/').skip_while(|s| s.is_empty());
    let mut user = split.next().ok_or_else(|| Error::NoUserInPath {
        path: path.to_string(),
    })?;

    if user == "v3-special-az-devops-case" {
        // Skip v3 in azure devops ssh urls.
        user = split.next().ok_or_else(|| Error::NoUserInPath {
        path: path.to_string(),})?;
    }

    let mut repo = split.next().ok_or_else(|| Error::NoRepoInPath {
        path: path.to_string(),
    })?;
    if repo.ends_with(".git") {
        // Slice '.git' from 'repo.git'
        repo = &repo[0..repo.len() - 4];
    } else if repo.ends_with("_git") {
        // Handle special case for weird azure urls.
        repo = split.next().ok_or_else(|| Error::NoRepoInPath {
        path: path.to_string(),
        })?;
    }
    Ok((user, repo))
}

fn preprocess_repo_to_url(repo: &str) -> Result<Url> {
    // Workaround Url::parse not being able to parse the SSH urls for AzureDevOps,
    // it seems like the URL's don't adhere to the RFC?
    let processed_repo = if repo.contains("visualstudio.com:v3") || repo.contains("azure.com:v3") {
        // Hack: It should be nice to find a cleaner solution than having
        // this special marker that slug_from_path knows about.
        repo.replace(":v3/", ":22/v3-special-az-devops-case/")
    } else {
        repo.to_string()
    };

    Url::parse(&processed_repo).map_err(|e| Error::BrokenUrl {
        url: processed_repo,
        msg: format!("{}", e),
    })
}

// Known URL formats
//  1. https://hosting_service.com/user/repo.git
//  2. git@hosting_service.com:user/repo.git (-> ssh://git@hosting_service.com:22/user/repo.git)
pub fn build_page_url(page: &Page, cfg: &Config) -> Result<String> {
    let repo_url = &cfg.repo;
    let url = preprocess_repo_to_url(&repo_url)?;
    let env = &cfg.env;

    let path = url.path();
    let (user, repo_name) = slug_from_path(path)?;
    let host = url.host_str().ok_or_else(|| Error::BrokenUrl {
        url: repo_url.to_string(),
        msg: "No host in URL".to_string(),
    })?;

    match host {
        "github.com" => {
            build_github_like_url(host, user, repo_name, Some("api.github.com"), cfg, page)
        }
        "gitlab.com" => build_gitlab_url(host, user, repo_name, cfg, page),
        "bitbucket.org" => build_bitbucket_url(user, repo_name, cfg, page),
        "visualstudio.com" => build_azdevops_url(user, repo_name, cfg, page),
        "vs-ssh.visualstudio.com" => build_azdevops_url(user, repo_name, cfg, page),
        "dev.azure.com" => build_azdevops_url(user, repo_name, cfg, page),
        "ssh.dev.azure.com" => build_azdevops_url(user, repo_name, cfg, page),
        _ => {
            let is_gitlab = host.starts_with("gitlab.");
            let port = if host.starts_with("github.") {
                env.ghe_ssh_port
            } else if is_gitlab {
                env.gitlab_ssh_port
            } else {
                match env.ghe_url_host {
                    Some(ref v) if v == host => env.ghe_ssh_port,
                    _ => {
                        return Err(Error::UnknownHostingService {
                            url: repo_url.to_string(),
                        });
                    }
                }
            };

            let host = match port {
                Some(port) => Cow::Owned(format!("{}:{}", host, port)),
                None => Cow::Borrowed(host),
            };

            if is_gitlab {
                build_gitlab_url(&host, user, repo_name, cfg, page)
            } else {
                build_github_like_url(
                    &host,
                    user,
                    repo_name,
                    Some(format!("{}/api/v3", host)),
                    cfg,
                    page,
                )
            }
        }
    }
}
<|endoftext|>


<commit_before>use log;
use std::collections::hashmap::HashMap;
use std::io::IoResult;
use std::io::IoError;
use std::io::InvalidInput;
use std::str::from_utf8;
use frame::Frame;
use connection::Connection;
use subscription::AckMode;
use subscription::Auto;
use subscription::AckOrNack;
use subscription::Client;
use subscription::ClientIndividual;
use headers::Subscription;
use headers::Ack;
use headers::Id;
use headers::StompHeaderSet;
use transaction::Transaction;

pub struct Session {
  next_transaction_id: uint,
  next_subscription_id: uint,
  subscriptions: HashMap<String, ::subscription::Subscription>,
  pub connection : Connection,
  error_callback: fn(Frame) -> ()
}

impl Session {
  pub fn new(conn: Connection) -> Session {
    Session {
      next_transaction_id: 0,
      next_subscription_id: 0,
      subscriptions: HashMap::with_capacity(1),
      connection: conn,
      error_callback: Session::default_error_callback
    }
  }

  fn default_error_callback(frame : Frame) {
    debug!("ERROR frame received:\n{}", frame);
  }

  pub fn on_error(&mut self, callback: fn(Frame)) {
    self.error_callback = callback;
  }

  fn generate_transaction_id(&mut self) -> uint {
    let id = self.next_transaction_id;
    self.next_transaction_id += 1;
    id
  }

  fn generate_subscription_id(&mut self) -> uint {
    let id = self.next_subscription_id;
    self.next_subscription_id += 1;
    id
  }

  pub fn send_text(&mut self, topic: &str, body: &str) -> IoResult<()> {
    Ok(try!(self.send_bytes(topic, "text/plain", body.as_bytes())))
  }

  pub fn send_bytes(&mut self, topic: &str, mime_type: &str, body: &[u8]) -> IoResult<()> {
    let send_frame = Frame::send(topic, mime_type, body);
    Ok(try!(send_frame.write(&mut self.connection.writer)))
  }

  pub fn subscribe(&mut self, topic: &str, ack_mode: AckMode, callback: fn(Frame)->AckOrNack)-> IoResult<String> {
    let next_id = self.generate_subscription_id();
    let sub = ::subscription::Subscription::new(next_id, topic, ack_mode, callback);
    let subscribe_frame = Frame::subscribe(sub.id.as_slice(), sub.topic.as_slice(), ack_mode);
    debug!("Sending frame:\n{}", subscribe_frame);
    try!(subscribe_frame.write(&mut self.connection.writer));
    debug!("Registering callback for subscription id: {}", sub.id);
    let id_to_return = sub.id.to_string();
    self.subscriptions.insert(sub.id.to_string(), sub);
    Ok(id_to_return)
  }

  pub fn unsubscribe(&mut self, sub_id: &str) -> IoResult<()> {
     let sub = self.subscriptions.pop_equiv(&sub_id);
     let unsubscribe_frame = Frame::unsubscribe(sub_id.as_slice());
     try!(unsubscribe_frame.write(&mut self.connection.writer));
     Ok(())
  }

  pub fn begin_transaction<'a>(&'a mut self) -> IoResult<Transaction<'a>> {
    let mut tx = Transaction::new(self.generate_transaction_id(), self);
    let _ = try!(tx.begin());
    Ok(tx)
  }

  pub fn receive(&mut self) -> IoResult<Frame> {
    Ok(try!(Frame::read(&mut self.connection.reader)))
  }

  pub fn dispatch(&mut self, frame: Frame) -> () {
    // Check for ERROR frame
    match frame.command.as_slice() {
       "ERROR" => return (self.error_callback)(frame),
        _ => {} // No operation
    };

    let sub : &::subscription::Subscription;
    let sub_id = match frame.headers.get_subscription() {
      Some(Subscription(ref s)) => s.to_string(),
      None => {
        debug!("Error: frame did not contain a subscription header.");
        debug!("Frame: {}", frame);
        return;
      }
    };
    sub = match self.subscriptions.find_equiv(&sub_id.as_slice()) {
      Some(sub) => sub,
      None => {
        debug!("Error: Received message for unknown subscription: {}", sub_id);
        return;
      }
    };
    debug!("Executing.");
    match sub.ack_mode {
      Auto => {
        debug!("Auto ack, no frame sent.");
        let _ = (sub.callback)(frame);
      }
      Client | ClientIndividual => {
        let ack_id = match frame.headers.get_ack() {
          Some(Ack(ack_id)) => ack_id.to_string(),
          _ => {
            debug!("Error: Message did not have an ack header.");
            return;
          }
        };
        match (sub.callback)(frame) {
          ::subscription::Ack => {
            let ack_frame = Frame::ack(ack_id.as_slice());
            match ack_frame.write(&mut self.connection.writer) {
              Err(error) => {
                debug!("Couldn't send ACK: {}", error);
                return;
              },
              _ => debug!("ACK sent.")
            }
          },
          ::subscription::Nack => {
            let nack_frame = Frame::nack(ack_id.as_slice());
            match nack_frame.write(&mut self.connection.writer) {
              Err(error) => {
                debug!("Couldn't send NACK: {}", error);
                return;
              },
              _ => debug!("NACK sent.")
            }
          } // Nack
        } // match
      } // Client | ...
    }
  }

  pub fn listen(&mut self) {
    loop {
      let frame = match self.receive() {
        Ok(f) => f,
        Err(e) => {
          debug!("Error receiving frame: {}", e);
          continue;
        }
      };
      debug!("Received message, dispatching.");
      self.dispatch(frame);
    }
  }
}
<commit_msg>Added send_bytes_with_receipt method to session.<commit_after>use log;
use std::collections::hashmap::HashMap;
use std::io::IoResult;
use std::io::IoError;
use std::io::InvalidInput;
use std::str::from_utf8;
use frame::Frame;
use connection::Connection;
use subscription::AckMode;
use subscription::Auto;
use subscription::AckOrNack;
use subscription::Client;
use subscription::ClientIndividual;
use headers::Header;
use headers::Subscription;
use headers::Ack;
use headers::Id;
use headers::StompHeaderSet;
use transaction::Transaction;

pub struct Session {
  next_transaction_id: uint,
  next_subscription_id: uint,
  next_receipt_id: uint,
  subscriptions: HashMap<String, ::subscription::Subscription>,
  pub connection : Connection,
  error_callback: fn(Frame) -> ()
}

impl Session {
  pub fn new(conn: Connection) -> Session {
    Session {
      next_transaction_id: 0,
      next_subscription_id: 0,
      next_receipt_id: 0,
      subscriptions: HashMap::with_capacity(1),
      connection: conn,
      error_callback: Session::default_error_callback
    }
  }

  fn default_error_callback(frame : Frame) {
    debug!("ERROR frame received:\n{}", frame);
  }

  pub fn on_error(&mut self, callback: fn(Frame)) {
    self.error_callback = callback;
  }

  fn generate_transaction_id(&mut self) -> uint {
    let id = self.next_transaction_id;
    self.next_transaction_id += 1;
    id
  }

  fn generate_subscription_id(&mut self) -> uint {
    let id = self.next_subscription_id;
    self.next_subscription_id += 1;
    id
  }

  fn generate_receipt_id(&mut self) -> uint {
    let id = self.next_receipt_id;
    self.next_subscription_id += 1;
    id
  }

  pub fn send_text(&mut self, topic: &str, body: &str) -> IoResult<()> {
    Ok(try!(self.send_bytes(topic, "text/plain", body.as_bytes())))
  }

  pub fn send_bytes(&mut self, topic: &str, mime_type: &str, body: &[u8]) -> IoResult<()> {
    let send_frame = Frame::send(topic, mime_type, body);
    Ok(try!(send_frame.write(&mut self.connection.writer)))
  }

  pub fn send_bytes_with_receipt(&mut self, topic: &str, mime_type: &str, body: &[u8]) -> IoResult<()> {
    let mut send_frame = Frame::send(topic, mime_type, body);
    let receipt_id = format!("message/{}", self.generate_receipt_id().to_string());
    send_frame.headers.push(
      Header::from_key_value("receipt", receipt_id.as_slice())
    );
    Ok(try!(send_frame.write(&mut self.connection.writer)))
  }

  pub fn subscribe(&mut self, topic: &str, ack_mode: AckMode, callback: fn(Frame)->AckOrNack)-> IoResult<String> {
    let next_id = self.generate_subscription_id();
    let sub = ::subscription::Subscription::new(next_id, topic, ack_mode, callback);
    let subscribe_frame = Frame::subscribe(sub.id.as_slice(), sub.topic.as_slice(), ack_mode);
    debug!("Sending frame:\n{}", subscribe_frame);
    try!(subscribe_frame.write(&mut self.connection.writer));
    debug!("Registering callback for subscription id: {}", sub.id);
    let id_to_return = sub.id.to_string();
    self.subscriptions.insert(sub.id.to_string(), sub);
    Ok(id_to_return)
  }

  pub fn unsubscribe(&mut self, sub_id: &str) -> IoResult<()> {
     let sub = self.subscriptions.pop_equiv(&sub_id);
     let unsubscribe_frame = Frame::unsubscribe(sub_id.as_slice());
     try!(unsubscribe_frame.write(&mut self.connection.writer));
     Ok(())
  }

  pub fn begin_transaction<'a>(&'a mut self) -> IoResult<Transaction<'a>> {
    let mut tx = Transaction::new(self.generate_transaction_id(), self);
    let _ = try!(tx.begin());
    Ok(tx)
  }

  pub fn receive(&mut self) -> IoResult<Frame> {
    Ok(try!(Frame::read(&mut self.connection.reader)))
  }

  pub fn dispatch(&mut self, frame: Frame) -> () {
    // Check for ERROR frame
    match frame.command.as_slice() {
       "ERROR" => return (self.error_callback)(frame),
       "RECEIPT" => {
          debug!("RECEIPT received:\n{}", frame);
          return;
        }
        _ => {} // No operation
    };

    let sub : &::subscription::Subscription;
    let sub_id = match frame.headers.get_subscription() {
      Some(Subscription(ref s)) => s.to_string(),
      None => {
        debug!("Error: frame did not contain a subscription header.");
        debug!("Frame: {}", frame);
        return;
      }
    };
    sub = match self.subscriptions.find_equiv(&sub_id.as_slice()) {
      Some(sub) => sub,
      None => {
        debug!("Error: Received message for unknown subscription: {}", sub_id);
        return;
      }
    };
    debug!("Executing.");
    match sub.ack_mode {
      Auto => {
        debug!("Auto ack, no frame sent.");
        let _ = (sub.callback)(frame);
      }
      Client | ClientIndividual => {
        let ack_id = match frame.headers.get_ack() {
          Some(Ack(ack_id)) => ack_id.to_string(),
          _ => {
            debug!("Error: Message did not have an ack header.");
            return;
          }
        };
        match (sub.callback)(frame) {
          ::subscription::Ack => {
            let ack_frame = Frame::ack(ack_id.as_slice());
            match ack_frame.write(&mut self.connection.writer) {
              Err(error) => {
                debug!("Couldn't send ACK: {}", error);
                return;
              },
              _ => debug!("ACK sent.")
            }
          },
          ::subscription::Nack => {
            let nack_frame = Frame::nack(ack_id.as_slice());
            match nack_frame.write(&mut self.connection.writer) {
              Err(error) => {
                debug!("Couldn't send NACK: {}", error);
                return;
              },
              _ => debug!("NACK sent.")
            }
          } // Nack
        } // match
      } // Client | ...
    }
  }

  pub fn listen(&mut self) {
    loop {
      let frame = match self.receive() {
        Ok(f) => f,
        Err(e) => {
          debug!("Error receiving frame: {}", e);
          continue;
        }
      };
      debug!("Received message, dispatching.");
      self.dispatch(frame);
    }
  }
}
<|endoftext|>


<commit_before>#![allow(non_snake_case)]

use std::str::from_utf8;
use std::collections::HashMap;
use std::thread;

use rustc_serialize::json;
use curl::http;

pub const SESSION_TTL: &'static str = "10s";

#[derive(RustcDecodable, RustcEncodable, Debug)]
pub struct SessionCreate {
    Name: String,
    TTL: String
}

#[derive(RustcDecodable, RustcEncodable, Debug)]
pub struct SessionID {
    ID: String
}

pub struct Session {
    endpoint: String
}

impl Session {
    pub fn new(address: &str) ->  Session {
        Session {
            endpoint: format!("http://{}/v1/session", address)
        }
    }

    pub fn create(&self, name: String) -> String{
        let url = format!("{}/create", self.endpoint);
        let session = SessionCreate {
            Name: name,
            TTL: self::SESSION_TTL.to_owned()
        };
        let json_str = json::encode(&session).unwrap();

        let resp = http::handle()
            .put(url, &json_str)
            .content_type("application/json")
            .exec().unwrap();
        if resp.get_code() != 200 {
            panic!("Consul: Error creating a session!");
        }
        let result = from_utf8(resp.get_body()).unwrap();
        let json_data = match json::Json::from_str(result) {
            Ok(value) => value,
            Err(err) => panic!("consul: Could not convert to json: {:?}", result)
        };
        super::get_string(&json_data, &["ID"])
    }

    pub fn renew(&self, session_id: &String) {
        let url = format!("{}/renew/{}", self.endpoint, session_id);

        for _ in 0..10 {
            let resp = http::handle()
                .put(url.clone(), "")
                .content_type("application/json")
                .exec().unwrap();
            if resp.get_code() != 200 {
                println!("Cound not renew session: {}. Sleeping for 1 sec", session_id);
                thread::sleep_ms(1000);
            }
        }
        panic!("Cound not renew session: {} after 10 tries. Panicing.", session_id);

    }

    pub fn end(&self, session_id: &String) {
        let url = format!("{}/destroy/{}", self.endpoint, session_id);
        let resp = http::handle()
            .put(url, "")
            .content_type("application/json")
            .exec().unwrap();
        if resp.get_code() != 200 {
            panic!("Cound not destroy session: {}", session_id);
        }

    }

}
<commit_msg>Bugfix<commit_after>#![allow(non_snake_case)]

use std::str::from_utf8;
use std::collections::HashMap;
use std::thread;

use rustc_serialize::json;
use curl::http;

pub const SESSION_TTL: &'static str = "10s";

#[derive(RustcDecodable, RustcEncodable, Debug)]
pub struct SessionCreate {
    Name: String,
    TTL: String
}

#[derive(RustcDecodable, RustcEncodable, Debug)]
pub struct SessionID {
    ID: String
}

pub struct Session {
    endpoint: String
}

impl Session {
    pub fn new(address: &str) ->  Session {
        Session {
            endpoint: format!("http://{}/v1/session", address)
        }
    }

    pub fn create(&self, name: String) -> String{
        let url = format!("{}/create", self.endpoint);
        let session = SessionCreate {
            Name: name,
            TTL: self::SESSION_TTL.to_owned()
        };
        let json_str = json::encode(&session).unwrap();

        let resp = http::handle()
            .put(url, &json_str)
            .content_type("application/json")
            .exec().unwrap();
        if resp.get_code() != 200 {
            panic!("Consul: Error creating a session!");
        }
        let result = from_utf8(resp.get_body()).unwrap();
        let json_data = match json::Json::from_str(result) {
            Ok(value) => value,
            Err(err) => panic!("consul: Could not convert to json: {:?}", result)
        };
        super::get_string(&json_data, &["ID"])
    }

    pub fn renew(&self, session_id: &String) {
        let url = format!("{}/renew/{}", self.endpoint, session_id);

        for _ in 0..10 {
            let resp = http::handle()
                .put(url.clone(), "")
                .content_type("application/json")
                .exec().unwrap();
            if resp.get_code() != 200 {
                println!("Cound not renew session: {}. Sleeping for 1 sec", session_id);
                if i == 10 {
                    panic!("Cound not renew session: {} after 10 tries. Panicing.", session_id);
                }
                thread::sleep_ms(1000);
            }
        }

    }

    pub fn end(&self, session_id: &String) {
        let url = format!("{}/destroy/{}", self.endpoint, session_id);
        let resp = http::handle()
            .put(url, "")
            .content_type("application/json")
            .exec().unwrap();
        if resp.get_code() != 200 {
            panic!("Cound not destroy session: {}", session_id);
        }

    }

}
<|endoftext|>


<commit_before>pub mod block {
    pub const FULL: &str = "█";
    pub const SEVEN_EIGHTHS: &str = "▉";
    pub const THREE_QUARTERS: &str = "▊";
    pub const FIVE_EIGHTHS: &str = "▋";
    pub const HALF: &str = "▌";
    pub const THREE_EIGHTHS: &str = "▍";
    pub const ONE_QUARTER: &str = "▎";
    pub const ONE_EIGHTH: &str = "▏";
}

pub mod bar {
    pub const FULL: &str = "█";
    pub const SEVEN_EIGHTHS: &str = "▇";
    pub const THREE_QUARTERS: &str = "▆";
    pub const FIVE_EIGHTHS: &str = "▅";
    pub const HALF: &str = "▄";
    pub const THREE_EIGHTHS: &str = "▃";
    pub const ONE_QUARTER: &str = "▂";
    pub const ONE_EIGHTH: &str = "▁";
}

pub mod line {
    pub const TOP_RIGHT: &str = "┐";
    pub const VERTICAL: &str = "│";
    pub const HORIZONTAL: &str = "─";
    pub const TOP_LEFT: &str = "┌";
    pub const BOTTOM_RIGHT: &str = "┘";
    pub const BOTTOM_LEFT: &str = "└";
    pub const VERTICAL_LEFT: &str = "┤";
    pub const VERTICAL_RIGHT: &str = "├";
    pub const HORIZONTAL_DOWN: &str = "┬";
    pub const HORIZONTAL_UP: &str = "┴";
}

pub const DOT: &str = "•";
<commit_msg>Added a verticle 'cross' to the symbols as it was missing for no real reason<commit_after>pub mod block {
    pub const FULL: &str = "█";
    pub const SEVEN_EIGHTHS: &str = "▉";
    pub const THREE_QUARTERS: &str = "▊";
    pub const FIVE_EIGHTHS: &str = "▋";
    pub const HALF: &str = "▌";
    pub const THREE_EIGHTHS: &str = "▍";
    pub const ONE_QUARTER: &str = "▎";
    pub const ONE_EIGHTH: &str = "▏";
}

pub mod bar {
    pub const FULL: &str = "█";
    pub const SEVEN_EIGHTHS: &str = "▇";
    pub const THREE_QUARTERS: &str = "▆";
    pub const FIVE_EIGHTHS: &str = "▅";
    pub const HALF: &str = "▄";
    pub const THREE_EIGHTHS: &str = "▃";
    pub const ONE_QUARTER: &str = "▂";
    pub const ONE_EIGHTH: &str = "▁";
}

pub mod line {
    pub const TOP_RIGHT: &str = "┐";
    pub const VERTICAL: &str = "│";
    pub const HORIZONTAL: &str = "─";
    pub const TOP_LEFT: &str = "┌";
    pub const BOTTOM_RIGHT: &str = "┘";
    pub const BOTTOM_LEFT: &str = "└";
    pub const VERTICAL_LEFT: &str = "┤";
    pub const VERTICAL_RIGHT: &str = "├";
    pub const HORIZONTAL_DOWN: &str = "┬";
    pub const HORIZONTAL_UP: &str = "┴";
    pub const CROSS: &str = "┼";
}

pub const DOT: &str = "•";
<|endoftext|>


<commit_before>//! Utilities for the test suite.

use std::sync::Arc;
use std::fmt::Debug;
use rand::random;

use lift::{ lift0, lift1 };
use signal::Signal;
use stream::Stream;

/// The identity function.
pub fn id<T>(t: T) -> T { t }

/// Trace equality of two signals.
pub fn signal_eq<T>(a: &Signal<T>, b: &Signal<T>) -> Signal<bool>
    where T: PartialEq + Clone + Send + Sync + 'static
{
    lift!(|a, b| a == b, a, b)
}

/// Error of stream equivalence.
#[derive(Copy, Clone, PartialEq, Debug)]
enum EquivError<T> {
    Mismatch(T, T),
    OnlyOne(T),
    Other,
}

/// Simple pairing function.
fn pair<T, U>(t: T, u: U) -> (T, U) {
    (t, u)
}

/// Trace equality of two streams.
pub fn stream_eq<T>(a: &Stream<T>, b: &Stream<T>) -> Signal<Result<bool, String>>
    where T: PartialEq + Clone + Send + Sync + 'static + Debug,
{
    use self::EquivError::*;
    let tagger = lift0(random::<u64>);
    let result = tagger.snapshot(a, pair)
        .merge(&tagger.snapshot(b, pair))
        .map(|x| Err(OnlyOne(x)))
        .coalesce(|a, b| match (a, b) {
            (Err(OnlyOne(a)), Err(OnlyOne(b))) =>
                if &a == &b { Ok(true) }
                else { Err(Mismatch(a, b)) },
            (Err(Mismatch(a, b)), Err(OnlyOne(_))) => Err(Mismatch(a, b)),
            (Ok(true), Err(OnlyOne(a))) => Err(OnlyOne(a)),
            (Err(Other), Err(OnlyOne(_))) => Err(Other),
            _ => unreachable!(),
        })
        .scan(Ok(true), |state, a| state.and(a));
    lift1(|r| r.map_err(|e| format!("{:?}", e)), &result)
}

/// A boxed function.
pub type ArcFn<A, B> = Arc<Box<Fn(A) -> B + Send + Sync + 'static>>;

/// Wrap a function into a constant signal of that function.
pub fn pure_fn<A, B, F: Fn(A) -> B + Send + Sync + 'static>(f: F) -> Signal<ArcFn<A, B>> {
    Signal::new(make_fn(f))
}

/// Box a function to erase its type.
pub fn make_fn<A, B, F: Fn(A) -> B + Send + Sync + 'static>(f: F) -> ArcFn<A, B> {
    Arc::new(Box::new(f))
}

/// Function composition on boxed functions.
pub fn comp<A, B, C>(f: ArcFn<B, C>, g: ArcFn<A, B>) -> ArcFn<A, C> {
    make_fn(move |a| f(g(a)))
}

/// Partially applied function composition.
pub fn partial_comp<A, B, C>(f: ArcFn<B, C>) -> ArcFn<ArcFn<A, B>, ArcFn<A, C>> {
    make_fn(move |g| comp(f.clone(), g))
}

/// Self-tests.
mod test {
    use signal::Signal;
    use stream::{ Stream, Sink };
    use super::{ stream_eq, signal_eq };

    #[test]
    fn signal_eq_const() {
        let a = Signal::new(3);
        let eq = signal_eq(&a, &a);
        assert!(eq.sample());
    }

    #[test]
    fn signal_eq_piecewise_const() {
        let sink = Sink::new();
        let sig = sink.stream().hold(4);
        let eq = signal_eq(&sig, &sig);
        for &a in &[1, 2, 3] {
            sink.send(a);
            assert!(eq.sample());
        }
    }

    #[test]
    fn signal_eq_algebraic() {
        let eq = signal_eq(&Stream::never().hold(3), &Signal::new(3));
        assert!(eq.sample());
    }

    #[test]
    fn stream_eq_never() {
        let eq = stream_eq(&Stream::<()>::never(), &Stream::never());
        assert_eq!(eq.sample(), Ok(true));
    }

    #[test]
    fn stream_eq_same_sink() {
        let sink = Sink::new();
        let eq = stream_eq(&sink.stream(), &sink.stream());
        for n in 0..20 { sink.send(n); }
        assert_eq!(eq.sample(), Ok(true));
    }

    #[test]
    fn stream_eq_algebraic() {
        fn f(n: i32) -> i32 { n + 3 }
        fn g(n: i32) -> i32 { 2 * n }
        fn h(n: i32) -> i32 { f(g(n)) }

        let sink = Sink::new();
        let eq = stream_eq(&sink.stream().map(g).map(f), &sink.stream().map(h));
        for n in 0..20 { sink.send(n); }
        assert_eq!(eq.sample(), Ok(true));
    }
}
<commit_msg>Remove double pointer indirection<commit_after>//! Utilities for the test suite.

use std::sync::Arc;
use std::fmt::Debug;
use rand::random;

use lift::{ lift0, lift1 };
use signal::Signal;
use stream::Stream;

/// The identity function.
pub fn id<T>(t: T) -> T { t }

/// Trace equality of two signals.
pub fn signal_eq<T>(a: &Signal<T>, b: &Signal<T>) -> Signal<bool>
    where T: PartialEq + Clone + Send + Sync + 'static
{
    lift!(|a, b| a == b, a, b)
}

/// Error of stream equivalence.
#[derive(Copy, Clone, PartialEq, Debug)]
enum EquivError<T> {
    Mismatch(T, T),
    OnlyOne(T),
    Other,
}

/// Simple pairing function.
fn pair<T, U>(t: T, u: U) -> (T, U) {
    (t, u)
}

/// Trace equality of two streams.
pub fn stream_eq<T>(a: &Stream<T>, b: &Stream<T>) -> Signal<Result<bool, String>>
    where T: PartialEq + Clone + Send + Sync + 'static + Debug,
{
    use self::EquivError::*;
    let tagger = lift0(random::<u64>);
    let result = tagger.snapshot(a, pair)
        .merge(&tagger.snapshot(b, pair))
        .map(|x| Err(OnlyOne(x)))
        .coalesce(|a, b| match (a, b) {
            (Err(OnlyOne(a)), Err(OnlyOne(b))) =>
                if &a == &b { Ok(true) }
                else { Err(Mismatch(a, b)) },
            (Err(Mismatch(a, b)), Err(OnlyOne(_))) => Err(Mismatch(a, b)),
            (Ok(true), Err(OnlyOne(a))) => Err(OnlyOne(a)),
            (Err(Other), Err(OnlyOne(_))) => Err(Other),
            _ => unreachable!(),
        })
        .scan(Ok(true), |state, a| state.and(a));
    lift1(|r| r.map_err(|e| format!("{:?}", e)), &result)
}

/// A boxed function.
pub type ArcFn<A, B> = Arc<Fn(A) -> B + Send + Sync + 'static>;

/// Wrap a function into a constant signal of that function.
pub fn pure_fn<A, B, F: Fn(A) -> B + Send + Sync + 'static>(f: F) -> Signal<ArcFn<A, B>> {
    Signal::new(make_fn(f))
}

/// Box a function to erase its type.
pub fn make_fn<A, B, F: Fn(A) -> B + Send + Sync + 'static>(f: F) -> ArcFn<A, B> {
    Arc::new(f)
}

/// Function composition on boxed functions.
pub fn comp<A, B, C>(f: ArcFn<B, C>, g: ArcFn<A, B>) -> ArcFn<A, C> {
    make_fn(move |a| f(g(a)))
}

/// Partially applied function composition.
pub fn partial_comp<A, B, C>(f: ArcFn<B, C>) -> ArcFn<ArcFn<A, B>, ArcFn<A, C>> {
    make_fn(move |g| comp(f.clone(), g))
}

/// Self-tests.
mod test {
    use signal::Signal;
    use stream::{ Stream, Sink };
    use super::{ stream_eq, signal_eq };

    #[test]
    fn signal_eq_const() {
        let a = Signal::new(3);
        let eq = signal_eq(&a, &a);
        assert!(eq.sample());
    }

    #[test]
    fn signal_eq_piecewise_const() {
        let sink = Sink::new();
        let sig = sink.stream().hold(4);
        let eq = signal_eq(&sig, &sig);
        for &a in &[1, 2, 3] {
            sink.send(a);
            assert!(eq.sample());
        }
    }

    #[test]
    fn signal_eq_algebraic() {
        let eq = signal_eq(&Stream::never().hold(3), &Signal::new(3));
        assert!(eq.sample());
    }

    #[test]
    fn stream_eq_never() {
        let eq = stream_eq(&Stream::<()>::never(), &Stream::never());
        assert_eq!(eq.sample(), Ok(true));
    }

    #[test]
    fn stream_eq_same_sink() {
        let sink = Sink::new();
        let eq = stream_eq(&sink.stream(), &sink.stream());
        for n in 0..20 { sink.send(n); }
        assert_eq!(eq.sample(), Ok(true));
    }

    #[test]
    fn stream_eq_algebraic() {
        fn f(n: i32) -> i32 { n + 3 }
        fn g(n: i32) -> i32 { 2 * n }
        fn h(n: i32) -> i32 { f(g(n)) }

        let sink = Sink::new();
        let eq = stream_eq(&sink.stream().map(g).map(f), &sink.stream().map(h));
        for n in 0..20 { sink.send(n); }
        assert_eq!(eq.sample(), Ok(true));
    }
}
<|endoftext|>