<commit_before>use std::error::Error;
use std::fmt;

/// Command line argument parser error types
#[derive(PartialEq, Debug)]
pub enum ClapErrorType {
    /// Error occurs when some possible values were set, but clap found unexpected value
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug").index(1)
    /// .possible_value("fast")
    /// .possible_value("slow")
    /// # ).get_matches_from_safe(vec!["", "other"]);
    /// ```
    InvalidValue,
    /// Error occurs when clap found unexpected flag or option
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::from_usage("-f, --flag 'some flag'")
    /// # ).get_matches_from_safe(vec!["", "--other"]);
    /// ```
    InvalidArgument,
    /// Error occurs when clap found unexpected subcommand
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg, SubCommand};
    /// # let result = App::new("myprog")
    /// #                    .subcommand(
    /// SubCommand::with_name("conifg")
    ///                .about("Used for configuration")
    ///                .arg(Arg::with_name("config_file")
    ///                           .help("The configuration file to use")
    ///                           .index(1))
    /// # ).get_matches_from_safe(vec!["", "other"]);
    /// ```
    InvalidSubcommand,
    /// Error occurs when option does not allow empty values but some was found
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug")
    /// .empty_values(false))
    /// #                 .arg(
    /// # Arg::with_name("color")
    /// # ).get_matches_from_safe(vec!["", "--debug", "--color"]);
    /// ```
    EmptyValue,
    /// Parser inner error
    OptionError,
    /// Parser inner error
    ArgumentError,
    /// Parser inner error
    ValueError,
    /// Error occurs when argument got more values then were expected
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug").index(1)
    /// .max_values(2)
    /// # ).get_matches_from_safe(vec!["", "too", "much", "values"]);
    /// ```
    TooMuchValues,
    /// Error occurs when argument got less values then were expected
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug").index(1)
    /// .min_values(3)
    /// # ).get_matches_from_safe(vec!["", "too", "few"]);
    /// ```
    TooFewValues,
    /// Error occurs when clap find two ore more conflicting arguments
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug")
    /// .conflicts_with("color")
    /// # ).get_matches_from_safe(vec!["", "--debug", "--color"]);
    /// ```
    ArgumentConflict,
    /// Error occurs when one or more required arguments missing
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug")
    /// .required(true)
    /// # ).get_matches_from_safe(vec![""]);
    /// ```
    MissingRequiredArgument,
    /// Error occurs when required subcommand missing
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg, AppSettings, SubCommand};
    /// # let result = App::new("myprog")
    /// #                    .setting(AppSettings::SubcommandRequired)
    /// #                    .subcommand(
    /// SubCommand::with_name("conifg")
    ///                .about("Used for configuration")
    ///                .arg(Arg::with_name("config_file")
    ///                           .help("The configuration file to use")
    ///                           .index(1))
    /// # ).get_matches_from_safe(vec![""]);
    /// ```
    MissingSubcommand,
    /// Error occurs when clap find argument while is was not expecting any
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App};
    /// # let result = App::new("myprog").get_matches_from_safe(vec!["", "--arg"]);
    /// ```
    UnexpectedArgument,
    /// Error occurs when argument was used multiple times and was not set as multiple.
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// # let result = App::new("myprog")
    /// #                 .arg(
    /// # Arg::with_name("debug")
    /// .multiple(false)
    /// # ).get_matches_from_safe(vec!["", "--debug", "--debug"]);
    /// ```
    UnexpectedMultipleUsage
}

/// Command line argument parser error
#[derive(Debug)]
pub struct ClapError {
    /// Formated error message
    pub error: String,
    /// Command line argument parser error type
    pub error_type: ClapErrorType
}

impl Error for ClapError {
    fn description(&self) -> &str {
        &*self.error
    }
}

impl fmt::Display for ClapError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.error)
    }
}<commit_msg>docs(ClapErrorType): changed examples content<commit_after>use std::error::Error;
use std::fmt;

/// Command line argument parser error types
#[derive(PartialEq, Debug)]
pub enum ClapErrorType {
    /// Error occurs when some possible values were set, but clap found unexpected value
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug").index(1)
    ///         .possible_value("fast")
    ///         .possible_value("slow"))
    ///     .get_matches_from_safe(vec!["", "other"]);
    /// ```
    InvalidValue,
    /// Error occurs when clap found unexpected flag or option
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::from_usage("-f, --flag 'some flag'"))
    ///     .get_matches_from_safe(vec!["", "--other"]);
    /// ```
    InvalidArgument,
    /// Error occurs when clap found unexpected subcommand
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg, SubCommand};
    /// let result = App::new("myprog")
    ///     .subcommand(SubCommand::with_name("conifg")
    ///         .about("Used for configuration")
    ///         .arg(Arg::with_name("config_file")
    ///             .help("The configuration file to use")
    ///             .index(1)))
    ///     .get_matches_from_safe(vec!["", "other"]);
    /// ```
    InvalidSubcommand,
    /// Error occurs when option does not allow empty values but some was found
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug")
    ///          .empty_values(false))
    ///     .arg(Arg::with_name("color"))
    ///     .get_matches_from_safe(vec!["", "--debug", "--color"]);
    /// ```
    EmptyValue,
    /// Parser inner error
    OptionError,
    /// Parser inner error
    ArgumentError,
    /// Parser inner error
    ValueError,
    /// Error occurs when argument got more values then were expected
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug").index(1)
    ///         .max_values(2))
    ///     .get_matches_from_safe(vec!["", "too", "much", "values"]);
    /// ```
    TooMuchValues,
    /// Error occurs when argument got less values then were expected
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug").index(1)
    ///         .min_values(3))
    ///     .get_matches_from_safe(vec!["", "too", "few"]);
    /// ```
    TooFewValues,
    /// Error occurs when clap find two ore more conflicting arguments
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug")
    ///         .conflicts_with("color"))
    ///     .get_matches_from_safe(vec!["", "--debug", "--color"]);
    /// ```
    ArgumentConflict,
    /// Error occurs when one or more required arguments missing
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug")
    ///         .required(true))
    ///     .get_matches_from_safe(vec![""]);
    /// ```
    MissingRequiredArgument,
    /// Error occurs when required subcommand missing
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg, AppSettings, SubCommand};
    /// let result = App::new("myprog")
    ///     .setting(AppSettings::SubcommandRequired)
    ///     .subcommand(SubCommand::with_name("conifg")
    ///         .about("Used for configuration")
    ///         .arg(Arg::with_name("config_file")
    ///             .help("The configuration file to use")
    ///             .index(1)))
    ///     .get_matches_from_safe(vec![""]);
    /// ```
    MissingSubcommand,
    /// Error occurs when clap find argument while is was not expecting any
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App};
    /// let result = App::new("myprog")
    ///     .get_matches_from_safe(vec!["", "--arg"]);
    /// ```
    UnexpectedArgument,
    /// Error occurs when argument was used multiple times and was not set as multiple.
    ///
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use clap::{App, Arg};
    /// let result = App::new("myprog")
    ///     .arg(Arg::with_name("debug")
    ///         .multiple(false))
    ///     .get_matches_from_safe(vec!["", "--debug", "--debug"]);
    /// ```
    UnexpectedMultipleUsage
}

/// Command line argument parser error
#[derive(Debug)]
pub struct ClapError {
    /// Formated error message
    pub error: String,
    /// Command line argument parser error type
    pub error_type: ClapErrorType
}

impl Error for ClapError {
    fn description(&self) -> &str {
        &*self.error
    }
}

impl fmt::Display for ClapError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.error)
    }
}<|endoftext|>


<commit_before>
use std::fmt;
use std::io::Read;  // Trait providing read_to_string()
use std::env;

use url;
use hyper;
use serde;
use serde_json;

// use Groups;

use ::errors::*;

pub const API_VERSION: u16 = 3;

#[derive(Default, Clone, Copy, Debug)]
pub struct Pagination {
    pub page: u16,
    pub per_page: u16,
}

pub struct GitLab {
    url: url::Url,
    private_token: String,
    pagination: Option<Pagination>,
    client: hyper::Client,
}

// Explicitly implement Debug trait for GitLab so we can hide the token.
impl fmt::Debug for GitLab {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,
               "GitLab {{ scheme: {}, domain: {}, port: {}, private_token: XXXXXXXXXXXXXXXXXXXX, \
                pagination: {:?} }}",
               self.url.scheme(),
               self.url.domain().unwrap_or("bad hostname provided"),
               self.url
                   .port()
                   .map(|port_u16| port_u16.to_string())
                   .unwrap_or("no port provided".to_string()),
               self.pagination)
    }
}

fn validate_url(scheme: &str, domain: &str, port: u16) -> Result<url::Url> {

    match domain.find('.') {
        None => {
            // pass
        }
        Some(index) => {
            if index == 0 {
                bail!(format!("invalid domain: '{}' cannot start with a dot", domain));
            }
        }
    };

    if domain.ends_with('.') {
        bail!(format!("invalid domain: '{}' cannot end with a dot", domain));
    }

    let url_string = format!("{}://{}/api/v{}/", scheme, domain, API_VERSION);
    let mut url = url::Url::parse(&url_string)
        .chain_err(|| format!("failure to parse URL '{}'", url_string))?;
    url.set_port(Some(port)).expect("bad port provided");

    {
        let url_host = url.host_str();
        if url_host.is_none() {
            bail!("failure to get URL's hostname");
        }
        if url_host.unwrap() != domain {
            bail!(format!("invalid hostname '{}'", domain));
        }
    }

    Ok(url)
}

