Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build deeply nested parameters #29

Merged
merged 2 commits into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions lib/rspec/openapi/schema_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def build_parameters(record)

record.path_params.each do |key, value|
parameters << {
name: key.to_s,
name: build_parameter_name(key, value),
in: 'path',
required: true,
schema: build_property(try_cast(value)),
Expand All @@ -60,7 +60,7 @@ def build_parameters(record)

record.query_params.each do |key, value|
parameters << {
name: key.to_s,
name: build_parameter_name(key, value),
in: 'query',
schema: build_property(try_cast(value)),
example: (try_cast(value) if example_enabled?),
Expand All @@ -71,6 +71,16 @@ def build_parameters(record)
parameters
end

def build_parameter_name(key, value)
key = key.to_s
if value.is_a?(Hash) && (value_keys = value.keys).size == 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1️⃣ build { "filter" => { "name" => "Benjamin" } } as filter[name] instead of filter

value_key = value_keys.first
build_parameter_name("#{key}[#{value_key}]", value[value_key])
else
key
end
end

def build_request_body(record)
return nil if record.request_content_type.nil?
return nil if record.request_params.empty?
Expand Down
12 changes: 10 additions & 2 deletions lib/rspec/openapi/schema_merger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ def normalize_keys(spec)
# Should we probably force-merge `summary` regardless of manual modifications?
def deep_reverse_merge!(base, spec)
spec.each do |key, value|
if base[key].is_a?(Hash) && value.is_a?(Hash)
deep_reverse_merge!(base[key], value)
base_key_value = base[key]
if base_key_value.is_a?(Hash) && value.is_a?(Hash)
deep_reverse_merge!(base_key_value, value)
elsif !base.key?(key)
base[key] = value
elsif base_key_value.is_a?(Array) && value.is_a?(Array)
if key == "parameters"
# merge arrays
base[key] |= value
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2️⃣ allow multiple parameter members to be built in the same request document

end
else
nil # no-op
end
end
base
Expand Down
34 changes: 29 additions & 5 deletions spec/rails/doc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ paths:
schema:
type: integer
example: 10
- name: filter[name]
in: query
schema:
type: object
properties:
name:
type: string
example:
name: Example Table
- name: filter[price]
in: query
schema:
type: object
properties:
price:
type: string
example:
price: '0'
responses:
'200':
description: returns a list of tables
Expand Down Expand Up @@ -63,7 +81,7 @@ paths:
database:
id: 2
name: production
null_sample:
null_sample:
storage_size: 12.3
created_at: '2020-07-17T00:00:00+00:00'
updated_at: '2020-07-17T00:00:00+00:00'
Expand Down Expand Up @@ -135,7 +153,7 @@ paths:
database:
id: 2
name: production
null_sample:
null_sample:
storage_size: 12.3
created_at: '2020-07-17T00:00:00+00:00'
updated_at: '2020-07-17T00:00:00+00:00'
Expand All @@ -151,6 +169,12 @@ paths:
schema:
type: integer
example: 1
- name: id
in: path
required: true
schema:
type: integer
example: 2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug when parameters are otherwise the same but have different examples

responses:
'200':
description: returns a table
Expand Down Expand Up @@ -188,7 +212,7 @@ paths:
database:
id: 2
name: production
null_sample:
null_sample:
storage_size: 12.3
created_at: '2020-07-17T00:00:00+00:00'
updated_at: '2020-07-17T00:00:00+00:00'
Expand Down Expand Up @@ -273,7 +297,7 @@ paths:
database:
id: 2
name: production
null_sample:
null_sample:
storage_size: 12.3
created_at: '2020-07-17T00:00:00+00:00'
updated_at: '2020-07-17T00:00:00+00:00'
Expand Down Expand Up @@ -325,7 +349,7 @@ paths:
database:
id: 2
name: production
null_sample:
null_sample:
storage_size: 12.3
created_at: '2020-07-17T00:00:00+00:00'
updated_at: '2020-07-17T00:00:00+00:00'
Expand Down
18 changes: 15 additions & 3 deletions spec/requests/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,21 @@

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)
context it 'returns a list of tables' do
it 'with flat query parameters' do
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
expect(response.status).to eq(200)
end

it 'with deep query parameters' do
get '/tables', params: { filter: { "name" => "Example Table" } }, headers: { authorization: 'k0kubun' }
Copy link
Collaborator

@k0kubun k0kubun Jun 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a guess, but maybe the test is passing ignoring this because the above test generates parameters, and running this probably doesn't override it. So copying the same endpoint to somewhere else and calling it from this spec might fix the test issue.

Copy link
Contributor Author

@bf4 bf4 Jun 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True enough, though I'd like to be able to generate a spec like

openapi: 3.0.3
info:
  title: rspec-openapi
  version: 1.0.0
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
      - name: "filter[name]"
        in: query
        schema:
          type: string
        example: Example Table
      - name: "filter[price]"
        in: query
        schema:
          type: integer
        example: 0
      responses:
        '200':
          description: returns a list of tables
          content:
            application/json:
              schema:
                anyOf:
                  - type: array
                  items:
                    type: object
                    # etc

it's been a while since I studied the oas spec, so perhaps I'm misunderstanding it...

https://swagger.io/docs/specification/describing-responses/

Can I have different responses based on a request parameter? Such as:
GET /something -> {200, schema_1}
GET /something?foo=bar -> {200, schema_2}

In OpenAPI 3.0, you can use `oneOf` to specify alternate schemas for the response and document possible dependencies verbally in the response description. However, there is no way to link specific schemas to certain parameter combinations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If I remove the 'with flat query parameters' spec and rm spec/rails/doc/openapi.yaml and run OPENAPI=1 bundle exec rspec spec/requests/rails_spec.rb it generates a new spec but still with only one param. )

Copy link
Collaborator

@k0kubun k0kubun Jun 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. That's something we should fix as well. But if you're not trying to address that, maybe we should separate that for now and possibly merge them in a PR that addresses the problem.

expect(response.status).to eq(200)
end

it 'with different deep query parameters' do
get '/tables', params: { filter: { "price" => 0 } }, headers: { authorization: 'k0kubun' }
expect(response.status).to eq(200)
end
end

it 'has a request spec which does not make any request' do
Expand Down