Coder Social home page Coder Social logo

rspec-openapi's Introduction

rspec-openapi Gem Version test codecov Ruby-toolbox

Generate OpenAPI schema from RSpec request specs.

What's this?

There are some gems which generate OpenAPI specs from RSpec request specs. However, they require a special DSL specific to these gems, and we can't reuse existing request specs as they are.

Unlike such existing gems, rspec-openapi can generate OpenAPI specs from request specs without requiring any special DSL. Furthermore, rspec-openapi keeps manual modifications when it merges automated changes to OpenAPI specs in case we can't generate everything from request specs.

Installation

Add this line to your application's Gemfile:

gem 'rspec-openapi', group: :test

Usage

Run rspec with OPENAPI=1 to generate doc/openapi.yaml for your request specs.

$ OPENAPI=1 bundle exec rspec

Example

Let's say you have a request spec like this:

RSpec.describe 'Tables', type: :request do
  describe '#index' do
    it 'returns a list of tables' do
      get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
      expect(response.status).to eq(200)
    end

    it 'does not return tables if unauthorized' do
      get '/tables'
      expect(response.status).to eq(401)
    end
  end

  # ...
end

If you run the spec with OPENAPI=1,

OPENAPI=1 rspec spec/requests/tables_spec.rb

It will generate doc/openapi.yaml file like:

openapi: 3.0.3
info:
  title: rspec-openapi
paths:
  "/tables":
    get:
      summary: index
      tags:
      - Table
      parameters:
      - name: page
        in: query
        schema:
          type: integer
        example: 1
      - name: per
        in: query
        schema:
          type: integer
        example: 10
      responses:
        '200':
          description: returns a list of tables
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    # ...

and the schema file can be used as an input of Swagger UI or Redoc.

Redoc example

Configuration

The following configurations are optional.

require 'rspec/openapi'

# Change the path to generate schema from `doc/openapi.yaml`
RSpec::OpenAPI.path = 'doc/schema.yaml'

# Change the output type to JSON
RSpec::OpenAPI.path = 'doc/schema.json'

# Or generate multiple partial schema files, given an RSpec example
RSpec::OpenAPI.path = -> (example) {
  case example.file_path
  when %r[spec/requests/api/v1/] then 'doc/openapi/v1.yaml'
  when %r[spec/requests/api/v2/] then 'doc/openapi/v2.yaml'
  else 'doc/openapi.yaml'
  end
}

RSpec::OpenAPI.title = 'OpenAPI Documentation'

# Disable generating `example`
RSpec::OpenAPI.enable_example = false

# Change `info.version`
RSpec::OpenAPI.application_version = '1.0.0'

# Set the info header details
RSpec::OpenAPI.info = {
  description: 'My beautiful API',
  license: {
    'name': 'Apache 2.0',
    'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'
  }
}

# Set request `headers` - generate parameters with headers for a request
RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]

# Set response `headers` - generate parameters with headers for a response
RSpec::OpenAPI.response_headers = %w[X-Cursor]

# Set `servers` - generate servers of a schema file
RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]

# Set `security_schemes` - generate security schemes
RSpec::OpenAPI.security_schemes = {
  'MyToken' => {
    description: 'Authenticate API requests via a JWT',
    type: 'http',
    scheme: 'bearer',
    bearerFormat: 'JWT',
  },
}

# Generate a comment on top of a schema file
RSpec::OpenAPI.comment = <<~EOS
  This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi

  When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will
  update this file automatically. You can also manually edit this file.
EOS

# Generate a custom description, given an RSpec example
RSpec::OpenAPI.description_builder = -> (example) { example.description }

# Generate a custom summary, given an RSpec example
# This example uses the summary from the example_group.
RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_group, :openapi, :summary) }

# Generate a custom tags, given an RSpec example
# This example uses the tags from the parent_example_group
RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }

# Change the example type(s) that will generate schema
RSpec::OpenAPI.example_types = %i[request]

# Configure which path params to ignore
# :controller and :action always exist. :format is added when routes is configured as such.
RSpec::OpenAPI.ignored_path_params = %i[controller action format]

# Configure which paths to ignore.
# You can exclude some specs via `openapi: false`.
# But, in a complex API usage scenario, you may need to include spec itself, but exclude some private paths.
# In that case, you can specify the paths to ignore.
# String or Regexp is acceptable.
RSpec::OpenAPI.ignored_paths = ["/admin/full/path/", Regexp.new("^/_internal/")]

# Your custom post-processing hook (like unrandomizing IDs)
RSpec::OpenAPI.post_process_hook = -> (path, records, spec) do
  RSpec::OpenAPI::HashHelper.matched_paths(spec, 'paths.*.*.responses.*.content.*.*.*.id').each do |paths|
    spec.dig(*paths[0..-2]).merge!(id: '123')
  end
end

Can I use rspec-openapi with $ref to minimize duplication of schema?

Yes, rspec-openapi v0.7.0+ supports $ref mechanism and generates schemas under #/components/schemas with some manual steps.

  1. First, generate plain OpenAPI file.
  2. Then, manually replace the duplications with $ref.
paths:
  "/users":
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
  "/users/{id}":
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
# Note) #/components/schemas is not needed to be defined.
  1. Then, re-run rspec-openapi. It will generate #/components/schemas with the referenced schema (User for example) newly-generated or updated.
paths:
  "/users":
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
  "/users/{id}":
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        role:
          type: array
          items:
            type: string

rspec-openapi also supports $ref in properties of schemas. Example)

paths:
  "/locations":
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Location"
components:
  schemas:
    Location:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        Coordinate:
          "$ref": "#/components/schemas/Coordinate"
    Coordinate:
      type: object
      properties:
        lat:
          type: string
        lon:
          type: string

Note that automatic schemas update feature is still new and may not work in complex scenario. If you find a room for improvement, open an issue.

How can I add information which can't be generated from RSpec?

rspec-openapi tries to preserve manual modifications as much as possible when generating specs. You can directly edit doc/openapi.yaml as you like without spoiling the automatic generation capability.

Can I exclude specific specs from OpenAPI generation?

Yes, you can specify openapi: false to disable the automatic generation.

RSpec.describe '/resources', type: :request, openapi: false do
  # ...
end

# or

RSpec.describe '/resources', type: :request do
  it 'returns a resource', openapi: false do
    # ...
  end
end

Customizations

Some examples' attributes can be overwritten via RSpec metadata options. Example:

  describe 'GET /api/v1/posts', openapi: {
    summary: 'list all posts',
    description: 'list all posts ordered by pub_date',
    tags: %w[v1 posts],
    required_request_params: %w[limit],
    security: [{"MyToken" => []}],
  } do
    # ...
  end

NOTE: description key will override also the one provided by RSpec::OpenAPI.description_builder method.

Experimental minitest support

Even if you are not using rspec this gem might help you with its experimental support for minitest.

Example:

class TablesTest < ActionDispatch::IntegrationTest
  openapi!

  test "GET /index returns a list of tables" do
    get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
    assert_response :success
  end

  test "GET /index does not return tables if unauthorized" do
    get '/tables'
    assert_response :unauthorized
  end

  # ...
end

It should work with both classes inheriting from ActionDispatch::IntegrationTest and with classes using Rack::Test directly, as long as you call openapi! in your test class.

Please note that not all features present in the rspec integration work with minitest (yet). For example, custom per test case metadata is not supported. A custom description_builder will not work either.

Run minitest with OPENAPI=1 to generate doc/openapi.yaml for your request specs.

$ OPENAPI=1 bundle exec rails t

Links

Existing RSpec plugins which have OpenAPI integration:

Acknowledgements

License

The gem is available as open source under the terms of the MIT License.

rspec-openapi's People

Contributors

alexeymatskevich avatar andyundso avatar as-alstar avatar bf4 avatar blocknotes avatar champierre avatar cyrusg avatar dannnylo avatar davelooi avatar dependabot[bot] avatar exoego avatar fruitriin avatar gobijan avatar hss-mateus avatar ipepe avatar jamerine avatar jorbs avatar k0kubun avatar ksol avatar kyoshidajp avatar mercedesb avatar natsuokawai avatar ohshita avatar oneiros avatar pbuckley avatar pcoliveira avatar tricknotes avatar uiur avatar yasuzuki avatar yykamei avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rspec-openapi's Issues

