Repository: iddm/urlshortener-rs (language: rust)

Issue:
Provider: kutt.it
How about support for [`kutt.it`](https://kutt.it/)? This would be cool because it's a shortener that is [open-source](https://github.com/thedevs-network/kutt) and easily self hosted in contrast to the current list of supported shorteners. A `kutt.it` instance provides an API and uses an API key.

What do you think? I might have some time to implement a provider for this.


Discussion / hints:
Hi! I'll absolutely appreciate having it implemented here! That is what this crate is for :)
But, as it is an open-source project, we might extend the urlshortener-rs API to support not only specification of custom API keys for the `kutt.it` url shortener, but also different domains then (if a user wants to self-host it, we should allow one to specify the url pointing to his own `kutt.it`).
@timvisee Do you want to implement yourself or?
I'll take a look into this now. Not sure whether I'm able to complete it though, due to limited time.

> but also different domains then

Exactly. I'm thinking of an (optional) provider field now. I'll check how it works out.

Fix patch:
diff --git a/README.md b/README.md
index aa755eed..58bf916f 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ With authentication:
 
 - `goo.gl`
 - `bit.ly`
+- `kutt.it` (supports self hosting)
 
 Without authentication:
 
diff --git a/examples/via_kutt_custom_host_provider.rs b/examples/via_kutt_custom_host_provider.rs
new file mode 100644
index 00000000..66326bc5
--- /dev/null
+++ b/examples/via_kutt_custom_host_provider.rs
@@ -0,0 +1,23 @@
+//! Generate a short URL by specifying which provider to use.
+
+extern crate urlshortener;
+
+use urlshortener::client::UrlShortener;
+use urlshortener::providers::Provider;
+
+fn main() {
+    let long_url = "https://doc.rust-lang.org/std/";
+
+    let us = UrlShortener::new().unwrap();
+    let key = "MY_API_KEY";
+    let host = "https://example.com";
+    let short_url = us.generate(
+        long_url,
+        &Provider::Kutt {
+            api_key: key.into(),
+            host: Some(host.into()),
+        },
+    );
+
+    println!("{:?}", short_url);
+}
diff --git a/src/providers.rs b/src/providers.rs
index a6bf1af8..623c270e 100644
--- a/src/providers.rs
+++ b/src/providers.rs
@@ -1,6 +1,7 @@
 //! Library service providers implementation.
 
 use request;
+use reqwest::header::HeaderMap;
 use url::form_urlencoded;
 
 /// A user agent for faking weird services.
@@ -106,6 +107,7 @@ macro_rules! request {
                 body: None,
                 content_type: None,
                 user_agent: None,
+                headers: None,
                 method: $method,
             }
         }
@@ -118,6 +120,7 @@ macro_rules! request {
                 body: Some(format!($body, url)),
                 content_type: None,
                 user_agent: None,
+                headers: None,
                 method: $method,
             }
         }
@@ -130,6 +133,7 @@ macro_rules! request {
                 body: Some(format!($body, url)),
                 content_type: Some($content_type),
                 user_agent: None,
+                headers: None,
                 method: $method,
             }
         }
@@ -163,6 +167,13 @@ pub enum Provider {
         /// An api key string which you may obtain on the provider web service page.
         api_key: String,
     },
+    /// https://kutt.it provider, can be self hosted
+    Kutt {
+        /// An api key string which you may obtain on the provider web service page.
+        api_key: String,
+        /// The api host, defaults to 'https://kutt.it'
+        host: Option<String>,
+    },
     /// https://hec.su provider
     ///
     /// Notes:
@@ -232,6 +243,10 @@ impl Provider {
             Provider::HmmRs => "hmm.rs",
             Provider::HecSu => "hec.su",
             Provider::IsGd => "is.gd",
+            Provider::Kutt { ref host, .. } => host
+                .as_ref()
+                .map(|h| h.rsplit("//").next().unwrap())
+                .unwrap_or("kutt.it"),
             Provider::NowLinks => "nowlinks.net",
             Provider::PhxCoIn => "phx.co.in",
             Provider::PsbeCo => "psbe.co",
@@ -272,6 +287,7 @@ fn bitly_req(url: &str, key: &str) -> request::Request {
         body: None,
         content_type: None,
         user_agent: None,
+        headers: None,
         method: request::Method::Get,
     }
 }
