Framework to define services as RESTful HTTP API services. Serde implementations for some core types.

This commit is contained in:
Ignotus Peverell 2017-03-07 17:00:34 -08:00
parent d900f0b934
commit f64d20749f
No known key found for this signature in database
GPG key ID: 99CD25F39F8F8211
17 changed files with 448 additions and 6 deletions

14
api/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "grin_api"
version = "0.1.0"
authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"]
[dependencies]
grin_chain = { path = "../chain" }
iron = "~0.5.1"
log = "~0.3"
router = "~0.5.1"
serde = "~0.9.10"
serde_derive = "~0.9.10"
serde_json = "~0.9.8"

3
api/rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
hard_tabs = true
wrap_comments = true
write_mode = "Overwrite"

65
api/src/endpoints.rs Normal file
View file

@ -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.
// pub struct HashID(pub [u8; 32]);
//
// impl FromStr for HashId {
// type Err = ;
//
// fn from_str(s: &str) -> Result<HashId, > {
// }
// }
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::thread;
use iron::method::Method;
use chain::{self, Tip};
use rest::*;
/// ApiEndpoint implementation for the blockchain. Exposes the current chain
/// state as a simple JSON object.
#[derive(Clone)]
pub struct ChainApi {
/// data store access
chain_store: Arc<chain::ChainStore>,
}
impl ApiEndpoint for ChainApi {
type ID = String;
type T = Tip;
fn methods(&self) -> Vec<Method> {
vec![Method::Get]
}
fn get(&self, id: String) -> ApiResult<Tip> {
self.chain_store.head().map_err(|e| ApiError::Internal(e.to_string()))
}
}
/// Start all server REST APIs. Just register all of them on a ApiServer
/// instance and runs the corresponding HTTP server.
pub fn start_rest_apis(addr: String, chain_store: Arc<chain::ChainStore>) {
thread::spawn(move || {
let mut apis = ApiServer::new("/v1".to_string());
apis.register_endpoint("/chain".to_string(), ChainApi { chain_store: chain_store });
apis.start(&addr[..]).unwrap_or_else(|e| {
error!("Failed to start API HTTP server: {}.", e);
});
});
}

29
api/src/lib.rs Normal file
View file

@ -0,0 +1,29 @@
// 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.
extern crate grin_chain as chain;
#[macro_use]
extern crate log;
extern crate iron;
extern crate router;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
mod endpoints;
mod rest;
pub use endpoints::start_rest_apis;

275
api/src/rest.rs Normal file
View file