Nested parameters

Hi there.

First off, thanks for the great work.

I'm currently looking into the various options for generating documentation using our existing specs as the base.

It doesn't look like this library correctly respects the content type when generating parameters. Specifically, it doesn't wrap the parameters as described here.

It currently generates this for ?filter[read]=false:

parameters:
- name: filter
  in: query
  schema:
    type: object
    properties:
      read:
        type: string
  example:
  read: 'false'

I would expect it to generate this when the Content-Type and Accept headers indicate a JSON mimetype.

parameters:
- name: filter
  in: query
  content:
    application/json:
      schema:
        type: object
        properties:
          read:
            type: string
      example:
        read: 'false'

Am I misunderstanding something?

Best,
Emil

Generated Request Body for Path is using 422 Response

Hi There,

I have an odd issue. I have a request PATCH /api/app_configs/:id where I have a successful example and a failed example. No matter what order they are run in, the overall body for the request is always to body of the failed example which isn't a great experience for developers viewing the docs.

The generated swagger has the expected 200 & 422 responses but the overall request body for the example is always being pulled from the 422 test. I have tried re-ordering the tests in the spec file, deleting everything and rebuilding, but no luck.

Here is my test:

# frozen_string_literal: true

require 'rails_helper'

module Api
  RSpec.describe 'AppConfigs', type: :request do
    let(:account) { create(:account, projects: [project]) }
    let(:project) { create(:project) }
    let(:headers) { { 'Content-Type': 'application/json', 'X-Project-Id': project.id } }

    before { set_current_account(account) }

    describe 'PATCH /api/app_configs/:id', openapi: { summary: 'Update the Current App Config', tags: ['AppConfig'] } do
      let(:update_params) do
        {
          interface: 'interface AppConfig { foo: string, bar: number }',
          config: { foo: 'bar', bar: 1 }
        }
      end

      it 'Updates the current app config' do
        patch("/api/app_configs/#{project.app_config.id}", params: { app_config: update_params }.to_json, headers: headers)

        expect(response).to have_http_status :ok

        app_config_json = json_response['app_config']
        expect(app_config_json['interface']).to eq update_params[:interface]
        expect(app_config_json['config'].symbolize_keys).to eq update_params[:config]
      end

      context 'with invalid params' do
        let(:update_params) { { interface: nil, config: nil } }

        it 'Returns an error' do
          patch("/api/app_configs/#{project.app_config.id}", params: { app_config: update_params }.to_json, headers: headers)

          expect(response).to have_http_status :unprocessable_entity
          expect(json_response['errors']).to include('interface')
        end
      end
    end
  end
end

And here is the generated swagger:

---
openapi: 3.0.3
info:
  title: Vessel Platform API
  version: 1.0.0
  description: Vessel Platform API
servers: []
components:
  schemas:
    AppConfig:
      type: object
      properties:
        id:
          type: string
        config:
          type: object
          properties: {}
        interface:
          type: string
        project_id:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
paths:
  "/api/app_configs/{id}":
    patch:
      summary: Update the Current App Config
      tags:
      - AppConfig
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        example: ea4497f0-ea99-474b-ab69-1dbb7b7b91e7
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                app_config:
                  type: object
                  properties:
                    interface:
                      type: string
                      nullable: true
                    config:
                      type: object
                      properties:
                        foo:
                          type: string
                        bar:
                          type: integer
                      items:
                        type: integer
                      nullable: true
            example:
              app_config:
                interface:
                config:
      responses:
        '200':
          description: Updates the current app config
          content:
            application/json:
              schema:
                type: object
                properties:
                  app_config:
                    type: object
                    properties:
                      id:
                        type: string
                      config:
                        type: object
                        properties:
                          foo:
                            type: string
                          bar:
                            type: integer
                      interface:
                        type: string
                      project_id:
                        type: string
                      created_at:
                        type: string
                      updated_at:
                        type: string
              example:
                app_config:
                  id: 1c75a8e9-f32e-4913-bf34-585f5b78ad1c
                  config:
                    foo: bar
                    bar: 1
                  interface: 'interface AppConfig { foo: string, bar: number }'
                  project_id: 1100ef21-f88c-4dc8-89a7-4938a03a819a
                  created_at: '2023-08-16T15:03:13Z'
                  updated_at: '2023-08-16T15:03:13Z'
        '422':
          description: Returns an error
          content:
            application/json:
              schema:
                type: object
                properties:
                  errors:
                    type: object
                    properties:
                      interface:
                        type: array
                        items:
                          type: string
              example:
                errors:
                  interface:
                  - can't be blank

Here's a screenshot of the swagger UI

Screenshot 2023-08-16 at 10 08 21 AM

Evaluate proc for RSpec::OpenAPI.title

My use case is generating different openapi files from different RSpec request files. The RSpec::OpenAPI.path proc is great to export YAML to different files, but I was surprised to see I couldn't do the same with RSpec::OpenAPI.title

Unfortunately the following snippet generates: title: !ruby/object:Proc {}

RSpec::OpenAPI.title = -> (example) {
  return "Customer API" if example.file_path == "./spec/requests/customer_api_spec.rb"
  return "Reseller API" if example.file_path == "./spec/requests/resellers_api_spec.rb"

  "API"
}

More intelligent schema updates

https://github.com/k0kubun/rspec-openapi/blob/v0.4.8/lib/rspec/openapi/schema_merger.rb#L28

Background

Currently, we have two ways to use rspec-openapi:

  1. Delete the OpenAPI schema file and then run rspec-openapi for all tests to build it from scratch.
  2. Just run rspec-openapi and update auto-generated fields while leaving all past changes.

By default, it works in mode 2 because it allows you to incrementally update the schema and also leave manual changes for fields that rspec-openapi cannot automatically generate.

Problem

However, the default behavior, mode 2, sometimes has a problem. As described in #47, when you delete a parameter, even if you remove the parameter and rerun the same spec, it will not delete such obsoleted fields for you.

Proposed Solution

We could possibly build each endpoint from scratch, but leaving keys that rspec-openapi does NOT support. When you run a single spec that requests a route, everything under the path/method should be erased while leaving keys that rspec-openapi cannot update as is, and then changes generated by rspec-openapi should be added to it.

It does mean that you will need to run multiple specs for the same endpoint when some tests are testing only some subset of parameters and you need to run all of them to document everything, but generally, running a single spec file or a describe block should be enough and it's probably not too hard.

How to use the experimental description_builder

Hi there.

I'm back at trying this out, and I want to improve the explanation given in the documentation so it has more context than just explaining the request/response pattern.

I see that there is a description_builder feature. But I don't see any examples or guidance on how to use it. Where should I look?

Include property descriptions in response schema?

Hello. Is there a way to have response schemas display property descriptions, in addition to the name and type?

Currently, running OPENAPI=1 bundle exec rspec generates a file like:

openapi: 3.0.3
info:
  title: rspec-openapi
paths:
  "/tables":
    get:
      summary: index
      tags:
      - Table
      parameters:
      - name: page
        in: query
        schema:
          type: integer
        example: 1
      - name: per
        in: query
        schema:
          type: integer
        example: 10
      responses:
        '200':
          description: does something cool
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    resource:
                      type: string
                    # ...
Screen Shot 2023-10-25 at 14 41 44

I'd like to generate a file including property descriptions:

openapi: 3.0.3
info:
  title: rspec-openapi
paths:
  "/tables":
    get:
      summary: index
      tags:
      - Table
      parameters:
      - name: page
        in: query
        schema:
          type: integer
        example: 1
      - name: per
        in: query
        schema:
          type: integer
        example: 10
      responses:
        '200':
          description: does something cool
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                      description: 'Description of ID from the schema'
                    resource:
                      type: string
                      description: 'Description of name from the schema'
                    # ...
