From a7499c9c12551e285928709a44b91ceb69543660 Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 20 Aug 2018 18:30:56 -0500 Subject: [PATCH] use failure --- Cargo.toml | 1 + src/client.rs | 171 +++++++++++++++++++------------------- src/database.rs | 213 +++++++++++++++++++++++++----------------------- src/document.rs | 60 +++++++++----- src/error.rs | 38 +-------- src/lib.rs | 20 +++-- 6 files changed, 247 insertions(+), 256 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 769e40b..06a445b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ include = [ ] [dependencies] +failure = "0.1" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" diff --git a/src/client.rs b/src/client.rs index 1e614b5..7c21680 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::time::Duration; -use std::error::Error; +use failure::Error; use serde_json::from_reader; use reqwest::{self, Url, Method, RequestBuilder, StatusCode}; @@ -22,27 +22,29 @@ pub struct Client { } impl Client { - pub fn new(uri: String) -> Client { + pub fn new(uri: String) -> Result { let client = reqwest::Client::builder() .gzip(true) .timeout(Duration::new(4, 0)) - .build().unwrap(); + .build()?; - Client { + Ok(Client { _client: client, uri: uri, _gzip: true, _timeout: 4, dbs: Vec::new(), db_prefix: String::new() - } + }) } - fn create_client(&self) -> reqwest::Client { - reqwest::Client::builder() + fn create_client(&self) -> Result { + let client = reqwest::Client::builder() .gzip(self._gzip) .timeout(Duration::new(self._timeout as u64, 0)) - .build().unwrap() + .build()?; + + Ok(client) } pub fn get_self(&mut self) -> &mut Self { @@ -59,148 +61,141 @@ impl Client { self } - pub fn gzip(&mut self, enabled: bool) -> &Self { + pub fn gzip(&mut self, enabled: bool) -> Result<&Self, Error> { self._gzip = enabled; - self._client = self.create_client(); - self + self._client = self.create_client()?; + + Ok(self) } - pub fn timeout(&mut self, to: u8) -> &Self { + pub fn timeout(&mut self, to: u8) -> Result<&Self, Error> { self._timeout = to; - self._client = self.create_client(); - self + self._client = self.create_client()?; + + Ok(self) } - pub fn list_dbs(&self) -> reqwest::Result> { - self.get(String::from("/_all_dbs"), None) - .send() - .unwrap() - .json::>() + pub fn list_dbs(&self) -> Result, Error> { + let mut response = self.get(String::from("/_all_dbs"), None)?.send()?; + let data = response.json::>()?; + + Ok(data) } fn build_dbname(&self, dbname: &'static str) -> String { self.db_prefix.clone() + dbname } - pub fn db(&self, dbname: &'static str) -> Result { + pub fn db(&self, dbname: &'static str) -> Result { + let name = self.build_dbname(dbname); + + let db = Database::new(name.clone(), self.clone()); + + let path = self.create_path(name, None)?; + + let head_response = self._client.head(&path) + .header(reqwest::header::ContentType::json()) + .send()?; + + match head_response.status() { + StatusCode::Ok => Ok(db), + _ => self.make_db(dbname), + } + } + + pub fn make_db(&self, dbname: &'static str) -> Result { let name = self.build_dbname(dbname); let db = Database::new(name.clone(), self.clone()); - let path = self.create_path(name, None); - let status_req = self._client.head(&path) + let path = self.create_path(name, None)?; + + let put_response = self._client.put(&path) .header(reqwest::header::ContentType::json()) - .send() - .unwrap(); + .send()?; + + let s: CouchResponse = from_reader(put_response)?; - match status_req.status() { - StatusCode::Ok => { - return Ok(db); + match s.ok { + Some(true) => Ok(db), + Some(false) | _ => { + let err = s.error.unwrap_or(s!("unspecified error")); + Err(SofaError(err).into()) }, - _ => { - let response = self._client.put(&path) - .header(reqwest::header::ContentType::json()) - .send() - .unwrap(); - - let s: CouchResponse = from_reader(response).unwrap(); - - if let Some(ok) = s.ok { - if ok { - return Ok(db); - } else { - return Err(SofaError::from(s.error.unwrap())); - } - } - - Err(SofaError::from(s.error.unwrap())) - } } } - pub fn destroy_db(&self, dbname: &'static str) -> bool { - let path = self.create_path(self.build_dbname(dbname), None); + pub fn destroy_db(&self, dbname: &'static str) -> Result { + let path = self.create_path(self.build_dbname(dbname), None)?; let response = self._client.delete(&path) .header(reqwest::header::ContentType::json()) - .send() - .unwrap(); + .send()?; - let s: CouchResponse = from_reader(response).unwrap(); + let s: CouchResponse = from_reader(response)?; - if let Some(ok) = s.ok { - ok - } else { - false - } + Ok(s.ok.unwrap_or(false)) } - pub fn check_status(&self) -> Option> { + pub fn check_status(&self) -> Result { let response = self._client.get(&self.uri) .header(reqwest::header::ContentType::json()) - .send() - .unwrap(); - - match from_reader(response) { - Ok(status) => return Some(Ok(status)), - Err(e) => { - let desc = s!(e.description()); - return Some(Err(SofaError::from(desc))) - } - } + .send()?; + + let status = from_reader(response)?; + + Ok(status) } fn create_path(&self, path: String, args: Option> - ) -> String { - let mut uri = Url::parse(&self.uri); - uri = uri.unwrap().join(&path); - - let mut final_uri = uri.unwrap(); + ) -> Result { + let mut uri = Url::parse(&self.uri)?.join(&path)?; if let Some(ref map) = args { - let mut qp = final_uri.query_pairs_mut(); + let mut qp = uri.query_pairs_mut(); for (k, v) in map { qp.append_pair(k, v); } } - final_uri.into_string() + Ok(uri.into_string()) } pub fn req(&self, method: Method, path: String, opts: Option> - ) -> RequestBuilder { - let uri = self.create_path(path, opts); + ) -> Result { + let uri = self.create_path(path, opts)?; let mut req = self._client.request(method, &uri); req.header(reqwest::header::Referer::new(uri.clone())); req.header(reqwest::header::ContentType::json()); - req + + Ok(req) } - pub fn get(&self, path: String, args: Option>) -> RequestBuilder { - self.req(Method::Get, path, args) + pub fn get(&self, path: String, args: Option>) -> Result { + Ok(self.req(Method::Get, path, args)?) } - pub fn post(&self, path: String, body: String) -> RequestBuilder { - let mut req = self.req(Method::Post, path, None); + pub fn post(&self, path: String, body: String) -> Result { + let mut req = self.req(Method::Post, path, None)?; req.body(body); - req + Ok(req) } - pub fn put(&self, path: String, body: String) -> RequestBuilder { - let mut req = self.req(Method::Put, path, None); + pub fn put(&self, path: String, body: String) -> Result { + let mut req = self.req(Method::Put, path, None)?; req.body(body); - req + Ok(req) } - pub fn head(&self, path: String, args: Option>) -> RequestBuilder { - self.req(Method::Head, path, args) + pub fn head(&self, path: String, args: Option>) -> Result { + Ok(self.req(Method::Head, path, args)?) } - pub fn delete(&self, path: String, args: Option>) -> RequestBuilder { - self.req(Method::Delete, path, args) + pub fn delete(&self, path: String, args: Option>) -> Result { + Ok(self.req(Method::Delete, path, args)?) } } diff --git a/src/database.rs b/src/database.rs index 86f3647..9b41353 100644 --- a/src/database.rs +++ b/src/database.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use reqwest::StatusCode; -use reqwest::Error; +use failure::Error; use serde_json; use serde_json::{Value, from_reader, to_string}; @@ -52,61 +52,60 @@ impl Database { pub fn compact(&self) -> bool { let mut path: String = self.name.clone(); path.push_str("/_compact"); - let response = self._client.post(path, "".into()) - .send() - .unwrap(); - match response.status() { - StatusCode::Accepted => true, - _ => false - } + let request = self._client.post(path, "".into()); + + request.and_then(|mut req| { + Ok(req.send().and_then(|res| { + Ok(res.status() == StatusCode::Accepted) + }).unwrap_or(false)) + }).unwrap_or(false) } /// Starts the compaction of all views pub fn compact_views(&self) -> bool { let mut path: String = self.name.clone(); path.push_str("/_view_cleanup"); - let response = self._client.post(path, "".into()) - .send() - .unwrap(); - match response.status() { - StatusCode::Accepted => true, - _ => false - } + let request = self._client.post(path, "".into()); + + request.and_then(|mut req| { + Ok(req.send().and_then(|res| { + Ok(res.status() == StatusCode::Accepted) + }).unwrap_or(false)) + }).unwrap_or(false) } /// Starts the compaction of a given index pub fn compact_index(&self, index: &'static str) -> bool { - let response = self._client.post(self.create_compact_path(index), "".into()) - .send() - .unwrap(); + let request = self._client.post(self.create_compact_path(index), "".into()); - match response.status() { - StatusCode::Accepted => true, - _ => false - } + request.and_then(|mut req| { + Ok(req.send().and_then(|res| { + Ok(res.status() == StatusCode::Accepted) + }).unwrap_or(false)) + }).unwrap_or(false) } /// Checks if a document ID exists pub fn exists(&self, id: DocumentId) -> bool { - let response = self._client.head(self.create_document_path(id), None) - .send() - .unwrap(); + let request = self._client.head(self.create_document_path(id), None); - match response.status() { - StatusCode::Ok | StatusCode::NotModified => true, - _ => false - } + request.and_then(|mut req| { + Ok(req.send().and_then(|res| { + Ok(match res.status() { + StatusCode::Ok | StatusCode::NotModified => true, + _ => false + }) + }).unwrap_or(false)) + }).unwrap_or(false) } /// Gets one document - pub fn get(&self, id: DocumentId) -> Result { - let response = self._client.get(self.create_document_path(id), None) - .send() - .unwrap(); + pub fn get(&self, id: DocumentId) -> Result { + let response = self._client.get(self.create_document_path(id), None)?.send()?; - Ok(Document::new(from_reader(response).unwrap())) + Ok(Document::new(from_reader(response)?)) } /// Gets documents in bulk with provided IDs list @@ -131,11 +130,11 @@ impl Database { let response = self._client.get( self.create_document_path("_all_docs".into()), Some(options) - ).body(to_string(&body).unwrap()) - .send() - .unwrap(); + )? + .body(to_string(&body)?) + .send()?; - Ok(DocumentCollection::new(from_reader(response).unwrap())) + Ok(DocumentCollection::new(from_reader(response)?)) } /// Gets all the documents in database @@ -157,138 +156,149 @@ impl Database { let response = self._client.get( self.create_document_path("_all_docs".into()), Some(options) - ) - .send() - .unwrap(); + )?.send()?; - Ok(DocumentCollection::new(from_reader(response).unwrap())) + Ok(DocumentCollection::new(from_reader(response)?)) } /// Finds a document in the database through a Mango query. Parameters here http://docs.couchdb.org/en/latest/api/database/find.html - pub fn find(&self, params: Value) -> Result { + pub fn find(&self, params: Value) -> Result { let path = self.create_document_path("_find".into()); - let response = self._client.post(path, js!(¶ms)) - .send() - .unwrap(); + let response = self._client.post(path, js!(¶ms))?.send()?; - let data: FindResult = from_reader(response).unwrap(); + let data: FindResult = from_reader(response)?; if let Some(doc_val) = data.docs { let documents: Vec = doc_val.iter() .filter(|d| { // Remove _design documents let id: String = json_extr!(d["_id"]); - id.chars().nth(0).unwrap() != '_' + !id.starts_with('_') }) .map(|v| Document::new(v.clone())) .collect(); Ok(DocumentCollection::new_from_documents(documents)) } else if let Some(err) = data.error { - Err(SofaError::from(err)) + Err(SofaError(err).into()) } else { Ok(DocumentCollection::default()) } } /// Updates a document - pub fn save(&self, doc: Document) -> Result { + pub fn save(&self, doc: Document) -> Result { let id = doc._id.to_owned(); let raw = doc.get_data(); let response = self._client.put( self.create_document_path(id), - to_string(&raw).unwrap() - ) - .send() - .unwrap(); - - let data: DocumentCreatedResult = from_reader(response).unwrap(); - if !data.ok.is_some() || !data.ok.unwrap() { - return Err(SofaError::from(data.error.unwrap())) - } + to_string(&raw)? + )?.send()?; - let mut val = doc.get_data(); - val["_rev"] = json!(data.rev.unwrap()); + let data: DocumentCreatedResult = from_reader(response)?; - Ok(Document::new(val)) + match data.ok { + Some(true) => { + let mut val = doc.get_data(); + val["_rev"] = json!(data.rev); + + Ok(Document::new(val)) + }, + Some(false) | _ => { + let err = data.error.unwrap_or(s!("unspecified error")); + return Err(SofaError(err).into()); + } + } } /// Creates a document from a raw JSON document Value. - pub fn create(&self, raw_doc: Value) -> Result { + pub fn create(&self, raw_doc: Value) -> Result { let response = self._client.post( self.name.clone(), - to_string(&raw_doc).unwrap() - ) - .send() - .unwrap(); - - let data: DocumentCreatedResult = from_reader(response).unwrap(); - if !data.ok.is_some() || !data.ok.unwrap() { - return Err(SofaError::from(data.error.unwrap())) + to_string(&raw_doc)? + )?.send()?; + + let data: DocumentCreatedResult = from_reader(response)?; + + match data.ok { + Some(true) => { + let data_id = match data.id { + Some(id) => id, + _ => return Err(SofaError(s!("invalid id")).into()), + }; + + let data_rev = match data.rev { + Some(rev) => rev, + _ => return Err(SofaError(s!("invalid rev")).into()), + }; + + let mut val = raw_doc.clone(); + val["_id"] = json!(data_id); + val["_rev"] = json!(data_rev); + + Ok(Document::new(val)) + }, + Some(false) | _ => { + let err = data.error.unwrap_or(s!("unspecified error")); + return Err(SofaError(err).into()); + } } - - let mut val = raw_doc.clone(); - - val["_id"] = json!(data.id.unwrap()); - val["_rev"] = json!(data.rev.unwrap()); - - Ok(Document::new(val)) } /// Removes a document from the database. Returns success in a `bool` pub fn remove(&self, doc: Document) -> bool { - let response = self._client.delete( + let request = self._client.delete( self.create_document_path(doc._id.clone()), Some({ let mut h = HashMap::new(); h.insert(s!("rev"), doc._rev.clone()); h }) - ) - .send() - .unwrap(); + ); - match response.status() { - StatusCode::Ok | StatusCode::Accepted => true, - _ => false - } + request.and_then(|mut req| { + Ok(req.send().and_then(|res| { + Ok(match res.status() { + StatusCode::Ok | StatusCode::Accepted => true, + _ => false + }) + }).unwrap_or(false)) + }).unwrap_or(false) } /// Inserts an index in a naive way, if it already exists, will throw an `Err` - pub fn insert_index(&self, name: String, spec: IndexFields) -> Result { + pub fn insert_index(&self, name: String, spec: IndexFields) -> Result { let response = self._client.post( self.create_document_path("_index".into()), js!(json!({ "name": name, "index": spec })) - ) - .send() - .unwrap(); + )?.send()?; + + let data: IndexCreated = from_reader(response)?; - let data: IndexCreated = from_reader(response).unwrap(); if data.error.is_some() { - Err(SofaError::from(data.error.unwrap())) + let err = data.error.unwrap_or(s!("unspecified error")); + Err(SofaError(err).into()) } else { Ok(data) } } /// Reads the database's indexes and returns them - pub fn read_indexes(&self) -> DatabaseIndexList { + pub fn read_indexes(&self) -> Result { let response = self._client.get( self.create_document_path("_index".into()), None - ) - .send() - .unwrap(); + )?.send()?; - from_reader(response).unwrap() + Ok(from_reader(response)?) } /// Method to ensure an index is created on the database with the following spec. /// Returns `true` when we created a new one, or `false` when the index was already existing. - pub fn ensure_index(&self, name: String, spec: IndexFields) -> Result { - let db_indexes = self.read_indexes(); + pub fn ensure_index(&self, name: String, spec: IndexFields) -> Result { + let db_indexes = self.read_indexes()?; // We look for our index for i in db_indexes.indexes.iter() { @@ -299,10 +309,7 @@ impl Database { } // Let's create it then - let res = self.insert_index(name, spec); - if res.is_err() { - return Err(res.err().unwrap()); - } + let _ = self.insert_index(name, spec)?; // Created and alright Ok(true) diff --git a/src/document.rs b/src/document.rs index 776557b..308bf9d 100644 --- a/src/document.rs +++ b/src/document.rs @@ -31,9 +31,11 @@ impl Document { /// Returns all document's keys pub fn get_keys(&self) -> Vec { let mut ret: Vec = Vec::new(); - let obj = self.doc.as_object().unwrap(); - for (k, _) in obj.iter() { - ret.push(k.clone()); + + if let Some(obj) = self.doc.as_object() { + for (k, _) in obj.iter() { + ret.push(k.clone()); + } } ret @@ -46,10 +48,12 @@ impl Document { /// Merges this document with a raw JSON value, useful to update data with a payload pub fn merge(&mut self, doc: Value) -> &Self { - for (k, v) in doc.as_object().unwrap().iter() { - match k.as_str() { - "_id" | "_rev" => { continue; } - _ => { self[k] = v.clone(); } + if let Some(obj) = doc.as_object() { + for (k, v) in obj.iter() { + match k.as_str() { + "_id" | "_rev" => { continue; } + _ => { self[k] = v.clone(); } + } } } @@ -63,22 +67,36 @@ impl Document { return self; } - let ids = val.as_array().unwrap() + let ids = val.as_array().unwrap_or(&Vec::new()) .iter() - .map(|v| s!(v.as_str().unwrap())) + .map(|v| s!(v.as_str().unwrap_or(""))) .collect(); - let docs = db.get_bulk(ids).unwrap(); - self[field] = docs.get_data().iter() - .filter_map(|d: &Value| { - let did = d["_id"].as_str().unwrap(); - if val[did] != Value::Null { - Some(d.clone()) - } else { - None - } - }) - .collect(); + let data = db.get_bulk(ids).and_then(|docs| { + Ok(docs.get_data()) + }); + + match data { + Ok(data) => { + self[field] = data.iter() + .filter_map(|d: &Value| { + let did = match d["_id"].as_str() { + Some(did) => did, + None => return None, + }; + + if val[did] != Value::Null { + Some(d.clone()) + } else { + None + } + }) + .collect(); + }, + Err(_) => { + return self; + }, + } self } @@ -130,7 +148,7 @@ impl DocumentCollection { let items: Vec = rows.iter() .filter(|d| { // Remove _design documents let id: String = json_extr!(d["doc"]["_id"]); - id.chars().nth(0).unwrap() != '_' + !id.starts_with('_') }) .map(|d| { let document: Value = json_extr!(d["doc"]); diff --git a/src/error.rs b/src/error.rs index 59ecad3..bc57522 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,35 +1,3 @@ -use std::{error, fmt}; - -/// Custom error type -#[derive(Debug)] -pub struct SofaError { - err: String -} - -impl error::Error for SofaError { - fn description(&self) -> &str { - &self.err - } - - fn cause(&self) -> Option<&error::Error> { - None - } -} - -impl fmt::Display for SofaError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.err) - } -} - -impl From for SofaError { - fn from(s: String) -> SofaError { - SofaError { err: s } - } -} - -impl From<&'static str> for SofaError { - fn from(s: &str) -> SofaError { - SofaError { err: s!(s) } - } -} +#[derive(Fail, Debug)] +#[fail(display = "Custom error: {}", _0)] +pub struct SofaError(pub String); diff --git a/src/lib.rs b/src/lib.rs index e05ac9d..b492f5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,8 @@ //! //! [Yellow Innovation's website and works](http://yellowinnovation.fr/en/) +#[macro_use] +extern crate failure; extern crate reqwest; extern crate serde; #[macro_use] @@ -135,21 +137,21 @@ mod sofa_tests { #[test] fn a_should_check_couchdbs_status() { - let client = Client::new("http://localhost:5984".into()); + let client = Client::new("http://localhost:5984".into()).unwrap(); let status = client.check_status(); - assert!(status.is_some()) + assert!(status.is_ok()) } #[test] fn b_should_create_sofa_test_db() { - let client = Client::new("http://localhost:5984".into()); + let client = Client::new("http://localhost:5984".into()).unwrap(); let dbw = client.db("sofa_test"); assert!(dbw.is_ok()); } #[test] fn c_should_create_a_document() { - let client = Client::new("http://localhost:5984".into()); + let client = Client::new("http://localhost:5984".into()).unwrap(); let dbw = client.db("sofa_test"); assert!(dbw.is_ok()); let db = dbw.unwrap(); @@ -166,8 +168,8 @@ mod sofa_tests { #[test] fn d_should_destroy_the_db() { - let client = Client::new("http://localhost:5984".into()); - assert!(client.destroy_db("sofa_test")); + let client = Client::new("http://localhost:5984".into()).unwrap(); + assert!(client.destroy_db("sofa_test").unwrap()); } } @@ -175,7 +177,7 @@ mod sofa_tests { use *; fn setup() -> (Client, Database, Document) { - let client = Client::new("http://localhost:5984".into()); + let client = Client::new("http://localhost:5984".into()).unwrap(); let dbw = client.db("sofa_test"); assert!(dbw.is_ok()); let db = dbw.unwrap(); @@ -193,7 +195,7 @@ mod sofa_tests { } fn teardown(client: Client) { - assert!(client.destroy_db("sofa_test")) + assert!(client.destroy_db("sofa_test").unwrap()) } #[test] @@ -248,7 +250,7 @@ mod sofa_tests { fn e_should_list_indexes_in_db() { let (client, db, _) = setup_create_indexes(); - let index_list = db.read_indexes(); + let index_list = db.read_indexes().unwrap(); assert!(index_list.indexes.len() > 1); let ref findex = index_list.indexes[1];