From b8dcc8fe30d9ed14c3cb18eb2ab67d7b976e6cf0 Mon Sep 17 00:00:00 2001 From: Jay Danielian Date: Sun, 21 Jul 2019 15:03:37 -0400 Subject: [PATCH] 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 --- .../lib/rswag/specs/example_group_helpers.rb | 65 ++++++++++++++++-- .../lib/rswag/specs/request_factory.rb | 5 +- test-app/app/controllers/blogs_controller.rb | 8 +++ test-app/config/routes.rb | 1 + test-app/spec/integration/blogs_spec.rb | 26 +++++++ test-app/spec/swagger_helper.rb | 10 +++ test-app/swagger/v1/swagger.json | 67 +++++++++++++++++++ 7 files changed, 171 insertions(+), 11 deletions(-) diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb index 9eaa7ba..b79d76f 100644 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_group_helpers.rb @@ -43,7 +43,7 @@ module Rswag # TODO: setup travis CI? # 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? # 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 @@ -75,12 +75,20 @@ module Rswag # 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 '$ref' key. Which points to #/components/examples/blog - if passed_examples.first.is_a?(Symbol) - example_key_name = passed_examples.first # can come up with better scheme here - # TODO: write more tests around this adding to the parameter - # 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 } - parameter(param_attributes) + passed_examples.each do |passed_example| + if passed_example.is_a?(Symbol) + example_key_name = passed_example + # TODO: write more tests around this adding to the parameter + # 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 } + 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 @@ -169,6 +177,45 @@ module Rswag metadata[:response][:examples] = example 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) # NOTE: rspec 2.x support 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] json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples] json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) } + example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples end end + + self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody] + end end end diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index adc673c..9fc20b3 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -152,12 +152,9 @@ module Rswag end 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 - # 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 ||= body_param[:param_value] source_body_param ? source_body_param.to_json : nil diff --git a/test-app/app/controllers/blogs_controller.rb b/test-app/app/controllers/blogs_controller.rb index 2719750..75c5ca7 100644 --- a/test-app/app/controllers/blogs_controller.rb +++ b/test-app/app/controllers/blogs_controller.rb @@ -19,6 +19,14 @@ class BlogsController < ApplicationController respond_with @blog 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 def upload @blog = Blog.find_by_id(params[:id]) diff --git a/test-app/config/routes.rb b/test-app/config/routes.rb index 83b9e90..e1fb12d 100644 --- a/test-app/config/routes.rb +++ b/test-app/config/routes.rb @@ -1,6 +1,7 @@ TestApp::Application.routes.draw do post '/blogs/flexible', to: 'blogs#flexible_create' + post '/blogs/alternate', to: 'blogs#alternate_create' resources :blogs put '/blogs/:id/upload', to: 'blogs#upload' diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index a3ec90c..8e3bccd 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -80,6 +80,32 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do 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 diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb index 4c1e7b8..06f597b 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/swagger_helper.rb @@ -69,6 +69,16 @@ RSpec.configure do |config| 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: { basic_auth: { type: :http, diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json index be26582..ed7938c 100644 --- a/test-app/swagger/v1/swagger.json +++ b/test-app/swagger/v1/swagger.json @@ -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}": { "get": { "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": { "basic_auth": { "type": "http",