@ -0,0 +1,275 @@
// 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.
//! RESTful API server to easily expose services as RESTful JSON/HTTP endpoints.
//! Fairly constrained on what the service API must look like by design.
//!
//! To use it, just have your service(s) implement the ApiEndpoint trait and
//! register them on a ApiServer.
use std::error::Error;
use std::fmt::{self, Display, Debug, Formatter};
use std::io::Read;
use std::net::ToSocketAddrs;
use std::ops::Index;
use std::string::ToString;
use std::str::FromStr;
use iron::{Iron, Request, Response, IronResult, IronError, status, headers};
use iron::method::Method;
use iron::modifiers::Header;
use iron::middleware::Handler;
use router::Router;
use serde::{Serialize, Deserialize};
use serde_json;
/// Errors that can be returned by an ApiEndpoint implementation.
#[derive(Debug)]
pub enum ApiError {
Internal(String),
Argument(String),
}
impl Display for ApiError {
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),
}
}
}
impl Error for ApiError {
fn description(&self) -> &str {
match *self {
ApiError::Argument(_) => "Bad arguments.",
ApiError::Internal(_) => "Internal error.",
}
}
}
impl From<ApiError> for IronError {
fn from(e: ApiError) -> IronError {
match e {
ApiError::Argument(_) => IronError::new(e, status::Status::BadRequest),
ApiError::Internal(_) => IronError::new(e, status::Status::InternalServerError),
}
}
}
pub type ApiResult<T> = ::std::result::Result<T, ApiError>;
/// Trait to implement to expose a service as a RESTful HTTP endpoint. Each
/// method corresponds to a specific relative URL and HTTP method following
/// basic REST principles:
///
/// * create: POST /
/// * get: GET /:id
/// * update: PUT /:id
/// * delete: DELETE /:id
///
/// The methods method defines which operation the endpoint implements, they're
/// all optional by default. It also allows the framework to automatically
/// define the OPTIONS HTTP method.
///
/// The type accepted by create and update, and returned by get, must implement
/// the serde Serialize and Deserialize traits. The identifier type returned by
/// create and accepted by all other methods must have a string representation.
pub trait ApiEndpoint: Clone + Send + Sync + 'static {
type ID: ToString + FromStr;
type T: Serialize + Deserialize;
fn methods(&self) -> Vec<Method>;
#[allow(unused_variables)]
fn create(&self, o: Self::T) -> ApiResult<Self::ID> {
unimplemented!()
}
#[allow(unused_variables)]
fn delete(&self, id: Self::ID) -> ApiResult<()> {
unimplemented!()
}
#[allow(unused_variables)]
fn update(&self, id: Self::ID, o: Self::T) -> ApiResult<()> {
unimplemented!()
}
#[allow(unused_variables)]
fn get(&self, id: Self::ID) -> ApiResult<Self::T> {
unimplemented!()
}
}
// Wrapper required to define the implementation below, Rust doesn't let us
// define the parametric implementation for trait from another crate.
struct ApiWrapper<E>(E);
impl<E> Handler for ApiWrapper<E>
where E: ApiEndpoint,
<<E as ApiEndpoint>::ID as FromStr>::Err: Debug + Send + Error
{
fn handle(&self, req: &mut Request) -> IronResult<Response> {
match req.method {
Method::Get => {
let res = self.0.get(extract_param(req, "id")?)?;
let res_json = serde_json::to_string(&res)
.map_err(|e| IronError::new(e, status::InternalServerError))?;
Ok(Response::with((status::Ok, res_json)))
}
Method::Put => {
let id = extract_param(req, "id")?;
let t: E::T = serde_json::from_reader(req.body.by_ref())
.map_err(|e| IronError::new(e, status::BadRequest))?;
self.0.update(id, t)?;
Ok(Response::with(status::NoContent))
}
Method::Delete => {
let id = extract_param(req, "id")?;
self.0.delete(id)?;
Ok(Response::with(status::NoContent))
}
Method::Post => {
let t: E::T = serde_json::from_reader(req.body.by_ref())
.map_err(|e| IronError::new(e, status::BadRequest))?;
let id = self.0.create(t)?;
Ok(Response::with((status::Created, id.to_string())))
}
_ => Ok(Response::with(status::MethodNotAllowed)),
}
}
}
fn extract_param<ID>(req: &mut Request, param: &'static str) -> IronResult<ID>
where ID: ToString + FromStr,
<ID as FromStr>::Err: Debug + Send + Error + 'static
{
let id = req.extensions.get::<Router>().unwrap().find(param).unwrap_or("");
id.parse::<ID>().map_err(|e| IronError::new(e, status::BadRequest))
}
/// HTTP server allowing the registration of ApiEndpoint implementations.
pub struct ApiServer {
root: String,
router: Router,
}
impl ApiServer {
/// Creates a new ApiServer that will serve ApiEndpoint implementations
/// under the root URL.
pub fn new(root: String) -> ApiServer {
ApiServer {
root: root,
router: Router::new(),
}
}
/// Starts the ApiServer at the provided address.
pub fn start<A: ToSocketAddrs>(self, addr: A) -> Result<(), String> {
Iron::new(self.router).http(addr).map(|_| ()).map_err(|e| e.to_string())
}
/// Register a new API endpoint, providing a relative URL for the new
/// endpoint.
pub fn register_endpoint<E>(&mut self, subpath: String, endpoint: E)
where E: ApiEndpoint,
<<E as ApiEndpoint>::ID as FromStr>::Err: Debug + Send + Error
{
assert_eq!(subpath.chars().nth(0).unwrap(), '/');
// declare a route for each method actually implemented by the endpoint
let route_postfix = &subpath[1..];
let root = self.root.clone() + &subpath;
for m in endpoint.methods() {
let full_path = match m {
Method::Get => root.clone() + "/:id",
Method::Put => root.clone() + "/:id",
Method::Delete => root.clone() + "/:id",
Method::Post => root.clone(),
_ => panic!(format!("Unsupported method: {}.", m)),
};
self.router.route(m.clone(),
full_path,
ApiWrapper(endpoint.clone()),
m.to_string() + "_" + route_postfix);
}
// support for the HTTP Options method by differentiating what's on the
// root resource vs the id resource
let (root_opts, sub_opts) = endpoint.methods().iter().fold((vec![], vec![]),
|mut acc, m| {
if *m == Method::Post {
acc.0.push(m.clone());
} else {
acc.1.push(m.clone());
}
acc
});
self.router.options(root.clone(),
move |_: &mut Request| {
Ok(Response::with((status::Ok,
Header(headers::Allow(root_opts.clone())))))
},
"option_".to_string() + route_postfix);
self.router.options(root.clone() + "/:id",
move |_: &mut Request| {
Ok(Response::with((status::Ok,
Header(headers::Allow(sub_opts.clone())))))
},
"option_id_".to_string() + route_postfix);
}
}
#[cfg(test)]
mod test {
use super::*;
use rest::*;
#[derive(Serialize, Deserialize)]
pub struct Animal {
name: String,
legs: u32,
lethal: bool,
}
#[derive(Clone)]
pub struct TestApi;
impl ApiEndpoint for TestApi {
type ID = String;
type T = Animal;
fn methods(&self) -> Vec<Method> {
vec![Method::Get]
}
fn get(&self, name: String) -> Result<Animal> {
Ok(Animal {
name: name,
legs: 4,
lethal: false,
})
}
}
#[test]
fn req_chain_json() {
let mut apis = ApiServer::new("/v1".to_string());
apis.register_endpoint("/animal".to_string(), TestApi);
}
}

View file

@ -7,6 +7,8 @@ authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"]
bitflags = "^0.7.0" bitflags = "^0.7.0"
byteorder = "^0.5" byteorder = "^0.5"
log = "^0.3" log = "^0.3"
serde = "~0.9.10"
serde_derive = "~0.9.10"
time = "^0.1" time = "^0.1"
grin_core = { path = "../core" } grin_core = { path = "../core" }

View file

@ -25,6 +25,9 @@ extern crate bitflags;
extern crate byteorder; extern crate byteorder;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate time; extern crate time;
extern crate grin_core as core; extern crate grin_core as core;

View file

@ -84,8 +84,8 @@ impl ChainStore for ChainKVStore {
} }
fn save_block_header(&self, bh: &BlockHeader) -> Result<(), Error> { fn save_block_header(&self, bh: &BlockHeader) -> Result<(), Error> {
self.db.put_ser( self.db.put_ser(&to_key(BLOCK_HEADER_PREFIX, &mut bh.hash().to_vec())[..],
&to_key(BLOCK_HEADER_PREFIX, &mut bh.hash().to_vec())[..], bh) bh)
} }
fn get_header_by_height(&self, height: u64) -> Result<BlockHeader, Error> { fn get_header_by_height(&self, height: u64) -> Result<BlockHeader, Error> {

View file

@ -24,7 +24,7 @@ use core::ser;
/// blockchain tree. References the max height and the latest and previous /// blockchain tree. References the max height and the latest and previous
/// blocks /// blocks
/// for convenience and the total difficulty. /// for convenience and the total difficulty.
#[derive(Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Tip { pub struct Tip {
/// Height of the tip (max height of the fork) /// Height of the tip (max height of the fork)
pub height: u64, pub height: u64,

View file

@ -8,6 +8,8 @@ byteorder = "^0.5"
num-bigint = "^0.1.35" num-bigint = "^0.1.35"
rust-crypto = "^0.2" rust-crypto = "^0.2"
rand = "^0.3" rand = "^0.3"
serde = "~0.9.10"
serde_derive = "~0.9.10"
time = "^0.1" time = "^0.1"
tiny-keccak = "1.1" tiny-keccak = "1.1"

View file

@ -27,7 +27,7 @@ pub const ZERO_HASH: Hash = Hash([0; 32]);
/// A hash to uniquely (or close enough) identify one of the main blockchain /// A hash to uniquely (or close enough) identify one of the main blockchain
/// constructs. Used pervasively for blocks, transactions and ouputs. /// constructs. Used pervasively for blocks, transactions and ouputs.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
pub struct Hash(pub [u8; 32]); pub struct Hash(pub [u8; 32]);
impl fmt::Display for Hash { impl fmt::Display for Hash {

View file

@ -17,9 +17,12 @@
//! the related difficulty, defined as the maximum target divided by the hash. //! the related difficulty, defined as the maximum target divided by the hash.
use byteorder::{ByteOrder, BigEndian}; use byteorder::{ByteOrder, BigEndian};
use std::fmt;
use std::error::Error;
use std::ops::Add; use std::ops::Add;
use bigint::BigUint; use bigint::BigUint;
use serde::{Serialize, Serializer, Deserialize, Deserializer, de};
use core::hash::Hash; use core::hash::Hash;
use ser::{self, Reader, Writer, Writeable, Readable}; use ser::{self, Reader, Writer, Writeable, Readable};
@ -78,3 +81,38 @@ impl Readable<Difficulty> for Difficulty {
Ok(Difficulty { num: BigUint::from_bytes_be(&data[..]) }) Ok(Difficulty { num: BigUint::from_bytes_be(&data[..]) })
} }
} }
impl Serialize for Difficulty {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
serializer.serialize_str(self.num.to_str_radix(10).as_str())
}
}
impl Deserialize for Difficulty {
fn deserialize<D>(deserializer: D) -> Result<Difficulty, D::Error>
where D: Deserializer
{
deserializer.deserialize_i32(DiffVisitor)
}
}
struct DiffVisitor;
impl de::Visitor for DiffVisitor {
type Value = Difficulty;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a difficulty")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where E: de::Error
{
let bigui = BigUint::parse_bytes(s.as_bytes(), 10).ok_or_else(|| {
de::Error::invalid_value(de::Unexpected::Str(s), &"a value number")
})?;
Ok(Difficulty { num: bigui })
}
}

