1个不稳定版本
使用旧Rust 2015
0.1.0 | 2017年5月29日 |
---|
#51 in #json-api
在rustiful-derive中使用
53KB
1.5K SLoC
简介
在Rust中创建JSONAPI,嗯,API。
Rustiful基于Iron,并支持稳定Rust(>=1.20)。
待办事项
这仍然是一个非常正在开发中的项目。API 将 会改变,而且目前还有许多特性尚未实现,包括但不限于
-
meta
信息 -
self
链接 - 一般链接
- 包含
- 关系
- 过滤器
- 分页
- 也许甚至rocket支持?
已实现的功能
- GET/POST/PATCH/DELETE
-
sort
- 这意味着您可以通过类型安全的方式访问排序参数。 -
fields
- 这意味着您可以通过类型安全的方式访问字段参数。
安装
如果您使用Cargo,请将rustiful、serde和iron添加到您的Cargo.toml中。您可能还需要uuid支持,这可以通过uuid
功能添加。
[dependencies]
iron = "0.5"
serde = "1.0"
serde_derive = "1.0"
rustiful = { version = "0.1", features = ["uuid", "iron"] }
rustiful-derive = { version = "0.1", features = ["uuid"] }
如何使用
首先,我们需要有一个我们想要表示为JSONAPI资源的类型。为此,我们需要一个至少有一个id字段的struct。id字段必须命名为id
或使用JsonApiId
属性注释。
一旦我们有这个,我们就可以给struct本身添加JsonApi
属性。这将为给定类型生成JSONAPI表示,以及生成一个类型安全的查询参数类型。
您还可以可选地添加Serde的Serialize
和Deserialize
属性,以防您需要重命名一个字段和/或重命名类型JSONAPI表示中的资源名称;如果您使用Serde的rename
属性,生成的JSONAPI类型将使用重命名属性在序列化和反序列化时使用。
extern crate rustiful;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate rustiful_derive;
use rustiful::*;
use rustiful::iron::*;
#[derive(Default, JsonApi)]
pub struct Todo {
id: String,
title: String,
body: Option<String>,
published: bool,
}
一旦有了要使用的类型,就需要一种方式来CRUD(创建、读取、更新、删除)资源。这是通过实现以下任一组合来完成的:JsonGet
、JsonPost
、JsonIndex
、JsonDelete
或JsonPatch
。这些特质每个都有一个Error
类型和一个Context
类型。《Error》类型需要实现《std::error::Error》,并在CRUD操作过程中处理可能发生的任何错误。可以为所有HTTP动词特质使用相同的错误类型,或者为每个HTTP方法实现一个错误类型。
对于错误类型,我们还需要指定错误对应的HTTP错误代码。这是通过使用《From》特质来完成的,其中您将您错误类型的引用转换为《Status》类型。
《Context》类型可以是任何实现《FromRequest》的类型。您可以使用它来初始化每个请求需要的东西(例如数据库连接)。《FromRequest》还需要设置一个错误类型。在《FromRequest》内部,您可以完全访问传入的Iron请求。如果在《FromRequest》中发生错误,则将返回500。目前不支持在《FromRequest》实现中为错误设置自定义HTTP错误代码。
以下是一个示例,其中包含针对《Status》和《FromRequest》的存根《From》实现。我们将用错误来模拟资源方法。
use rustiful::status::Status;
// `std::error::Error` implementation omitted
pub struct MyErr(String);
// Converts an error to a status code.
impl<'a> From<&'a MyErr> for Status {
fn from(err: &'a MyErr) -> Self {
rustiful::status::InternalServerError
}
}
pub struct Context {}
// Initializes a `Context` from a request.
impl FromRequest for Context {
type Error = MyErr;
fn from_request(request: &Request) -> Result<Self, Self::Error> {
Ok(Context {})
}
}
impl JsonGet for Todo {
type Error = MyErr;
type Context = Context;
fn find(id: String,
_: &Self::Params,
ctx: Self::Context)
-> Result<Option<JsonApiData<Self>>, Self::Error> {
Err(MyErr("Unimplemented"))
}
}
impl JsonIndex for Todo {
type Error = MyErr;
type Context = Context;
fn find_all(params: &Self::Params, ctx: Self::Context) -> Result<Vec<JsonApiData<Self>>, Self::Error> {
Err(MyErr("Unimplemented"))
}
}
impl JsonDelete for Todo {
type Error = MyErr;
type Context = Context;
fn delete(id: String, ctx: Self::Context) -> Result<(), Self::Error> {
Err(MyErr("Unimplemented"))
}
}
impl JsonPost for Todo {
type Error = MyErr;
type Context = Context;
fn create(json: JsonApiData<Self>,
params: &Self::Params,
ctx: Self::Context)
-> Result<JsonApiData<Self>, Self::Error> {
Err(MyErr("Unimplemented"))
}
}
impl JsonPatch for Todo {
type Error = MyErr;
type Context = Context;
fn update(id: String,
json: JsonApiData<Self>,
params: &Self::Params,
ctx: Self::Context)
-> Result<JsonApiData<Self>, Self::Error> {
Err(MyErr("Unimplemented"))
}
}
最后,我们需要将资源连接起来,以便它实际上可以通过HTTP进行访问。为此,我们有一个《JsonApiRouterBuilder》,它将构建一个用于启动Web服务器的Iron链。
extern crate iron;
fn app_router() -> iron::Chain {
let mut router = JsonApiRouterBuilder::default();
router.jsonapi_get::<Todo>();
router.jsonapi_post::<Todo>();
router.jsonapi_index::<Todo>();
router.jsonapi_patch::<Todo>();
router.jsonapi_delete::<Todo>();
router.build()
}
fn main() {
Iron::new(app_router()).http("localhost:3000").unwrap()
}
一旦构建了链,我们将其添加到Iron构造函数中,并启动Web服务器。资源路径是资源类型名称的复数形式和连字符名称的小写形式。在上面的示例中,这意味着路由如下所示
GET /todos
GET /todos/:id
POST /todos
PATCH /todos
DELETE /todos
如果我们在上面运行示例,并使用《curl》请求服务器,我们将返回一个JSONAPI错误对象。
$ curl -i https://127.0.0.1:3000/todos/
HTTP/1.1 500 Internal Server Error
Content-Type: application/vnd.api+json
Content-Length: 78
Date: Thu, 25 May 2017 15:50:25 GMT
{"errors":[{"title":"Unimplemented","status":"500","detail":"Unimplemented"}]}
让我们修改《JsonIndex》实现,使其返回一个包含单个项目的列表。
impl JsonIndex for Todo {
type Error = MyErr;
type Context = Context;
fn find_all(params: &Self::Params, ctx: Self::Context) -> Result<Vec<JsonApiData<Self>>, Self::Error> {
Ok(vec![Todo {
id: "1".to_string(),
body: "test".to_string(),
title: "test".to_string(),
published: true
}.into_json(params)])
}
}
如果我们现在使用《curl》,返回的JSON应该看起来像这样
$ curl -i https://127.0.0.1:3000/todos/
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
Content-Length: 97
Date: Thu, 25 May 2017 16:16:20 GMT
[{"data":{"id":"1","type":"todos","attributes":{"title":"test","body":"test","published":true}}}]
这应该足以开始;在仓库的《examples》目录中有一个更复杂的示例,使用Diesel和连接池。
还有一件事情要展示。您可以通过params参数(《Self::Params》)完全访问《sort》和《fields》参数。到目前为止,这仅在《JsonGet》和《JsonIndex》中实现。
《Self::Params》类型是《JsonApiParams
// Rustiful
use self::todo::sort::*;
use rustiful::SortOrder::*;
// Diesel
use self::todos as column;
use self::todos::dsl::todos as table;
impl JsonIndex for Todo {
type Error = MyErr;
type Context = Context;
fn find_all(params: &Self::Params, ctx: Self::Context) -> Result<Vec<JsonApiData<Self>>, Self::Error> {
let mut query = table.into_boxed();
{
use self::todo::sort::*;
use self::todos as column;
use rustiful::SortOrder::*;
let mut order_columns: Vec<Box<BoxableExpression<table, Pg, SqlType=()>>> = Vec::new();
for order in ¶ms.sort.fields {
match *order {
title(Asc) => {
order_columns.push(Box::new(column::title.asc()));
}
title(Desc) => {
order_columns.push(Box::new(column::title.desc()));
}
body(Asc) => {
order_columns.push(Box::new(column::body.asc()));
}
body(Desc) => {
order_columns.push(Box::new(column::body.desc()));
}
published(Asc) => {
order_columns.push(Box::new(column::published.asc()));
}
published(Desc) => {
order_columns.push(Box::new(column::published.desc()));
}
};
}
// TODO: Hopefully there's a nicer way to get multiple ORDER BY clauses in this query.
match order_columns.len() {
1 => query = query.order(order_columns.remove(0)),
2 => query = query.order((order_columns.remove(0), order_columns.remove(0))),
3 => query = query.order((order_columns.remove(0), order_columns.remove(0), order_columns.remove(0))),
4 => query = query.order((order_columns.remove(0), order_columns.remove(0), order_columns.remove(0), order_columns.remove(0))),
_ => return Err(MyErr("too many sort columns".to_string()))
}
}
query
.load::<Todo>(/* Add connection here */)
.map(|r| r.into_json(params))
.map_err(|e| MyErr("Failed to load query"))
}
}
如果您有任何问题或想提交错误报告,请随时提交一个Github问题。
依赖项
~8MB
~190K SLoC