#json-api #field #iron #parameters #serde #intended #rustiful

rustiful-derive

这是一个 derive 库,旨在与 rustiful 库一起使用

1 个不稳定版本

使用旧的 Rust 2015

0.1.0 2017年5月29日

#57 in #json-api

Apache-2.0

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 的 SerializeDeserialize 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资源。这可以通过实现以下任意组合来完成:JsonGetJsonPostJsonIndexJsonDeleteJsonPatch。这些特质每个都有一个Error类型和一个Context类型。《code>Error类型需要实现std::error::Error,用于处理CRUD操作过程中可能发生的任何错误。您可以对所有HTTP动词特质使用相同的错误类型,或者为每个HTTP方法实现一个错误类型。

对于错误类型,我们还需要指定错误对应的HTTP错误代码。这通过使用From特质来完成,您将错误类型的引用转换为Status类型。

Context类型可以是任何实现了FromRequest的类型。您可以使用它来初始化每个请求需要的事物(如数据库连接)。《code>FromRequest还需要设置一个错误类型。在FromRequest内部,您可以完全访问传入的Iron请求。如果在FromRequest中发生任何错误,将返回500。目前不支持在FromRequest实现中设置自定义HTTP错误代码。

下面是一个示例,展示了针对StatusFromRequest的存根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参数完全访问sortfields参数(Self::Params)。到目前为止,这仅在JsonGetJsonIndex中实现。

《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
~187K SLoC