Screen Shot 2023-10-25 at 14 43 37

requestBody should not merge examples for error

Hey ๐Ÿ˜„
We are having some issues on a project where we have multiple tests for the same endpoint.

I prepared a PR to expose the situation: #139

Some notes:

  • the changes to spec/rails/app/controllers/tables_controller.rb simulate a validation error;
  • the test 'fails to create a table' covers a situation where a required parameter is missing;
  • the test 'fails to create a table (2)' covers a situation where a required parameter has an invalid value.

My points/doubts about the outcome:

  • in the create schema request, in the example, the name value gets overridden: name: k0kubun => name: some_invalid_name - so a value from the test 'fails to create a table' is mixed with 'returns a table' test;
  • name is not a required parameter anymore.

Is this behaviour expected?

I also tried using openapi: false to exclude specific tests but I think that it's not always possible to avoid the merging, especially if you want to document both good & bad paths (/status codes).

openapi-cli linter warnings

Hey :)
Checking the sample OpenAPI YAML with the linter there are some warnings.
It would be nice to handle them, WDYT? Can I try another PR?

Command

npx @redocly/openapi-cli lint spec/rails/doc/openapi.yaml

Output

Click to expand!
No configurations were defined in extends -- using built in recommended configuration by default.

validating spec/rails/doc/openapi.yaml...
[1] spec/rails/doc/openapi.yaml:10:1 at #/servers

Servers must be a non-empty array.

 8 |   title: rspec-openapi
 9 |   version: 1.0.0
10 | servers: []
11 | paths:
12 |   "/tables":

Error was generated by the no-empty-servers rule.


[2] spec/rails/doc/openapi.yaml:7:1 at #/info/description

Info object should contain `description` field.

5 | ---
6 | openapi: 3.0.3
7 | info:
8 |   title: rspec-openapi
9 |   version: 1.0.0

Warning was generated by the info-description rule.


[3] spec/rails/doc/openapi.yaml:7:1 at #/info

Info object should contain `license` field.

5 | ---
6 | openapi: 3.0.3
7 | info:
8 |   title: rspec-openapi
9 |   version: 1.0.0

Warning was generated by the info-license rule.


[4] spec/rails/doc/openapi.yaml:13:5 at #/paths/~1tables/get/operationId

Operation object should contain `operationId` field.

11 | paths:
12 |   "/tables":
13 |     get:
14 |       summary: index
15 |       tags:

Warning was generated by the operation-operationId rule.


[5] spec/rails/doc/openapi.yaml:34:17 at #/paths/~1tables/get/responses/200/content/application~1json/schema

Example validation errored: "nullable" cannot be used without "type".

32 | application/json:
33 |   schema:
34 |     type: array
35 |     items:
 โ€ฆ |     < 23 more lines >
59 |           type: string
60 |   example:
61 |   - id: 1

referenced from spec/rails/doc/openapi.yaml:33:15

Warning was generated by the no-invalid-media-type-examples rule.


[6] spec/rails/doc/openapi.yaml:106:5 at #/paths/~1tables/post/operationId

Operation object should contain `operationId` field.

104 |     example:
105 |       price: '0'
106 | post:
107 |   summary: create
108 |   tags:

Warning was generated by the operation-operationId rule.


[7] spec/rails/doc/openapi.yaml:126:7 at #/paths/~1tables/post/responses

Operation must have at least one `4xx` response.

124 |         description: description
125 |         database_id: 2
126 | responses:
127 |   '201':
128 |     description: returns a table

Warning was generated by the operation-4xx-response rule.


[8] spec/rails/doc/openapi.yaml:132:17 at #/paths/~1tables/post/responses/201/content/application~1json/schema

Example validation errored: "nullable" cannot be used without "type".

130 | application/json:
131 |   schema:
132 |     type: object
133 |     properties:
  โ€ฆ |     < 21 more lines >
155 |         type: string
156 |   example:
157 |     id: 1

referenced from spec/rails/doc/openapi.yaml:131:15

Warning was generated by the no-invalid-media-type-examples rule.


[9] spec/rails/doc/openapi.yaml:168:5 at #/paths/~1tables~1{id}/get/operationId

Operation object should contain `operationId` field.

166 |               updated_at: '2020-07-17T00:00:00+00:00'
167 | "/tables/{id}":
168 |   get:
169 |     summary: show
170 |     tags:

Warning was generated by the operation-operationId rule.


[10] spec/rails/doc/openapi.yaml:185:17 at #/paths/~1tables~1{id}/get/responses/200/content/application~1json/schema

Example validation errored: "nullable" cannot be used without "type".

183 | application/json:
184 |   schema:
185 |     type: object
186 |     properties:
  โ€ฆ |     < 21 more lines >
208 |         type: string
209 |   example:
210 |     id: 1

referenced from spec/rails/doc/openapi.yaml:184:15

Warning was generated by the no-invalid-media-type-examples rule.


[11] spec/rails/doc/openapi.yaml:305:5 at #/paths/~1tables~1{id}/delete/operationId

Operation object should contain `operationId` field.

303 |             created_at: '2020-07-17T00:00:00+00:00'
304 |             updated_at: '2020-07-17T00:00:00+00:00'
305 | delete:
306 |   summary: destroy
307 |   tags:

Warning was generated by the operation-operationId rule.


[12] spec/rails/doc/openapi.yaml:316:7 at #/paths/~1tables~1{id}/delete/responses

Operation must have at least one `4xx` response.

314 |     type: integer
315 |   example: 1
316 | responses:
317 |   '200':
318 |     description: returns a table

Warning was generated by the operation-4xx-response rule.


[13] spec/rails/doc/openapi.yaml:322:17 at #/paths/~1tables~1{id}/delete/responses/200/content/application~1json/schema

Example validation errored: "nullable" cannot be used without "type".

320 | application/json:
321 |   schema:
322 |     type: object
323 |     properties:
  โ€ฆ |     < 21 more lines >
345 |         type: string
346 |   example:
347 |     id: 1

referenced from spec/rails/doc/openapi.yaml:321:15

Warning was generated by the no-invalid-media-type-examples rule.


[14] spec/rails/doc/openapi.yaml:242:5 at #/paths/~1tables~1{id}/patch/operationId

Operation object should contain `operationId` field.

240 |           example:
241 |             message: not found
242 | patch:
243 |   summary: update
244 |   tags:

Warning was generated by the operation-operationId rule.


[15] spec/rails/doc/openapi.yaml:264:7 at #/paths/~1tables~1{id}/patch/responses

Operation must have at least one `4xx` response.

262 |       example:
263 |         image: test.png
264 | responses:
265 |   '200':
266 |     description: returns a table

Warning was generated by the operation-4xx-response rule.


[16] spec/rails/doc/openapi.yaml:270:17 at #/paths/~1tables~1{id}/patch/responses/200/content/application~1json/schema

Example validation errored: "nullable" cannot be used without "type".

268 | application/json:
269 |   schema:
270 |     type: object
271 |     properties:
  โ€ฆ |     < 21 more lines >
293 |         type: string
294 |   example:
295 |     id: 1

referenced from spec/rails/doc/openapi.yaml:269:15

Warning was generated by the no-invalid-media-type-examples rule.


[17] spec/rails/doc/openapi.yaml:370:5 at #/paths/~1images~1{id}/get/operationId

Operation object should contain `operationId` field.

368 |             no_content: 'true'
369 | "/images/{id}":
370 |   get:
371 |     summary: show
372 |     tags:

Warning was generated by the operation-operationId rule.


[18] spec/rails/doc/openapi.yaml:381:7 at #/paths/~1images~1{id}/get/responses

Operation must have at least one `4xx` response.

379 |     type: integer
380 |   example: 1
381 | responses:
382 |   '200':
383 |     description: returns a image payload

Warning was generated by the operation-4xx-response rule.


[19] spec/rails/doc/openapi.yaml:390:5 at #/paths/~1test_block/get/operationId

Operation object should contain `operationId` field.

388 |               format: binary
389 | "/test_block":
390 |   get:
391 |     summary: GET /test_block
392 |     tags: []