impl GitLab {
    pub fn _new(scheme: &str, domain: &str, port: u16, private_token: &str) -> Result<GitLab> {
        if private_token.len() != 20 {
            bail!(format!("private token should be a 20 characters string (not {})",
                          private_token.len()));
        }

        let url: url::Url = validate_url(scheme, domain, port).chain_err(|| "invalid URL")?;

        Ok(GitLab {
            url: url,
            private_token: private_token.to_string(),
            pagination: None,
            client: match env::var("HTTP_PROXY") {
                Ok(proxy) => {
                    let proxy: Vec<&str> = proxy.trim_left_matches("http://").split(':').collect();
                    let hostname = proxy[0].to_string();
                    let port = proxy[1].parse()
                        .chain_err(|| format!("failure to set port to {}", port))?;

                    hyper::Client::with_http_proxy(hostname, port)
                }
                Err(_) => hyper::Client::new(),
            },
        })
    }

    pub fn new_insecure(domain: &str, private_token: &str) -> Result<GitLab> {
        warn!("Using insecure http:// protocol: Token will be sent in clear!");
        GitLab::_new("http", domain, 80, private_token)
    }

    pub fn new(domain: &str, private_token: &str) -> Result<GitLab> {
        GitLab::_new("https", domain, 443, private_token)
    }

    pub fn port(mut self, port: u16) -> Self {
        self.url.set_port(Some(port)).unwrap();
        self
    }

    pub fn scheme(mut self, scheme: &str) -> Self {
        self.url.set_scheme(scheme).unwrap();
        self
    }

    /// Build a URL used to access GitLab instance, including some parameters.
    ///
    /// # Examples
    ///
    /// Example from GitLab: https://docs.gitlab.com/ce/api/#basic-usage
    ///
    /// ```
    /// use gitlab_api::GitLab;
    ///
    /// let expected_url = "https://gitlab.example.com\
    ///                     /api/v3/groups?order_by=path&private_token=XXXXXXXXXXXXXXXXXXXX";
    ///
    /// let gl = GitLab::new("gitlab.example.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();
    ///
    /// assert_eq!(gl.build_url("groups?order_by=path").unwrap(), expected_url);
    /// ```
    pub fn build_url(&self, query: &str) -> Result<String> {
        let mut new_url = self.url
            .clone()
            .join(query)
            .chain_err(|| {
                format!("Failure to join query '{}' to url {}",
                        query,
                        self.url.as_str())
            })?;
        new_url.query_pairs_mut().append_pair("private_token", &self.private_token);
        self.pagination.as_ref().map(|pagination| {
            new_url.query_pairs_mut().append_pair("page", &pagination.page.to_string());
            new_url.query_pairs_mut().append_pair("per_page", &pagination.per_page.to_string());
        });

        Ok(new_url.into_string())
    }

    // pub fn attempt_connection(&self) -> Result<hyper::client::Response, hyper::Error> {
    //     let url = self.build_url("version");
    //     // Close connections after each GET.
    //     self.client.get(&url).header(hyper::header::Connection::close()).send()
    // }

    /// Set pagination information
    ///
    /// # Examples
    ///
    /// ```
    /// use gitlab_api::{GitLab, Pagination};
    ///
    /// let expected_url = "https://gitlab.example.com\
    ///                     /api/v3/groups?order_by=path&\
    ///                     private_token=XXXXXXXXXXXXXXXXXXXX&page=2&per_page=5";
    ///
    /// let mut gl = GitLab::new("gitlab.example.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();
    /// gl.set_pagination(Pagination {page: 2, per_page: 5});
    /// assert_eq!(gl.build_url("groups?order_by=path").unwrap(), expected_url);
    /// ```
    pub fn set_pagination(&mut self, pagination: Pagination) {
        self.pagination = Some(pagination);
    }

    pub fn get<T>(&self, query: &str) -> Result<T>
        where T: serde::Deserialize
    {
        let url = self.build_url(query)
            .chain_err(|| format!("failure to build url for query '{}'", query))?;
        info!("url: {:?}", url);

        // Close connections after each GET.
        let mut res: hyper::client::Response = self.client
            .get(&url)
            .header(hyper::header::Connection::close())
            .send()
            .chain_err(|| format!("cannot send request '{}' to {:?}", query, self))?;
        info!("res.status: {:?}", res.status);
        debug!("res.headers: {:?}", res.headers);
        debug!("res.url: {:?}", res.url);

        let mut body = String::new();
        res.read_to_string(&mut body).chain_err(|| "cannot read response body")?;
        debug!("body:\n{:?}", body);

        if res.status != hyper::status::StatusCode::Ok {
            bail!(format!("status code '{}', not '200 OK'", res.status));
        }

        serde_json::from_str(body.as_str())
            .chain_err(|| format!("cannot build Rust struct from JSON data: {}", body))
    }

    pub fn version(&self) -> Result<::Version> {
        self.get("version").chain_err(|| "cannot query 'version'")
    }

    pub fn groups(&self) -> ::groups::GroupsLister {
        ::groups::GroupsLister::new(self)
    }

    pub fn projects(&self) -> ::projects::ProjectsLister {
        ::projects::ProjectsLister::new(self)
    }

    pub fn issues(&self) -> ::issues::IssuesLister {
        ::issues::IssuesLister::new(self)
    }

    pub fn merge_requests(&self, project_id: i64) -> ::merge_requests::MergeRequestsLister {
        ::merge_requests::MergeRequestsLister::new(self, project_id)
    }

    // pub fn groups(&mut self, listing: ::groups::Listing) -> Result<Groups, serde_json::Error> {
    //     let query = listing.build_query();
    //     // self.get(&query)
    //     unimplemented!();
    // }

    // pub fn owned_groups(&mut self) -> Result<Groups, serde_json::Error> {
    //     let query = ::groups::owned_groups::Listing::new().build_query();
    //     info!("query: {:?}", query);
    //     self.get(&query)
    // }
}

#[cfg(test)]
mod tests {
    use std::fmt;
    use gitlab::*;
    use errors::*;

    fn verify_ok<T>(result: &Result<T>) {
        if let &Err(ref e) = result {
            println!("error: {}", e);

            for e in e.iter().skip(1) {
                println!("caused by: {}", e);
            }

            // The backtrace is not always generated. Try to run this example
            // with `RUST_BACKTRACE=1`.
            if let Some(backtrace) = e.backtrace() {
                println!("backtrace: {:?}", backtrace);
            }
        }
        assert!(result.is_ok());
    }

    fn verify_err<T>(result: &Result<T>)
        where T: fmt::Debug
    {
        match result {
            &Err(_) => {
                // pass
            }
            &Ok(ref t) => {
                panic!(format!("Expected an Err(), got an Ok(t), with t: {:?}", t));
            }
        }
    }

    #[test]
    fn impl_debug_for_gitlab() {
        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();

        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: https, domain: gitlab.com, port: no port provided, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }", debug);

