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
endAs 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
endstatus 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
endThe above code mainly serves two aspects:
-
Call the
presentmethod in the interface logic without explicitly specifying theEntitytype, 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
statusdeclaration has already known how to render thearticleentity. -
It can generate corresponding documentation based on
statusDSL.
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
endIt is better than Grape::Entity.documentation, with the following improvements:
-
typecan be written as string:expose :foo, documentation: { type:'string' }
-
Some additional parameters can be mixed into
documentationhash option, such asparam_type:expose :foo, documentation: { param_type:'body' }
-
Properly process
is_arrayoption:expose :bar, documentation: { type: String, is_array: true }
-
Declare
required:expose :foo, documentation: { required: true }
-
use
scopeto 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, articleYou 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.gitIf 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.gitUnder 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
endThe 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
endHowever, 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
endThe above code mainly serves two aspects:
-
Call the
presentmethod in the interface logic without explicitly specifying theEntitytype, 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
statusdeclaration has already known how to render thearticleentity. -
It can generate corresponding documentation based on
statusDSL.
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
endAfter Rails 5, it recommender integration testing more:
class ArticlesControllerTest <ActionDispatch::IntegrationTest
test "should get index" do
get articles_url
assert_response :success
end
endNote 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, usersThen 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.gitThe 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.