English | 简体中文
As you may know, expose
can be nested. If a block without parameters is passed, nested rendering will be performed:
class Entities::Article <Grape::Entity
expose :user do
expose :name {|article| article.user.name}
expose :age {|article| article.user.age}
end
end
As above, a data structure like the following will be rendered:
{
"user": {
"name": "Jim",
"age": 18
}
}
If deep: true
is passed in at this time, the instance bound to the nesting layer will be different. The following example shows the same effect as the above one:
class Entities::Article <Grape::Entity
expose :user, deep: true do
expose :name
expose :age
end
end
status
is used to declare the response entity (success
, fail
, entity
are actually aliases with special circumstances). It has the following invoking forms:
status 200 do
expose :article, using: Entities::Article
end
status 200,'Return article data' do
expose :article, using: Entities::Article
end
status 400,'The request parameter is wrong' do
expose :code, documentation: { desc: 'error code' }
expose :message, documentation: { desc: 'error message' }
end
success 'Request is successful' do
expose :article, using: Entities::Article
end
fail 'Request failed' do
expose :code, documentation: { desc: 'error code' }
expose :message, documentation: { desc: 'error message' }
end
entity do
expose :article, using: Entities::Article
end
The above code mainly serves two aspects:
-
Call the
present
method in the interface logic without explicitly specifying theEntity
type, which is automatically resolved.Previously, you had to write as following:
present :article, article, with: Entities::Article
For now, just write it as:
present :article, article
Because the
status
declaration has already known how to render thearticle
entity. -
It can generate corresponding documentation based on
status
DSL.
It adds a new method to_params
in Grape::Entity
, allowing you to reuse it in parameter declarations:
params do
requires :article, type: Hash do
optional :some, using: Entities::Article.to_params
end
end
It is better than Grape::Entity.documentation
, with the following improvements:
-
type
can be written as string:expose :foo, documentation: { type:'string' }
-
Some additional parameters can be mixed into
documentation
hash option, such asparam_type
:expose :foo, documentation: { param_type:'body' }
-
Properly process
is_array
option:expose :bar, documentation: { type: String, is_array: true }
-
Declare
required
:expose :foo, documentation: { required: true }
-
use
scope
to define it is only a param or response value:# Only param expose :foo, documentation: { scope: :param } # Only response value expose :bar, documentation: { scope: :entity } expose :bar, documentation: { param: false } # Both param and response value expose :car, documentation: { scope: [:param, :entity]} expose :car, documentation: {}
Now you can use the programming style to test the return value of the interface, no need to test such as JSON string, XML text and so on. If you implement the interface like this:
present :article, article
You can test it just like:
get'/article/1'
assert_equal 200, last_response.status
assert_equal articles(:one), presents(:article)
Pay attention to the presents
method, it is the magical artifact I provide for you.
Note that this function is not implemented in the framework, you need to clone the scaffolding project:
git clone https://github.com/run27017/grape-app-demo.git
If you are a complete novice, it is recommended to get familiar with the Grape framework first. I suggest you read the document of my forked repository. After you are very familiar with the Grape framework, read the above part of my improvements to the Grape framework. For the design concept of the entire framework, you can read the following content of this article.
You can start a project started with my scaffolding project, and all features are already integrated:
git clone https://github.com/run27017/grape-app-demo.git
Under current era, there are many development paradigms for back-end developers to choose from, such as test-driven development, behavior-driven development, agile software development and so on. In contrast, I proposed a new idea, which is called document-oriented development.
When writing an API project, it is necessary to prepare documentation. I don't know how everyone prepares the documentation, but usually there is a strange circle: I have to repeat myself both in writing logic code and writing documentation. why can't I have completed the document synchronously while writing the interface logic? If I can invent a DSL that can control the behavior of the interface while writing a document, isn't that what I want?
Just do it!
I found that Grape framework already provides a similar DSL. For example, you can specify parameters like this:
params do
requires :user, type: Hash do
requires :name, type: String, desc:'name of user'
requires :age, type: Integer, desc:'age of user'
end
end
The above code can restrict the parameters to the two fields of name
and age
, and restricting their types to String
and Integer
respectively. At the same time, one library called grape-swagger can render the macro definition of params
as part of the Swagger document. Documentation and implementation are combined perfectly.
In addition, the Grape framework provides the desc
macro, which is a pure document statement for third-party libraries to read and will not affect the interface behavior in any way.
desc 'Create new user' do
tags 'users'
entity Entities::User
end
However, the Grape framework is not a complete document-oriented development framework. It has many important missions, so the seamless connection between it and the document is limited to these. As you can see, the params
macro is a perfect example, the desc
macro is unfortunately only related to document rendering, and then nothing else.
Since the Grape framework is an open source framework, it is easy to modify it to add a few new functions. It took me a few days to add a status
macro, which can be used to declare the response entity:
status 200 do
expose :user, deep: true do
expose :id, documentation: { type: Integer, desc:'id of user' }
expose :name, documentation: { type: String, desc:'name of user' }
expose :age, documentation: { type: Integer, desc: 'age of user' }
end
end
The above code mainly serves two aspects:
-
Call the
present
method in the interface logic without explicitly specifying theEntity
type, which is automatically resolved.Previously, you had to write as following:
present :article, article, with: Entities::Article
For now, just write it as:
present :article, article
Because the
status
declaration has already known how to render thearticle
entity. -
It can generate corresponding documentation based on
status
DSL.
Everything is just the tip of the iceberg.
Regarding the unit testing of interfaces, there are two points to debate: Which is more reasonable, the integration testing or the controller testing? Integration testing is like a black box. Developers call the interface and then test the view returned by the interface. The Controller test will also call the interface in the same way, but will test the internal state.
Through the following two cases, you can intuitively feel the differences of controller test and integration test in Rails.
In earlier versions of Rails, it exists controller test:
class ArticlesControllerTest <ActionController::TestCase
test "should get index" do
get :index
assert_response :success
assert_equal users(:one, :two, :three), assigns(:articles)
end
end
After Rails 5, it recommender integration testing more:
class ArticlesControllerTest <ActionDispatch::IntegrationTest
test "should get index" do
get articles_url
assert_response :success
end
end
Note that there is no corresponding assert_equal
statement in the Integration test, it is because writing it is very difficult. For example, if the view returns JSON data, you can try the following equivalent code:
assert_equal users(:one, :two, :three).as_json, JSON.parse(last_response.body)
But it is often fail because it closely depend on view changes. The above one is just a try.
I don't want to purpose a discussion of which is better between controller test and integration test, even you may have been aware of my tendency from all I have written. This topic can be discussed from 2012 to 2020. If you don’t believe me, you can read this post. I don't want to make it worse.
Maybe those refusing controller testing consider it is not elegant, because it need to invest the instance variables, which is the internal state of controller test. Fortunately, I did some simple work to make it more elegant to test it. You only need to specify the rendering data using the present
method in the logical interface:
present :users, users
Then test it with the special presents
method in the test case:
assert_equal users(:one, :two, :three), presents(:users)
It's similar to assigns
, but it's more comfortable, isn't it?
This is my transformation of the Grape framework, which has already begun and will continue. My transformation concept is nothing more than two ideas: better documentation integration and better testing. In fact, it only takes a little time to make it work.
If you also want to use the inproveoment version of the Grape framework, just clone my scaffold directly:
git clone https://github.com/run27017/grape-app-demo.git
The scaffold uses the frame set of my fork. They are:
Click the links and have a look, maybe you can also become a participants in open source world.