Warning was generated by the operation-operationId rule.


[20] spec/rails/doc/openapi.yaml:393:7 at #/paths/~1test_block/get/responses

Operation must have at least one `4xx` response.

391 | summary: GET /test_block
392 | tags: []
393 | responses:
394 |   '200':
395 |     description: returns the block content

Warning was generated by the operation-4xx-response rule.


spec/rails/doc/openapi.yaml: validated in 197ms

โŒ Validation failed with 1 error and 19 warnings.
run `openapi lint --generate-ignore-file` to add all problems to the ignore file.

Evaluations

  • Servers must be a non-empty array: we could add a server to the config spec;
  • Info object should contain description field / Info object should contain license field: an option that I think could be nice... a config method (like RSpec::OpenAPI.description_builder) to override parameters in default schema;
  • other warnings related to specific endpoints - we can think about them later.

Authorization HTTP header is inserted as parameter even though there is securityScheme defined

I have defined securitySchemes as follows:

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: AUTHORIZATION

But when I'm generating a schema file, seems like AUTHORIZATION header is being constantly suggested as parameter:

      parameters:
      - name: AUTHORIZATION
        in: header
        required: true
        schema:
          type: string
        example: AuthorizationExampleSecretKey

From my understanding this isn't necessary or even contradicts to OpenAPI specification. Can we somehow parse securitySchemes for headers that should not be added as parameters?

is the library rails specific?

I try to setup simple application written on Roda. For request test I use rspec + rack-test
My simple spec

describe "School", type: :request do
  ...
  it "return 200" do
    post "/school", {id: 1}.to_json, "CONTENT_TYPE" => "application/json"
    expect(last_response.status).to eq(200)
  end
end

When I run the tests, they pass successfully but nothing is generated and does not happen.

Allow the type of RSpec test to be configurable (not just :request)

Current Behaviour

All OpenAPI requests get run on the type: :request.

Desired Behaviour

We would like to be able to run on one or more arbitrary types, e.g. type: :flow. This would mean changing file
hooks.rb to be configurable.

This will allow us to generate our schema.yaml from our flow specs as well as from our request specs.

Hanami framework support

After updating the dependencies, I noticed that the parsed_body

def safe_parse_body(response)
response.parsed_body
rescue JSON::ParserError
nil
end
uses new code from action_dispatch/testing/request_encoder.rb.
https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/request_encoder.rb#L57

register_encoder :html, response_parser: -> body { Rails::Dom::Testing.html_document.parse(body) }

Since rspec-openapi depends on actionpack this leads to dependency on rails regardless of the rails-dom-testing installation.

My project uses Roda and Hanami and no rails.

Failures:

  1) #create-account NOT create new account
     Failure/Error: example.run

     NameError:
       uninitialized constant ActionDispatch::RequestEncoder::Rails
     # /usr/local/bundle/gems/actionpack-7.1.3.2/lib/action_dispatch/testing/request_encoder.rb:55:in `block in <class:RequestEncoder>'
     # /usr/local/bundle/gems/actionpack-7.1.3.2/lib/action_dispatch/testing/test_response.rb:50:in `parsed_body'
     # /usr/local/bundle/bundler/gems/rspec-openapi-5469c81f4671/lib/rspec/openapi/record_builder.rb:44:in `safe_parse_body'
     # /usr/local/bundle/bundler/gems/rspec-openapi-5469c81f4671/lib/rspec/openapi/record_builder.rb:34:in `build'
     # /usr/local/bundle/bundler/gems/rspec-openapi-5469c81f4671/lib/rspec/openapi/rspec_hooks.rb:8:in `block in <top (required)>'
     # ./spec/support/database_cleaner.rb:23:in `block (4 levels) in <top (required)>'
     # /usr/local/bundle/gems/database_cleaner-core-2.0.1/lib/database_cleaner/strategy.rb:30:in `cleaning'
     # ./spec/support/database_cleaner.rb:22:in `block (3 levels) in <top (required)>'
     # /usr/local/bundle/gems/database_cleaner-core-2.0.1/lib/database_cleaner/strategy.rb:30:in `cleaning'
     # ./spec/support/database_cleaner.rb:21:in `block (2 levels) in <top (required)>'
     # /usr/local/bundle/gems/webmock-3.23.0/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'

Can we support 'required' ?

Can we support 'required' ? Like this

requestBody:
  content:
    application/x-www-form-urlencoded:
      encoding: {}
      schema:
        required:
          - name

Suggestion for the readOnly property

I believe many people use id or uuid in their schematics. And always, this field is read-only.

I suggest to automatically set readOnly: true for such fields.

[FEATURE] Create multiple examples for multiple tests for same error code.

Great gem, thank you for sharing!

Here's a bug and a suggested feature to fix it.

The below is pseudo-code -- apologies if it doesn't work.

Steps to reproduce:

In your rspec tests, write something like:

context "when params have an issue, returns bad request" do
  it "notifies that params are missing" do
    post "/mypath"

    expect(response.code).to eq("400")
    expect(response.body).to eq("missing required parameter: param_a")
  end

  it "notifies that params do not match schema " do
    post "/mypath", with: {param_a: 123}

    expect(response.code).to eq("400")
    expect(response.body).to eq("param_a must be a string")
  end
end

Actual result:

It seems that only the response body received for the first or last test that is run is placed into examples. Because the test order is randomized, the output is therefore not deterministic. In other words, sometimes you'll see:

...
   example:
     "param_a must be a string"

and sometimes you'll see

...
  example: 
    "missing required parameter: param_a"

Expected result:

The output should be deterministic (ordered the same way every time, i.e. alphabetically) and should include all of the examples from tests.

i.e.

...
  description: when params have an issue, returns bad request
  examples:
    notifies_that_params_are_missing: -
      missing required parameter, param_a
    notifies_that_params_do_not_match_schema: -
      param_a must be a string

If you're open to it, I can work on a PR!

Thanks again!

`openapi: false` is not respected in some case (routing spec?)

I want to exclude some routing specs from OpenAPI.
According to README, one can exclude by adding openapi: false to specs, but it seems that exclusion does not work on some cases.

RSpec.describe Foo, type: :routing do
  it 'routes to foo#bar', openapi: false do
    expect(get('/foo/bar')).to route_to(controller: 'foo', action: 'bar')
  end
end

generates /foo/bar in openapi.yaml.

Versions

  • ruby: 2.7.6
  • rails: 7.0.3.1
  • rspec-openapi: 0.7.0

Error when executing POST json request

I am trying to generate the spec for a POST request that sends json message in the body and I getting the error below.

 ActionDispatch::Http::Parameters::ParseError:
            no implicit conversion of nil into String
          # /usr/local/bundle/gems/actionpack-6.0.3.3/lib/action_dispatch/http/parameters.rb:114:in `rescue in parse_formatted_parameters'
          # /usr/local/bundle/gems/actionpack-6.0.3.3/lib/action_dispatch/http/parameters.rb:110:in `parse_formatted_parameters'
          # /usr/local/bundle/gems/actionpack-6.0.3.3/lib/action_dispatch/http/request.rb:389:in `block in POST'
          # /usr/local/bundle/gems/rack-2.2.3/lib/rack/request.rb:69:in `fetch'
          # /usr/local/bundle/gems/rack-2.2.3/lib/rack/request.rb:69:in `fetch_header'
          # /usr/local/bundle/gems/actionpack-6.0.3.3/lib/action_dispatch/http/request.rb:388:in `POST'
          # /usr/local/bundle/gems/rspec-openapi-0.3.13/lib/rspec/openapi/record_builder.rb:94:in `raw_request_params'
          # /usr/local/bundle/gems/rspec-openapi-0.3.13/lib/rspec/openapi/record_builder.rb:41:in `build'
          # /usr/local/bundle/gems/rspec-openapi-0.3.13/lib/rspec/openapi/hooks.rb:12:in `block in <top (required)>'
          # ------------------
          # --- Caused by: ---
          # TypeError:
          #   no implicit conversion of nil into String
          #   /usr/local/bundle/gems/activesupport-6.0.3.3/lib/active_support/json/decoding.rb:23:in `decode'

