#json-api #iron #sorting #field #serde #access #type-safe

rustiful

这个crate用于创建一个由Iron支持的JSONAPI后端。

1个不稳定版本

使用旧Rust 2015

0.1.0 2017年5月29日

#51 in #json-api


rustiful-derive中使用

Apache-2.0

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的SerializeDeserialize属性,以防您需要重命名一个字段和/或重命名类型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(创建、读取、更新、删除)资源。这是通过实现以下任一组合来完成的:JsonGetJsonPostJsonIndexJsonDeleteJsonPatch。这些特质每个都有一个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》的别名,它有三个字段:《sort》提供对排序查询参数的访问,《fieldset》提供对《fields》查询参数的访问,以及《query_params》提供对其他所有查询参数的访问。以下是一个使用排序参数与Diesel一起使用的示例(这假设您已经在《Todo》上设置了适当的Diesel属性)。

// 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 &params.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