Adds support for proper requestBody examples. Adds mechanism to allow for adding additional ways to add request body examples

Can add externalValue or  it will work and produce valid swagger spec.

The Symbol name matching the let parameter is always required
This commit is contained in:
Jay Danielian 2019-07-21 15:03:37 -04:00
parent 4c2097e017
commit b8dcc8fe30
7 changed files with 171 additions and 11 deletions

View File

@ -43,7 +43,7 @@ module Rswag
# TODO: setup travis CI? # TODO: setup travis CI?
# MUST HAVES # MUST HAVES
# TODO: look at handling different ways schemas can be defined in 3.0 for requestBody and response # TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response
# can we handle all of them? # can we handle all of them?
# Then can look at handling different request_body things like $ref, etc # Then can look at handling different request_body things like $ref, etc
# TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0 # TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0
@ -75,12 +75,20 @@ module Rswag
# it can contain a 'value' key which is a direct hash (easiest) # it can contain a 'value' key which is a direct hash (easiest)
# it can contain a 'external_value' key which makes an external call to load the json # it can contain a 'external_value' key which makes an external call to load the json
# it can contain a '$ref' key. Which points to #/components/examples/blog # it can contain a '$ref' key. Which points to #/components/examples/blog
if passed_examples.first.is_a?(Symbol) passed_examples.each do |passed_example|
example_key_name = passed_examples.first # can come up with better scheme here if passed_example.is_a?(Symbol)
example_key_name = passed_example
# TODO: write more tests around this adding to the parameter # TODO: write more tests around this adding to the parameter
# if symbol try and use save_request_example # if symbol try and use save_request_example
param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema } param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema }
parameter(param_attributes) parameter(param_attributes)
elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema }
parameter(param_attributes)
elsif passed_example.is_a?(Hash) && passed_example['$ref']
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema }
parameter(param_attributes)
end
end end
end end
end end
@ -169,6 +177,45 @@ module Rswag
metadata[:response][:examples] = example metadata[:response][:examples] = example
end end
# checks the examples in the parameters should be able to add $ref and externalValue examples.
# This syntax would look something like this in the integration _spec.rb file
#
# request_body_json schema: { '$ref' => '#/components/schemas/blog' },
# examples: [:blog, {name: :external_blog,
# externalValue: 'http://api.sample.org/myjson_example'},
# {name: :another_example,
# '$ref' => '#/components/examples/flexible_blog_example'}]
# The first value :blog, points to a let param of the same name, and is used to make the request in the
# integration test (it is used to build the request payload)
#
# The second item in the array shows how to add an externalValue for the examples in the requestBody section
# The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
#
# NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
# will not show it yet
def merge_other_examples!(example_metadata)
# example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
content_node = example_metadata[:operation][:requestBody][:content]['application/json']
return unless content_node
external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {}
ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {}
examples_node = content_node[:examples] ||= {}
nodes_to_add = []
nodes_to_add << external_example unless external_example.empty?
nodes_to_add << ref_example unless ref_example.empty?
nodes_to_add.each do |node|
json_request_examples = examples_node ||= {}
other_name = node[:name][:name]
other_key = node[:name][:externalValue] ? :externalValue : '$ref'
if other_name
json_request_examples.merge!(other_name => {other_key => node[:param_value]})
end
end
end
def run_test!(&block) def run_test!(&block)
# NOTE: rspec 2.x support # NOTE: rspec 2.x support
if RSPEC_VERSION < 3 if RSPEC_VERSION < 3
@ -202,9 +249,13 @@ module Rswag
example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples] example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples] json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) } json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples
end end
end end
self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
end end
end end
end end

View File

@ -152,12 +152,9 @@ module Rswag
end end
def build_json_payload(parameters, example) def build_json_payload(parameters, example)
body_param = parameters.select { |p| p[:in] == :body }.first body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first
return nil unless body_param return nil unless body_param
# p "example is #{example.send(body_param[:name]).to_json} ** AND body param is #{body_param}" if body_param
# body_param ? example.send(body_param[:name]).to_json : nil
source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name]) source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name])
source_body_param ||= body_param[:param_value] source_body_param ||= body_param[:param_value]
source_body_param ? source_body_param.to_json : nil source_body_param ? source_body_param.to_json : nil