this is my rspec:


      it 'create a user' do
        header 'CONTENT_TYPE', 'application/json'
        json_data = { name: 'juan'}.to_json
        post '/users', json_data, { 'Content-Type' => 'application/json' }
        expect(last_response.status).to eq(201)
      end

I notice that if I remove the fist line (header 'CONTENT_TYPE', 'application/json') then the execution completes successfully but the result yaml spec is wrong, it specifies request.content type as application/x-www-form-urlencoded instead of application/json.

I am running ruby 2.5.7p206 and my application is padrino application (not rails).

If anyone can give some guidance I my try to submit a fix.

Difference in Empty Values Handling in Examples

I've encountered an issue with the gem related to how empty values are handled in generated examples. This issue seems to be platform-specific, causing inconsistencies between Linux (in my case, Ubuntu LTS 20.04) and macOS (in my case, Ventura 13.4.1) systems.

On Linux systems, when generating examples, the gem inserts a space character instead of null values. However, on macOS, it doesn't insert anything in place of empty values. As a result, when generating examples on different platforms, we get different results.

This discrepancy not only creates unnecessary changes in the Git history but also blocks merge requests due to CI checks for example generation. My repository is hosted on an Ubuntu machine, which expects spaces in empty values. However, when generating examples on macOS, these spaces are missing.

I'm using version 0.7.2 of the gem, and I haven't found a fix for this issue in the release notes. If there has been a fix for this problem in subsequent updates, please let me know.

Running tests with OPENAPI=1 causes all request tests to fail

Hi there,

I'm hoping to use this gem to automatically document our api, a wonderful thing. However, when I run our tests with OPENAPI=1 rspec spec every request spec fails with No route matched for ..... These tests pass when running rspec without OPENAPI=1.

All I have done is install the gem into the :test group and bundle install

Ideas?

Mounting engine to root "/" causes "No route matched for" in some scenarios

When running OPENAPI=1 bundle exec rspec any routes that are listed after mounting engine to root mount Breaker::Engine => "/" will generate No route matched for RuntimeError:

Failures:

  1) Api::V1::TestersController#index returns hello
     Failure/Error: raise "No route matched for #{request.request_method} #{request.path_info}"
     
     RuntimeError:
       No route matched for GET /testers
     # /Users/trikic/.rvm/gems/ruby-3.0.3/gems/rspec-openapi-0.5.0/lib/rspec/openapi/record_builder.rb:90:in `find_rails_route'
     # /Users/trikic/.rvm/gems/ruby-3.0.3/gems/rspec-openapi-0.5.0/lib/rspec/openapi/record_builder.rb:84:in `block in find_rails_route'
     # /Users/trikic/.rvm/gems/ruby-3.0.3/gems/rspec-openapi-0.5.0/lib/rspec/openapi/record_builder.rb:81:in `find_rails_route'
     # /Users/trikic/.rvm/gems/ruby-3.0.3/gems/rspec-openapi-0.5.0/lib/rspec/openapi/record_builder.rb:21:in `build'
     # /Users/trikic/.rvm/gems/ruby-3.0.3/gems/rspec-openapi-0.5.0/lib/rspec/openapi/hooks.rb:14:in `block in <top (required)>'

Finished in 0.26384 seconds (files took 4.66 seconds to load)
1 example, 1 failure

Running bundle exec rspec does not generate such errors.

Here is a sample routes.rb:

  namespace :api do
    namespace :v1 do

      # gives errors
      # mount Breaker::Engine => "/", as: :breaker
      # mount Breaker::Engine => "/"

      # works
      # mount Breaker::Engine => "/breaker"

      resources :testers, only: [:index]

      # works
      mount Breaker::Engine => "/"
    end
  end

Full project that reproduces the error can be found at crnastena/openapi-test.

I guess this more of an enquiry as I am not sure if it is a problem with rspec-openapi or not. What do you think?

Minitest support

First of all a big thank you to everyone involved in this fantastic project. I used to use rspec_api_documentation, which is sadly no longer maintained, and I believe one big reason is the many output formats it supports. Focusing on OpenAPI is the right way to go imho, so please keep up the good work!

I finally have a project I would really like to try this gem out on, but sadly it is using Rails' built-in test facilities, i.e. minitest, not rspec. I do not think I can really sell converting the existing test suite to rspec, so I almost gave up. But then I got this crazy (?) idea and started looking into what it would take to make this minitest-compatible.

As far as I can see, the only rspec-specific code is in lib/rspec/openapi/hooks.rb. With this is mind, I quickly hacked together a proof of concept, to see if porting this to minitest is feasible: oneiros@5441904

It is really quick and dirty, but it does work, kind of ๐Ÿ˜ฌ

So now I wonder how I should proceed with this. I see the following possible paths:

  1. If you are interested in providing minitest support from within this gem, I could polish my solution some more and prepare a pull request.
  2. This gem could be split in two. One new gem providing the general facilities to generate OpenAPI files from data gathered during test runs. And this gem would then only provide the rspec specific stuff. Other people would be free to implement minitest-based solutions depending on the new gem.
  3. I could fork this gem, rename it and rip out everyting rspec-specific.

What do you think?

Referenced components in "items" not generated?

paths:
  "/foo/{id}":
    get:
      responses:
        '200':
          description: get Foo
          content:
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Foo"
      ...
components:
  schemas:
    Foo:
      type: object
      properties:
        id:
          type: string
        nested:
          type: object
          properties:
           bars:
              type: array
              items:
                "$ref": "#/components/schemas/Bar"

If we have something like the above, the missing component Bar should be generated, but it is not.
https://github.com/k0kubun/rspec-openapi#can-i-use-rspec-openapi-with-ref-to-minimize-duplication-of-schema

"No route matched for" error when using engine routes

Hey :)
In a project we have an engine which provides some routes.
When using rspec-openapi the routes seem to be missing => No route matched for GET /test is raised.

I reproduced the issue here: https://github.com/blocknotes/rspec-openapi/tree/issue-engine-routes/issue

  • app.rb is a one-file Rails app;
  • vendor/my_engine is a sample engine with a test route;
  • spec includes the request spec that I prepared.

You can try in the issue folder, running OPENAPI=1 rspec directly.

It could be similar to #25 - but there isn't an example there.

I'm also trying a fix here: blocknotes@1201edd

I saw that this line https://github.com/k0kubun/rspec-openapi/blob/master/lib/rspec/openapi/record_builder.rb#L79 recognize more than a route (it looks weird), but the first one is from the engine and let fail the method.

Using `RSpec::OpenAPI.path = ->` but for different audiences

Contextual snippet:

RSpec::OpenAPI.path = -> (example) {
  case example.file_path
  when %r[spec/requests/api/v1/] then 'doc/openapi/v1.yaml'
  when %r[spec/requests/api/v2/] then 'doc/openapi/v2.yaml'
  else 'doc/openapi.yaml'
  end
}

I really like the idea behind generating multiple schemas, but the current configuration causes a caveat that makes this feature challenging to use. (I didn't test this in practice but that is what I assume happens based on documentation)

It's because these different schema files, which might be targeted at different audiences are sharing common values for other configuration options like:

RSpec::OpenAPI.title = 'OpenAPI Documentation'
RSpec::OpenAPI.enable_example = false
RSpec::OpenAPI.application_version = '1.0.0'
RSpec::OpenAPI.info = {
  description: 'My beautiful API',
  license: {
    'name': 'Apache 2.0',
    'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'
  }
}
RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]
RSpec::OpenAPI.response_headers = %w[X-Cursor]
RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
RSpec::OpenAPI.security_schemes = {
  'MyToken' => {
    description: 'Authenticate API requests via a JWT',
    type: 'http',
    scheme: 'bearer',
    bearerFormat: 'JWT',
  },
}
RSpec::OpenAPI.comment = 'comment'
RSpec::OpenAPI.description_builder = -> (example) { example.description }
RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_group, :openapi, :summary) }
RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
RSpec::OpenAPI.example_types = %i[request]
RSpec::OpenAPI.ignored_path_params = %i[controller action format]