        let gl = gl.scheme("http").port(80);
        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: http, domain: gitlab.com, port: no port provided, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }", debug);

        let mut gl = gl.port(81);
        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: http, domain: gitlab.com, port: 81, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }", debug);

        gl.set_pagination(Pagination {page: 2, per_page: 5});
        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: http, domain: gitlab.com, port: 81, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: Some(Pagination { page: 2, per_page: 5 }) }", debug);
    }

    #[test]
    fn new_valid() {
        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);

        let gl = GitLab::new_insecure("gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);

        let gl = GitLab::new("localhost", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);

        let gl = GitLab::new_insecure("localhost", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);
    }

    #[test]
    fn new_invalid_url_1() {
        let gl = GitLab::new("", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_2() {
        let gl = GitLab::new("gitla/b.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("gitla/b.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_3() {
        let gl = GitLab::new("/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_4() {
        let gl = GitLab::new("http:/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("http:/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_5() {
        let gl = GitLab::new("http:///gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("http:///gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_6() {
        let gl = GitLab::new(".gitlab", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure(".gitlab", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_7() {
        let gl = GitLab::new(".gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure(".gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_8() {
        let gl = GitLab::new("gitlab.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("gitlab.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_10() {
        let gl = GitLab::new("gitlab.com.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("gitlab.com.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_token() {
        let gl = GitLab::new("gitlab.com", "");
        assert!(gl.is_err());

        let gl = GitLab::new("gitlab.com", "X");
        assert!(gl.is_err());

        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXX");
        assert!(gl.is_err());

        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXXX");
        assert!(gl.is_err());
    }
}
<commit_msg>New unit test: `gitlab_listers_groups()`<commit_after>
use std::fmt;
use std::io::Read;  // Trait providing read_to_string()
use std::env;

use url;
use hyper;
use serde;
use serde_json;

// use Groups;

use ::errors::*;

pub const API_VERSION: u16 = 3;

#[derive(Default, Clone, Copy, Debug)]
pub struct Pagination {
    pub page: u16,
    pub per_page: u16,
}

pub struct GitLab {
    url: url::Url,
    private_token: String,
    pagination: Option<Pagination>,
    client: hyper::Client,
}

// Explicitly implement Debug trait for GitLab so we can hide the token.
impl fmt::Debug for GitLab {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,
               "GitLab {{ scheme: {}, domain: {}, port: {}, private_token: XXXXXXXXXXXXXXXXXXXX, \
                pagination: {:?} }}",
               self.url.scheme(),
               self.url.domain().unwrap_or("bad hostname provided"),
               self.url
                   .port()
                   .map(|port_u16| port_u16.to_string())
                   .unwrap_or("no port provided".to_string()),
               self.pagination)
    }
}

fn validate_url(scheme: &str, domain: &str, port: u16) -> Result<url::Url> {

    match domain.find('.') {
        None => {
            // pass
        }
        Some(index) => {
            if index == 0 {
                bail!(format!("invalid domain: '{}' cannot start with a dot", domain));
            }
        }
    };

    if domain.ends_with('.') {
        bail!(format!("invalid domain: '{}' cannot end with a dot", domain));
    }

    let url_string = format!("{}://{}/api/v{}/", scheme, domain, API_VERSION);
    let mut url = url::Url::parse(&url_string)
        .chain_err(|| format!("failure to parse URL '{}'", url_string))?;
    url.set_port(Some(port)).expect("bad port provided");

    {
        let url_host = url.host_str();
        if url_host.is_none() {
            bail!("failure to get URL's hostname");
        }
        if url_host.unwrap() != domain {
            bail!(format!("invalid hostname '{}'", domain));
        }
    }

    Ok(url)
}

impl GitLab {
    pub fn _new(scheme: &str, domain: &str, port: u16, private_token: &str) -> Result<GitLab> {
        if private_token.len() != 20 {
            bail!(format!("private token should be a 20 characters string (not {})",
                          private_token.len()));
        }

        let url: url::Url = validate_url(scheme, domain, port).chain_err(|| "invalid URL")?;

        Ok(GitLab {
            url: url,
            private_token: private_token.to_string(),
            pagination: None,
            client: match env::var("HTTP_PROXY") {
                Ok(proxy) => {
                    let proxy: Vec<&str> = proxy.trim_left_matches("http://").split(':').collect();
                    let hostname = proxy[0].to_string();
                    let port = proxy[1].parse()
                        .chain_err(|| format!("failure to set port to {}", port))?;

                    hyper::Client::with_http_proxy(hostname, port)
                }
                Err(_) => hyper::Client::new(),
            },
        })
    }

    pub fn new_insecure(domain: &str, private_token: &str) -> Result<GitLab> {
        warn!("Using insecure http:// protocol: Token will be sent in clear!");
        GitLab::_new("http", domain, 80, private_token)
    }

    pub fn new(domain: &str, private_token: &str) -> Result<GitLab> {
        GitLab::_new("https", domain, 443, private_token)
    }

    pub fn port(mut self, port: u16) -> Self {
        self.url.set_port(Some(port)).unwrap();
        self
    }

    pub fn scheme(mut self, scheme: &str) -> Self {
        self.url.set_scheme(scheme).unwrap();
        self
    }

    /// Build a URL used to access GitLab instance, including some parameters.
    ///
    /// # Examples
    ///
    /// Example from GitLab: https://docs.gitlab.com/ce/api/#basic-usage
    ///
    /// ```
    /// use gitlab_api::GitLab;
    ///
    /// let expected_url = "https://gitlab.example.com\
    ///                     /api/v3/groups?order_by=path&private_token=XXXXXXXXXXXXXXXXXXXX";
    ///
    /// let gl = GitLab::new("gitlab.example.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();
    ///
    /// assert_eq!(gl.build_url("groups?order_by=path").unwrap(), expected_url);
    /// ```
    pub fn build_url(&self, query: &str) -> Result<String> {
        let mut new_url = self.url
            .clone()
            .join(query)
            .chain_err(|| {
                format!("Failure to join query '{}' to url {}",
                        query,
                        self.url.as_str())
            })?;
        new_url.query_pairs_mut().append_pair("private_token", &self.private_token);
        self.pagination.as_ref().map(|pagination| {
            new_url.query_pairs_mut().append_pair("page", &pagination.page.to_string());
            new_url.query_pairs_mut().append_pair("per_page", &pagination.per_page.to_string());
        });

        Ok(new_url.into_string())
    }

    // pub fn attempt_connection(&self) -> Result<hyper::client::Response, hyper::Error> {
    //     let url = self.build_url("version");
    //     // Close connections after each GET.
    //     self.client.get(&url).header(hyper::header::Connection::close()).send()
    // }

    /// Set pagination information
    ///
    /// # Examples
    ///
    /// ```
    /// use gitlab_api::{GitLab, Pagination};
    ///
    /// let expected_url = "https://gitlab.example.com\
    ///                     /api/v3/groups?order_by=path&\
    ///                     private_token=XXXXXXXXXXXXXXXXXXXX&page=2&per_page=5";
    ///
    /// let mut gl = GitLab::new("gitlab.example.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();
    /// gl.set_pagination(Pagination {page: 2, per_page: 5});
    /// assert_eq!(gl.build_url("groups?order_by=path").unwrap(), expected_url);
    /// ```
    pub fn set_pagination(&mut self, pagination: Pagination) {
        self.pagination = Some(pagination);
    }

    pub fn get<T>(&self, query: &str) -> Result<T>
        where T: serde::Deserialize
    {
        let url = self.build_url(query)
            .chain_err(|| format!("failure to build url for query '{}'", query))?;
        info!("url: {:?}", url);

        // Close connections after each GET.
        let mut res: hyper::client::Response = self.client
            .get(&url)
            .header(hyper::header::Connection::close())
            .send()
            .chain_err(|| format!("cannot send request '{}' to {:?}", query, self))?;
        info!("res.status: {:?}", res.status);
        debug!("res.headers: {:?}", res.headers);
        debug!("res.url: {:?}", res.url);

        let mut body = String::new();
        res.read_to_string(&mut body).chain_err(|| "cannot read response body")?;
        debug!("body:\n{:?}", body);

        if res.status != hyper::status::StatusCode::Ok {
            bail!(format!("status code '{}', not '200 OK'", res.status));
        }

        serde_json::from_str(body.as_str())
            .chain_err(|| format!("cannot build Rust struct from JSON data: {}", body))
    }

    pub fn version(&self) -> Result<::Version> {
        self.get("version").chain_err(|| "cannot query 'version'")
    }

    pub fn groups(&self) -> ::groups::GroupsLister {
        ::groups::GroupsLister::new(self)
    }

    pub fn projects(&self) -> ::projects::ProjectsLister {
        ::projects::ProjectsLister::new(self)
    }

    pub fn issues(&self) -> ::issues::IssuesLister {
        ::issues::IssuesLister::new(self)
    }

    pub fn merge_requests(&self, project_id: i64) -> ::merge_requests::MergeRequestsLister {
        ::merge_requests::MergeRequestsLister::new(self, project_id)
    }

    // pub fn groups(&mut self, listing: ::groups::Listing) -> Result<Groups, serde_json::Error> {
    //     let query = listing.build_query();
    //     // self.get(&query)
    //     unimplemented!();
    // }

    // pub fn owned_groups(&mut self) -> Result<Groups, serde_json::Error> {
    //     let query = ::groups::owned_groups::Listing::new().build_query();
    //     info!("query: {:?}", query);
    //     self.get(&query)
    // }
}

#[cfg(test)]
mod tests {
    use std::fmt;
    use gitlab::*;
    use errors::*;

    fn verify_ok<T>(result: &Result<T>) {
        if let &Err(ref e) = result {
            println!("error: {}", e);

            for e in e.iter().skip(1) {
                println!("caused by: {}", e);
            }

            // The backtrace is not always generated. Try to run this example
            // with `RUST_BACKTRACE=1`.
            if let Some(backtrace) = e.backtrace() {
                println!("backtrace: {:?}", backtrace);
            }
        }
        assert!(result.is_ok());
    }

    fn verify_err<T>(result: &Result<T>)
        where T: fmt::Debug
    {
        match result {
            &Err(_) => {
                // pass
            }
            &Ok(ref t) => {
                panic!(format!("Expected an Err(), got an Ok(t), with t: {:?}", t));
            }
        }
    }

    #[test]
    fn impl_debug_for_gitlab() {
        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();

        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: https, domain: gitlab.com, port: no port provided, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }", debug);

        let gl = gl.scheme("http").port(80);
        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: http, domain: gitlab.com, port: no port provided, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }", debug);

        let mut gl = gl.port(81);
        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: http, domain: gitlab.com, port: 81, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }", debug);

        gl.set_pagination(Pagination {page: 2, per_page: 5});
        let debug = format!("{:?}", gl);
        assert_eq!("GitLab { scheme: http, domain: gitlab.com, port: 81, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: Some(Pagination { page: 2, per_page: 5 }) }", debug);
    }

    #[test]
    fn gitlab_listers_groups() {
        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXX").unwrap();
        let groups_lister = gl.groups();
        let debug = format!("{:?}", groups_lister);
        assert_eq!("GroupsLister { gl: GitLab { scheme: https, domain: gitlab.com, port: no \
                    port provided, private_token: XXXXXXXXXXXXXXXXXXXX, pagination: None }, \
                    internal: GroupsListerInternal { skip_groups: None, all_available: None, \
                    search: None, order_by: None, sort: None } }", debug);
    }

    #[test]
    fn new_valid() {
        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);

        let gl = GitLab::new_insecure("gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);

        let gl = GitLab::new("localhost", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);

        let gl = GitLab::new_insecure("localhost", "XXXXXXXXXXXXXXXXXXXX");
        verify_ok(&gl);
    }

    #[test]
    fn new_invalid_url_1() {
        let gl = GitLab::new("", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_2() {
        let gl = GitLab::new("gitla/b.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("gitla/b.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_3() {
        let gl = GitLab::new("/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_4() {
        let gl = GitLab::new("http:/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("http:/gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_5() {
        let gl = GitLab::new("http:///gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("http:///gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_6() {
        let gl = GitLab::new(".gitlab", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure(".gitlab", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_7() {
        let gl = GitLab::new(".gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure(".gitlab.com", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_8() {
        let gl = GitLab::new("gitlab.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("gitlab.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_url_10() {
        let gl = GitLab::new("gitlab.com.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);

        let gl = GitLab::new_insecure("gitlab.com.", "XXXXXXXXXXXXXXXXXXXX");
        verify_err(&gl);
    }

    #[test]
    fn new_invalid_token() {
        let gl = GitLab::new("gitlab.com", "");
        assert!(gl.is_err());

        let gl = GitLab::new("gitlab.com", "X");
        assert!(gl.is_err());

        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXX");
        assert!(gl.is_err());

        let gl = GitLab::new("gitlab.com", "XXXXXXXXXXXXXXXXXXXXX");
        assert!(gl.is_err());
    }
}
<|endoftext|>


<commit_before>
use BuildQuery;
use Groups;

// https://docs.gitlab.com/ce/api/groups.html
// List groups:                 GET /groups
// List owned groups:           GET /groups/owned
// List a group's projects:     GET /groups/:id/projects
// Details of a group:          GET /groups/:id
// New group:                   POST /groups
// Transfer project to group:   POST  /groups/:id/projects/:project_id
// Update group:                PUT /groups/:id
// Remove group:                DELETE /groups/:id
// Search for group:            GET /groups?search=foobar

// https://docs.gitlab.com/ce/api/groups.html#list-groups

#[derive(Debug, Copy, Clone)]
enum GroupListerOptionsOrderBy {
    Name,
    Path,
}

// impl Default for GroupListerOptionsOrderBy {
//     fn default() -> GroupListerOptionsOrderBy { GroupListerOptionsOrderBy::Name }
// }

#[derive(Debug, Copy, Clone)]
enum GroupListerOptionsSort {
    Asc,
    Desc,
}

// impl Default for GroupListerOptionsSort {
//     fn default() -> GroupListerOptionsSort { GroupListerOptionsSort::Asc }
// }

fn append_group_lister_options_order_by(order_by: GroupListerOptionsOrderBy, s: &mut String) {
    s.push_str(match order_by {
        GroupListerOptionsOrderBy::Name => "name",
        GroupListerOptionsOrderBy::Path => "path",
    });
}

fn append_group_lister_options_sort(order_by: GroupListerOptionsSort, s: &mut String) {
    s.push_str(match order_by {
        GroupListerOptionsSort::Asc => "asc",
        GroupListerOptionsSort::Desc => "desc",
    });
}

/// https://docs.gitlab.com/ce/api/groups.html#list-groups
#[derive(Default, Debug)]
struct GroupListerOptions {
    /// Skip the group IDs passes
    pub skip_groups: Option<Vec<i64>>,
    /// Show all the groups you have access to
    pub all_available: Option<bool>,
    /// Return list of authorized groups matching the search criteria
    pub search: Option<String>,
    /// Order groups by `name` or `path`. Default is `name`
    pub order_by: Option<GroupListerOptionsOrderBy>,
    /// Order groups in `asc` or `desc` order. Default is `asc`
    pub sort: Option<GroupListerOptionsSort>,
}

#[derive(Default, Debug)]
pub struct GroupListing {
    options: GroupListerOptions,
}

impl BuildQuery for GroupListing {
    fn build_query(&self) -> String {

        let options = &self.options;

        let mut query = String::from("groups");
        let mut previous_option_present = false;

        // FIXME: Add skip_groups

        // Append a "?", only if one of the `Option` is `Some(_)`
        query.push_str(match (&options.skip_groups,
                              &options.all_available,
                              &options.search,
                              &options.order_by,
                              &options.sort) {
            (&None, &None, &None, &None, &None) => "",
            _ => "?",
        });

        options.all_available.map(|all_available| {
            if previous_option_present {
                query.push_str("&");
            }
            if all_available {
                query.push_str("all_available=true")
            } else {
                query.push_str("all_available=false")
            }
            previous_option_present = true;
        });

        options.search.as_ref().map(|search| {
            if previous_option_present {
                query.push_str("&");
            }
            query.push_str("search=");
            query.push_str(search);
            previous_option_present = true;
        });

        options.order_by.map(|order_by| {
            if previous_option_present {
                query.push_str("&");
            }
            query.push_str("order_by=");
            append_group_lister_options_order_by(order_by, &mut query);
            previous_option_present = true;
        });

        options.sort.map(|sort| {
            if previous_option_present {
                query.push_str("&");
            }
            query.push_str("sort=");
            append_group_lister_options_sort(sort, &mut query);
            previous_option_present = true;
        });

        query
    }
}

impl GroupListing {
    fn list(&self) -> Groups {
        let groups: Groups = vec![];

        groups
    }
}

#[test]
fn test_append_group_lister_options_order_by() {
    let expected_string = "Initialnamepathpathname";

    let mut s = String::from("Initial");

    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Name, &mut s);
    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Path, &mut s);
    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Path, &mut s);
    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Name, &mut s);

    assert_eq!(s, expected_string);
}

#[test]
fn test_append_group_lister_options_sort() {
    let expected_string = "Initialascdescascdesc";

    let mut s = String::from("Initial");

    append_group_lister_options_sort(GroupListerOptionsSort::Asc, &mut s);
    append_group_lister_options_sort(GroupListerOptionsSort::Desc, &mut s);
    append_group_lister_options_sort(GroupListerOptionsSort::Asc, &mut s);
    append_group_lister_options_sort(GroupListerOptionsSort::Desc, &mut s);

    assert_eq!(s, expected_string);
}

#[test]
fn groups_build_query_default() {
    let expected_string = "groups";
    let listing: GroupListing = Default::default();
    let query = listing.build_query();
    assert_eq!(query, expected_string);

    let expected_string = "groups";
    let listing = GroupListing { options: Default::default() };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

// FIXME: Add for 'skip_groups'

#[test]
fn groups_build_query_all_available() {
    let expected_string = "groups?all_available=true";
    let listing = GroupListing {
        options: GroupListerOptions {
            all_available: Some(true),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);

    let expected_string = "groups?all_available=false";
    let listing = GroupListing {
        options: GroupListerOptions {
            all_available: Some(false),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_search() {
    let expected_string = "groups?search=SearchPattern";
    let listing = GroupListing {
        options: GroupListerOptions {
            search: Some(String::from("SearchPattern")),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_order_by_name() {
    let expected_string = "groups?order_by=name";
    let listing = GroupListing {
        options: GroupListerOptions {
            order_by: Some(GroupListerOptionsOrderBy::Name),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_order_by_path() {
    let expected_string = "groups?order_by=path";
    let listing = GroupListing {
        options: GroupListerOptions {
            order_by: Some(GroupListerOptionsOrderBy::Path),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_sort() {
    let expected_string = "groups?sort=asc";
    let listing = GroupListing {
        options: GroupListerOptions {
            sort: Some(GroupListerOptionsSort::Asc),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);

    let expected_string = "groups?sort=desc";
    let listing = GroupListing {
        options: GroupListerOptions {
            sort: Some(GroupListerOptionsSort::Desc),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_search_order_by_path() {
    let expected_string = "groups?search=SearchPattern&order_by=path";
    let listing = GroupListing {
        options: GroupListerOptions {
            order_by: Some(GroupListerOptionsOrderBy::Path),
            search: Some(String::from("SearchPattern")),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}
<commit_msg>Add test `groups_build_query_skip_groups()`<commit_after>
use BuildQuery;
use Groups;

// https://docs.gitlab.com/ce/api/groups.html
// List groups:                 GET /groups
// List owned groups:           GET /groups/owned
// List a group's projects:     GET /groups/:id/projects
// Details of a group:          GET /groups/:id
// New group:                   POST /groups
// Transfer project to group:   POST  /groups/:id/projects/:project_id
// Update group:                PUT /groups/:id
// Remove group:                DELETE /groups/:id
// Search for group:            GET /groups?search=foobar

// https://docs.gitlab.com/ce/api/groups.html#list-groups

#[derive(Debug, Copy, Clone)]
enum GroupListerOptionsOrderBy {
    Name,
    Path,
}

// impl Default for GroupListerOptionsOrderBy {
//     fn default() -> GroupListerOptionsOrderBy { GroupListerOptionsOrderBy::Name }
// }

#[derive(Debug, Copy, Clone)]
enum GroupListerOptionsSort {
    Asc,
    Desc,
}

// impl Default for GroupListerOptionsSort {
//     fn default() -> GroupListerOptionsSort { GroupListerOptionsSort::Asc }
// }

fn append_group_lister_options_order_by(order_by: GroupListerOptionsOrderBy, s: &mut String) {
    s.push_str(match order_by {
        GroupListerOptionsOrderBy::Name => "name",
        GroupListerOptionsOrderBy::Path => "path",
    });
}

fn append_group_lister_options_sort(order_by: GroupListerOptionsSort, s: &mut String) {
    s.push_str(match order_by {
        GroupListerOptionsSort::Asc => "asc",
        GroupListerOptionsSort::Desc => "desc",
    });
}

/// https://docs.gitlab.com/ce/api/groups.html#list-groups
#[derive(Default, Debug)]
struct GroupListerOptions {
    /// Skip the group IDs passes
    pub skip_groups: Option<Vec<i64>>,
    /// Show all the groups you have access to
    pub all_available: Option<bool>,
    /// Return list of authorized groups matching the search criteria
    pub search: Option<String>,
    /// Order groups by `name` or `path`. Default is `name`
    pub order_by: Option<GroupListerOptionsOrderBy>,
    /// Order groups in `asc` or `desc` order. Default is `asc`
    pub sort: Option<GroupListerOptionsSort>,
}

#[derive(Default, Debug)]
pub struct GroupListing {
    options: GroupListerOptions,
}

impl BuildQuery for GroupListing {
    fn build_query(&self) -> String {

        let options = &self.options;

        let mut query = String::from("groups");
        let mut previous_option_present = false;

        // FIXME: Add skip_groups

        // Append a "?", only if one of the `Option` is `Some(_)`
        query.push_str(match (&options.skip_groups,
                              &options.all_available,
                              &options.search,
                              &options.order_by,
                              &options.sort) {
            (&None, &None, &None, &None, &None) => "",
            _ => "?",
        });

        options.all_available.map(|all_available| {
            if previous_option_present {
                query.push_str("&");
            }
            if all_available {
                query.push_str("all_available=true")
            } else {
                query.push_str("all_available=false")
            }
            previous_option_present = true;
        });

        options.search.as_ref().map(|search| {
            if previous_option_present {
                query.push_str("&");
            }
            query.push_str("search=");
            query.push_str(search);
            previous_option_present = true;
        });

        options.order_by.map(|order_by| {
            if previous_option_present {
                query.push_str("&");
            }
            query.push_str("order_by=");
            append_group_lister_options_order_by(order_by, &mut query);
            previous_option_present = true;
        });

        options.sort.map(|sort| {
            if previous_option_present {
                query.push_str("&");
            }
            query.push_str("sort=");
            append_group_lister_options_sort(sort, &mut query);
            previous_option_present = true;
        });

        query
    }
}

impl GroupListing {
    fn list(&self) -> Groups {
        let groups: Groups = vec![];

        groups
    }
}

#[test]
fn test_append_group_lister_options_order_by() {
    let expected_string = "Initialnamepathpathname";

    let mut s = String::from("Initial");

    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Name, &mut s);
    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Path, &mut s);
    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Path, &mut s);
    append_group_lister_options_order_by(GroupListerOptionsOrderBy::Name, &mut s);

    assert_eq!(s, expected_string);
}

#[test]
fn test_append_group_lister_options_sort() {
    let expected_string = "Initialascdescascdesc";

    let mut s = String::from("Initial");

    append_group_lister_options_sort(GroupListerOptionsSort::Asc, &mut s);
    append_group_lister_options_sort(GroupListerOptionsSort::Desc, &mut s);
    append_group_lister_options_sort(GroupListerOptionsSort::Asc, &mut s);
    append_group_lister_options_sort(GroupListerOptionsSort::Desc, &mut s);

    assert_eq!(s, expected_string);
}

#[test]
fn groups_build_query_default() {
    let expected_string = "groups";
    let listing: GroupListing = Default::default();
    let query = listing.build_query();
    assert_eq!(query, expected_string);

    let expected_string = "groups";
    let listing = GroupListing { options: Default::default() };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_skip_groups() {
    let expected_string = "groups?skip_groups[]=1&skip_groups[]=2&skip_groups[]=3";
    let listing = GroupListing {
        options: GroupListerOptions {
            skip_groups: Some(vec![1,2,3]),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_all_available() {
    let expected_string = "groups?all_available=true";
    let listing = GroupListing {
        options: GroupListerOptions {
            all_available: Some(true),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);

    let expected_string = "groups?all_available=false";
    let listing = GroupListing {
        options: GroupListerOptions {
            all_available: Some(false),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_search() {
    let expected_string = "groups?search=SearchPattern";
    let listing = GroupListing {
        options: GroupListerOptions {
            search: Some(String::from("SearchPattern")),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_order_by_name() {
    let expected_string = "groups?order_by=name";
    let listing = GroupListing {
        options: GroupListerOptions {
            order_by: Some(GroupListerOptionsOrderBy::Name),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_order_by_path() {
    let expected_string = "groups?order_by=path";
    let listing = GroupListing {
        options: GroupListerOptions {
            order_by: Some(GroupListerOptionsOrderBy::Path),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_sort() {
    let expected_string = "groups?sort=asc";
    let listing = GroupListing {
        options: GroupListerOptions {
            sort: Some(GroupListerOptionsSort::Asc),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);

    let expected_string = "groups?sort=desc";
    let listing = GroupListing {
        options: GroupListerOptions {
            sort: Some(GroupListerOptionsSort::Desc),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}

#[test]
fn groups_build_query_search_order_by_path() {
    let expected_string = "groups?search=SearchPattern&order_by=path";
    let listing = GroupListing {
        options: GroupListerOptions {
            order_by: Some(GroupListerOptionsOrderBy::Path),
            search: Some(String::from("SearchPattern")),
            ..Default::default()
        },
    };
    let query = listing.build_query();
    assert_eq!(query, expected_string);
}
<|endoftext|>


<commit_before>// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::{anyhow, bail, Result};
use directories::{BaseDirs, ProjectDirs};
use erased_serde::Serialize;
use log::LevelFilter;
use nix::sys::signal::{self, Signal};
use nix::unistd::{dup2, setsid, Pid};
use std::env::{self, args_os, ArgsOs};
use std::ffi::OsString;
use std::fs::{self, read_to_string, File};
use std::io::{self, ErrorKind, Write};
use std::iter::{IntoIterator, Iterator};
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::process::{self, ChildStdout, Command, Stdio};
use std::str::FromStr;
use std::time::Duration;
use structopt::StructOpt;

use opentitanlib::backend;
use opentitanlib::proxy::SessionHandler;

#[derive(Debug, StructOpt)]
#[structopt(
    name = "opentitansession",
    about = "A tool for interacting with OpenTitan chips."
)]
struct Opts {
    #[structopt(
        long,
        default_value = "config",
        help = "Filename of a default flagsfile.  Relative to $XDG_CONFIG_HOME/opentitantool."
    )]
    rcfile: PathBuf,

    #[structopt(long, default_value = "off")]
    logging: LevelFilter,

    #[structopt(flatten)]
    backend_opts: backend::BackendOpts,

    #[structopt(
        long,
        help = "Stop a running session, optionally combine with --listen_port for disambiguation"
    )]
    stop: bool,

    #[structopt(
        long,
        help = "Optional, defaults to 9900 or nearest higher available port."
    )]
    listen_port: Option<u16>,

    #[structopt(long, help = "Start session, staying in foreground (do not daemonize)")]
    debug: bool,

    #[structopt(
        long,
        help = "Internal, used to tell the child process run as a daemon."
    )]
    child: bool,
}

// Given some existing option configuration, maybe re-evaluate command
// line options by reading an `rcfile`.
fn parse_command_line(opts: Opts, mut args: ArgsOs) -> Result<Opts> {
    // Initialize the logger if the user requested the non-defualt option.
    let logging = opts.logging;
    if logging != LevelFilter::Off {
        env_logger::Builder::from_default_env()
            .filter(None, opts.logging)
            .init();
    }
    if opts.rcfile.as_os_str().is_empty() {
        // No rcfile to parse.
        return Ok(opts);
    }

    // Construct the rcfile path based on the user's config directory
    // (ie: $HOME/.config/opentitantool/<filename>).
    let rcfile = if let Some(base) = ProjectDirs::from("org", "opentitan", "opentitantool") {
        base.config_dir().join(&opts.rcfile)
    } else {
        opts.rcfile
    };

    // argument[0] is the executable name.
    let mut arguments = vec![args.next().unwrap()];

    // Read in the rcfile and extend the argument list.
    match read_to_string(&rcfile) {
        Ok(content) => {
            for line in content.split('\n') {
                // Strip basic comments as shellwords won't handle comments.
                let (line, _) = line.split_once('#').unwrap_or((line, ""));
                arguments.extend(shellwords::split(line)?.iter().map(OsString::from));
            }
            Ok(())
        }
        Err(e) if e.kind() == ErrorKind::NotFound => {
            log::warn!("Could not read {:?}. Ignoring.", rcfile);
            Ok(())
        }
        Err(e) => Err(anyhow::Error::new(e).context(format!("Reading file {:?}", rcfile))),
    }?;

    // Extend the argument list with all remaining command line arguments.
    arguments.extend(args.into_iter());
    let opts = Opts::from_iter(&arguments);
    if opts.logging != logging {
        // Try re-initializing the logger.  Ignore errors.
        let _ = env_logger::Builder::from_default_env()
            .filter(None, opts.logging)
            .try_init();
    }
    Ok(opts)
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct SessionStartResult {
    port: u16,
}

/// Spawn a child process, passing all the same arguments to the child, letting it instantiate a
/// Transport based on the command line arguments, listen on a TCP socket, and run as a daemon
/// process serving network requests.  Success of the child is verified by means of a
/// `SessionStartResult` JSON message sent through the standard output pipe.
fn start_session(run_file_fn: impl FnOnce(u16) -> PathBuf) -> Result<Box<dyn Serialize>> {
    let mut child = Command::new(env::current_exe()?) // Same executable
        .arg("--child") // Add argument to let the new process know it is the daemon child
        .args(args_os().skip(1)) // Propagate all existing arguments: --interface, etc.
        .stdin(Stdio::null()) // Not used by child, disconnect from terminal
        .stdout(Stdio::piped()) // Used for signalling completion of daemon startup
        .stderr(Stdio::inherit()) // May be used for error messages during daemon startup
        .spawn()?;

    match serde_json::from_reader::<&mut ChildStdout, Result<SessionStartResult, String>>(
        child.stdout.as_mut().unwrap(),
    ) {
        Ok(Ok(result)) => {
            // Create a pid file corresponding to the requested TCP port.
            let path = run_file_fn(result.port);
            File::create(path)?.write_all(format!("{}\n", child.id()).as_bytes())?;
            Ok(Box::new(result))
        }
        Ok(Err(e)) => bail!(e),
        Err(e) => bail!("Child process failed to start: {}", e),
    }
}

// This method runs in the daemon child.  It will instantiate SessionHandler to bind to a
// socket, then report the chosen port number to the parent process by means of a serialized
// `SessionStartResult` sent through the stdout anonymous pipe, and finally enter an infnite
// loop, processing connections on that socket
fn session_child(listen_port: Option<u16>, backend_opts: &backend::BackendOpts) -> Result<()> {
    let transport = backend::create(backend_opts)?;
    let mut session = SessionHandler::init(&transport, listen_port)?;
    // Instantiation of Transport backend, and binding to a socket was successful, now go
    // through the process of making this process a daemon, disconnected from the
    // terminal that was used to start it.

    // All configuration files have been processed (relative to current direction), we can now
    // drop the reference to the file system, (in case the admin wants to unmount while this
    // daemon is still running.)
    env::set_current_dir("/")?;

    // Close stderr, which remained open in order to allow any errors from the above code to
    // surface, but needs to be severed in order for the daemon to avoid being killed by SIGHUP
    // if the user closes the terminal window.
    dup2(File::open("/dev/null")?.as_raw_fd(), 2)?;

    // After severing the only connection to the controlling terminal inherited from the parent,
    // we can now establish a new Unix "session" for this process, which will not be
    // "controlled" by any terminal.  This means that this daemon will not be killed by SIGHUP,
    // in case the terminal that was used for running `session start` is later closed.
    setsid()?;

    // Report startup success to parent process.
    serde_json::to_writer::<io::Stdout, Result<SessionStartResult, String>>(
        io::stdout(),
        &Ok(SessionStartResult {
            port: session.get_port(),
        }),
    )?;
    io::stdout().flush()?;

    // Closing the standard output pipe is the signal to the parent process that this child has
    // started up successfully.  We close the pipe indirectly, by replacing file descriptor 1
    // with one pointing to /dev/null.  This will ensure that any subsequent accidentally
    // executed println!() will be a no-op, rather than trigger termination via SIGPIPE.
    dup2(2, 1)?;

    // Indefinitely run command processing loop in this daemon process.
    session.run_loop()
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct SessionStopResult {}

/// Load .pid file based on given port number, and send SIGTERM to the process identified in the
/// file, to request the daemon gracefully shut down.
fn stop_session(run_file_fn: impl FnOnce(u16) -> PathBuf, port: u16) -> Result<Box<dyn Serialize>> {
    // Read the pid file corresponding to the requested TCP port.
    let path = run_file_fn(port);
    let pid: i32 = FromStr::from_str(&fs::read_to_string(&path)?.trim())?;
    // Send signal to daemon process, asking it to terminate.
    signal::kill(Pid::from_raw(pid), Signal::SIGTERM)?;
    // Wait for daemon process to stop.
    loop {
        std::thread::sleep(Duration::from_millis(100));
        // Send "signal 0", meaning that the kernel performs error checks (among those, checking
        // that the target process exists), without actually sending any signal.
        match signal::kill(Pid::from_raw(pid), None) {
            Ok(()) => (), // Process still running, repeat.
            Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
                // Process could not be found, meaning that it has terminated, as expected.
                fs::remove_file(&path)?;
                return Ok(Box::new(SessionStopResult {}));
            }
            Err(e) => bail!("Unexpected error querying process presence: {}", e),
        }
    }
}

fn main() -> Result<()> {
    let opts = parse_command_line(Opts::from_args(), args_os())?;

    if opts.debug {
        // Start session process in foreground (do not daemonize)
        let transport = backend::create(&opts.backend_opts)?;
        let mut session = SessionHandler::init(&transport, opts.listen_port)?;
        println!("Listening on port {}", session.get_port());
        session.run_loop()?;
        return Ok(());
    }

    if opts.child {
        // This process is a child, which is supposed to stay running as a daemon.
        match session_child(opts.listen_port, &opts.backend_opts) {
            Ok(()) => process::exit(0),
            Err(e) => {
                // Report any error to parent process though stdout pipe.
                serde_json::to_writer::<io::Stdout, Result<SessionStartResult, String>>(
                    io::stdout(),
                    &Err(format!("{}", e).to_string()),
                )?;
                process::exit(1)
            }
        }
    }

    // Locate directory to use for .pid files
    let base_dirs = BaseDirs::new().unwrap();
    let run_user_dir = base_dirs
        .runtime_dir()
        .ok_or(anyhow!("No /run/user directory"))?;
    let run_file_fn = |port: u16| {
        let mut p = PathBuf::from(run_user_dir);
        p.push(format!("opentitansession.{}.pid", port));
        p
    };

    let value = if opts.stop {
        // Send signal to daemon process to stop
        stop_session(run_file_fn, opts.listen_port.unwrap_or(9900))?
    } else {
        // Fork a daemon process
        start_session(run_file_fn)?
    };
    println!("{}", serde_json::to_string_pretty(&value)?);
    Ok(())
}
<commit_msg>[cleanup] Fix clippy lints on opentitansession<commit_after>// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::{anyhow, bail, Result};
use directories::{BaseDirs, ProjectDirs};
use erased_serde::Serialize;
use log::LevelFilter;
use nix::sys::signal::{self, Signal};
use nix::unistd::{dup2, setsid, Pid};
use std::env::{self, args_os, ArgsOs};
use std::ffi::OsString;
use std::fs::{self, read_to_string, File};
use std::io::{self, ErrorKind, Write};
use std::iter::{IntoIterator, Iterator};
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::process::{self, ChildStdout, Command, Stdio};
use std::str::FromStr;
use std::time::Duration;
use structopt::StructOpt;

use opentitanlib::backend;
use opentitanlib::proxy::SessionHandler;

#[derive(Debug, StructOpt)]
#[structopt(
    name = "opentitansession",
    about = "A tool for interacting with OpenTitan chips."
)]
struct Opts {
    #[structopt(
        long,
        default_value = "config",
        help = "Filename of a default flagsfile.  Relative to $XDG_CONFIG_HOME/opentitantool."
    )]
    rcfile: PathBuf,

    #[structopt(long, default_value = "off")]
    logging: LevelFilter,

    #[structopt(flatten)]
    backend_opts: backend::BackendOpts,

    #[structopt(
        long,
        help = "Stop a running session, optionally combine with --listen_port for disambiguation"
    )]
    stop: bool,

    #[structopt(
        long,
        help = "Optional, defaults to 9900 or nearest higher available port."
    )]
    listen_port: Option<u16>,

    #[structopt(long, help = "Start session, staying in foreground (do not daemonize)")]
    debug: bool,

    #[structopt(
        long,
        help = "Internal, used to tell the child process run as a daemon."
    )]
    child: bool,
}

// Given some existing option configuration, maybe re-evaluate command
// line options by reading an `rcfile`.
fn parse_command_line(opts: Opts, mut args: ArgsOs) -> Result<Opts> {
    // Initialize the logger if the user requested the non-defualt option.
    let logging = opts.logging;
    if logging != LevelFilter::Off {
        env_logger::Builder::from_default_env()
            .filter(None, opts.logging)
            .init();
    }
    if opts.rcfile.as_os_str().is_empty() {
        // No rcfile to parse.
        return Ok(opts);
    }

    // Construct the rcfile path based on the user's config directory
    // (ie: $HOME/.config/opentitantool/<filename>).
    let rcfile = if let Some(base) = ProjectDirs::from("org", "opentitan", "opentitantool") {
        base.config_dir().join(&opts.rcfile)
    } else {
        opts.rcfile
    };

    // argument[0] is the executable name.
    let mut arguments = vec![args.next().unwrap()];

    // Read in the rcfile and extend the argument list.
    match read_to_string(&rcfile) {
        Ok(content) => {
            for line in content.split('\n') {
                // Strip basic comments as shellwords won't handle comments.
                let (line, _) = line.split_once('#').unwrap_or((line, ""));
                arguments.extend(shellwords::split(line)?.iter().map(OsString::from));
            }
            Ok(())
        }
        Err(e) if e.kind() == ErrorKind::NotFound => {
            log::warn!("Could not read {:?}. Ignoring.", rcfile);
            Ok(())
        }
        Err(e) => Err(anyhow::Error::new(e).context(format!("Reading file {:?}", rcfile))),
    }?;

    // Extend the argument list with all remaining command line arguments.
    arguments.extend(args.into_iter());
    let opts = Opts::from_iter(&arguments);
    if opts.logging != logging {
        // Try re-initializing the logger.  Ignore errors.
        let _ = env_logger::Builder::from_default_env()
            .filter(None, opts.logging)
            .try_init();
    }
    Ok(opts)
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct SessionStartResult {
    port: u16,
}

/// Spawn a child process, passing all the same arguments to the child, letting it instantiate a
/// Transport based on the command line arguments, listen on a TCP socket, and run as a daemon
/// process serving network requests.  Success of the child is verified by means of a
/// `SessionStartResult` JSON message sent through the standard output pipe.
fn start_session(run_file_fn: impl FnOnce(u16) -> PathBuf) -> Result<Box<dyn Serialize>> {
    let mut child = Command::new(env::current_exe()?) // Same executable
        .arg("--child") // Add argument to let the new process know it is the daemon child
        .args(args_os().skip(1)) // Propagate all existing arguments: --interface, etc.
        .stdin(Stdio::null()) // Not used by child, disconnect from terminal
        .stdout(Stdio::piped()) // Used for signalling completion of daemon startup
        .stderr(Stdio::inherit()) // May be used for error messages during daemon startup
        .spawn()?;

    match serde_json::from_reader::<&mut ChildStdout, Result<SessionStartResult, String>>(
        child.stdout.as_mut().unwrap(),
    ) {
        Ok(Ok(result)) => {
            // Create a pid file corresponding to the requested TCP port.
            let path = run_file_fn(result.port);
            File::create(path)?.write_all(format!("{}\n", child.id()).as_bytes())?;
            Ok(Box::new(result))
        }
        Ok(Err(e)) => bail!(e),
        Err(e) => bail!("Child process failed to start: {}", e),
    }
}

// This method runs in the daemon child.  It will instantiate SessionHandler to bind to a
// socket, then report the chosen port number to the parent process by means of a serialized
// `SessionStartResult` sent through the stdout anonymous pipe, and finally enter an infnite
// loop, processing connections on that socket
fn session_child(listen_port: Option<u16>, backend_opts: &backend::BackendOpts) -> Result<()> {
    let transport = backend::create(backend_opts)?;
    let mut session = SessionHandler::init(&transport, listen_port)?;
    // Instantiation of Transport backend, and binding to a socket was successful, now go
    // through the process of making this process a daemon, disconnected from the
    // terminal that was used to start it.

    // All configuration files have been processed (relative to current direction), we can now
    // drop the reference to the file system, (in case the admin wants to unmount while this
    // daemon is still running.)
    env::set_current_dir("/")?;

    // Close stderr, which remained open in order to allow any errors from the above code to
    // surface, but needs to be severed in order for the daemon to avoid being killed by SIGHUP
    // if the user closes the terminal window.
    dup2(File::open("/dev/null")?.as_raw_fd(), 2)?;

    // After severing the only connection to the controlling terminal inherited from the parent,
    // we can now establish a new Unix "session" for this process, which will not be
    // "controlled" by any terminal.  This means that this daemon will not be killed by SIGHUP,
    // in case the terminal that was used for running `session start` is later closed.
    setsid()?;

    // Report startup success to parent process.
    serde_json::to_writer::<io::Stdout, Result<SessionStartResult, String>>(
        io::stdout(),
        &Ok(SessionStartResult {
            port: session.get_port(),
        }),
    )?;
    io::stdout().flush()?;

    // Closing the standard output pipe is the signal to the parent process that this child has
    // started up successfully.  We close the pipe indirectly, by replacing file descriptor 1
    // with one pointing to /dev/null.  This will ensure that any subsequent accidentally
    // executed println!() will be a no-op, rather than trigger termination via SIGPIPE.
    dup2(2, 1)?;

    // Indefinitely run command processing loop in this daemon process.
    session.run_loop()
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct SessionStopResult {}

/// Load .pid file based on given port number, and send SIGTERM to the process identified in the
/// file, to request the daemon gracefully shut down.
fn stop_session(run_file_fn: impl FnOnce(u16) -> PathBuf, port: u16) -> Result<Box<dyn Serialize>> {
    // Read the pid file corresponding to the requested TCP port.
    let path = run_file_fn(port);
    let pid: i32 = FromStr::from_str(fs::read_to_string(&path)?.trim())?;
    // Send signal to daemon process, asking it to terminate.
    signal::kill(Pid::from_raw(pid), Signal::SIGTERM)?;
    // Wait for daemon process to stop.
    loop {
        std::thread::sleep(Duration::from_millis(100));
        // Send "signal 0", meaning that the kernel performs error checks (among those, checking
        // that the target process exists), without actually sending any signal.
        match signal::kill(Pid::from_raw(pid), None) {
            Ok(()) => (), // Process still running, repeat.
            Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
                // Process could not be found, meaning that it has terminated, as expected.
                fs::remove_file(&path)?;
                return Ok(Box::new(SessionStopResult {}));
            }
            Err(e) => bail!("Unexpected error querying process presence: {}", e),
        }
    }
}

fn main() -> Result<()> {
    let opts = parse_command_line(Opts::from_args(), args_os())?;

    if opts.debug {
        // Start session process in foreground (do not daemonize)
        let transport = backend::create(&opts.backend_opts)?;
        let mut session = SessionHandler::init(&transport, opts.listen_port)?;
        println!("Listening on port {}", session.get_port());
        session.run_loop()?;
        return Ok(());
    }

    if opts.child {
        // This process is a child, which is supposed to stay running as a daemon.
        match session_child(opts.listen_port, &opts.backend_opts) {
            Ok(()) => process::exit(0),
            Err(e) => {
                // Report any error to parent process though stdout pipe.
                serde_json::to_writer::<io::Stdout, Result<SessionStartResult, String>>(
                    io::stdout(),
                    &Err(format!("{}", e)),
                )?;
                process::exit(1)
            }
        }
    }

    // Locate directory to use for .pid files
    let base_dirs = BaseDirs::new().unwrap();
    let run_user_dir = base_dirs
        .runtime_dir()
        .ok_or_else(|| anyhow!("No /run/user directory"))?;
    let run_file_fn = |port: u16| {
        let mut p = PathBuf::from(run_user_dir);
        p.push(format!("opentitansession.{}.pid", port));
        p
    };

    let value = if opts.stop {
        // Send signal to daemon process to stop
        stop_session(run_file_fn, opts.listen_port.unwrap_or(9900))?
    } else {
        // Fork a daemon process
        start_session(run_file_fn)?
    };
    println!("{}", serde_json::to_string_pretty(&value)?);
    Ok(())
}
<|endoftext|>


<commit_before>#![deny(warnings)]

extern crate cargo_registry;
extern crate conduit_middleware;
extern crate civet;
extern crate git2;
extern crate env_logger;
extern crate s3;

use cargo_registry::env;
use civet::Server;
use std::env;
use std::fs::{self, File};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::channel;

#[allow(dead_code)]
fn main() {
    env_logger::init().unwrap();
    let url = env("GIT_REPO_URL");
    let checkout = PathBuf::from(env("GIT_REPO_CHECKOUT"));

    let repo = match git2::Repository::open(&checkout) {
        Ok(r) => r,
        Err(..) => {
            let _ = fs::remove_dir_all(&checkout);
            fs::create_dir_all(&checkout).unwrap();
            let mut cb = git2::RemoteCallbacks::new();
            cb.credentials(cargo_registry::git::credentials);
            let mut opts = git2::FetchOptions::new();
            opts.remote_callbacks(cb);
            git2::build::RepoBuilder::new()
                                     .fetch_options(opts)
                                     .clone(&url, &checkout).unwrap()
        }
    };
    let mut cfg = repo.config().unwrap();
    cfg.set_str("user.name", "bors").unwrap();
    cfg.set_str("user.email", "bors@rust-lang.org").unwrap();

    let heroku = env::var("HEROKU").is_ok();
    let cargo_env = if heroku {
        cargo_registry::Env::Production
    } else {
        cargo_registry::Env::Development
    };
    let api_protocol = String::from("https");
    let uploader = cargo_registry::Uploader::S3 {
        bucket: s3::Bucket::new(env("S3_BUCKET"),
                                env::var("S3_REGION").ok(),
                                env("S3_ACCESS_KEY"),
                                env("S3_SECRET_KEY"),
                                &api_protocol),
        proxy: None,
    };

    let config = cargo_registry::Config {
        uploader: uploader,
        session_key: env("SESSION_KEY"),
        git_repo_checkout: checkout,
        gh_client_id: env("GH_CLIENT_ID"),
        gh_client_secret: env("GH_CLIENT_SECRET"),
        db_url: env("DATABASE_URL"),
        env: cargo_env,
        max_upload_size: 10 * 1024 * 1024,
        mirror: env::var("MIRROR").is_ok(),
        api_protocol: api_protocol,
    };
    let app = cargo_registry::App::new(&config);
    let app = cargo_registry::middleware(Arc::new(app));

    cargo_registry::categories::sync().unwrap();

    let port = if heroku {
        8888
    } else {
        env::var("PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(8888)
    };
    let threads = if cargo_env == cargo_registry::Env::Development {1} else {50};
    let mut cfg = civet::Config::new();
    cfg.port(port).threads(threads).keep_alive(true);
    let _a = Server::start(cfg, app);
    println!("listening on port {}", port);
    if heroku {
        File::create("/tmp/app-initialized").unwrap();
    }

    // TODO: handle a graceful shutdown by just waiting for a SIG{INT,TERM}
    let (_tx, rx) = channel::<()>();
    rx.recv().unwrap();
}
<commit_msg>Require s3 vars to be set in production, allow unset in dev<commit_after>#![deny(warnings)]

extern crate cargo_registry;
extern crate conduit_middleware;
extern crate civet;
extern crate git2;
extern crate env_logger;
extern crate s3;

use cargo_registry::env;
use civet::Server;
use std::env;
use std::fs::{self, File};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::channel;

#[allow(dead_code)]
fn main() {
    env_logger::init().unwrap();
    let url = env("GIT_REPO_URL");
    let checkout = PathBuf::from(env("GIT_REPO_CHECKOUT"));

    let repo = match git2::Repository::open(&checkout) {
        Ok(r) => r,
        Err(..) => {
            let _ = fs::remove_dir_all(&checkout);
            fs::create_dir_all(&checkout).unwrap();
            let mut cb = git2::RemoteCallbacks::new();
            cb.credentials(cargo_registry::git::credentials);
            let mut opts = git2::FetchOptions::new();
            opts.remote_callbacks(cb);
            git2::build::RepoBuilder::new()
                                     .fetch_options(opts)
                                     .clone(&url, &checkout).unwrap()
        }
    };
    let mut cfg = repo.config().unwrap();
    cfg.set_str("user.name", "bors").unwrap();
    cfg.set_str("user.email", "bors@rust-lang.org").unwrap();

    let api_protocol = String::from("https");
    let mirror = env::var("MIRROR").is_ok();

    let heroku = env::var("HEROKU").is_ok();
    let cargo_env = if heroku {
        cargo_registry::Env::Production
    } else {
        cargo_registry::Env::Development
    };

    let uploader = match (cargo_env, mirror) {
        (cargo_registry::Env::Production, false) => {
            // `env` panics if these vars are not set
            cargo_registry::Uploader::S3 {
                bucket: s3::Bucket::new(env("S3_BUCKET"),
                                        env::var("S3_REGION").ok(),
                                        env("S3_ACCESS_KEY"),
                                        env("S3_SECRET_KEY"),
                                        &api_protocol),
                proxy: None,
            }
        },
        (cargo_registry::Env::Production, true) => {
            // Read-only mirrors don't need access key or secret key,
            // but they might have them. Definitely need bucket though.
            cargo_registry::Uploader::S3 {
                bucket: s3::Bucket::new(env("S3_BUCKET"),
                                        env::var("S3_REGION").ok(),
                                        env::var("S3_ACCESS_KEY").unwrap_or(String::new()),
                                        env::var("S3_SECRET_KEY").unwrap_or(String::new()),
                                        &api_protocol),
                proxy: None,
            }
        },
        (cargo_registry::Env::Development, _) => {
            // Allow for any of these to be blank in development
            cargo_registry::Uploader::S3 {
                bucket: s3::Bucket::new(env::var("S3_BUCKET").unwrap_or(String::new()),
                                        env::var("S3_REGION").ok(),
                                        env::var("S3_ACCESS_KEY").unwrap_or(String::new()),
                                        env::var("S3_SECRET_KEY").unwrap_or(String::new()),
                                        &api_protocol),
                proxy: None,
            }
        },
        // See immediately before this match where we choose either prod or dev
        (cargo_registry::Env::Test, _) => unreachable!(),
    };

    let config = cargo_registry::Config {
        uploader: uploader,
        session_key: env("SESSION_KEY"),
        git_repo_checkout: checkout,
        gh_client_id: env("GH_CLIENT_ID"),
        gh_client_secret: env("GH_CLIENT_SECRET"),
        db_url: env("DATABASE_URL"),
        env: cargo_env,
        max_upload_size: 10 * 1024 * 1024,
        mirror: mirror,
        api_protocol: api_protocol,
    };
    let app = cargo_registry::App::new(&config);
    let app = cargo_registry::middleware(Arc::new(app));

    cargo_registry::categories::sync().unwrap();

    let port = if heroku {
        8888
    } else {
        env::var("PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(8888)
    };
    let threads = if cargo_env == cargo_registry::Env::Development {1} else {50};
    let mut cfg = civet::Config::new();
    cfg.port(port).threads(threads).keep_alive(true);
    let _a = Server::start(cfg, app);
    println!("listening on port {}", port);
    if heroku {
        File::create("/tmp/app-initialized").unwrap();
    }

    // TODO: handle a graceful shutdown by just waiting for a SIG{INT,TERM}
    let (_tx, rx) = channel::<()>();
    rx.recv().unwrap();
}
<|endoftext|>


<commit_before>use crate::controllers::frontend_prelude::*;

use crate::models::{CrateOwner, OwnerKind, User};
use crate::schema::{crate_owners, crates, users};
use crate::util::errors::ChainError;
use crate::views::EncodablePublicUser;

/// Handles the `GET /users/:user_id` route.
pub fn show(req: &mut dyn RequestExt) -> EndpointResult {
    use self::users::dsl::{gh_login, id, users};

    let name = crate::lower(&req.params()["user_id"]);
    let conn = req.db_conn()?;
    let user: User = users
        .filter(crate::lower(gh_login).eq(name))
        .order(id.desc())
        .first(&*conn)?;

    #[derive(Serialize)]
    struct R {
        user: EncodablePublicUser,
    }
    Ok(req.json(&R { user: user.into() }))
}

/// Handles the `GET /users/:user_id/stats` route.
pub fn stats(req: &mut dyn RequestExt) -> EndpointResult {
    use diesel::dsl::sum;

    let user_id = &req.params()["user_id"]
        .parse::<i32>()
        .chain_error(|| bad_request("invalid user_id"))?;
    let conn = req.db_conn()?;

    let data: i64 = CrateOwner::by_owner_kind(OwnerKind::User)
        .inner_join(crates::table)
        .filter(crate_owners::owner_id.eq(user_id))
        .select(sum(crates::downloads))
        .first::<Option<i64>>(&*conn)?
        .unwrap_or(0);

    #[derive(Serialize)]
    struct R {
        total_downloads: i64,
    }
    Ok(req.json(&R {
        total_downloads: data,
    }))
}
<commit_msg>controllers::user::other: Use `json!` macro to simplify JSON serialization code<commit_after>use crate::controllers::frontend_prelude::*;

use crate::models::{CrateOwner, OwnerKind, User};
use crate::schema::{crate_owners, crates, users};
use crate::util::errors::ChainError;
use crate::views::EncodablePublicUser;

/// Handles the `GET /users/:user_id` route.
pub fn show(req: &mut dyn RequestExt) -> EndpointResult {
    use self::users::dsl::{gh_login, id, users};

    let name = crate::lower(&req.params()["user_id"]);
    let conn = req.db_conn()?;
    let user: User = users
        .filter(crate::lower(gh_login).eq(name))
        .order(id.desc())
        .first(&*conn)?;

    Ok(req.json(&json!({ "user": EncodablePublicUser::from(user) })))
}

/// Handles the `GET /users/:user_id/stats` route.
pub fn stats(req: &mut dyn RequestExt) -> EndpointResult {
    use diesel::dsl::sum;

    let user_id = &req.params()["user_id"]
        .parse::<i32>()
        .chain_error(|| bad_request("invalid user_id"))?;
    let conn = req.db_conn()?;

    let data: i64 = CrateOwner::by_owner_kind(OwnerKind::User)
        .inner_join(crates::table)
        .filter(crate_owners::owner_id.eq(user_id))
        .select(sum(crates::downloads))
        .first::<Option<i64>>(&*conn)?
        .unwrap_or(0);

    Ok(req.json(&json!({ "total_downloads": data })))
}
<|endoftext|>