REST-like API micro-framework for Rust. Works with Iron.
REST-like API micro-framework for Rust. Works with Iron.
Rustless is a REST-like API micro-framework for Rust. It's designed to provide a simple DSL to easily develop RESTful APIs on top of the Iron web framework. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more.
Rustless in a port of Grape library from Ruby world. Based on hyper - an HTTP library for Rust.
Like Rust itself, Rustless is still in the early stages of development, so don't be surprised if APIs change and things break. If something's not working properly, file an issue or submit a pull request!
# Cargo.toml
[dependencies.rustless]
git = "https://github.com/rustless/rustless"Rustless is based on Iron, which is based on Hyper, which is synchronous. Hyper has a lot of limitations right now, and can't handle many simultaneous connections, especially with keep-alive. So it is highly recommended to use light asynchronous web server such as Nginx as a reverse proxy server with Rustless.
Below is a simple example showing some of the more common features of Rustless.
extern crate rustless;
extern crate hyper;
extern crate iron;
extern crate rustc_serialize as serialize;
extern crate valico;
use valico::json_dsl;
use rustless::server::status::StatusCode;
use rustless::{
Application, Api, Nesting, Versioning
};
use rustless::json::ToJson;
fn main() {
let api = Api::build(|api| {
// Specify API version
api.version("v1", Versioning::AcceptHeader("chat"));
api.prefix("api");
// Create API for chats
api.mount(Api::build(|chats_api| {
chats_api.after(|client, _params| {
client.set_status(StatusCode::NotFound);
Ok(())
});
// Add namespace
chats_api.namespace("chats/:id", |chat_ns| {
// Valico settings for this namespace
chat_ns.params(|params| {
params.req_typed("id", json_dsl::u64())
});
// Create endpoint for POST /chats/:id/users/:user_id
chat_ns.post("users/:user_id", |endpoint| {
// Add description
endpoint.desc("Update user");
// Valico settings for endpoint params
endpoint.params(|params| {
params.req_typed("user_id", json_dsl::u64());
params.req_typed("id", json_dsl::string())
});
endpoint.handle(|client, params| {
client.json(¶ms.to_json())
})
});
});
}));
});
let app = Application::new(api);
iron::Iron::new(app).http("0.0.0.0:4000").unwrap();
println!("On 4000");
println!("Rustless server started!");
}To easily build the example, you can set your Cargo.toml file approximately as follows:
[package]
name = "rustless-example"
version = "0.1.0"
[dependencies]
rustless = "0.10.0"
hyper = "0.10.5"
rustc-serialize = "0.3"
valico = "1"
[dependencies.iron]
version = "*"If you want to see how you can write some complex application using Rustless, please see the example.
In the example, please note the following aspects:
In Rustless you can use three core entities to build your RESTful app: Api, Namespace and Endpoint.
Api::build(|api| {
// Api inside Api example
api.mount(Api::build(|nested_api| {
// Endpoint definition
nested_api.get("nested_info", |endpoint| {
// endpoint.params(|params| {});
// endpoint.desc("Some description");
// Endpoint handler
endpoint.handle(|client, _params| {
client.text("Some usefull info".to_string())
})
});
}))
// The namespace method has a number of aliases, including: group,
// resource, resources, and segment. Use whichever reads the best
// for your API.
api.namespace("ns1", |ns1| {
ns1.group("ns2", |ns2| {
ns2.resource("ns3", |ns3| {
ns3.resources("ns4", |ns4| {
ns4.segment("ns5", |ns5| {
// ...
);
})
})
})
})
})You can define validations and coercion options for your parameters using a DSL block inside Endpoint and Namespace definition. See Valico for more info about what you can do.
api.get("users/:user_id/messages/:message_id", |endpoint| {
endpoint.params(|params| {
params.req_typed("user_id", Valico::u64());
params.req_typed("message_id", Valico::u64());
});
// ...
})Also you can use JSON Schema (IETF's draft v4) to validate your parameters. To use schemes in your application you need to use the following setup:
use valico::json_schema;
use rustless::batteries::schemes;
let scope = json_schema::Scope::new();
// ... You can insert some external schemes here ...
schemes::enable_schemes(&mut app, scope).unwrap();See Valico for more info about JSON Scheme usage inside DSL blocks.
Rustless is intergated with queryst to allow smart query-string parsing
end decoding (even with nesting, like foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb). See queryst for more info.
There are three strategies in which clients can reach your API's endpoints:
api.version("v1", Path);Using this versioning strategy, clients should pass the desired version in the URL.
curl -H http://localhost:3000/v1/chats/api.version("v1", AcceptHeader("chat"));Using this versioning strategy, clients should pass the desired version in the HTTP Accept head.
curl -H Accept:application/vnd.chat.v1+json http://localhost:3000/chatsAccept version format is the same as Github (uses)[https://developer.github.com/v3/media/].
api.version("v1", Param("ver"));Using this versioning strategy, clients should pass the desired version as a request parameter in the URL query.
curl -H http://localhost:9292/statuses/public_timeline?ver=v1By default Rustless returns a 200 status code for GET-Requests and 201 for POST-Requests. You can use status and set_status to query and set the actual HTTP Status Code
client.set_status(NotFound);Request parameters are available through the params: JsonObject inside Endpoint handlers and all callbacks. This includes GET, POST and PUT parameters, along with any named parameters you specify in your route strings.
The request:
curl -d '{"text": "hello from echo"}' 'http://localhost:3000/echo' -H Content-Type:application/json -vThe Rustless endpoint:
api.post("", |endpoint| {
endpoint.handle(|client, params| {
client.json(params)
})
});In the case of conflict between either of:
GET, POST and PUT parametersPOST and PUTroute string parameters will have precedence.
You can redirect to a new url temporarily (302) or permanently (301).
client.redirect("http://google.com");client.redirect_permanent("http://google.com");You can abort the execution of an API method by raising errors with error.
Define your error like this:
use rustless::errors::{Error, ErrorRefExt};
#[deriving(Show)]
pub struct UnauthorizedError;
impl std::error::Error for UnauthorizedError {
fn description(&self) -> &str {
return "UnauthorizedError";
}
}And then throw:
client.error(UnauthorizedError);By default Rustless wil respond all errors with status::InternalServerError.
Rustless can be told to rescue specific errors and return them in the custom API format.
api.error_formatter(|err, _media| {
match err.downcast::() {
Some(_) => {
return Some(Response::from_string(StatusCode::Unauthorized, "Please provide correct `token` parameter".to_string()))
},
None => None
}
});Blocks can be executed before or after every API call, using before, after,
before_validation and after_validation.
Before and after callbacks execute in the following order:
beforebefore_validationafter_validationafterSteps 4, 5 and 6 only happen if validation succeeds.
The block applies to every API call within and below the current nesting level.
Api::build(|api| {
api.prefix("api");
api.version("v1", Versioning::Path);
api.error_formatter(|err, _media| {
match err.downcast::() {
Some(_) => {
return Some(Response::from_string(StatusCode::Unauthorized, "Please provide correct `token` parameter".to_string()))
},
None => None
}
});
api.namespace("admin", |admin_ns| {
admin_ns.params(|params| {
params.req_typed("token", Valico::string())
});
// Using after_validation callback to check token
admin_ns.after_validation(|&: _client, params| {
match params.get("token") {
// We can unwrap() safely because token in validated already
Some(token)No open issues yet, or sync has not completed.