I don't have a suggestion on how this should be changed. But currently, I cannot generate schema for our frontend API team that is internal and second one for external and public API schema for our consumers.

Is it supported on Hanami?

I have a Hanami project (https://hanamirb.org/) and tried to use this gem, but the following error happens:

Failures:

  1) Companies #index returns a list of companies
     Failure/Error: get '/api/companies'
     
     NoMethodError:
       undefined method `get' for #<RSpec::ExampleGroups::Companies::Index:0x00007f8cc8b53a18>
       Did you mean?  gets
                      gem
     # ./spec/api/controllers/companies_controller_spec.rb:4:in `block (3 levels) in <top (required)>'

Can we override schema version to 3.0.0 ?

Hi,
First of all thanks for this wonderful gem. It makes life easy without learning another DSL for documentation.
Can we change the default version to 3.0.0 ? I see it is 3.0.3 in the source code.

Setting additionalProperties = false

When migrating from RSwag, I noticed that in many places we have additionalProperties = false, to ensure that the documentation is complete. Would it make sense to have a "strict mode" where additionalProperties are set by default to false in objects?

I imagine we could set this to false manually, but it could be convenient.

Support Minitest's parallel execution

This morning, We encountered a strange issue when integrating this gem with our Minitest test suite. Sometimes, the OpenAPI schema was generated, sometimes not. In the end, we discovered that no schema was generated when running all tests simultaneously, but it worked fine when only running a specific controller test.

Rails offers a helper named "parallelize" to run multiple tests simultaneously. However, this only comes into effect when running a more considerable amount of tests. Single tests are still executed in one process.

My assumption right now, which explains the phenomenon explained earlier, is when Rails starts parallelizing the tests, each process has its copy of RSpec::OpenAPI.path_records. The primary process does not execute any tests, and therefore, its version of the path records remains empty, and no OpenAPI schema is generated.

Parallel test execution is a native feature of Minitest. It would be nice if RSpec OpenAPI could support it.

Is there any way to define additionalProperties? or Free-Form Object

Hi, I have some rspec test with data:

    let(:data) {
      {
        description: {},
        type: "closed", 
        max_rating: 1,
        answer_options: [
          {id: first_answer_option.id, description: "description", correct: false},
          {id: second_answer_option.id, description: "NOT description", correct: true},
          {description: "New answer", correct: false}
        ]
      }
    }

When generating the documentation, I have this description for the descriopion field

            description:
              type: object
              properties: {}

The description field in my business is a field within which I pass data from the wysiwyg editor, and get an arbitrary object.
I'm trying to define it as a Free Form Object, according to this documentation: https://swagger.io/docs/specification/data-models/data-types/#object
I add this key additionalProperties: {} and remove this key properties: {}

When I run the generation, I get again properties: {} key.

Is there any way to deal with this?

I can try to make a contribution if required.

RFC: Update shared schemas in /components/schemas section

Summary

When rspec-openapi detects $ref in the existing item to be modified, rspec-openapi update the referenced item in components section.

Basic example

Let's say we have the openapi below, generated with rspec-openapi and modified manually to use $ref.

  "/tables":
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/Table"
              example:
              - (snip)
  "/tables/{id}":
    get:
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
        example: 1
      responses:
        '200':
          content:
             "$ref": "#/components/schemas/Table"
components:
  schemas:
      Table:
        type: object
        properties:
          id:
            type: integer
          name:
            type: string

If the implementation added a new field storage_size, Table in the #/components/schemas/ get updates.

Motivation

If there are "duplicated" schemas, such as Table in get /tables, post /tables andget /tables/{id}, some OpenAPI client generators generate separate types for each of duplicated schemas.
One example is openapi-typescript-codegen generating API client codes like below:

    public static getTables( ..snip.. ): CancelablePromise<Array<{
        id?: number;
        name?: string;
        description?: string;
        database?: {
            id?: number;
            name?: string;
        };
        ..snip..
    }>> {..snip..}

    public static postTables(..snip..): CancelablePromise<{
        id?: number;
        name?: string;
        description?: string;
        database?: {
            id?: number;
            name?: string;
        };
        ..snip..
    }> {..snip..}

    public static getTables1(id: number): CancelablePromise<{
        id?: number;
        name?: string;
        description?: string;
        database?: {
            id?: number;
            name?: string;
        };
        ..snip..
    }> {..snip.. }

In this example, there are three unnamed structural types that represent Table.
Those three types have the exact same type and are "compatible" if the type system at the client-side supports structural typing.
TypeScript does, but Java not.

There is another problem.
Since those types have no name, it is a bit tricky to refer the types in a nominal manner.
(One may use ReturnType<func> and other utility types).

Users may reduce duplications by manually lifting the schemas into /components/schemas/Table and using $refs with it.
However, /components/schemas/Table are not updated by rspec-openapi.
Users need to update /components/schemas/ by themselves, which diminishes the value of rspec-openapi somewhat.

If the schema in /components/schemas gets updated automatically, it helps leverage OpenAPI files with client-generating tools with strong types.

Related issues

Path params need to be required

Hi!

Swagger Editor complains about an error, that the generated path params need to be required.

Adding required: true in the generation, should fix it.

Can we use rspec context description stack for describe responses?

Example

context "when user send not match password do
    it "return some error" do
      post "/login", {password: "password"}

      expect(reponse.body).to eq(" ... some error ...")
    end
  end

It generate description: return some error in openapi.yaml
It would be more convenient to add a description from the entire description stack context.

context "when user ... foo" do
  ...
  context "but not ... bar" do
  ...
     it "return some error" do
  ...

For example, this would generate description: when user ... foo but not ... bar it return some error
Then we will have all the power of the rspec as a BDD in the API description.

Although it is possible use description into the response is not entirely correct, I lack a technical understanding in this matter.

Working with refs?

Hello again,
I know that this can be VERY tricky thing, but...

When I have existing specification with components schemas, like:

paths:
  "/api/incidents/{id}":
    get:
      summary: show
      tags:
      - API::Versioning
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
        example: 60
      - name: version
        in: query
        schema:
          type: integer
        example: 2
      responses:
        '200':
          description: returns incident as json

          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ticketStored"


components:
  schemas:
    ticketStored:
      type: object
      example:
          id: 12345
          vd_id: 12abc12-a123-1a23-1234-123a45bc67d8
          account_id: 1234
      properties:
        id:
          type: integer
          description: unique ID assigned automatically to each ticket
          format: int64
          readOnly: true
          example: 12345
          nullable: false
        vd_id:
          type: string
          format: "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
          description: Only used for tickets generated by algorithms
            and is used to backtrace a ticket to the data cluster that was used
            to generate it
          readOnly: true
          example: 12abc12-a123-1a23-1234-123a45bc67d8
          default:
          nullable: true
        account_id:
          type: integer
          format: int64
          description: unique numeric ID describing each account
          example: 1234
          nullable: false  
          
          .....

and run specs I get modified specification like:

paths:
  "/api/incidents/{id}":
    get:
      summary: show
      tags:
      - API::Versioning
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
        example: 60
      - name: version
        in: query
        schema:
          type: integer
        example: 2
      responses:
        '200':
          description: returns incident as json

          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ticketStored"
                type: object
                properties:
                  id:
                    type: integer
                  vd_id:
                    type: string
                  account_id:
                    type: integer


components:
  schemas:
    ticketStored:
      type: object
      example:
          id: 12345
          vd_id: 12abc12-a123-1a23-1234-123a45bc67d8
          account_id: 1234
      properties:
        id:
          type: integer
          description: unique ID assigned automatically to each ticket
          format: int64
          readOnly: true
          example: 12345
          nullable: false
        vd_id:
          type: string
          format: "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
          description: Only used for tickets generated by algorithms
            and is used to backtrace a ticket to the data cluster that was used
            to generate it
          readOnly: true
          example: 12abc12-a123-1a23-1234-123a45bc67d8
          default:
          nullable: true
        account_id:
          type: integer
          format: int64
          description: unique numeric ID describing each account
          example: 1234
          nullable: false  
          
          .....

