24 releases
Uses old Rust 2015
0.10.0 | Dec 8, 2016 |
---|---|
0.9.0 | Aug 14, 2016 |
0.8.0 |
|
0.7.3 | Apr 4, 2016 |
0.1.0 | Nov 29, 2014 |
#784 in HTTP server
105KB
2K
SLoC
Table of Contents
- What is Rustless?
- Usage warning
- Basic Usage
- Complex example
- Mounting
- Parameters validation and coercion
- Use JSON Schema
- Query strings
- API versioning
- Respond with custom HTTP Status Code
- Use parameters
- Redirecting
- Errors firing
- Errors handling
- Before and After callbacks
- Secure API example
- JSON responses
- Swagger 2.0 support
- Integration with PostgreSQL
- Integration with Deuterium ORM
What is Rustless?
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"
See also
- Valico - Rust JSON validator and coercer. See Api docs.
- Queryst - Rust query string parser with nesting support. See Api docs.
- JsonWay - JSON building DSL and configurable serializers for Rust. See Api docs.
Usage warning
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.
Basic Usage
Below is a simple example showing some of the more common features of Rustless.
#[macro_use]
extern crate rustless;
extern crate hyper;
extern crate iron;
extern crate rustc_serialize as serialize;
extern crate valico;
use valico::json_dsl;
use hyper::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!");
}
Complex example
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:
- Complex nested API with versioning.
- CRUD operations with rust-postgres.
- Swagger 2.0 intergration.
- JSON Schema validations.
- Error reporting.
- Serializers.
- File structure.
- Integration with docopt.
- Integration with deuterium-orm. Database migrations.
Mounting
In Rustless you can use three core entities to build your RESTful app: Api
, Namespace
and Endpoint
.
- Api can mount Api, Namespace and Endpoint
- Namespace can mount 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| {
// ...
);
})
})
})
})
})
Parameters validation and coercion
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());
});
// ...
})
Use JSON Schema
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.
Query strings
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.
API versioning
There are three strategies in which clients can reach your API's endpoints:
- Path
- AcceptHeader
- Param
Path versioning strategy
api.version("v1", Path);
Using this versioning strategy, clients should pass the desired version in the URL.
curl -H http://localhost:3000/v1/chats/
Header versioning strategy
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/chats
Accept version format is the same as Github (uses)https://developer.github.com/v3/media/.
Param versioning strategy
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=v1
Respond with custom HTTP Status Code
By 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);
Use parameters
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 -v
The Rustless endpoint:
api.post("", |endpoint| {
endpoint.handle(|client, params| {
client.json(params)
})
});
In the case of conflict between either of:
- route string parameters
GET
,POST
andPUT
parameters- the contents of the request body on
POST
andPUT
route string parameters will have precedence.
Redirecting
You can redirect to a new url temporarily (302) or permanently (301).
client.redirect("http://google.com");
client.redirect_permanent("http://google.com");
Errors firing
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);
Errors handling
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::<UnauthorizedError>() {
Some(_) => {
return Some(Response::from_string(StatusCode::Unauthorized, "Please provide correct `token` parameter".to_string()))
},
None => None
}
});
Before and After callbacks
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:
before
before_validation
- validations
after_validation
- the API call
after
Steps 4, 5 and 6 only happen if validation succeeds.
The block applies to every API call within and below the current nesting level.
Secure API example
Api::build(|api| {
api.prefix("api");
api.version("v1", Versioning::Path);
api.error_formatter(|err, _media| {
match err.downcast::<UnauthorizedError>() {
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) => if token.as_string().unwrap().as_slice() == "password1" { return Ok(()) },
None => ()
}
// Fire error from callback is token is wrong
return Err(Box::new(UnauthorizedError) as Box<Error>)
});
// This `/api/admin/server_status` endpoint is secure now
admin_ns.get("server_status", |endpoint| {
endpoint.handle(|client, _params| {
{
let cookies = client.request.cookies();
let signed_cookies = cookies.signed();
let user_cookie = Cookie::new("session".to_string(), "verified".to_string());
signed_cookies.add(user_cookie);
}
client.text("Everything is OK".to_string())
})
});
})
})
JSON responses
Rustless includes JsonWay library to offer both complex JSON building DSL and configurable serializers for your objects. See API docs for details.
Also feel free to use any other serialization library you want.
Swagger 2.0
Rustless has a basic implementation of Swagger 2.0 specification. It is not fully complete and in future we need to implement:
- JSON Schema support (when some appropriate JSON Schema library will appear);
- Security parts of the specification;
But now you can already use Swagger 2.0:
let mut app = rustless::Application::new(rustless::Api::build(|api| {
// ...
api.mount(swagger::create_api("api-docs"));
// ...
}))
swagger::enable(&mut app, swagger::Spec {
info: swagger::Info {
title: "Example API".to_string(),
description: Some("Simple API to demonstration".to_string()),
contact: Some(swagger::Contact {
name: "Stanislav Panferov".to_string(),
url: Some("http://panferov.me".to_string()),
..std::default::Default::default()
}),
license: Some(swagger::License {
name: "MIT".to_string(),
url: "http://opensource.org/licenses/MIT".to_string()
}),
..std::default::Default::default()
},
host: "localhost:4000".to_string(),
..std::default::Default::default()
});
After that you can use /api-docs
path in Swagger UI to render your API structure.
Integration with PostgreSQL
We have an annotated example of such integration in postgres_example. Please try it and feel free to say your opinion.
Integration with Deuterium ORM
TODO: Example
Dependencies
~13MB
~270K SLoC