remote 1.0.1

Generate remote repo URLs
extern crate clap;

use clap::{App, Arg, AppSettings};

type STR = &'static str;

#[derive(Clone, Copy)]
enum Service {
  BitbucketHG,
  BitbucketHTTPS,
  BitbucketSSH,
  GitHubHTTPS,
  GitHubSSH,
  GitLabHTTPS,
  GitLabSSH,
  PikacodeHTTPS,
  PikacodeSSH,
}

use Service::*;

impl Service {
  fn name(self) -> STR {
    match self {
      BitbucketHG    => "bitbucket-hg",
      BitbucketHTTPS => "bitbucket-https",
      BitbucketSSH   => "bitbucket-ssh",
      GitHubHTTPS    => "github-https",
      GitHubSSH      => "github-ssh",
      GitLabHTTPS    => "gitlab-https",
      GitLabSSH      => "gitlab-ssh",
      PikacodeHTTPS  => "pikacode-https",
      PikacodeSSH    => "pikacode-ssh",
    }
  }

  fn from_name(name: &str) -> Option<Service> {
    match name {
      "bitbucket-hg"    => Some(BitbucketHG),
      "bitbucket-https" => Some(BitbucketHTTPS),
      "bitbucket-ssh"   => Some(BitbucketSSH),
      "github-https"    => Some(GitHubHTTPS),
      "github-ssh"      => Some(GitHubSSH),
      "gitlab-https"    => Some(GitLabHTTPS),
      "gitlab-ssh"      => Some(GitLabSSH),
      "pikacode-https"  => Some(PikacodeHTTPS),
      "pikacode-ssh"    => Some(PikacodeSSH),
      _                 => None,
    }
  }

  fn template(self) -> Template {
    match self {
      BitbucketHG    => Template::new("ssh://hg@bitbucket.org/",  "/", ""),
      BitbucketHTTPS => Template::new("https://bitbucket.org/",   "/", ".git"),
      BitbucketSSH   => Template::new("git@bitbucket.org:",       "/", ".git"),
      GitHubHTTPS    => Template::new("https://github.com/",      "/", ".git"),
      GitHubSSH      => Template::new("git@github.com:",          "/", ".git"),
      GitLabHTTPS    => Template::new("https://gitlab.com/",      "/", ".git"),
      GitLabSSH      => Template::new("git@gitlab.com:",          "/", ".git"),
      PikacodeHTTPS  => Template::new("https://v2.pikacode.com/", "/", ".git"),
      PikacodeSSH    => Template::new("git@v2.pikacode.com:",     "/", ".git"),
    }
  }
}

#[derive(Copy, Clone)]
struct Template {
  prefix:  STR,
  infix:   STR,
  postfix: STR,
}

impl Template {
  fn new(prefix: STR, infix: STR, postfix: STR) -> Template {
    Template {
      prefix:  prefix,
      infix:   infix,
      postfix: postfix,
    }
  }

  fn instantiate(self, user: &str, project: &str) -> String {
    [self.prefix, user, self.infix, project, self.postfix].join("")
  }
} 

static SERVICES: &'static [Service] = &[
  BitbucketHG,
  BitbucketHTTPS,
  BitbucketSSH,
  GitHubHTTPS,
  GitHubSSH,
  GitLabHTTPS,
  GitLabSSH,
  PikacodeHTTPS,
  PikacodeSSH,
];

static AFTER_HELP: STR = "DESCRIPTION:

    A little program that prints repository URLs.

    On its own it does not save a great deal of typing, but you can create
    aliases in your shell's configuration file like so:

    alias github=`remote github-ssh gazebo`
    alias bitbucket=`remote bitbucket-ssh gazebo`

    Assuming you have the username `gazebo` on both github and bitbucket, you
    can then clone your own repositories easily:

    $ git clone `github foo`
    Cloning into 'foo'...
    ...

    Or add new remotes to existing repos:

    $ git remote add `github foo`

    And of course you can always use remote directly:

    $ git clone `remote github-ssh rust-lang cargo`
    Cloning into 'cargo'...
    ...";

fn run<I, T>(args: I) -> String
  where I: IntoIterator<Item = T>,
        T: Into<std::ffi::OsString> + Clone,
{
  let service_names = SERVICES.iter().map(|service| service.name()).collect::<Vec<_>>();

  let matches = App::new("remote")
    .version(concat!("v", env!("CARGO_PKG_VERSION")))
    .author("Casey Rodarmor <casey@rodarmor.com>")
    .about("Generate remote repo URLs - https://github.com/casey/remote")
    .setting(AppSettings::ColoredHelp)
    .after_help(AFTER_HELP)
    .arg(Arg::with_name("service")
         .possible_values(&service_names)
         .required(true))
    .arg(Arg::with_name("user")
         .required(true))
    .arg(Arg::with_name("project")
         .required(true))
    .get_matches_from(args);

  let service_name = matches.value_of("service").expect("service argument had no value");
  let service      = Service::from_name(service_name).expect("service name had bad value");
  let user         = matches.value_of("user").expect("user argument had no value");
  let project      = matches.value_of("project").expect("project argument had no value");

  service.template().instantiate(user, project)
}

fn main() {
  println!("{}", run(std::env::args_os()));
}

#[cfg(test)]
mod tests {
  macro_rules! test {
    ($name:ident, $service:expr, $user:expr, $project:expr, $expected:expr) => {
      #[test]
      fn $name() {
        let result = super::run(&["remote", $service, $user, $project]);
        if result != $expected {
          panic!("{} != {}", result, $expected);
        }
      }
    };
  }

  test! {
    github_ssh, "github-ssh", "casey", "remote", "git@github.com:casey/remote.git"
  }

  test! {
    bitbucket_https, "bitbucket-https", "casey", "remote", "https://bitbucket.org/casey/remote.git"
  }
}