View file

@ -26,6 +26,9 @@ extern crate crypto;
extern crate num_bigint as bigint; extern crate num_bigint as bigint;
extern crate rand; extern crate rand;
extern crate secp256k1zkp as secp; extern crate secp256k1zkp as secp;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate time; extern crate time;
extern crate tiny_keccak; extern crate tiny_keccak;

View file

@ -4,6 +4,7 @@ version = "0.1.0"
authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"] authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"]
[dependencies] [dependencies]
grin_api = { path = "../api" }
grin_chain = { path = "../chain" } grin_chain = { path = "../chain" }
grin_core = { path = "../core" } grin_core = { path = "../core" }
grin_store = { path = "../store" } grin_store = { path = "../store" }

View file

@ -32,6 +32,7 @@ extern crate time;
extern crate tokio_core; extern crate tokio_core;
extern crate tokio_timer; extern crate tokio_timer;
extern crate grin_api as api;
extern crate grin_chain as chain; extern crate grin_chain as chain;
#[macro_use] #[macro_use]
extern crate grin_core as core; extern crate grin_core as core;

View file

@ -24,6 +24,7 @@ use futures::{future, Future};
use tokio_core::reactor; use tokio_core::reactor;
use adapters::{NetToChainAdapter, ChainToNetAdapter}; use adapters::{NetToChainAdapter, ChainToNetAdapter};
use api;
use chain; use chain;
use chain::ChainStore; use chain::ChainStore;
use core; use core;
@ -69,6 +70,9 @@ pub struct ServerConfig {
/// Directory under which the rocksdb stores will be created /// Directory under which the rocksdb stores will be created
pub db_root: String, pub db_root: String,
/// Network address for the Rest API HTTP server.
pub api_http_addr: String,
/// Allows overriding the default cuckoo cycle size /// Allows overriding the default cuckoo cycle size
pub cuckoo_size: u8, pub cuckoo_size: u8,
@ -86,6 +90,7 @@ impl Default for ServerConfig {
fn default() -> ServerConfig { fn default() -> ServerConfig {
ServerConfig { ServerConfig {
db_root: ".grin".to_string(), db_root: ".grin".to_string(),
api_http_addr: "127.0.0.1:13415".to_string(),
cuckoo_size: 0, cuckoo_size: 0,
capabilities: p2p::FULL_NODE, capabilities: p2p::FULL_NODE,
seeding_type: Seeding::None, seeding_type: Seeding::None,
@ -150,6 +155,8 @@ impl Server {
evt_handle.spawn(server.start(evt_handle.clone()).map_err(|_| ())); evt_handle.spawn(server.start(evt_handle.clone()).map_err(|_| ()));
api::start_rest_apis(config.api_http_addr.clone(), chain_store.clone());
warn!("Grin server started."); warn!("Grin server started.");
Ok(Server { Ok(Server {
config: config, config: config,

View file

@ -13,8 +13,7 @@
// limitations under the License. // limitations under the License.
//! Provides a connection wrapper that handles the lower level tasks in sending //! Provides a connection wrapper that handles the lower level tasks in sending
//! or //! or receiving data from the TCP socket, as well as dealing with timeouts.
//! receiving data from the TCP socket, as well as dealing with timeouts.
use std::iter; use std::iter;
use std::ops::Deref; use std::ops::Deref;