So the definition of properties is actually doubled. And this can be useless and, for rich objects, can build extremely long specification.

Is there possibily to match object "tree" against defined schemas and pick the corresponding one? Whithout repeating itself.

Dealing with responses with different possible bodies for the same status code

While using this gem in one of my recents projects, I have faced the following issue:

A specific controller action can return a 200 and a 401 when the client is not authorized.
However, the 200 response can have very different bodies based on the headers that the client passes to the server (with very different, I mean completely different JSON structures).

I have written very comprehensive request specs, testing every possible response which can be returned by that controller action (basically, I have written a test for each possible 200 response, and the 401 response).

Currently rspec-openapi will generate an openapi.yaml with:

  • A 200 response, taken from the last example (I guess?) of the request specs for my controller
  • A 401 response

As of now, the only way I have found to add the other 200 responses is to manually edit the openapi.yaml file. Is there another way to achieve this behaviour maybe?

Thanks for your time, and thanks to the maintainers for this amazing gem โค๏ธ ๐Ÿ˜ƒ

"format" in path parameter

Hello. Thank you for such a useful gem!

I have a problem.

When I make a request in json format, the path parameter should contain a key named format, so the generated OpenAPI schema has a key named format as a path parameter.
So the generated OpenAPI schema has a format field in the path parameters list.
Since I don't use this field as a variable in the actual path, the error occurs because there is no format in the URL when loaded by swagger-editor.

Semantic error at paths./api/v1/xxxxx.get.parameters.0.name
Path parameter "format" must have the corresponding {format} segment in the "/api/v1/xxxxx" path

Is there a problem with skipping the format in lib/rspec/openapi/schema_builder.rb:39 in the same way as the action and controller?

Ideia: CI schema merger

Given the following steps:

A) Assuming we only generate schemas (without examples), then when running locally we could run only the files we care (where there were changes), and this would patch the existing schema (append mode).

B) In big project, one usually configures CI to run test accoss multiple nodes.
If CI would generate a swagger file each run then we need an ability to join files.
Would be nice to have cli/rake to join all files, and compile into a single file.

C) If A) and B) are addressed, then we could add a CI check to test if schema of A) matches schema of B), and fail the CI with the diff.

This would make sure our documentation is up to date - that removed routes are not present, as well as removed attributes, etc.

Wdyt?

Issue with nested file uploads

Hey :)
I get this linting error with an endpoint that receives an uploaded file: unknown tag !<!ruby/object:File>

The spec is structured in this way:

    let(:valid_attributes) do
      {
        look_media_attachment: {
          visible: true,
          caption: 'This is the caption text',
          image: fixture_file_upload("#{Rails.root}/spec/fixtures/images/test_image.jpg", 'image/jpeg')
        }
      }
    end

    it do
      post "/api/v1/look_media_attachments.json", params: valid_attributes, headers: auth_credentials
      # ...
    end

The failing output is the following (the error is pointing to the line tmpfile: &1 !ruby/object:File {} below):

      # ...
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                look_media_attachment:
                  type: object
                  properties:
                    visible:
                      type: string
                    caption:
                      type: string
                    image:
                      type: string
                      format: binary
            example:
              look_media_attachment:
                visible: 'true'
                caption: This is the caption text
                image: !ruby/object:ActionDispatch::Http::UploadedFile
                  tempfile: !ruby/object:Tempfile
                    unlinked: true
                    mode: 2562
                    tmpfile: &1 !ruby/object:File {}
                    opts:
                      :perm: 384
                    delegate_dc_obj: *1
                  original_filename: test_image.jpg
                  content_type: image/jpeg
                  headers: "Content-Disposition: form-data; name=\"look_media_attachment[image]\";
                    filename=\"test_image.jpg\"\r\nContent-Type: image/jpeg\r\nContent-Length:
                    74165\r\n"
      # ...

Investigating a bit...

I think that the problem is here: schema_builder.rb:161
In my example value includes another level look_media_attachment:

{"look_media_attachment"=>
  {"visible"=>"true",
   "caption"=>"This is the caption text",
   "image"=>
    #<ActionDispatch::Http::UploadedFile:0x00007ff2b647c088
     @content_type="image/jpeg",
     @headers="Content-Disposition: form-data; name=\"look_media_attachment[image]\"; filename=\"test_image.jpg\"\r\nContent-Type: image/jpeg\r\nContent-Length: 74165\r\n",
     @original_filename="test_image.jpg",
     @tempfile=#<File:/var/folders/vm/jtxmp1t120b57cfnv0q322kr0000gn/T/RackMultipart20220326-18892-dzbg74.jpg (closed)>>}}

Perhaps in build_example the value hash should be traversed recursively.

Getting invalid byte sequence in UTF-8 when the response is a pdf

I was giving a try to rspec-openapi and after trying I'm getting an error when returning a pdf attachment like this:

      respond_to do |format|
        format.pdf do
          # params[:content] can be a plain text string like "foo"
          pdf = Princely::Pdf.new.pdf_from_string(params[:content]) 
          # pdf is a String that looks like
          # "%PDF-1.5\n%\xE2\xE3\xCF\xD3\n\n1 0 obj\n<</Type /Catalog\n/Pages 2 0 R\n/Outlines 5 0 R>>\nendobj\n\n6 0 obj\n<</Length 16 0 R\n/Filter /FlateDecode\n/Type /ObjStm\n/N 10\n/First 63>>\nstream\nx\x9C\x8D\x91OK\...
          send_data pdf,
            type: 'application/pdf',
            filename: filename,
            disposition: 'attachment'

I get this error:

An error occurred in an `after(:suite)` hook.
Failure/Error: elsif o =~ /\n(?!\Z)/  # match \n except blank line at the end of string

ArgumentError:
  invalid byte sequence in UTF-8
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:268:in `=~'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:268:in `visit_String'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:136:in `accept'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:175:in `block in visit_Struct'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:173:in `each'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:173:in `visit_Struct'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:136:in `accept'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/visitors/yaml_tree.rb:118:in `push'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych.rb:512:in `dump'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/3.2.0/psych/core_ext.rb:13:in `to_yaml'
# /Users/david/.local/share/rtx/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-openapi-0.8.1/lib/rspec/openapi/result_recorder.rb:22:in `block (4 levels) in record_results!'

How could I work around this? I was looking how to overwrite a part of this endpoints' specification but didn't manage.

Preserving a predictable output

Hey :)
Using this gem we noticed that having predictable data in different run can be useful if you are going to regenerate the OpenAPI documentation (fully or partially).

To achieve this goal we are using this configuration block:

if ENV['OPENAPI'].present?
  seed = ENV.fetch('OPENAPI_SEED', 0xFFFF)
  srand(seed)
  Faker::Config.random = Random.new(seed) if defined? Faker

  RSpec.configure do |config|
    config.order = :defined

    config.before do
      allow(Devise).to receive(:friendly_token) { ('A'..'Z').to_a.sample(20).join } if defined? Devise

      ActiveRecord::Base.connection.tables.each do |t|
        # to preserve IDs
        ActiveRecord::Base.connection.reset_pk_sequence!(t)
      end
    end

    config.include ActiveSupport::Testing::TimeHelpers

    config.around do |example|
      time = Time.zone.local(2022, 10, 15, 12, 34, 56)
      travel_to(time) # to preserve dates & times
      example.run
      travel_back
    end
  end
end

Perhaps these kind of options shouldn't be included in the gem (they are configuration) but they could be useful in the README to help others.

WDYT?

Keep the schema synced when a request test is modified or even removed?

Hey :)

I see that:

  • when a new parameter is added to a request spec the schema is updated;
  • when an existing parameter is removed the schema remains the same;
  • when a test case is removed the schema remains the same.

