Add this line to your application's Gemfile:
gem 'lobanov', github: 'spajic/lobanov', tag: 'v0.0.0' # see Changelog.md
And then execute:
$ bundle
Or install it yourself as:
$ gem install lobanov
There is a couple of similar projects (see the section and comparison below)
What I see as key strong / unique points of Lobanov:
:lobanov
tag to one spec.
When another.
So you can start small. You can generate schema only for what you want.:lobanov
tag and you are all set.TODO
If this ENV-variable is set, all :lobanov
tags would work as :lobanov
This may be useful in case whey you want to rebuild a bunch of schemas.
If this ENV-variable is set, lobanov
would update tags of operations.
It would affect only operations covered by the specs that would be executed.
It would not validate schema.
It would merge existing tags with tags that would be calculated this time.
This may be useful when lobanov
logic of generating tags is being updated.
And you want to incorporate new tags to your schema.
In that case you should launch your complete test suite with UPDATE_TAGS=1
.
https://github.com/exoego/rspec-openapi
Overall very nice project, it has many things done just right.
Key differences:
openapi: false
, rather than include with :lobanov
Questions:
Some points:
OPENAPI=1 bundle exec rspec
type: :request
openapi: false
tag (lobanov
goes the opposite way)lobanov
has zero DSL)required
: https://github.com/exoego/rspec-openapi/issues/89https://github.com/rswag/rswag
For me the showstopper here is that you have to effectively write spec by hand, but in DSL
For me it is too tedious. And than I tried to work with rswag I very soon get stuck with that DSL - I didn't understand how can I achieve the required result through the DSL.
Questions:
Some points:
TODO
That was the inspiration for rspec-openapi
TODO
TODO: use this checklist for Features and Usage
section
requestBody
Вообще схемы хранения могут быть разные, но начнём с одной.
Пусть есть ресурс и вложенный ресурс, например пусть Фрукты и Отзывы на фрукты
И пусть ещё всё это в неймспейсе wapi/
GET /fruits (index)
GET /fruits/:id (show)
POST /fruits (create)
PUT /fruits/:id (update)
DELETE /fruits/:id (destroy)
GET /fruits/:id/reviews (index)
POST /fruits/:id/reviews (create)
GET /fruits/:id/reviews/:review_id (show)
PUT /fruits/:id/reviews/:review_id (update)
DELETE /fruits/:id/reviews/:review_id (destroy)
И может быть ещё что-то не по REST'у
При этом надо учесть, что один и тот же эндпоинт может возвращать ответы с разными статусами.
Как это всё будет храниться в нашей схеме
api-backend-specification
По идее внутри файла с описание path лежат разные HTTP statuses и verbs
Кажется единственный простой и понятный вариант - всегда называть сам файл path.yaml, а класть его в соответствии собственно с путём. Иначе возникают special-cases типа index.yaml / root.yaml - в зависимости от того, есть ли что-то внутри папки, или нет
index.yaml
paths:
"/fruits": # Здесь нужно совместить GET index и POST create
"$ref": "./paths/fruits/path.yaml"
"/fruits/{id}": # Здесь нужно как-то совместить GET show и DELETE destroy и PUT update
"$ref": "./paths/fruits/[id]/path.yaml"
"/fruits/{id}/reviews": # GET, POST
"$ref": "./paths/fruits/[id]/reviews/path.yaml"
"/fruits/{id}/reviews/{review_id}": # GET, PUT, DELETE
"$ref": "./paths/fruits/[id]/reviews/[review_id]/path.yaml"
"/fruits/{id}/reviews/{review_id}/upvote":
"$ref": "./paths/fruits/[id]/reviews/[review_id]/upvote/path.yaml"
"/fruits/{id}/reviews/{review_id}/downvote":
"$ref": "./paths/fruits/[id]/reviews/[review_id]/downvote/path.yaml"
Внутри paths/fruits/[id]/reviews/path.yaml
Тут в принципе всё понятно, надо продумать
Компоненты
# успешные ответы сохраняем в components/responses
# формируем название как ResourсeNestedActionResponse.yaml
# Action - это название action'a контроллера, который обрабатывает запрос
- ./components/responses/FruitsIndexResponse.yaml
- ./components/responses/FruitsShowResponse.yaml
- ./components/responses/FruitsCreateResponse.yaml
- ./components/responses/FruitsReviewsDownvoteResponse.yaml
Это responses - это база, которую понятно как полностью автоматизировать и на которой мы будем дальше строить.
Для любых неуспешных ответов будем возвращать класс ErrorResponse.yaml (error_code, message, payload)
- ./components/shared/ErrorResponse.yaml
- ./components/models/FruitModel.yaml # здесь то что возвращает FruitsShow и FruitsIndex
- ./components/params/PaginationParams.yaml # здесь например общие повторящиеся описания параметров типа пагинации
Что делать с тем, что index по идее возвращает массив моделей из Show? Если это реально будет соблюдаться, можно сделать генерацию моделей и подстановку в постпроцессинге.
Но сейчас это не всегда так на самом деле. В индексе может быть, например, объект, где одно поле items, а другие например про пагинацию или какие-то доп поля.
Если у нас будет какое-то соглашение, то можно будет тоже это как-то автоматизировать, завернуть во что-то типа IndexWithPagination. Но наверно лучше привести к тому, чтобы index возвращал только коллекцию.
Возможно кстати для POST, PUT, DELETE даже в случае успеха схема компонента чтобы была стандартная, типа SuccessfulCreate, SuccessfulUpdate, SuccessfulDelete или т.п.
Когда речь идёт про POST, PUT намного важнее схема параметров, в т.ч. в body
При DELETE чаще особо ничего не важно наверно, удалили по id успешно, да и всё.
Ну и всегда схему можно доработать руками - Lobanov
же не будет её автоматически переписывать, но сможет контроллировать корректность
get:
summary: Get fruit reviews
responses:
200:
description: OK
content:
application/json:
schema:
"$ref": "../../../components/Fruits/item.yaml"
put:
summary: Edit deal
parameters:
- in: path
name: id
schema:
type: string
required: true
example: "1"
responses:
200:
description: OK
content:
application/json:
schema:
"$ref": "../../../components/Deals/Update/200.yaml"
422:
description: Unprocessable Entity
content:
application/json:
schema:
"$ref": "../../../components/Deals/Update/422.yaml"
В config/initializers/lobanov.rb
Lobanov.configure do |config|
config.specification_folder = 'openapi'
config.namespaces = {
'wapi' => 'wapi',
'api/v6' => 'private/v6',
}
end
With the above setup, Lobanov would work as following.
For api-calls to /wapi/any_resource
it would maintain a subdirectory openapi/wapi
with all the folders and files, describing this openapi schema.
For api-calls to /api/v6/any_resource
it would maintain an analogous subdirectory openapi/private/v6
.
For any other api-calls like /any/other/root
it would maintain openapi description in the root of openapi
folder.
For now we just use git tags like v0.1.1 and refer to them from Gemfile
We do not push to Rubygems now.
First of all, testing for lobanov is a bit intricated.
What we need to test is something like
If I have some rails app and some specs and some stored openapi schema
When I run rspec tests
What will be results of the specs run?
And how files will change?
To test this behavior we use Cucumber
scenarios.
To run these scenarios use bundle exec appraisal rails-61 cucumber
or bin/cucum your_test_app_name
To run scenarios for all test apps use bin/cucum all
Even more, we have to test lobanov with different versions of Rails.
Because the internals on which we rely to hook into rails routes may change.
To achieve this we use Appraisal gem
For now we only have rails-61 example app, but we have plans to support rails-7.
Execute bundle exec appraisal install
to setup appraisal.
And also we have RSpec specs to test Lobanov internals in unit-test fashion.
Run the test suite with rspec
And finally it may be useful to launch specs of generated test apps.
This may be handy for debug. If cucumber will encounter binding.pry during it's execution it will halt. So to debug you may launch test-app spec directly.
cd test_apps/rails_61
rspec spec/requests/fruits_controller_spec.rb
Bug reports and pull requests are welcome on GitHub at https://github.com/spajic/lobanov.