├── .gitignore ├── .travis.yml ├── src ├── memory │ ├── mod.rs │ ├── channel.rs │ └── adapter.rs ├── util │ └── mod.rs ├── handler.rs ├── lib.rs ├── channel.rs ├── request.rs ├── error.rs ├── reply.rs ├── adapter.rs └── server.rs ├── examples ├── 1.rs ├── 2.rs ├── 3.rs ├── 5.rs ├── 4.rs ├── 7.rs ├── 6.rs └── hello.rs ├── Cargo.toml ├── license-mit.txt ├── readme.md └── license-apache.txt /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly -------------------------------------------------------------------------------- /src/memory/mod.rs: -------------------------------------------------------------------------------- 1 | mod adapter; 2 | pub use self::adapter::MemoryAdapter; 3 | 4 | mod channel; 5 | pub use self::channel::MemoryChannel; 6 | -------------------------------------------------------------------------------- /examples/1.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | // code will be added here 11 | server.listen("127.0.0.1:3000"); 12 | } -------------------------------------------------------------------------------- /examples/2.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | server.resource("/cats", move |req: Request| { 11 | Error::server_error("we haven't implemented this yet!") 12 | }); 13 | server.listen("127.0.0.1:3000"); 14 | } -------------------------------------------------------------------------------- /examples/3.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | let database = memory::MemoryAdapter::new(); 11 | server.resource("/cats", move |req: Request| { 12 | database.handle(req) 13 | }); 14 | server.listen("127.0.0.1:3000"); 15 | } -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use {Channel, Reply, Method}; 2 | pub fn send_from_reply(reply: Reply, chan: &C) -> Reply { 3 | match reply.method() { 4 | Method::Delete | Method::Post | Method::Patch | Method::Action(_) => { 5 | match reply.data() { 6 | Some(data) => chan.send(&reply.method().as_string(), data), 7 | None => (), 8 | } 9 | }, 10 | _ => (), 11 | } 12 | 13 | reply 14 | } 15 | -------------------------------------------------------------------------------- /examples/5.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | use std::sync::Arc; 11 | let database = Arc::new(memory::MemoryAdapter::new()); 12 | server.resource("/cats", move |req: Request| { 13 | let database1 = database.clone(); 14 | req 15 | .and_then(move |req| { 16 | database1.handle(req) 17 | }) 18 | }); 19 | server.listen("127.0.0.1:3000"); 20 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backtalk" 3 | version = "0.1.0" 4 | authors = ["Robert Lord "] 5 | description = "An asynchronous, streaming web server for JSON APIs" 6 | repository = "https://github.com/lord/backtalk" 7 | documentation = "https://docs.rs/backtalk" 8 | readme = "readme.md" 9 | license = "MIT/Apache-2.0" 10 | keywords = ["async", "web", "server", "framework"] 11 | 12 | [dependencies] 13 | futures = "0.1.11" 14 | tokio-core = "0.1.6" 15 | serde_json = "0.9.8" 16 | queryst-prime = "2.0.0" 17 | hyper = "0.11" 18 | uuid = { version = "0.4", features = ["v4"] } 19 | -------------------------------------------------------------------------------- /examples/4.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | let database = memory::MemoryAdapter::new(); 11 | server.resource("/cats", move |req: Request| { 12 | database.handle(req).and_then(|mut reply| { 13 | { 14 | let mut data = reply.data_mut().unwrap(); 15 | data.insert("example".to_string(), json!("data")); 16 | } 17 | reply 18 | }) 19 | }); 20 | server.listen("127.0.0.1:3000"); 21 | } -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use {Reply, Request, Error}; 2 | use futures::{BoxFuture, Future}; 3 | 4 | /** 5 | Anything that returns a future reply for a request. 6 | 7 | You'll probably implement a bunch of these with your application-specific code. For simplicity, 8 | any closure with the signature `Fn(Request) -> Future` is automatically a Handler. 9 | */ 10 | pub trait Handler: Send + Sync { 11 | fn handle(&self, req: Request) -> BoxFuture; 12 | } 13 | 14 | impl Handler for T 15 | where T: Fn(Request) -> F + Send + Sync, 16 | F: Future + Send + 'static { 17 | fn handle(&self, req: Request) -> BoxFuture { 18 | self(req).boxed() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/memory/channel.rs: -------------------------------------------------------------------------------- 1 | use {Sender, Channel, JsonObject}; 2 | use std::sync::Mutex; 3 | 4 | pub struct MemoryChannel { 5 | senders: Mutex>, 6 | } 7 | 8 | impl MemoryChannel { 9 | pub fn new() -> MemoryChannel { 10 | MemoryChannel { 11 | senders: Mutex::new(Vec::new()), 12 | } 13 | } 14 | } 15 | 16 | impl Channel for MemoryChannel { 17 | fn join(&self, sender: Sender, _: Option, _: JsonObject) { 18 | self.senders.lock().unwrap().push(sender) 19 | } 20 | 21 | fn send(&self, message_kind: &str, msg: &JsonObject) { 22 | for sender in self.senders.lock().unwrap().iter_mut() { 23 | // TODO maybe handle this bug? 24 | let _res = sender.send(message_kind, msg.clone()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/7.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | use std::sync::Arc; 11 | use std::ops::Deref; 12 | let database = Arc::new(memory::MemoryAdapter::new()); 13 | let chan = Arc::new(memory::MemoryChannel::new()); 14 | server.resource("/cats", move |req: Request| { 15 | let database1 = database.clone(); 16 | let chan1 = chan.clone(); 17 | let chan2 = chan.clone(); 18 | req 19 | .and_then(move |req| { 20 | match req.method() { 21 | Method::Listen => chan1.handle(req), 22 | _ => database1.handle(req), 23 | } 24 | }) 25 | .and_then(move |reply| { 26 | util::send_from_reply(reply, chan2.deref()) 27 | }) 28 | }); 29 | server.listen("127.0.0.1:3000"); 30 | } -------------------------------------------------------------------------------- /examples/6.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | use backtalk::*; 3 | extern crate futures; 4 | use futures::future::Future; 5 | #[macro_use] 6 | extern crate serde_json; 7 | 8 | fn main() { 9 | let mut server = Server::new(); 10 | use std::sync::Arc; 11 | let database = Arc::new(memory::MemoryAdapter::new()); 12 | server.resource("/cats", move |req: Request| { 13 | let database1 = database.clone(); 14 | req 15 | .and_then(move |req| { 16 | if req.method() == Method::Delete { 17 | if let &JsonValue::String(ref password) = req.param("password") { 18 | if password != "meow" { 19 | return Error::forbidden("incorrect password"); 20 | } 21 | } else { 22 | return Error::unauthorized("please provide a password"); 23 | } 24 | } 25 | req.boxed() 26 | }) 27 | .and_then(move |req| { 28 | database1.handle(req) 29 | }) 30 | }); 31 | server.listen("127.0.0.1:3000"); 32 | } -------------------------------------------------------------------------------- /license-mit.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Robert Lord 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | extern crate backtalk; 2 | extern crate futures; 3 | 4 | use backtalk::*; 5 | use std::sync::Arc; 6 | 7 | fn main() { 8 | let mut s = Server::new(); 9 | s.resource("/meow", |_req: Request| { 10 | Error::forbidden("not allowed! sorry.") 11 | }); 12 | let adapter = Arc::new(memory::MemoryAdapter::new()); 13 | let channel = Arc::new(memory::MemoryChannel::new()); 14 | s.resource("/hello2", move |req: Request| { 15 | req 16 | .and_then(|req| { 17 | req.into_reply(JsonObject::new()) 18 | }) 19 | }); 20 | s.resource("/hello", move |req: Request| { 21 | let adapter = adapter.clone(); 22 | let channel1 = channel.clone(); 23 | // let channel2 = channel.clone(); 24 | req 25 | .and_then(move |req| { 26 | match req.method().clone() { 27 | Method::Action(_) => Error::forbidden("not allowed! sorry."), 28 | Method::Listen => channel1.handle(req), 29 | _ => adapter.handle(req), 30 | } 31 | }) 32 | // .and_then(move |reply| { util::send_from_reply(reply, channel2.deref()) }) 33 | }); 34 | s.listen("127.0.0.1:3000"); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Backtalk is an experimental mini-library for writing asynchronous, realtime JSON web APIs served 3 | over HTTP. If you're getting started, it's probably easiest to dive into the examples in the 4 | `examples` folder, which should be well-documented with comments. 5 | 6 | Backtalk is still being refined, but (especially once we build a database adapter for a proper 7 | database) it should be useful enough at this point to build real things with it! If you have 8 | feedback, comments, suggestions, pull requests, bug reports, they'd be much appreciated, since I 9 | don't really know what I'm doing tbqh. 10 | */ 11 | 12 | extern crate futures; 13 | extern crate tokio_core; 14 | #[macro_use] 15 | extern crate serde_json; 16 | extern crate hyper; 17 | extern crate queryst_prime as queryst; 18 | extern crate uuid; 19 | 20 | pub use serde_json::Value as JsonValue; 21 | pub type JsonObject = serde_json::value::Map; 22 | 23 | mod request; 24 | pub use request::{Request, Method}; 25 | 26 | mod server; 27 | pub use server::Server; 28 | 29 | mod reply; 30 | pub use reply::Reply; 31 | 32 | mod adapter; 33 | pub use adapter::Adapter; 34 | 35 | mod handler; 36 | pub use handler::{Handler}; 37 | 38 | mod channel; 39 | pub use channel::{Channel, Sender}; 40 | 41 | mod error; 42 | pub use error::{Error, ErrorKind}; 43 | 44 | pub mod memory; 45 | pub mod util; 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | Backtalk: API Web Server 3 |
4 | Build Status 5 | Crate Info 6 | Documentation 7 |