An example is shown here: https://github.com/k0kubun/rspec-openapi/compare/master...blocknotes:merge-strategies?expand=1

  1. I modified the test from get '/tables', params: { page: '1', per: '10' } to get '/tables', params: { page: '1', scope: 'recent' } - the new scope parameter is added but per is not removed
  2. I modified the spec to remove the #show test but the schema is not updated

I'm using OPENAPI=1 bundle exec rspec spec/requests/rails_spec.rb to regenerate the schema.

Shouldn't the schema be kept in sync also in these situations?

strange behavior with GET param

Hello,
thank for the gem, it 's helpfull.

I have problem with spec like:

  let(:account) { Account.last }
  let(:authorized_admin) { account.supervisors.first }
  let(:incident) { account.incidents.last }

  describe "GET /api/incidents/:id" do
    context "when user is NOT authorized" do
      it "Kick off unauthenticated/unauthorized user" do
        get "/api/incidents/#{incident.id}", headers: valid_headers.except("Authorization"), params: { version: 2 }

        expect(response.body).to eq "You can not request this API until valid Authorization token is provided"

        get "/api/incidents/#{incident.id}", headers: valid_headers.merge({ "Authorization" => "Bearer InvalidToken" })

        expect(response.body).to eq "You can not request this API until valid Authorization token is provided"
      end
    end

    context "when user is authorized" do
      context "when incident exists" do
        it "returns incident as json" do
          expected_response_body = incident_to_json(incident)

          get "/api/incidents/#{incident.id}", headers: valid_headers, params: { version: 2 }

          expect(response).to have_http_status(:ok)
          expect(response.headers["Content-Type"]).to eq("application/json; charset=utf-8")
          expect(response.body).to eq expected_response_body
        end
      end

      context "when incident do not exists at all" do
        it "returns 404" do
          incident_id = Incident.maximum(:id) + 1

          get "/api/incidents/#{incident_id}", headers: valid_headers, params: { version: 2 }

          expect(response).to have_http_status(:not_found)
        end
      end
    end
  end

When run with OPENAPI=1 it creates this specification (shortened):

---
openapi: 3.0.3
info:
  title: some  app
  version: 2.0.0
paths:
  "/api/incidents/{id(}/{version)}":
    get:
      summary: show
      tags:
      - API::Versioning
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
        example: 60
      - name: version
        in: query
        schema:
          type: integer
        example: 2
      responses:
        '200':
           ..... all attributes  and so on.....

See "/api/incidents/{id(}/{version)}": path.
I am expecting "/api/incidents/{id}"path and listedversionparameter inparameters` key (it is,correctly, there).

Do I do something wrong?

Regenerating (remove file and run rspec) the schema without changing the code, generates different file.

Regenerating (remove file and run rspec) the schema without changing the code, generates different file. I assume this is because RSpec is executing tests in different order each time and this causes schema to be generated differently. Would be nice to have some counter-measures for this like:

  • http status code keys are ordered in ascending manner
    • this should prevent code 400 and 500 to be in different order each time based on rspec randomization of order of tests
  • any schema keys are ordered alphabetically
    • this should prevent situations where payload is being extended by one or other test

Manually edited oneOf in response schema is overridden

I'm evaluating this gem for a project I'm working on and it looks much nicer than rswag. Well done! ๐Ÿ‘๐Ÿป

Sorry for the long message that follows.

Now, my Rails app has a model that itself has a property that can be an instance of several different sub-models. The JSON for this can look something like:

{
  "name": "some_name",
  "data": {
    "email": "[email protected]"
  }
}

or:

{
  "name": "some_name",
  "data": {
    "selfie_url": "https://somewhere.com/public/image.jpg"
  }
}

I have request specs that return both variants of this model. By default rspec-openapi generates an OpenAPI spec that looks something like this:

components:
  schemas:
    MyModel:
      type: object
      properties:
        name:
          type: string
        data:
          type: object
          properties:
            email:
              nullable: true
            selfie_url:
              nullable: true

Which is fine but not quite what I want. I'd like to use the oneOf keyword to differentiate the response types. So I edit my OpenAPI spec to look like this:

components:
  schemas:
    MyModel:
      type: object
      properties:
        name:
          type: string
        data:
          oneOf:
            - type: object
              properties:
                email:
                  type: string
                  format: email
            - type: object
               properties:
                 selfie_url:
                   type: string
                   format: uri

But when I rerun the spec generation, rspec-openapi alters the spec to this:

components:
  schemas:
    MyModel:
      type: object
      properties:
        name:
          type: string
        data:
          oneOf:
            - type: object
              properties:
                email:
                  type: string
                  format: email
            - type: object
               properties:
                 selfie_url:
                   type: string
                   format: uri
            type: object
            properties:
              email:
                nullable: true
              selfie_url:
                nullable: true

Notice that it keeps my oneOf: but adds back the sub-model with all properties being nullable. Is this a known limitation or something I'm doing wrong?

API schema that is not subject to rspec execution is deleted.

When running OPENAPI=1 rspec spec/requests/samples_spec.rb:15 is run, the schema that is not under test is also deleted.

request spec

describe 'Sample API TEST', :api_admin, type: :request do
  let(:headers) { { headersample: "headersample" } }
  let(:sample) { create(:sample, name: "srockstyle") }
  let(:params) { { } }

  before do
    restaurant
  end
  it 'Test A' do
    get "/v1/sample", headers: headers, params: params
    expect(response).to(have_http_status(200))
  end

  it 'Test B' do
    get "/v1/sample/#{sample.id}", headers: headers, params: params
    expect(response).to(have_http_status(200))
  end
end

openapi.yaml created after running OPENAPI=1 rspec spec/requests/samples_spec.rb

---
  openapi: 3.0.3
  info:
    title: app
    version: 1.0.0
  servers: []
  paths:
    "/v1/samples":
      post:
        summary: index
        tags:
        - Sample
        parameters:
          - name: id
            in: path
            required: true
            schema:
              type: integer
            example: 1
        responses:
          '200':
            description: Index sample
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                example:
                - id: 1
                  name: srockstyle
                  job: sample
    "/v1/samples/{id}":
      post:
        summary: index
        tags:
        - Sample
        responses:
          '200':
            description: Index sample
            content:
              application/json:
                schema:             
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                example:
                  id: 1
                  name: srockstyle
                  job: sample

Then spec was specified on a line and the command was executed OPENAPI=1 rspec spec/requests/samples_spec.rb:10,
yaml left only the specified parts and deleted the parts that were not specified.

---
  openapi: 3.0.3
  info:
    title: app
    version: 1.0.0
  servers: []
  paths:
    "/v1/samples":
      post:
        summary: index
        tags:
        - Sample
        parameters:
          - name: id
            in: path
            required: true
            schema:
              type: integer
            example: 1
        responses:
          '200':
            description: Index sample
            content:
              application/json:
                schema:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                example:
                - id: 1
                  name: srockstyle
                  job: sample

I expect the following behavior, are there any options?

  • Do not delete schema that is not to be executed
  • get request" is processed as "post request", so we want to treat it as "get".

undefined method `script_name' for nil:NilClass

Hi. When I type RAILS_ENV=test OPENAPI=1 bundle exec rspec [spec file path] the following error happens.

  NoMethodError:
    undefined method `script_name' for nil:NilClass
  # /usr/local/bundle/gems/rspec-openapi-0.3.12/lib/rspec/openapi/record_builder.rb:64:in `find_rails_route'
  # /usr/local/bundle/gems/rspec-openapi-0.3.12/lib/rspec/openapi/record_builder.rb:19:in `build'
  # /usr/local/bundle/gems/rspec-openapi-0.3.12/lib/rspec/openapi/hooks.rb:12:in `block in <top (required)>'

The following is the content of the target test file.

require 'rails_helper'

RSpec.describe V2::Admin::NotificationsController, type: :request do
  describe 'GET /v2/admin/notifications' do
    subject { get v2_admin_notifications_url, params: params }
    let!(:users) { create_list(:user) }
    let(:params) { nil }

    it { is_expected.to be 200 }
  end
end

I found that if I do not use create_list, no error occurs.

gem version: 0.3.12

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.