diff --git a/api/Cargo.toml b/api/Cargo.toml index 5805bc16b..8978a7fc9 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -7,6 +7,7 @@ workspace = ".." [dependencies] grin_chain = { path = "../chain" } +hyper = "~0.10.6" iron = "~0.5.1" log = "~0.3" router = "~0.5.1" diff --git a/api/src/client.rs b/api/src/client.rs new file mode 100644 index 000000000..a07532110 --- /dev/null +++ b/api/src/client.rs @@ -0,0 +1,65 @@ +// Copyright 2016 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! High level JSON/HTTP client API + +use hyper; +use hyper::client::Response; +use hyper::status::StatusClass; +use serde::{Serialize, Deserialize}; +use serde_json; + +use rest::Error; + +/// Helper function to easily issue a HTTP GET request against a given URL that +/// returns a JSON object. Handles request building, JSON deserialization and +/// response code checking. +pub fn get<'a, T>(url: &'a str) -> Result + where T: Deserialize +{ + let client = hyper::Client::new(); + let res = check_error(client.get(url).send())?; + serde_json::from_reader(res) + .map_err(|e| Error::Internal(format!("Server returned invalid JSON: {}", e))) +} + +/// Helper function to easily issue a HTTP POST request with the provided JSON +/// object as body on a given URL that returns a JSON object. Handles request +/// building, JSON serialization and deserialization, and response code +/// checking. +pub fn post<'a, IN, OUT>(url: &'a str, input: &IN) -> Result + where IN: Serialize, + OUT: Deserialize +{ + let in_json = serde_json::to_string(input) + .map_err(|e| Error::Internal(format!("Could not serialize data to JSON: {}", e)))?; + let client = hyper::Client::new(); + let res = check_error(client.post(url).body(&mut in_json.as_bytes()).send())?; + serde_json::from_reader(res) + .map_err(|e| Error::Internal(format!("Server returned invalid JSON: {}", e))) +} + +// convert hyper error and check for non success response codes +fn check_error(res: hyper::Result) -> Result { + if let Err(e) = res { + return Err(Error::Internal(format!("Error during request: {}", e))); + } + let response = res.unwrap(); + match response.status.class() { + StatusClass::Success => Ok(response), + StatusClass::ServerError => Err(Error::Internal(format!("Server error."))), + StatusClass::ClientError => Err(Error::Argument(format!("Argument error"))), + _ => Err(Error::Internal(format!("Unrecognized error."))), + } +} diff --git a/api/src/endpoints.rs b/api/src/endpoints.rs index dcc3c3154..4c5d02ae9 100644 --- a/api/src/endpoints.rs +++ b/api/src/endpoints.rs @@ -38,15 +38,15 @@ pub struct ChainApi { impl ApiEndpoint for ChainApi { type ID = String; type T = Tip; - type OP_IN = (); - type OP_OUT = (); + type OP_IN = (); + type OP_OUT = (); fn operations(&self) -> Vec { vec![Operation::Get] } fn get(&self, id: String) -> ApiResult { - self.chain_store.head().map_err(|e| ApiError::Internal(e.to_string())) + self.chain_store.head().map_err(|e| Error::Internal(e.to_string())) } } diff --git a/api/src/lib.rs b/api/src/lib.rs index 4668b0f25..5b598415b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -14,6 +14,7 @@ extern crate grin_chain as chain; +extern crate hyper; #[macro_use] extern crate log; extern crate iron; @@ -21,6 +22,7 @@ extern crate router; extern crate serde; extern crate serde_json; +pub mod client; mod endpoints; mod rest; diff --git a/api/src/rest.rs b/api/src/rest.rs index e04d3a838..b274c0048 100644 --- a/api/src/rest.rs +++ b/api/src/rest.rs @@ -18,7 +18,7 @@ //! To use it, just have your service(s) implement the ApiEndpoint trait and //! register them on a ApiServer. -use std::error::Error; +use std::error; use std::fmt::{self, Display, Debug, Formatter}; use std::io::Read; use std::net::ToSocketAddrs; @@ -35,34 +35,34 @@ use serde_json; /// Errors that can be returned by an ApiEndpoint implementation. #[derive(Debug)] -pub enum ApiError { +pub enum Error { Internal(String), Argument(String), } -impl Display for ApiError { +impl Display for Error { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match *self { - ApiError::Argument(ref s) => write!(f, "Bad arguments: {}", s), - ApiError::Internal(ref s) => write!(f, "Internal error: {}", s), + Error::Argument(ref s) => write!(f, "Bad arguments: {}", s), + Error::Internal(ref s) => write!(f, "Internal error: {}", s), } } } -impl Error for ApiError { +impl error::Error for Error { fn description(&self) -> &str { match *self { - ApiError::Argument(_) => "Bad arguments.", - ApiError::Internal(_) => "Internal error.", + Error::Argument(_) => "Bad arguments.", + Error::Internal(_) => "Internal error.", } } } -impl From for IronError { - fn from(e: ApiError) -> IronError { +impl From for IronError { + fn from(e: Error) -> IronError { match e { - ApiError::Argument(_) => IronError::new(e, status::Status::BadRequest), - ApiError::Internal(_) => IronError::new(e, status::Status::InternalServerError), + Error::Argument(_) => IronError::new(e, status::Status::BadRequest), + Error::Internal(_) => IronError::new(e, status::Status::InternalServerError), } } } @@ -89,7 +89,7 @@ impl Operation { } } -pub type ApiResult = ::std::result::Result; +pub type ApiResult = ::std::result::Result; /// Trait to implement to expose a service as a RESTful HTTP endpoint. Each /// method corresponds to a specific relative URL and HTTP method following @@ -147,7 +147,7 @@ struct ApiWrapper(E); impl Handler for ApiWrapper where E: ApiEndpoint, - <::ID as FromStr>::Err: Debug + Send + Error + <::ID as FromStr>::Err: Debug + Send + error::Error { fn handle(&self, req: &mut Request) -> IronResult { match req.method { @@ -200,7 +200,7 @@ impl Handler for OpWrapper fn extract_param(req: &mut Request, param: &'static str) -> IronResult where ID: ToString + FromStr, - ::Err: Debug + Send + Error + 'static + ::Err: Debug + Send + error::Error + 'static { let id = req.extensions.get::().unwrap().find(param).unwrap_or(""); @@ -232,7 +232,7 @@ impl ApiServer { /// endpoint. pub fn register_endpoint(&mut self, subpath: String, endpoint: E) where E: ApiEndpoint, - <::ID as FromStr>::Err: Debug + Send + Error + <::ID as FromStr>::Err: Debug + Send + error::Error { assert_eq!(subpath.chars().nth(0).unwrap(), '/'); @@ -251,7 +251,7 @@ impl ApiServer { }; let full_path = format!("{}", root.clone()); self.router.route(op.to_method(), full_path.clone(), wrapper, route_name); - info!("POST {}", full_path); + info!("POST {}", full_path); } else { // regular REST operations @@ -264,7 +264,7 @@ impl ApiServer { }; let wrapper = ApiWrapper(endpoint.clone()); self.router.route(op.to_method(), full_path.clone(), wrapper, route_name); - info!("{} {}", op.to_method(), full_path); + info!("{} {}", op.to_method(), full_path); } }