8 | 9 | Backtalk is a web framework for Rust. Much is subject to change and it's not ready for writing production sites, but the structure is there, and I'm glad to answer questions/help out if the documentation isn't enough. 10 | 11 | - **Asynchronous** – use Futures for everything, handle thousands of concurrent connections. 12 | - **Realtime** – expose a streaming API, and push live events to clients. 13 | - **Simple** – only a couple hundred lines of code. 14 | - **Opinionated** – exclusively for JSON-based RESTful APIs. 15 | - **Magicless** – no macros, no unsafe, runs on stable Rust. 16 | 17 | A simple server example: 18 | 19 | ```rust 20 | let mut server = Server::new(); 21 | let database = memory::MemoryAdapter::new(); 22 | server.resource("/cats", move |req: Request| { 23 | database.handle(req) 24 | }); 25 | server.listen("127.0.0.1:3000"); 26 | ``` 27 | 28 | You can look in the `examples` directory for more information, or the [blog post](https://lord.io/blog/2017/backtalk) walking through the examples. 29 | 30 | ## Inspiration 31 | 32 | - Feathers.js 33 | - Phoenix 34 | - Rocket.rs 35 | -------------------------------------------------------------------------------- /src/channel.rs: -------------------------------------------------------------------------------- 1 | use {Request, Reply, Error, JsonObject, Method}; 2 | use reply::make_streamed_reply; 3 | use futures::Future; 4 | use futures; 5 | use futures::future::ok; 6 | use futures::future::BoxFuture; 7 | 8 | type ValueSender = futures::sync::mpsc::UnboundedSender<(String, JsonObject)>; 9 | 10 | /** 11 | Sends JSON objects over a realtime stream to a single connected client 12 | 13 | You almost certainly will only use a `Sender` inside of a `Channel`. 14 | */ 15 | 16 | pub struct Sender { 17 | inner: ValueSender, 18 | } 19 | 20 | // only used internally 21 | pub fn new_sender(chunk_sender: ValueSender) -> Sender { 22 | Sender { 23 | inner: chunk_sender, 24 | } 25 | } 26 | 27 | impl Sender { 28 | /** 29 | Sends the object `val` to the client connected to this `Sender`. 30 | 31 | `event_type` is some sort of event type, like `"post"` or `"delete"`, but it can be whatever 32 | you'd like. The `Result` returned is `Ok(())` if the send was successful, and `Err(())` if the 33 | send was a failure. Note that this function returns instantly; the messages are queued in memory 34 | before being sent out to the client. 35 | */ 36 | pub fn send>(&mut self, event_type: S, val: JsonObject) -> Result<(), ()> { 37 | self.inner.send((event_type.into(), val)).map_err(|_| ()) 38 | } 39 | } 40 | 41 | /** 42 | Converts a Request into a streaming Reply, and routes incoming messages to outgoing streams. 43 | 44 | If you're using a pre-existing `Channel` implementation, you probably just want to use the `handle` 45 | function, which creates the streaming replies. Third-party `Channel`s that implement `join` and 46 | `send` get the `handle` function for free. 47 | */ 48 | pub trait Channel: Send + Sync { 49 | /** 50 | Called when a new streamed reply is created in the `handle` function. The `Sender` is the object 51 | representing the connected client — you can call `sender.send` to send messages to this client. 52 | The `Option` is the ID in the URL — for instance, a subscription to `/cats` would 53 | have an ID of `None`, and a subscription to `/cats/123` would have an ID of `Some("123")`. The 54 | `JsonObject` is the params object of the request, and can be used for authenticating users. 55 | */ 56 | fn join(&self, Sender, Option, JsonObject); 57 | 58 | /** 59 | Called by application code to send a new message to connected clients. Channel implementors are 60 | also free to add additional functions that send messages with additional paramaters, such as 61 | specifying which users 62 | get notified. 63 | */ 64 | fn send(&self, &str, &JsonObject); 65 | 66 | /** 67 | Takes a `Request` and returns a `Reply` future with a streaming `Reply` body. If you're using a 68 | channel in your server's application code, this is the function you'll want to use. 69 | */ 70 | fn handle(&self, req: Request) -> BoxFuture { 71 | if req.method().clone() != Method::Listen { 72 | return Error::server_error("passed a non-listen request to channel") 73 | } 74 | let params = req.params().clone(); 75 | let id = req.id().clone(); 76 | let (sender, reply) = make_streamed_reply(req); 77 | self.join(sender, id, params); 78 | ok(reply).boxed() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/memory/adapter.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{Future, BoxFuture, ok, err}; 2 | use {JsonValue, ErrorKind, Adapter, JsonObject}; 3 | use std::collections::HashMap; 4 | use std::sync::Mutex; 5 | 6 | fn std_error(kind: ErrorKind, err_str: &str) -> (ErrorKind, JsonValue) { 7 | let val = json!({ 8 | "error": { 9 | "type": kind.as_string(), 10 | "message": err_str.to_string(), 11 | } 12 | }); 13 | (kind, val) 14 | } 15 | 16 | pub struct MemoryAdapter { 17 | inside: Mutex, 18 | } 19 | 20 | struct Inside { 21 | datastore: HashMap, 22 | last_num: i64, 23 | } 24 | 25 | impl MemoryAdapter { 26 | pub fn new() -> MemoryAdapter { 27 | MemoryAdapter { 28 | inside: Mutex::new(Inside { 29 | datastore: HashMap::new(), 30 | last_num: 0, 31 | }), 32 | } 33 | } 34 | } 35 | 36 | impl Adapter for MemoryAdapter { 37 | /// currently this function only supports equality matching — we'd probably want to add more 38 | /// kinds of matching and querying in the future, maybe by building into query object 39 | fn list(&self, params: &JsonObject) -> BoxFuture { 40 | let inside = self.inside.lock().unwrap(); 41 | let res: Vec = inside.datastore 42 | .iter() 43 | .map(|(_, item)| item) 44 | .filter(|item| { 45 | for (param_key, param_val) in params { 46 | if item.get(param_key) != Some(param_val) { 47 | return false; 48 | } 49 | } 50 | true 51 | }) 52 | .map(|item| JsonValue::Object(item.clone())) 53 | .collect(); 54 | let mut dat = JsonObject::new(); 55 | dat.insert("data".to_string(), JsonValue::Array(res)); 56 | ok(dat).boxed() 57 | } 58 | 59 | fn get(&self, id: &str, _params: &JsonObject) -> BoxFuture { 60 | let inside = self.inside.lock().unwrap(); 61 | match inside.datastore.get(id) { 62 | Some(val) => ok(val.clone()).boxed(), 63 | None => err(std_error(ErrorKind::NotFound, "couldn't find object with that id")).boxed(), 64 | } 65 | } 66 | 67 | fn post(&self, data: &JsonObject, _params: &JsonObject) -> BoxFuture { 68 | let mut inside = self.inside.lock().unwrap(); 69 | inside.last_num += 1; 70 | let mut data = data.clone(); // TODO remove clones? 71 | let id_str = inside.last_num.to_string(); 72 | data.insert("id".to_string(), JsonValue::String(id_str.clone())); 73 | inside.datastore.insert(id_str, data.clone()); 74 | ok(data).boxed() 75 | } 76 | 77 | fn patch(&self, id: &str, data: &JsonObject, _params: &JsonObject) -> BoxFuture { 78 | let mut inside = self.inside.lock().unwrap(); 79 | if let Some(_) = data.get("id") { 80 | return err(std_error(ErrorKind::BadRequest, "can't update id")).boxed(); 81 | } 82 | let dbdata = match inside.datastore.get_mut(id) { 83 | Some(val) => val, 84 | None => return err(std_error(ErrorKind::NotFound, "couldn't find object with that id")).boxed(), 85 | }; 86 | // TODO should probably recursively update children too instead of replacing, there's a JSON update spec that you can read 87 | for (key, val) in data.clone().into_iter() { 88 | dbdata.insert(key, val); 89 | } 90 | ok(dbdata.clone()).boxed() 91 | } 92 | 93 | fn delete(&self, id: &str, _params: &JsonObject) -> BoxFuture { 94 | let mut inside = self.inside.lock().unwrap(); 95 | inside.datastore.remove(id); 96 | let mut data = JsonObject::new(); 97 | data.insert("id".to_string(), JsonValue::String(id.to_string())); 98 | ok(data).boxed() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use super::{JsonObject, JsonValue, Reply, Error}; 2 | use reply::make_reply; 3 | use futures::future::{IntoFuture, ok, FutureResult, AndThen, Future, BoxFuture}; 4 | 5 | /** 6 | A type of request, for instance "List" or "Post". 7 | 8 | These mostly correspond to the HTTP methods, with the addition of `List`, `Listen`, and `Action`. 9 | 10 | - `List` is a `GET` request with an ID on a resource, such as `GET /cats`. 11 | - `Listen` is a `GET` 12 | request with a `Accept: text/event-stream` header. `Listen` requests may or may not have IDs, so 13 | both `GET /cats` and `GET /cats/123` with the `event-stream` header would be a `Listen` request. 14 | - `Action` is a custom action on a specific resource ID. For instance, `POST /cats/123/feed` would 15 | be `Action("feed")`. 16 | 17 | Note that we don't support `PUT` requests currently, for simplicity. 18 | */ 19 | #[derive(Debug, Clone, PartialEq, Eq)] 20 | pub enum Method { 21 | /// `GET /resource`, indempotent 22 | List, 23 | /// `GET /resource/123`, indempotent 24 | Get, 25 | /// `DELETE /resource/123`, indempotent 26 | Delete, 27 | /// `POST /resource` 28 | Post, 29 | /// `PATCH /resource/123` 30 | Patch, 31 | /// Either `GET /resource/` or `GET /resource/123`, with the `Accept: text/event-stream` header 32 | Listen, 33 | /// `POST /resource/123/actionname` 34 | Action(String), 35 | } 36 | 37 | impl Method { 38 | pub fn as_string(&self) -> String { 39 | match self { 40 | &Method::List => "list", 41 | &Method::Get => "get", 42 | &Method::Delete => "delete", 43 | &Method::Post => "post", 44 | &Method::Patch => "patch", 45 | &Method::Listen => "listen", 46 | &Method::Action(ref action) => action, 47 | }.to_string() 48 | } 49 | } 50 | 51 | /** 52 | A request containing data from the client. 53 | */ 54 | #[derive(Debug)] 55 | pub struct Request { 56 | id: Option, 57 | params: JsonObject, 58 | data: JsonObject, 59 | resource: String, 60 | method: Method, 61 | null: JsonValue, 62 | } 63 | 64 | impl Request { 65 | pub fn new(resource: String, method: Method, id: Option, data: JsonObject, params: JsonObject) -> Request { 66 | Request { 67 | resource: resource, 68 | method: method, 69 | id: id, 70 | data: data, 71 | params: params, 72 | null: JsonValue::Null, 73 | } 74 | } 75 | 76 | pub fn into_reply(self, reply: JsonObject) -> Reply { 77 | make_reply(self, reply) 78 | } 79 | 80 | // TODO data_then accepts a function that returns a future 81 | 82 | pub fn method(&self) -> Method { 83 | self.method.clone() 84 | } 85 | 86 | pub fn resource(&self) -> &str { 87 | &self.resource 88 | } 89 | 90 | pub fn id(&self) -> &Option { 91 | &self.id 92 | } 93 | 94 | pub fn params(&self) -> &JsonObject { 95 | &self.params 96 | } 97 | 98 | pub fn params_mut(&mut self) -> &mut JsonObject { 99 | &mut self.params 100 | } 101 | 102 | pub fn param(&self, key: &str) -> &JsonValue { 103 | self.params.get(key).unwrap_or(&self.null) 104 | } 105 | 106 | pub fn set_param(&mut self, key: String, val: JsonValue) { 107 | self.params.insert(key, val); 108 | } 109 | 110 | pub fn data(&self) -> &JsonObject { 111 | &self.data 112 | } 113 | 114 | pub fn data_mut(&mut self) -> &mut JsonObject { 115 | &mut self.data 116 | } 117 | 118 | pub fn boxed(self) -> BoxFuture { 119 | ok(self).boxed() 120 | } 121 | 122 | pub fn and_then(self, f: F) -> AndThen, B, F> 123 | where F: FnOnce(Request) -> B, 124 | B: IntoFuture 125 | { 126 | ok::(self).and_then(f) 127 | } 128 | } 129 | 130 | impl IntoFuture for Request { 131 | type Item = Request; 132 | type Error = Error; 133 | type Future = FutureResult; 134 | fn into_future(self) -> Self::Future { 135 | ok(self) 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use {JsonValue}; 2 | use reply::Body; 3 | use hyper::server as http; 4 | use hyper::header::{ContentLength,ContentType}; 5 | use hyper::mime; 6 | use hyper::StatusCode; 7 | use futures::future::{err, BoxFuture, Future}; 8 | 9 | /** 10 | An error response to be sent back to the client. 11 | 12 | Contains a JSON error that the client can reply to. The easiest way to create one of these 13 | is the various `Error::bad_request`, `Error::unavailable` etc. functions, which automatically 14 | return a `BoxFuture`, so you can return them directly from an `and_then` closure without 15 | wrapping in a future or boxing. 16 | 17 | If you need custom JSON in your error, you can use the `Error::new` function directly. 18 | 19 | Currently there's no way to access or change the insides of an `Error`, but that probably will 20 | change in the near future. 21 | */ 22 | #[derive(Debug)] 23 | pub struct Error { 24 | data: JsonValue, 25 | kind: ErrorKind, 26 | } 27 | 28 | /** 29 | A type of error, for instance "Bad Request" or "Server Error". 30 | */ 31 | #[derive(Debug)] 32 | pub enum ErrorKind { 33 | /// The route requires authorization, and it was not provided or was invalid. 34 | Unauthorized, 35 | /// The authorization was valid, but the authorized user has insufficient permissions for this route. 36 | Forbidden, 37 | /// The client has been sending requests too quickly, and needs to slow down. 38 | RateLimited, 39 | /// The URL wasn't found. 40 | NotFound, 41 | /// The request was invalid or bad for some reason. 42 | BadRequest, 43 | /// The request was invalid or bad for some reason. 44 | ServerError, 45 | /// The server is temporarily overloaded or down for maintenance. 46 | Unavailable, 47 | /// This HTTP method isn't allowed at this URL, and another method would be valid. 48 | MethodNotAllowed, 49 | } 50 | 51 | impl ErrorKind { 52 | fn to_hyper_status(&self) -> StatusCode { 53 | match self { 54 | &ErrorKind::Unauthorized => StatusCode::Unauthorized, 55 | &ErrorKind::Forbidden => StatusCode::Forbidden, 56 | &ErrorKind::RateLimited => StatusCode::TooManyRequests, 57 | &ErrorKind::NotFound => StatusCode::NotFound, 58 | &ErrorKind::BadRequest => StatusCode::BadRequest, 59 | &ErrorKind::ServerError => StatusCode::InternalServerError, 60 | &ErrorKind::Unavailable => StatusCode::ServiceUnavailable, 61 | &ErrorKind::MethodNotAllowed => StatusCode::MethodNotAllowed, 62 | } 63 | } 64 | 65 | /** 66 | Returns the string version of the error. For instance, `BadRequest` returns `"bad_request"`. 67 | */ 68 | pub fn as_string(&self) -> String { 69 | match self { 70 | &ErrorKind::Unauthorized => "authorization", 71 | &ErrorKind::Forbidden => "authorization", 72 | &ErrorKind::RateLimited => "rate_limit", 73 | &ErrorKind::NotFound => "not_found", 74 | &ErrorKind::BadRequest => "bad_request", 75 | &ErrorKind::ServerError => "server", 76 | &ErrorKind::Unavailable => "server", 77 | &ErrorKind::MethodNotAllowed => "bad_request", 78 | }.to_string() 79 | } 80 | } 81 | 82 | fn std_error(kind: ErrorKind, err_str: &str) -> Error { 83 | let val = json!({ 84 | "error": { 85 | "type": kind.as_string(), 86 | "message": err_str.to_string(), 87 | } 88 | }); 89 | Error::new( 90 | kind, 91 | val 92 | ) 93 | } 94 | 95 | impl Error { 96 | /** 97 | 98 | */ 99 | pub fn new(kind: ErrorKind, data: JsonValue) -> Error { 100 | Error { 101 | kind: kind, 102 | data: data, 103 | } 104 | } 105 | 106 | pub fn unauthorized(msg: &str) -> BoxFuture { 107 | err(std_error(ErrorKind::Unauthorized, msg)).boxed() 108 | } 109 | pub fn forbidden(msg: &str) -> BoxFuture { 110 | err(std_error(ErrorKind::Forbidden, msg)).boxed() 111 | } 112 | pub fn rate_limited(msg: &str) -> BoxFuture { 113 | err(std_error(ErrorKind::RateLimited, msg)).boxed() 114 | } 115 | pub fn not_found(msg: &str) -> BoxFuture { 116 | err(std_error(ErrorKind::NotFound, msg)).boxed() 117 | } 118 | pub fn bad_request(msg: &str) -> BoxFuture { 119 | err(std_error(ErrorKind::BadRequest, msg)).boxed() 120 | } 121 | pub fn server_error(msg: &str) -> BoxFuture { 122 | err(std_error(ErrorKind::ServerError, msg)).boxed() 123 | } 124 | pub fn unavailable(msg: &str) -> BoxFuture { 125 | err(std_error(ErrorKind::Unavailable, msg)).boxed() 126 | } 127 | pub fn method_not_allowed(msg: &str) -> BoxFuture { 128 | err(std_error(ErrorKind::MethodNotAllowed, msg)).boxed() 129 | } 130 | 131 | pub fn to_http(self) -> http::Response { 132 | let resp = http::Response::new(); 133 | let resp_str = self.data.to_string(); 134 | resp 135 | .with_status(self.kind.to_hyper_status()) 136 | .with_header(ContentLength(resp_str.len() as u64)) 137 | .with_header(ContentType(mime::APPLICATION_JSON)) 138 | .with_body(Body::Once(Some(resp_str.into()))) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/reply.rs: -------------------------------------------------------------------------------- 1 | use {JsonValue, Request, Method, JsonObject, Error, channel}; 2 | use std::fmt; 3 | use hyper::server as http; 4 | use hyper::Error as HyperError; 5 | use hyper::header::{ContentLength, ContentType}; 6 | use hyper::mime; 7 | use hyper::Chunk as HyperChunk; 8 | use futures::{Poll, Stream, Async, IntoFuture}; 9 | use futures::future::{ok, FutureResult, BoxFuture, Future}; 10 | use futures::stream::BoxStream; 11 | use futures::sync::mpsc; 12 | use Sender; 13 | 14 | type ChunkReceiver = BoxStream; 15 | 16 | /** 17 | A successful response with JSON data to be sent back to the client. 18 | 19 | There are two kinds of replies. Static replies represent JSON data that is ready. Most requests 20 | return static replies. Streaming replies represent a stream of JSON data that will stream from 21 | a `Channel` directly to the client. You can't access the data of a streaming reply through the 22 | `Reply` struct, since it's not ready yet. If you want to transform or edit the reply data for a 23 | stream, you'll need to implement a custom `Channel` instead. 24 | 25 | These are several ways to create a Reply: 26 | 27 | - pass a Request to an Adapter to get a static response from a database 28 | - pass a Request to a Channel to get a streaming response 29 | - in your custom Resource, call `request.into_reply(data)` to create a Reply object. 30 | 31 | Reply implements `IntoFuture`, so you can return it directly from a `and_then` block. 32 | */ 33 | #[derive(Debug)] 34 | pub struct Reply { 35 | data: ReplyData, 36 | req: Request, 37 | } 38 | 39 | enum ReplyData { 40 | Value(JsonObject), 41 | Stream(ChunkReceiver), 42 | } 43 | 44 | impl fmt::Debug for ReplyData { 45 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 46 | match self { 47 | &ReplyData::Value(ref val) => write!(f, "ReplyData::Value({:?})", val), 48 | &ReplyData::Stream(_) => write!(f, "ReplyData::Stream()"), 49 | } 50 | } 51 | } 52 | 53 | // only used internally 54 | pub fn make_reply(req: Request, data: JsonObject) -> Reply { 55 | Reply { 56 | req: req, 57 | data: ReplyData::Value(data), 58 | } 59 | } 60 | 61 | // only used internally 62 | pub fn make_streamed_reply(req: Request) -> (Sender, Reply) { 63 | let (tx, rx) = mpsc::unbounded(); 64 | let rx = rx 65 | .map(|val: (String, JsonObject)| -> HyperChunk { 66 | format!("event:{}\ndata:{}\n\n", val.0, JsonValue::Object(val.1)).into() 67 | }) 68 | .boxed(); 69 | let reply = Reply { 70 | req: req, 71 | data: ReplyData::Stream(rx) 72 | }; 73 | let sender = channel::new_sender(tx); 74 | (sender, reply) 75 | } 76 | 77 | impl Reply { 78 | pub fn data(&self) -> Option<&JsonObject> { 79 | match self.data { 80 | ReplyData::Value(ref dat) => Some(dat), 81 | _ => None, 82 | } 83 | } 84 | 85 | pub fn data_mut(&mut self) -> Option<&mut JsonObject> { 86 | match self.data { 87 | ReplyData::Value(ref mut dat) => Some(dat), 88 | _ => None, 89 | } 90 | } 91 | 92 | // TODO data_then accepts a function that returns a future 93 | 94 | pub fn to_http(self) -> http::Response { 95 | let resp = http::Response::new(); 96 | 97 | match self.data { 98 | ReplyData::Value(val) => { 99 | let resp_str = JsonValue::Object(val).to_string(); 100 | resp 101 | .with_header(ContentLength(resp_str.len() as u64)) 102 | .with_header(ContentType(mime::APPLICATION_JSON)) 103 | .with_body(Body::Once(Some(resp_str.into()))) 104 | }, 105 | ReplyData::Stream(stream) => { 106 | resp 107 | .with_header(ContentType(mime::TEXT_EVENT_STREAM)) 108 | .with_body(Body::Stream(stream)) 109 | }, 110 | } 111 | } 112 | 113 | pub fn method(&self) -> Method { 114 | self.req.method() 115 | } 116 | 117 | pub fn resource(&self) -> &str { 118 | &self.req.resource() 119 | } 120 | 121 | pub fn id(&self) -> &Option { 122 | &self.req.id() 123 | } 124 | 125 | pub fn params(&self) -> &JsonObject { 126 | &self.req.params() 127 | } 128 | 129 | pub fn param(&self, key: &str) -> &JsonValue { 130 | self.req.param(key) 131 | } 132 | 133 | pub fn boxed(self) -> BoxFuture { 134 | ok(self).boxed() 135 | } 136 | 137 | pub fn request_data(&self) -> &JsonObject { 138 | self.req.data() 139 | } 140 | } 141 | 142 | impl IntoFuture for Reply { 143 | type Item = Reply; 144 | type Error = Error; 145 | type Future = FutureResult; 146 | fn into_future(self) -> Self::Future { 147 | ok(self) 148 | } 149 | } 150 | 151 | /// A `Stream` for `HyperChunk`s used in requests and responses. 152 | pub enum Body { 153 | Once(Option), 154 | Stream(ChunkReceiver), 155 | } 156 | 157 | impl Stream for Body { 158 | type Item = HyperChunk; 159 | type Error = HyperError; 160 | 161 | fn poll(&mut self) -> Poll, HyperError> { 162 | match self { 163 | &mut Body::Once(ref mut opt) => Ok(Async::Ready(opt.take())), 164 | &mut Body::Stream(ref mut stream) => { 165 | match stream.poll() { 166 | Ok(u) => Ok(u), 167 | Err(()) => Ok(Async::Ready(None)), // this probably can never happen? 168 | } 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/adapter.rs: -------------------------------------------------------------------------------- 1 | use {JsonObject, Request, Reply, Method, ErrorKind, Error}; 2 | use futures::{BoxFuture, Future}; 3 | use serde_json::Value as JsonValue; 4 | 5 | /** 6 | Converts a Request to a static Reply from a database. 7 | 8 | An `Adapter` talks to a database. If you're using an `Adapter`, and not implementing your own, you 9 | probably just want to use the `handle` function. By implementing the five functions `list`, `get`, 10 | `post`, `patch` and `delete`, an `Adapter` gets the `handle` function for free. 11 | 12 | You most likely won't want to implement your own Adapter, since these are generic and don't contain 13 | project-specific code. Backtalk implements `memory::MemoryAdapter` for development, but you will 14 | hopefully eventually be able to find third-party adapters for various databases in other crates. 15 | */ 16 | 17 | pub trait Adapter: Send + Sync { 18 | fn list(&self, params: &JsonObject) -> BoxFuture; 19 | fn get(&self, id: &str, params: &JsonObject) -> BoxFuture; 20 | fn post(&self, data: &JsonObject, params: &JsonObject) -> BoxFuture; 21 | fn patch(&self, id: &str, data: &JsonObject, params: &JsonObject) -> BoxFuture; 22 | fn delete(&self, id: &str, params: &JsonObject) -> BoxFuture; 23 | 24 | /** 25 | Takes a `Request`, passes it to the appropriate function, and turns the response into a proper 26 | `Reply` future. If you're using an `Adapter` in your webapp, this is the function you want to 27 | call. 28 | */ 29 | fn handle(&self, req: Request) -> BoxFuture { 30 | let res = match (req.method().clone(), req.id().clone()) { 31 | (Method::List, _) => self.list(req.params()), 32 | (Method::Post, _) => self.post(req.data(), req.params()), 33 | (Method::Get, Some(ref id)) => self.get(id, req.params()), 34 | (Method::Delete, Some(ref id)) => self.delete(id, req.params()), 35 | (Method::Patch, Some(ref id)) => self.patch(id, req.data(), req.params()), 36 | (_, None) => return Error::bad_request("missing id in request"), 37 | (Method::Listen, _) => return Error::server_error("passed listen request to database adapter"), 38 | (Method::Action(_), _) => return Error::server_error("passed action request to database adapter"), 39 | }; 40 | res.then(move |res| match res { 41 | Ok(val) => Ok(req.into_reply(val)), 42 | Err((kind, val)) => Err(Error::new(kind, val)), 43 | }).boxed() 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | use futures::future::{ok, err}; 51 | struct TestAdapter; 52 | 53 | impl Adapter for TestAdapter { 54 | fn list(&self, _params: &JsonObject) -> BoxFuture { 55 | let mut obj = JsonObject::new(); 56 | obj.insert("method".to_string(), JsonValue::String("find".to_string())); 57 | ok(obj).boxed() 58 | } 59 | fn get(&self, _id: &str, _params: &JsonObject) -> BoxFuture { 60 | let mut obj = JsonObject::new(); 61 | obj.insert("method".to_string(), JsonValue::String("get".to_string())); 62 | ok(obj).boxed() 63 | } 64 | fn post(&self, _data: &JsonObject, _params: &JsonObject) -> BoxFuture { 65 | err((ErrorKind::ServerError, json!({"error": "testerror"}))).boxed() 66 | } 67 | fn patch(&self, _id: &str, _data: &JsonObject, _params: &JsonObject) -> BoxFuture { 68 | let mut obj = JsonObject::new(); 69 | obj.insert("method".to_string(), JsonValue::String("patch".to_string())); 70 | ok(obj).boxed() 71 | } 72 | fn delete(&self, _id: &str, _params: &JsonObject) -> BoxFuture { 73 | let mut obj = JsonObject::new(); 74 | obj.insert("method".to_string(), JsonValue::String("delete".to_string())); 75 | ok(obj).boxed() 76 | } 77 | } 78 | 79 | fn make_req(m: Method, id: Option<&str>) -> Request { 80 | Request::new("resource".to_string(), m, id.map(|s| s.to_string()), JsonObject::new(), JsonObject::new()) 81 | } 82 | 83 | #[test] 84 | fn adapter_can_list() { 85 | let adapter = TestAdapter{}; 86 | let res = adapter.handle(make_req(Method::List, None)).wait().unwrap(); 87 | assert!(res.data().unwrap().get("method").unwrap() == "find"); 88 | } 89 | 90 | #[test] 91 | fn adapter_can_get() { 92 | let adapter = TestAdapter{}; 93 | let res = adapter.handle(make_req(Method::Get, Some("12"))).wait().unwrap(); 94 | assert!(res.data().unwrap().get("method").unwrap() == "get"); 95 | } 96 | 97 | #[test] 98 | fn adapter_can_patch() { 99 | let adapter = TestAdapter{}; 100 | let res = adapter.handle(make_req(Method::Patch, Some("12"))).wait().unwrap(); 101 | assert!(res.data().unwrap().get("method").unwrap() == "patch"); 102 | } 103 | 104 | #[test] 105 | fn adapter_can_delete() { 106 | let adapter = TestAdapter{}; 107 | let res = adapter.handle(make_req(Method::Delete, Some("12"))).wait().unwrap(); 108 | assert!(res.data().unwrap().get("method").unwrap() == "delete"); 109 | } 110 | 111 | #[test] 112 | fn adapter_rejects_without_id() { 113 | let adapter = TestAdapter{}; 114 | for method in vec![Method::Patch, Method::Delete, Method::Get] { 115 | let _res = adapter.handle(make_req(method, None)).wait().unwrap_err(); 116 | } 117 | } 118 | 119 | #[test] 120 | fn adapter_can_show_errors() { 121 | let adapter = TestAdapter{}; 122 | let _res = adapter.handle(make_req(Method::Post, None)).wait().unwrap_err(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use {JsonValue, JsonObject, Reply, Request, Handler, Method, Error, ErrorKind}; 2 | use futures::future::{ok, err}; 3 | use futures::{BoxFuture, Future}; 4 | use std::collections::HashMap; 5 | use hyper; 6 | use hyper::mime; 7 | use hyper::header::{Accept,q}; 8 | use hyper::server as http; 9 | use hyper::Method as HttpMethod; 10 | use futures::Stream; 11 | use futures::future::FutureResult; 12 | use std::sync::Arc; 13 | use queryst::parse as query_parse; 14 | use serde_json::value::Map; 15 | use reply::Body; 16 | use serde_json; 17 | 18 | fn std_error(kind: ErrorKind, err_str: &str) -> Error { 19 | let val = json!({ 20 | "error": { 21 | "type": kind.as_string(), 22 | "message": err_str.to_string(), 23 | } 24 | }); 25 | Error::new( 26 | kind, 27 | val 28 | ) 29 | } 30 | 31 | pub fn http_to_req(method: &HttpMethod, path: &str, query: &str, headers: &hyper::Headers, body: Option>, server: &Arc) -> Result { 32 | let default_accept = Accept::star(); 33 | let accepts = headers.get::().unwrap_or(&default_accept).as_slice().iter(); 34 | let (_, is_eventsource) = accepts.fold((q(0), false), |prev, quality_item| { 35 | let (mut best_qual, mut is_eventsource) = prev; 36 | let this_quality = quality_item.quality; 37 | if this_quality > best_qual { 38 | best_qual = this_quality; 39 | let (ref top_level, ref sub_level) = (quality_item.item.type_(), quality_item.item.subtype()); 40 | is_eventsource = top_level == &mime::TEXT && sub_level == &mime::EVENT_STREAM; 41 | } 42 | (best_qual, is_eventsource) 43 | }); 44 | 45 | let body = if let Some(b) = body { 46 | b 47 | } else { 48 | return Err(std_error(ErrorKind::BadRequest, "TODO error in request body")); 49 | }; 50 | let body_str = match String::from_utf8(body) { 51 | Ok(s) => s, 52 | _ => return Err(std_error(ErrorKind::BadRequest, "TODO invalid unicode in request body")), 53 | }; 54 | let body_obj = if body_str == "" { 55 | JsonObject::new() 56 | } else { 57 | match serde_json::from_str(&body_str) { 58 | Ok(o) => o, 59 | _ => return Err(std_error(ErrorKind::BadRequest, "TODO invalid json in request body")), 60 | } 61 | }; 62 | let query = match query_parse(query) { 63 | Ok(JsonValue::Null) => Map::new(), 64 | Ok(JsonValue::Object(u)) => u, 65 | _ => return Err(std_error(ErrorKind::BadRequest, "TODO failed to parse query string")) 66 | }; 67 | let mut parts: Vec<&str> = path.split("/").skip(1).collect(); 68 | // remove trailing `/` part if present 69 | if let Some(&"") = parts.last() { 70 | parts.pop(); 71 | } 72 | 73 | let resource_url = format!("/{}", parts.join("/")); 74 | if server.has_resource(&resource_url) { 75 | if is_eventsource { // TODO should only work for GET? 403 otherwise? better spec compliance 76 | return Ok(Request::new( 77 | resource_url, 78 | Method::Listen, 79 | None, 80 | JsonObject::new(), 81 | query 82 | )) 83 | } else if method == &HttpMethod::Get { 84 | return Ok(Request::new( 85 | resource_url, 86 | Method::List, 87 | None, 88 | JsonObject::new(), 89 | query 90 | )) 91 | } else if method == &HttpMethod::Post { 92 | return Ok(Request::new( 93 | resource_url, 94 | Method::Post, 95 | None, 96 | body_obj, 97 | query 98 | )) 99 | } else { 100 | return Err(std_error(ErrorKind::MethodNotAllowed, "invalid HTTP method for this URL")); 101 | } 102 | } 103 | 104 | let (id, parts) = match parts.split_last() { 105 | Some(t) => t, 106 | None => return Err(std_error(ErrorKind::NotFound, "handler not found")) 107 | }; 108 | let resource_url = format!("/{}", parts.join("/")); 109 | if server.has_resource(&resource_url) { 110 | if is_eventsource { 111 | return Ok(Request::new( 112 | resource_url, 113 | Method::Listen, 114 | Some(id.to_string()), 115 | JsonObject::new(), 116 | query 117 | )) 118 | } else if method == &HttpMethod::Get { 119 | return Ok(Request::new( 120 | resource_url, 121 | Method::Get, 122 | Some(id.to_string()), 123 | JsonObject::new(), 124 | query 125 | )) 126 | } else if method == &HttpMethod::Patch { 127 | return Ok(Request::new( 128 | resource_url, 129 | Method::Patch, 130 | Some(id.to_string()), 131 | body_obj, 132 | query 133 | )) 134 | } else if method == &HttpMethod::Delete { 135 | return Ok(Request::new( 136 | resource_url, 137 | Method::Delete, 138 | Some(id.to_string()), 139 | JsonObject::new(), 140 | query 141 | )) 142 | } else { 143 | return Err(std_error(ErrorKind::MethodNotAllowed, "invalid HTTP method for this URL")); 144 | } 145 | } 146 | 147 | let action_name = id; 148 | let (id, parts) = match parts.split_last() { 149 | Some(t) => t, 150 | None => return Err(std_error(ErrorKind::NotFound, "handler not found")) 151 | }; 152 | let resource_url = format!("/{}", parts.join("/")); 153 | if server.has_resource(&resource_url) { 154 | if method == &HttpMethod::Post { 155 | return Ok(Request::new( 156 | resource_url, 157 | Method::Action(action_name.to_string()), 158 | Some(id.to_string()), 159 | JsonObject::new(), 160 | query 161 | )) 162 | } else { 163 | return Err(std_error(ErrorKind::MethodNotAllowed, "invalid HTTP method for this URL")); 164 | } 165 | } 166 | 167 | Err(std_error(ErrorKind::NotFound, "handler not found")) 168 | } 169 | 170 | // only one is created 171 | #[derive(Clone)] 172 | struct HttpService { 173 | server: Arc, 174 | } 175 | 176 | impl http::Service for HttpService { 177 | type Request = http::Request; 178 | type Response = http::Response; 179 | type Error = hyper::Error; 180 | type Future = BoxFuture; 181 | 182 | fn call(&self, http_req: http::Request) -> Self::Future { 183 | let (method, uri, _, headers, body) = http_req.deconstruct(); 184 | 185 | let server = self.server.clone(); 186 | let body_prom = body.fold(Vec::new(), |mut a, b| -> FutureResult, hyper::Error> { a.extend_from_slice(&b[..]); ok(a) }); 187 | 188 | body_prom.then(move |body_res| { 189 | match http_to_req(&method, uri.path(), uri.query().unwrap_or(""), &headers, body_res.ok(), &server) { 190 | Ok(req) => server.handle(req), 191 | Err(reply) => err(reply).boxed(), 192 | } 193 | }).then(|reply| { 194 | let http_resp = match reply { 195 | Ok(r) => r.to_http(), 196 | Err(r) => r.to_http(), 197 | }; 198 | ok(http_resp) 199 | }).boxed() 200 | } 201 | } 202 | 203 | /** 204 | Routes requests to various `Handler`s based on the request URL, and runs the actual HTTP server 205 | and async event loop. 206 | */ 207 | pub struct Server { 208 | route_table: HashMap> 209 | } 210 | 211 | impl Server { 212 | pub fn new() -> Server { 213 | Server{ 214 | route_table: HashMap::new() 215 | } 216 | } 217 | 218 | fn has_resource(&self, s: &str) -> bool { 219 | self.route_table.get(s).is_some() 220 | } 221 | 222 | pub fn handle(&self, req: Request) -> BoxFuture { 223 | // TODO maybe instead do some sort of indexing instead of all this string hashing, so like, the webhooks calls get_route_ref or something 224 | match self.route_table.get(req.resource()) { 225 | Some(resource) => resource.handle(req), 226 | None => err(Error::new(ErrorKind::NotFound, JsonValue::String("TODO not found error here".to_string()))).boxed() 227 | } 228 | } 229 | 230 | pub fn resource, R: Handler + 'static>(&mut self, route: T, handler: R) { 231 | self.route_table.insert(route.into(), Box::new(handler)); 232 | } 233 | 234 | pub fn listen + Send + 'static>(self, bind_addr: T) { 235 | let addr: String = bind_addr.into(); 236 | let http_addr = addr.as_str().parse().unwrap(); 237 | let server_arc = Arc::new(self); 238 | let server_clone = server_arc.clone(); 239 | let server = http::Http::new().bind::<_, Body>(&http_addr, move || { 240 | Ok(HttpService{server: (&server_clone).clone()}) 241 | }).unwrap(); 242 | println!("Listening on http://{} with 1 thread.", server.local_addr().unwrap()); 243 | server.run().unwrap(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /license-apache.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------