Repository: gsquire/sendgrid-rs (language: rust)

Issue:
Return a custom sendgrid error
There are a lot of projects that do this. I think it makes sense for this library to return custom send grid errors instead of hyper's stuff. 


Discussion / hints:
Hi @lancecarlson, I will look into this soon along with #18 as I plan on doing a little refactoring.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 0d6ad4a..3f469df 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "sendgrid"
-version = "0.5.3"
+version = "0.6.0"
 authors = ["Garrett Squire <garrettsquire@gmail.com>"]
 description = "An unofficial client library for the SendGrid API"
 repository = "https://github.com/gsquire/sendgrid-rs"
@@ -8,11 +8,12 @@ license = "MIT"
 homepage = "https://sendgrid.com"
 keywords = ["email"]
 documentation = "https://docs.rs/sendgrid"
+readme = "README.md"
 
 [dependencies]
 data-encoding = "1.2"
-hyper = "0.10"
-hyper-native-tls = "0.2"
+error-chain = "0.10"
+reqwest = "0.7"
 serde = "1.0"
 serde_derive = "1.0"
 serde_json = "1.0"
diff --git a/examples/v3.rs b/examples/v3.rs
index 129f824..df6dfcc 100644
--- a/examples/v3.rs
+++ b/examples/v3.rs
@@ -3,26 +3,26 @@ extern crate sendgrid;
 use sendgrid::v3::*;
 
 fn main() {
-	let mut m = SGMailV3::new();
+    let mut m = SGMailV3::new();
 
-	let mut e = Email::new();
-	e.set_email("g@gmail.com");
+    let mut e = Email::new();
+    e.set_email("g@gmail.com");
 
-	let mut c = Content::new();
-	c.set_content_type("text/html");
-	c.set_value("Test");
+    let mut c = Content::new();
+    c.set_content_type("text/html");
+    c.set_value("Test");
 
-	let mut p = Personalization::new();
-	p.add_to(e.clone());
+    let mut p = Personalization::new();
+    p.add_to(e.clone());
 
-	m.set_from(e.clone());
-	m.set_subject("Subject");
-	m.add_content(c);
-	m.add_personalization(p);
+    m.set_from(e.clone());
+    m.set_subject("Subject");
+    m.add_content(c);
+    m.add_personalization(p);
 
-	let mut env_vars = ::std::env::vars();
-	let api_key = env_vars.find(|v| v.0 == "SG_API_KEY").unwrap();
-	let sender = V3Sender::new(api_key.1);
-	let code = sender.send(&m);
-	println!("{:?}", code);
+    let mut env_vars = ::std::env::vars();
+    let api_key = env_vars.find(|v| v.0 == "SG_API_KEY").unwrap();
+    let sender = V3Sender::new(api_key.1);
+    let code = sender.send(&m);
+    println!("{:?}", code);
 }
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..5f80b88
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,23 @@
+use std::io;
+
+use reqwest;
+use serde_json;
+
+error_chain! {
+    types {
+        SendgridError, SendgridErrorKind, SendgridResultExt, SendgridResult;
+    }
+
+    foreign_links {
+        Io(io::Error);
+        JSONDecode(serde_json::Error);
+        ReqwestError(reqwest::Error);
+    }
+
+    errors {
+        InvalidFilename {
+            description("invalid filename")
+            display("could not UTF-8 decode this filename")
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index dcd2f1b..99b1427 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,13 +1,16 @@
+#[macro_use]
+extern crate error_chain;
+
 #[macro_use]
 extern crate serde_derive;
 
 extern crate data_encoding;
-extern crate hyper;
-extern crate hyper_native_tls;
+extern crate reqwest;
 extern crate serde;
 extern crate serde_json;
 extern crate url;
 
-pub mod sg_client;
+pub mod errors;
 pub mod mail;
+pub mod sg_client;
 pub mod v3;
diff --git a/src/mail.rs b/src/mail.rs
index 3c9437a..06a3559 100644
--- a/src/mail.rs
+++ b/src/mail.rs
@@ -1,6 +1,9 @@
+use errors::{SendgridErrorKind, SendgridResult};
+
 use std::collections::HashMap;
 use std::fs::File;
 use std::io::Read;
+use std::path::Path;
 
 use serde_json;
 
@@ -22,7 +25,7 @@ pub struct Mail {
     pub attachments: HashMap<String, String>,
     pub content: HashMap<String, String>,
     pub headers: HashMap<String, String>,
-    pub x_smtpapi: String
+    pub x_smtpapi: String,
 }
 
 impl Mail {
@@ -114,19 +117,18 @@ impl Mail {
     /// let mut message = Mail::new();
     /// message.add_attachment("/path/to/file/contents.txt");
     /// ```
-    pub fn add_attachment(&mut self, path: &str) {
-        let file = File::open(path);
-        match file {
-            Ok(mut f) => {
-                let mut data = String::new();
-                let read = f.read_to_string(&mut data);
-                match read {
-                    Ok(_) => { self.attachments.insert(path.to_string(), data); },
-                    Err(e) => { panic!("Could not read file: {:?}", e); }
-                }
-            },
-            Err(e) => { panic!("Could not open file: {:?}", e); }
+    pub fn add_attachment<P: AsRef<Path>>(&mut self, path: P) -> SendgridResult<()> {
+        let mut file = File::open(&path)?;
+        let mut data = String::new();
+        file.read_to_string(&mut data)?;
+
+        if let Some(name) = path.as_ref().to_str() {
+            self.attachments.insert(String::from(name), data);
+        } else {
+            return Err(SendgridErrorKind::InvalidFilename.into());
         }
+
+        Ok(())
     }
 
     /// Add content for inline images in the message.
@@ -141,12 +143,9 @@ impl Mail {
     }
 
     /// Used internally for string encoding. Not needed for message building.
-    pub fn make_header_string(&mut self) -> String {
-        let headers = serde_json::to_string(&self.headers);
-        match headers {
-            Ok(h) => h,
-            Err(e) => { panic!("Could not encode headers: {:?}", e); }
-        }
+    pub fn make_header_string(&mut self) -> SendgridResult<String> {
+        let string = serde_json::to_string(&self.headers)?;
+        Ok(string)
     }
 
     /// Add an X-SMTPAPI string to the message. This can be done by using the
diff --git a/src/sg_client.rs b/src/sg_client.rs
index 21cc750..080a4f9 100644
--- a/src/sg_client.rs
+++ b/src/sg_client.rs
@@ -1,14 +1,11 @@
+use errors::SendgridResult;
+
 use mail::Mail;
 
 use std::io::Read;
 
-use hyper::Client;
-use hyper::error::Error;
-use hyper::net::HttpsConnector;
-use hyper::header::{Authorization, Bearer, ContentType, Headers, UserAgent};
-use hyper::mime::{Mime, TopLevel, SubLevel};
-
-use hyper_native_tls::NativeTlsClient;
+use reqwest::header::{Authorization, Bearer, ContentType, Headers, UserAgent};
+use reqwest::Client;
 
 use url::form_urlencoded::Serializer;
 
@@ -32,7 +29,7 @@ fn make_form_key(form: &str, key: &str) -> String {
 }
 
 // Use the URL form encoder to properly generate the body used in the mail send request.
-fn make_post_body(mut mail_info: Mail) -> String {
+fn make_post_body(mut mail_info: Mail) -> SendgridResult<String> {
     let body = String::new();
     let mut encoder = Serializer::new(body);
 
@@ -67,47 +64,36 @@ fn make_post_body(mut mail_info: Mail) -> String {
     encoder.append_pair("fromname", &mail_info.from_name);
     encoder.append_pair("replyto", &mail_info.reply_to);
     encoder.append_pair("date", &mail_info.date);
-    encoder.append_pair("headers", &mail_info.make_header_string());
+    encoder.append_pair("headers", &mail_info.make_header_string()?);
     encoder.append_pair("x-smtpapi", &mail_info.x_smtpapi);
 
-    encoder.finish()
+    Ok(encoder.finish())
 }
 
 impl SGClient {
     /// Makes a new SendGrid cient with the specified API key.
     pub fn new(key: String) -> SGClient {
-        SGClient {api_key: key}
+        SGClient { api_key: key }
     }
 
     /// Sends a messages through the SendGrid API. It takes a Mail struct as an
     /// argument. It returns the string response from the API as JSON.
     /// It sets the Content-Type to be application/x-www-form-urlencoded.
-    pub fn send(self, mail_info: Mail) -> Result<String, Error> {
-        let ssl = NativeTlsClient::new().unwrap();
-        let connector = HttpsConnector::new(ssl);
-        let client = Client::with_connector(connector);
+    pub fn send(self, mail_info: Mail) -> SendgridResult<String> {
+        let client = Client::new()?;
         let mut headers = Headers::new();
-        headers.set(
-            Authorization(
-                Bearer { token: self.api_key.to_owned() }
-                )
-        );
-        headers.set(
-            ContentType(
-                Mime(TopLevel::Application, SubLevel::WwwFormUrlEncoded, vec![])
-                )
-        );
-        headers.set(
-            UserAgent("sendgrid-rs".to_owned())
-        );
-
-        let post_body = make_post_body(mail_info);
-        let mut res = try!(client.post(API_URL)
+        headers.set(Authorization(Bearer { token: self.api_key.to_owned() }));
+        headers.set(ContentType::form_url_encoded());
+        headers.set(UserAgent::new("sendgrid-rs"));
+
+        let post_body = make_post_body(mail_info)?;
+        let mut res = client
+            .post(API_URL)?
             .headers(headers)
-            .body(&post_body)
-            .send());
+            .body(post_body)
+            .send()?;
         let mut body = String::new();
-        try!(res.read_to_string(&mut body));
+        res.read_to_string(&mut body)?;
         Ok(body)
     }
 }
@@ -123,7 +109,7 @@ fn basic_message_body() {
     let body = make_post_body(m);
     let want = "to%5B%5D=test%40example.com&from=me%40example.com&subject=Test&\
                 html=&text=It+works&fromname=&replyto=&date=&headers=%7B%7D&x-smtpapi=";
-    assert_eq!(body, want);
+    assert_eq!(body.unwrap(), want);
 }
 
 #[test]
diff --git a/src/v3.rs b/src/v3.rs
index 10b9e3e..c209810 100644
--- a/src/v3.rs
+++ b/src/v3.rs
@@ -1,19 +1,14 @@
-use std::collections::HashMap;
+use errors::SendgridResult;
 
-use hyper::Client;
-use hyper::error::Error;
-use hyper::net::HttpsConnector;
-use hyper::header::{Authorization, Bearer, ContentType, Headers, UserAgent};
-use hyper::mime::{Mime, TopLevel, SubLevel};
+use std::collections::HashMap;
 
-use hyper_native_tls::NativeTlsClient;
+use reqwest::{Client, StatusCode};
+use reqwest::header::{Authorization, Bearer, ContentType, Headers, UserAgent};
 
 use data_encoding::base64;
 
 use serde_json;
 
-pub use hyper::status::StatusCode;
-
 const V3_API_URL: &'static str = "https://api.sendgrid.com/v3/mail/send";
 
 /// Just a redefinition of a map to store string keys and values.
@@ -21,7 +16,7 @@ pub type SGMap = HashMap<String, String>;
 
 /// Used to send a V3 message body.
 pub struct V3Sender {
-    api_key: String
+    api_key: String,
 }
 
 /// The main structure for a V3 API mail send call. This is composed of many other smaller
@@ -33,7 +28,7 @@ pub struct SGMailV3 {
     content: Vec<Content>,
     personalizations: Vec<Personalization>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     attachments: Option<Vec<Attachment>>,
 }
 
@@ -42,14 +37,14 @@ pub struct SGMailV3 {
 pub struct Email {
     email: String,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     name: Option<String>,
 }
 
 /// The body of an email with the content type and the message.
 #[derive(Clone, Serialize)]
 pub struct Content {
-    #[serde(rename="type")]
+    #[serde(rename = "type")]
     content_type: String,
     value: String,
 }
@@ -60,25 +55,25 @@ pub struct Content {
 pub struct Personalization {
     to: Vec<Email>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     cc: Option<Vec<Email>>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     bcc: Option<Vec<Email>>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     subject: Option<String>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     headers: Option<SGMap>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     substitutions: Option<SGMap>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     custom_args: Option<SGMap>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     send_at: Option<u64>,
 }
 
@@ -91,13 +86,13 @@ pub struct Attachment {
 
     filename: String,
 
-    #[serde(rename="type", skip_serializing_if="check_encode")]
+    #[serde(rename = "type", skip_serializing_if = "check_encode")]
     mime_type: Option<String>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     disposition: Option<String>,
 
-    #[serde(skip_serializing_if="check_encode")]
+    #[serde(skip_serializing_if = "check_encode")]
     content_id: Option<String>,
 }
 
@@ -105,7 +100,7 @@ pub struct Attachment {
 fn check_encode<T>(value: &Option<T>) -> bool {
     match *value {
         Some(_) => false,
-        None => true
+        None => true,
     }
 }
 
@@ -116,21 +111,16 @@ impl V3Sender {
     }
 
     /// Send a V3 message and return the status code or an error from the request.
-    pub fn send(&self, mail: &SGMailV3) -> Result<StatusCode, Error> {
-        let ssl = NativeTlsClient::new().unwrap();
-        let connector = HttpsConnector::new(ssl);
-        let client = Client::with_connector(connector);
+    pub fn send(&self, mail: &SGMailV3) -> SendgridResult<StatusCode> {
+        let client = Client::new()?;
         let mut headers = Headers::new();
         headers.set(Authorization(Bearer { token: self.api_key.to_owned() }));
-        headers.set(ContentType(Mime(TopLevel::Application, SubLevel::Json, vec![])));
-        headers.set(UserAgent("sendgrid-rs".to_owned()));
+        headers.set(ContentType::json());
+        headers.set(UserAgent::new("sendgrid-rs"));
 
         let body = mail.gen_json();
-        let res = try!(client.post(V3_API_URL)
-                           .headers(headers)
-                           .body(&body)
-                           .send());
-        Ok(res.status)
+        let res = client.post(V3_API_URL)?.headers(headers).body(body).send()?;
+        Ok(res.status())
     }
 }
 
@@ -174,7 +164,7 @@ impl SGMailV3 {
                 attachments.push(a);
                 self.attachments = Some(attachments);
             }
-            Some(ref mut attachments) => { attachments.push(a) }
+            Some(ref mut attachments) => attachments.push(a),
         };
     }
 
@@ -250,8 +240,10 @@ impl Personalization {
                 let mut ccs = Vec::new();
                 ccs.push(cc);
                 self.cc = Some(ccs);
-            },
-            Some(ref mut c) => { c.push(cc); }
+            }
+            Some(ref mut c) => {
+                c.push(cc);
+            }
         }
     }
 }


Test patch: