5 unstable releases

0.3.1 Nov 17, 2019
0.2.2 Nov 10, 2019
0.2.1 Nov 10, 2019
0.2.0 Nov 10, 2019
0.1.0 Nov 2, 2019

#5 in #relationships


Used in rabbithole-endpoint-actix

MIT license

72KB
1.5K SLoC

rabbithole-rs Build Status crates.io

The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, so suddenly that Alice had not a moment to think about stopping herself before she found herself falling down what seemed to be a very deep well.

-- Alice's Adventures in Wonderland, by Lewis Carroll

Introduction

Rabbithole-rs is a nearly well-typed, user-friendly JSON:API type system, with an easy-to-use Macro System to help you modelling the your data.

Inspired a lot by jsonapi-rust, in fact, all of the sample data in tests are just from this crate. Nice job, michiel!

So what is JSON:API?

If you’ve ever argued with your team about the way your JSON responses should be formatted, JSON:API can be your anti-bikeshedding tool.

By following shared conventions, you can increase productivity, take advantage of generalized tooling, and focus on what matters: your application.

When you are designing a RESTful API, the most troubling problem is how to design the Data Structure, especially how to design the Error System. So JSON:API design a Specification for the people like you to specify some rules to help you handling the design problem and free your days!

Maybe the specification is LONG LONG and boring, like reading a textbook, but believe me, you will learn a lot from it, just like a textbook. :)

So why yet another JSON:API crate?

RSQL support needed

One of the main reason of this crate is that I need to support RSQL/FIQL, for I think it is a well-defined query system for complex querying. The JSON:API does not given a official Query/Filter solution, but I think RSQL/FIQL is good enough to handle my project.

For more infomation about RSQL/FIQL, see:

Well Typed System

As a Scala player, I believe a well designed type system can just avoid a lot of problem. And I want to know if Rust's ADT System can handle the problem with this kind of complexity. In fact, it can handle it well.

An user-friendly Macro and Modelling

As a Java developer, I prefer the annotation system a lot. Thankfully, Rust uses proc_macro system to give the users "most-exactly-the-same" experience.

For example, instead of:

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Dog {
    id: String,
    name: String,
    age: i32,
    main_flea: Flea,
    fleas: Vec<Flea>,
}
jsonapi_model!(Dog; "dog"; has one main_flea; has many fleas);

I can model my data structure like this:

#[derive(rbh_derive::EntityDecorator, Serialize, Deserialize, Clone)]
#[entity(type = "dogs")]
pub struct Dog<'a> {
    #[entity(id)]
    pub id: String,
    pub name: String,
    #[entity(to_many)]
    pub fleas: Vec<Flea>,
    #[entity(to_many)]
    pub friends: Vec<Dog<'a>>,
    #[entity(to_one)]
    #[serde(bound(deserialize = "Box<Human<'a>>: Deserialize<'de>"))]
    pub master: Box<Human<'a>>,
    #[entity(to_one)]
    pub best_one: Option<Box<Dog<'a>>>,
}

For me, the second one is more beautiful.

Features

Some Problems

Query with Relationship Path does not work

See the issue for detail.

In specification, when using page query,

  • the meta object should add a totalPage field
  • the links object should add prev, next, first and last links but rabbithole cannot handle it automatically, users should add these fields by implementing in Fetching::vec_to_document manually。

Future Works

Type checking and error hints in Marco System

There are lots of type restrictions in rabbithole-rs. for example:

  • [#to_one] decorator can only be use on a field with the type of being:

    • a rabbithole::entity::Entity
    • or a wrapper class of rabbithole::entity::Entity, where wrapper class is one of:
      • Option<T>
      • Box<T>
      • &T
      • A wrapper class inside another, like Option<Box<T>>
  • #[to_many] decorator can only use on a field with (all of):

    • an iterator,
    • the inner type of the iterator should meet the above restriction
    • no nested List (discussing)

Now because of lacking the Reflection in Rust, the macro now can not check type errors at all, so some solutions may needed.

A high performance Server

Because the API interface of JSON:API is complex, I think it's a redundant and boring work to write all the API interface following the specification yourself, so I will do the boring things for you!

My final goal to this project

The final goal of the project is just like crnk or elide, who can auto generate a bunch of API based on JUST the definition of the models (maybe DAOs). Here I want to just show what will the project look like finally.

Define the models

The first step is define some API-friendly models.

// This is the derive crate which you can use to generate JSON:API specific traits
extern crate rabbithole_derive as rbh_derive;

#[derive(rbh_derive::EntityDecorator, Serialize, Deserialize, Clone)]
#[entity(type = "people")]
pub struct Human {
    #[entity(id)]
    pub id_code: Uuid,
    pub name: String,
    #[entity(to_many)]
    pub dogs: Vec<Dog>,
}

#[derive(rbh_derive::EntityDecorator, Serialize, Deserialize, Clone)]
#[entity(type = "dogs")]
pub struct Dog {
    #[entity(id)]
    pub id: Uuid,
    pub name: String,
}

Write your own DAOs

rabbithole does not bind with any specific databases, which means you have to write your own DAOs.

See rabbithole-endpoint-actix/examples/mock_gen.rs for more details.

What is Fetching trait

Fetching trait is a mapping of "fetching data" part in JSON:API, which define a several operations:

  • Fetching Resources
    • Single resource: GET /articles/1
    • Multiple resources: GET /articles
    • Related resource: GET /articles/1/author, which can be both single and multiple
  • Fetching relationships
    • Relationship: GET /articles/1/relationships/comments
  • Fenching with query parameters
    • Inclusion of Related Resources (include part)
    • Sparse Fieldsets (fields[TYPE] part)
    • Sorting (sort part)
    • Pagination (page part)

These are all we need to know in fetching data part. So these operation are abstracted into Fenching trait.

What is vec_to_document part?

If you want to transform a Vec<SingleEntity> into Document, it will do a lot of things like excluding un-included resources, retaining sparse fields, etc. and etc., and of course I can help you in the background (using Entity::to_document_automatically). But more than extracting all the fields from databases and dropping them later, why not just leaving them in databases? So here is the point. If you don't want to write the Vec<SingleEntity> to Document code, just use Entity::to_document_automatically, or, you could assemble the Document directly from the database.

What is ...(any other) part?

  • fetch_collection will be mapped into: /<ty>?<query>
  • fetch_single will be mapped into: /<ty>/<id>?<query>
  • fetch_relationship will be mapped into: /<ty>/<id>/relationships/<related_field>?<query>
  • fetch_related will be mapped into: /<ty>/<id>/<related_field>?<query>
  • type Error will be mapped into the error responses if possible
  • type Item must be a SingleEntity

Dependencies

~9–18MB
~249K SLoC