spajic / lobanov

7 stars 2 forks source link

Lobanov Ruby Style Guide

Installation

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

Why Lobanov

There is a couple of similar projects (see the section and comparison below)

What I see as key strong / unique points of Lobanov:

Usage and Features

TODO

Flags

FORCE_LOBANOV

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.

UPDATE_TAGS

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.

Alternatives and related projects

expego/rspec-openapi

https://github.com/exoego/rspec-openapi

Overall very nice project, it has many things done just right.

Key differences:

Questions:

Some points:

rswag/rswag

https://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:

rspec-rails-swagger

TODO

rspec_api_documentation

r7kamura/autodoc

That was the inspiration for rspec-openapi

lurker

TODO

Что должен уметь Lobanov: checklist

Now

TODO: use this checklist for Features and Usage section

Later

Notes

Схема хранения

Вообще схемы хранения могут быть разные, но начнём с одной.

Пусть есть ресурс и вложенный ресурс, например пусть Фрукты и Отзывы на фрукты

И пусть ещё всё это в неймспейсе wapi/

И может быть ещё что-то не по 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"

Configuration

В 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.

Development

Publishing of new versions

For now we just use git tags like v0.1.1 and refer to them from Gemfile

We do not push to Rubygems now.

Specs

Cucumber

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

Appraisal

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.

RSpec unit-tests

And also we have RSpec specs to test Lobanov internals in unit-test fashion.

Run the test suite with rspec

Generated rspec tests for test apps

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

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/spajic/lobanov.