View File

@ -19,6 +19,14 @@ class BlogsController < ApplicationController
respond_with @blog respond_with @blog
end end
# POST /blogs/alternate
def alternate_create
# contrived example to show different :examples in the requestBody section
@blog = Blog.create(params.require(:blog).permit(:title, :content))
respond_with @blog
end
# Put /blogs/1 # Put /blogs/1
def upload def upload
@blog = Blog.find_by_id(params[:id]) @blog = Blog.find_by_id(params[:id])

View File

@ -1,6 +1,7 @@
TestApp::Application.routes.draw do TestApp::Application.routes.draw do
post '/blogs/flexible', to: 'blogs#flexible_create' post '/blogs/flexible', to: 'blogs#flexible_create'
post '/blogs/alternate', to: 'blogs#alternate_create'
resources :blogs resources :blogs
put '/blogs/:id/upload', to: 'blogs#upload' put '/blogs/:id/upload', to: 'blogs#upload'

View File

@ -80,6 +80,32 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
end end
end end
path '/blogs/alternate' do
post 'Creates a blog - different :examples in requestBody' do
tags 'Blogs'
description 'Creates a new blog from provided data'
operationId 'createAlternateBlog'
consumes 'application/json'
produces 'application/json'
# NOTE: the externalValue: http://... is valid 3.0 spec, but swagger-UI does NOT support it yet
# https://github.com/swagger-api/swagger-ui/issues/5433
request_body_json schema: { '$ref' => '#/components/schemas/blog' },
examples: [:blog, {name: :external_blog,
externalValue: 'http://api.sample.org/myjson_example'},
{name: :another_example,
'$ref' => '#/components/examples/flexible_blog_example'}]
let(:blog) { { blog: { title: 'alt title', content: 'alt bar' } } }
response '201', 'blog created' do
schema '$ref' => '#/components/schemas/blog'
run_test!
end
end
end
path '/blogs/{id}' do path '/blogs/{id}' do

View File

@ -69,6 +69,16 @@ RSpec.configure do |config|
required: %w[id headline] required: %w[id headline]
} }
}, },
examples: {
flexible_blog_example: {
summary: 'Sample example of a flexible blog',
value: {
id: 1,
headline: 'This is a headline',
text: 'Some sample text'
}
}
},
securitySchemes: { securitySchemes: {
basic_auth: { basic_auth: {
type: :http, type: :http,

View File

@ -258,6 +258,63 @@
} }
} }
}, },
"/blogs/alternate": {
"post": {
"summary": "Creates a blog - different :examples in requestBody",
"tags": [
"Blogs"
],
"description": "Creates a new blog from provided data",
"operationId": "createAlternateBlog",
"requestBody": {
"required": true,
"content": {
"application/json": {
"examples": {
"blog": {
"value": {
"blog": {
"title": "alt title",
"content": "alt bar"
}
}
},
"external_blog": {
"externalValue": "http://api.sample.org/myjson_example"
},
"another_example": {
"$ref": "#/components/examples/flexible_blog_example"
}
},
"schema": {
"$ref": "#/components/schemas/blog"
}
}
}
},
"parameters": [
],
"responses": {
"201": {
"description": "blog created",
"content": {
"application/json": {
"example": {
"id": 1,
"title": "alt title",
"content": "alt bar",
"thumbnail": null
},
"schema": {
"$ref": "#/components/schemas/blog"
}
}
}
}
}
}
},
"/blogs/{id}": { "/blogs/{id}": {
"get": { "get": {
"summary": "Retrieves a blog", "summary": "Retrieves a blog",
@ -435,6 +492,16 @@
] ]
} }
}, },
"examples": {
"flexible_blog_example": {
"summary": "Sample example of a flexible blog",
"value": {
"id": 1,
"headline": "This is a headline",
"text": "Some sample text"
}
}
},
"securitySchemes": { "securitySchemes": {
"basic_auth": { "basic_auth": {
"type": "http", "type": "http",