1 个不稳定版本
使用旧的 Rust 2015
0.1.0 | 2017年5月29日 |
---|
#57 in #json-api
74KB
1.5K SLoC
简介
创建 Rust 中的 JSONAPI,嗯,API。
Rustiful 基于 Iron,并与稳定 Rust (> =1.20) 一起工作。
待办事项
这仍然是一个非常正在进行中的项目。API 将会更改,并且目前尚未实现很多功能,包括但不限于
-
meta
信息 -
self
链接 - 一般链接
- 包含
- 关系
- 过滤器
- 分页
- 也许甚至支持 rocket?
已实现的功能
- GET/POST/PATCH/DELETE
-
sort
- 这意味着您可以以类型安全的方式访问排序参数。 -
fields
- 这意味着您可以以类型安全的方式访问字段参数。
安装
如果您使用 Cargo,请在 Cargo.toml 中添加 rustiful、serde 和 iron。您可能需要 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
属性注解。
一旦我们有了这个,我们就可以将 JsonApi
属性添加到 struct 本身。这将为给定类型生成 JSONAPI 表示,同时生成一个类型安全的查询参数类型。
您还可以可选地添加 Serde 的 Serialize
和 Deserialize
derive,如果您需要重命名字段和/或重命名类型在 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
类型。《code>Error类型需要实现std::error::Error
,用于处理CRUD操作过程中可能发生的任何错误。您可以对所有HTTP动词特质使用相同的错误类型,或者为每个HTTP方法实现一个错误类型。
对于错误类型,我们还需要指定错误对应的HTTP错误代码。这通过使用From
特质来完成,您将错误类型的引用转换为Status
类型。
Context
类型可以是任何实现了FromRequest
的类型。您可以使用它来初始化每个请求需要的事物(如数据库连接)。《code>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
如果我们使用cargo run
运行上面的示例,然后使用curl
服务器,我们将得到一个JSONAPI错误对象。
$ curl -i http://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 http://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参数完全访问sort
和fields
参数(Self::Params
)。到目前为止,这仅在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
~187K SLoC