@@ -297,6 +313,7 @@ fn googl_req(url: &str, key: &str) -> request::Request {
         body: Some(format!(r#"{{"longUrl": "{}"}}"#, url)),
         content_type: Some(request::ContentType::Json),
         user_agent: None,
+        headers: None,
         method: request::Method::Post,
     }
 }
@@ -308,6 +325,7 @@ fn hmmrs_req(url: &str) -> request::Request {
         body: Some(format!(r#"{{"url": "{}"}}"#, url)),
         content_type: Some(request::ContentType::Json),
         user_agent: Some(request::UserAgent(FAKE_USER_AGENT.to_owned())),
+        headers: None,
         method: request::Method::Post,
     }
 }
@@ -326,6 +344,21 @@ request!(
     "https://is.gd/create.php?format=simple&url={}"
 );
 
+parse_json_tag!(kutt_parse, "shortUrl", "");
+fn kutt_req(url: &str, api_key: &str, host: Option<&str>) -> request::Request {
+    let mut headers = HeaderMap::new();
+    headers.insert("X-API-Key", api_key.parse().unwrap());
+
+    request::Request {
+        url: format!("{}/api/url/submit", host.unwrap_or("https://kutt.it")),
+        body: Some(format!(r#"{{"target": "{}"}}"#, url)),
+        content_type: Some(request::ContentType::Json),
+        user_agent: None,
+        headers: Some(headers),
+        method: request::Method::Post,
+    }
+}
+
 parse_noop!(nowlinks_parse);
 request!(
     nowlinks_req,
@@ -429,6 +462,7 @@ pub fn parse(res: &str, provider: &Provider) -> Result<String, ProviderError> {
         Provider::HmmRs => hmmrs_parse(res),
         Provider::HecSu => hecsu_parse(res),
         Provider::IsGd => isgd_parse(res),
+        Provider::Kutt { .. } => kutt_parse(res),
         Provider::NowLinks => nowlinks_parse(res),
         Provider::PhxCoIn => phxcoin_parse(res),
         Provider::PsbeCo => psbeco_parse(res),
@@ -465,13 +499,17 @@ pub fn request(url: &str, provider: &Provider) -> request::Request {
     match *provider {
         Provider::Abv8 => abv8_req(url),
         Provider::BamBz => bambz_req(url),
-        Provider::BitLy { token: ref key } => bitly_req(url, &key),
+        Provider::BitLy { ref token } => bitly_req(url, &token),
         Provider::Bmeo => bmeo_req(url),
         Provider::FifoCc => fifocc_req(url),
-        Provider::GooGl { api_key: ref key } => googl_req(url, &key),
+        Provider::GooGl { ref api_key } => googl_req(url, &api_key),
         Provider::HmmRs => hmmrs_req(url),
         Provider::HecSu => hecsu_req(url),
         Provider::IsGd => isgd_req(url),
+        Provider::Kutt {
+            ref api_key,
+            ref host,
+        } => kutt_req(url, &api_key, host.as_ref().map(|h| &**h)),
         Provider::NowLinks => nowlinks_req(url),
         Provider::PhxCoIn => phxcoin_req(url),
         Provider::PsbeCo => psbeco_req(url),
diff --git a/src/request.rs b/src/request.rs
index 2c635eac..706d6c24 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -1,5 +1,8 @@
 #[cfg(feature = "client")]
-use reqwest::{header, Client, Response};
+use reqwest::{
+    header::{self, HeaderMap},
+    Client, Response,
+};
 
 const CONTENT_JSON: &str = "application/json";
 const CONTENT_FORM_URL_ENCODED: &str = "application/x-www-form-urlencoded";
@@ -37,6 +40,8 @@ pub struct Request {
     pub content_type: Option<ContentType>,
     /// The user agent.
     pub user_agent: Option<UserAgent>,
+    /// Request headers.
+    pub headers: Option<HeaderMap>,
     /// The HTTP method.
     pub method: Method,
 }
@@ -54,6 +59,10 @@ impl Request {
             builder = builder.header(header::USER_AGENT, agent.0);
         }
 
+        if let Some(headers) = self.headers.clone() {
+            builder = builder.headers(headers);
+        }
+
         if let Some(content_type) = self.content_type {
             builder = match content_type {
                 ContentType::Json => builder.header(header::CONTENT_TYPE, CONTENT_JSON),


Test patch: