diff --git a/.gitignore b/.gitignore index d7a22a8..e0ab798 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ **/*/node_modules *.swp Gemfile.lock +/.idea/ +**/test-app/.byebug_history diff --git a/.ruby-version b/.ruby-version index 2bf1c1c..73462a5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.5.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 618eda5..9a20885 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,11 +11,16 @@ Set up your machine: ``` bundle -cd spec/dummy +cd test-app bundle exec rake db:setup cd - ``` +Initialize the rswag-ui repo with assets. +``` +ci/build.sh +``` + Make sure the tests pass: ``` diff --git a/Gemfile b/Gemfile index bbd3d73..ba8c5fd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,12 @@ -source "https://rubygems.org" +# frozen_string_literal: true + +source 'https://rubygems.org' # Allow the rails version to come from an ENV setting so Travis can test multiple versions. # See http://www.schneems.com/post/50991826838/testing-against-multiple-rails-versions/ rails_version = ENV['RAILS_VERSION'] || '5.1.2' -gem 'rails', "#{rails_version}" +gem 'rails', rails_version.to_s case rails_version.split('.').first when '3' @@ -13,23 +15,29 @@ when '4', '5' gem 'responders' end -gem 'sqlite3' +gem 'sqlite3', '~> 1.3.6' -gem 'rswag-api', path: './rswag-api' -gem 'rswag-ui', path: './rswag-ui' +gem 'open_api-rswag-api', path: './rswag-api' +gem 'open_api-rswag-ui', path: './rswag-ui' group :test do - gem 'test-unit' - gem 'rspec-rails' - gem 'generator_spec' gem 'capybara' gem 'capybara-webkit' - gem 'rswag-specs', path: './rswag-specs' + gem 'generator_spec' + gem 'rspec-rails' + gem 'open_api-rswag-specs', path: './rswag-specs' + gem 'test-unit' +end + +group :development do + gem 'guard-rspec', require: false + gem 'open_api-rswag-specs', path: './rswag-specs' + gem 'rubocop' end group :assets do - gem 'uglifier' gem 'therubyracer' + gem 'uglifier' end gem 'byebug' diff --git a/README.md b/README.md index 0ac0e3e..0c230d3 100644 --- a/README.md +++ b/README.md @@ -532,3 +532,16 @@ bundle exec rake rswag:ui:copy_assets[public/api-docs] ``` __NOTE:__: The provided subfolder MUST correspond to the UI mount prefix - "api-docs" by default. + + +Notes to test swagger output locally with swagger editor +``` +docker pull swaggerapi/swagger-editor +``` +``` +docker run -d -p 80:8080 swaggerapi/swagger-editor +``` +This will run the swagger editor in the docker daemon and can be accessed +at ```http://localhost```. From here, you can use the UI to load the generated swagger.json to validate the output. + + diff --git a/rswag-api/bin/rails b/rswag-api/bin/rails index 1ef582f..28e3103 100755 --- a/rswag-api/bin/rails +++ b/rswag-api/bin/rails @@ -2,7 +2,7 @@ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/rswag/api/engine', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/open_api/rswag/api/engine', __FILE__) # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) diff --git a/rswag-api/lib/generators/rswag/api/install/USAGE b/rswag-api/lib/generators/rswag/api/install/USAGE index 87b8bc5..b5b56cc 100644 --- a/rswag-api/lib/generators/rswag/api/install/USAGE +++ b/rswag-api/lib/generators/rswag/api/install/USAGE @@ -5,4 +5,4 @@ Example: rails generate rswag:api:install This will create: - config/initializers/rswag-api.rb + config/initializers/rswag_api.rb diff --git a/rswag-api/lib/generators/rswag/api/install/install_generator.rb b/rswag-api/lib/generators/rswag/api/install/install_generator.rb index 744f2b1..f76d064 100644 --- a/rswag-api/lib/generators/rswag/api/install/install_generator.rb +++ b/rswag-api/lib/generators/rswag/api/install/install_generator.rb @@ -7,7 +7,7 @@ module Rswag source_root File.expand_path('../templates', __FILE__) def add_initializer - template('rswag-api.rb', 'config/initializers/rswag-api.rb') + template('rswag_api.rb', 'config/initializers/rswag_api.rb') end def add_routes diff --git a/rswag-api/lib/generators/rswag/api/install/templates/rswag-api.rb b/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb similarity index 94% rename from rswag-api/lib/generators/rswag/api/install/templates/rswag-api.rb rename to rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb index 5f3ddc4..28d4297 100644 --- a/rswag-api/lib/generators/rswag/api/install/templates/rswag-api.rb +++ b/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb @@ -1,4 +1,4 @@ -Rswag::Api.configure do |c| +OpenApi::Rswag::Api.configure do |c| # Specify a root folder where Swagger JSON files are located # This is used by the Swagger middleware to serve requests for API descriptions diff --git a/rswag-api/lib/open_api/rswag/api.rb b/rswag-api/lib/open_api/rswag/api.rb new file mode 100644 index 0000000..a678900 --- /dev/null +++ b/rswag-api/lib/open_api/rswag/api.rb @@ -0,0 +1,19 @@ +module OpenApi + +end +require 'open_api/rswag/api/configuration' +require 'open_api/rswag/api/engine' + +module OpenApi + module Rswag + module Api + def self.configure + yield(config) + end + + def self.config + @config ||= Configuration.new + end + end + end +end diff --git a/rswag-api/lib/open_api/rswag/api/configuration.rb b/rswag-api/lib/open_api/rswag/api/configuration.rb new file mode 100644 index 0000000..eae3987 --- /dev/null +++ b/rswag-api/lib/open_api/rswag/api/configuration.rb @@ -0,0 +1,14 @@ +module OpenApi + module Rswag + module Api + class Configuration + attr_accessor :swagger_root, :swagger_filter + + def resolve_swagger_root(env) + path_params = env['action_dispatch.request.path_parameters'] || {} + path_params[:swagger_root] || swagger_root + end + end + end + end +end diff --git a/rswag-api/lib/open_api/rswag/api/engine.rb b/rswag-api/lib/open_api/rswag/api/engine.rb new file mode 100644 index 0000000..9e7f524 --- /dev/null +++ b/rswag-api/lib/open_api/rswag/api/engine.rb @@ -0,0 +1,15 @@ +require 'open_api/rswag/api/middleware' + +module OpenApi + module Rswag + module Api + class Engine < ::Rails::Engine + isolate_namespace OpenApi::Rswag::Api + + initializer 'rswag-api.initialize' do |app| + middleware.use OpenApi::Rswag::Api::Middleware, OpenApi::Rswag::Api.config + end + end + end + end +end diff --git a/rswag-api/lib/open_api/rswag/api/middleware.rb b/rswag-api/lib/open_api/rswag/api/middleware.rb new file mode 100644 index 0000000..d7f791b --- /dev/null +++ b/rswag-api/lib/open_api/rswag/api/middleware.rb @@ -0,0 +1,39 @@ +require 'json' + +module OpenApi + module Rswag + module Api + class Middleware + + def initialize(app, config) + @app = app + @config = config + end + + def call(env) + path = env['PATH_INFO'] + filename = "#{@config.resolve_swagger_root(env)}/#{path}" + + if env['REQUEST_METHOD'] == 'GET' && File.file?(filename) + swagger = load_json(filename) + @config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil? + + return [ + '200', + { 'Content-Type' => 'application/json' }, + [ JSON.dump(swagger) ] + ] + end + + return @app.call(env) + end + + private + + def load_json(filename) + JSON.parse(File.read(filename)) + end + end + end + end +end diff --git a/rswag-api/lib/rswag/api.rb b/rswag-api/lib/rswag/api.rb deleted file mode 100644 index 894072b..0000000 --- a/rswag-api/lib/rswag/api.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rswag/api/configuration' -require 'rswag/api/engine' - -module Rswag - module Api - def self.configure - yield(config) - end - - def self.config - @config ||= Configuration.new - end - end -end diff --git a/rswag-api/lib/rswag/api/configuration.rb b/rswag-api/lib/rswag/api/configuration.rb deleted file mode 100644 index ff56180..0000000 --- a/rswag-api/lib/rswag/api/configuration.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Rswag - module Api - class Configuration - attr_accessor :swagger_root, :swagger_filter - - def resolve_swagger_root(env) - path_params = env['action_dispatch.request.path_parameters'] || {} - path_params[:swagger_root] || swagger_root - end - end - end -end diff --git a/rswag-api/lib/rswag/api/engine.rb b/rswag-api/lib/rswag/api/engine.rb deleted file mode 100644 index 893cf5f..0000000 --- a/rswag-api/lib/rswag/api/engine.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rswag/api/middleware' - -module Rswag - module Api - class Engine < ::Rails::Engine - isolate_namespace Rswag::Api - - initializer 'rswag-api.initialize' do |app| - middleware.use Rswag::Api::Middleware, Rswag::Api.config - end - end - end -end diff --git a/rswag-api/lib/rswag/api/middleware.rb b/rswag-api/lib/rswag/api/middleware.rb deleted file mode 100644 index 118c987..0000000 --- a/rswag-api/lib/rswag/api/middleware.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'json' - -module Rswag - module Api - class Middleware - - def initialize(app, config) - @app = app - @config = config - end - - def call(env) - path = env['PATH_INFO'] - filename = "#{@config.resolve_swagger_root(env)}/#{path}" - - if env['REQUEST_METHOD'] == 'GET' && File.file?(filename) - swagger = load_json(filename) - @config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil? - - return [ - '200', - { 'Content-Type' => 'application/json' }, - [ JSON.dump(swagger) ] - ] - end - - return @app.call(env) - end - - private - - def load_json(filename) - JSON.parse(File.read(filename)) - end - end - end -end diff --git a/rswag-api/rswag-api.gemspec b/rswag-api/open_api-rswag-api.gemspec similarity index 79% rename from rswag-api/rswag-api.gemspec rename to rswag-api/open_api-rswag-api.gemspec index 299172d..fce54c7 100644 --- a/rswag-api/rswag-api.gemspec +++ b/rswag-api/open_api-rswag-api.gemspec @@ -2,11 +2,11 @@ $:.push File.expand_path("../lib", __FILE__) # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "rswag-api" + s.name = "open_api-rswag-api" s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] + s.authors = ["Richie Morris", "Jay Danielian"] s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" + s.homepage = "https://github.com/jdanielian/rswag" s.summary = "A Rails Engine that exposes Swagger files as JSON endpoints" s.description = "Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints" s.license = "MIT" diff --git a/rswag-api/spec/generators/rswag/api/install_generator_spec.rb b/rswag-api/spec/generators/rswag/api/install_generator_spec.rb index fe2294d..6e983fc 100644 --- a/rswag-api/spec/generators/rswag/api/install_generator_spec.rb +++ b/rswag-api/spec/generators/rswag/api/install_generator_spec.rb @@ -1,6 +1,7 @@ require 'generator_spec' require 'generators/rswag/api/install/install_generator' + module Rswag module Api @@ -17,7 +18,7 @@ module Rswag end it 'installs the Rails initializer' do - assert_file('config/initializers/rswag-api.rb') + assert_file('config/initializers/rswag_api.rb') end # Don't know how to test this @@ -25,3 +26,4 @@ module Rswag end end end + diff --git a/rswag-api/spec/rswag/api/fixtures/config/routes.rb b/rswag-api/spec/open_api/rswag/api/fixtures/config/routes.rb similarity index 100% rename from rswag-api/spec/rswag/api/fixtures/config/routes.rb rename to rswag-api/spec/open_api/rswag/api/fixtures/config/routes.rb diff --git a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json b/rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.json similarity index 78% rename from rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json rename to rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.json index 11b296d..5711e2d 100644 --- a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json +++ b/rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.json @@ -1,5 +1,5 @@ { - "swagger": "2.0", + "openapi": "3.0.0", "info": { "title": "API V1", "version": "v1" diff --git a/rswag-api/spec/rswag/api/middleware_spec.rb b/rswag-api/spec/open_api/rswag/api/middleware_spec.rb similarity index 93% rename from rswag-api/spec/rswag/api/middleware_spec.rb rename to rswag-api/spec/open_api/rswag/api/middleware_spec.rb index aaa148b..3273c19 100644 --- a/rswag-api/spec/rswag/api/middleware_spec.rb +++ b/rswag-api/spec/open_api/rswag/api/middleware_spec.rb @@ -1,7 +1,7 @@ -require 'rswag/api/middleware' -require 'rswag/api/configuration' +require 'open_api/rswag/api/middleware' +require 'open_api/rswag/api/configuration' -module Rswag +module OpenApi::Rswag module Api describe Middleware do @@ -61,7 +61,7 @@ module Rswag it 'locates files at the provided swagger_root' do expect(response.length).to eql(3) expect(response[1]).to include( 'Content-Type' => 'application/json') - expect(response[2].join).to include('"swagger":"2.0"') + expect(response[2].join).to include('"openapi":"3.0.0"') end end diff --git a/rswag-specs/Guardfile b/rswag-specs/Guardfile new file mode 100644 index 0000000..e1ed8e2 --- /dev/null +++ b/rswag-specs/Guardfile @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +# directories %w(app lib config test spec features) \ +# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} + +## Note: if you are using the `directories` clause above and you are not +## watching the project directory ('.'), then you will want to move +## the Guardfile to a watched dir and symlink it back, e.g. +# +# $ mkdir config +# $ mv Guardfile config/ +# $ ln -s config/Guardfile . +# +# and, you'll have to watch "config/Guardfile" instead of "Guardfile" + +# Note: The cmd option is now required due to the increasing number of ways +# rspec may be run, below are examples of the most common uses. +# * bundler: 'bundle exec rspec' +# * bundler binstubs: 'bin/rspec' +# * spring: 'bin/rspec' (This will use spring if running and you have +# installed the spring binstubs per the docs) +# * zeus: 'zeus rspec' (requires the server to be started separately) +# * 'just' rspec: 'rspec' + +guard :rspec, cmd: 'bundle exec rspec' do + require 'guard/rspec/dsl' + dsl = Guard::RSpec::Dsl.new(self) + + # Feel free to open issues for suggestions and improvements + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + # Ruby files + ruby = dsl.ruby + dsl.watch_spec_files_for(ruby.lib_files) + + # Rails files + rails = dsl.rails(view_extensions: %w[erb haml slim]) + dsl.watch_spec_files_for(rails.app_files) + dsl.watch_spec_files_for(rails.views) + + watch(rails.controllers) do |m| + [ + rspec.spec.call("routing/#{m[1]}_routing"), + rspec.spec.call("controllers/#{m[1]}_controller"), + rspec.spec.call("acceptance/#{m[1]}") + ] + end + + # Rails config changes + watch(rails.spec_helper) { rspec.spec_dir } +end diff --git a/rswag-specs/lib/open_api/rswag/specs.rb b/rswag-specs/lib/open_api/rswag/specs.rb new file mode 100644 index 0000000..8ede89a --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs.rb @@ -0,0 +1,29 @@ +require 'rspec/core' +require 'open_api/rswag/specs/example_group_helpers' +require 'open_api/rswag/specs/example_helpers' +require 'open_api/rswag/specs/configuration' +require 'open_api/rswag/specs/railtie' if defined?(Rails::Railtie) + +module OpenApi + module Rswag + module Specs + + # Extend RSpec with a swagger-based DSL + ::RSpec.configure do |c| + c.add_setting :swagger_root + c.add_setting :swagger_docs + c.add_setting :swagger_dry_run + c.extend ExampleGroupHelpers, type: :request + c.include ExampleHelpers, type: :request + end + + def self.config + @config ||= Configuration.new(RSpec.configuration) + end + + # Support Rails 3+ and RSpec 2+ (sigh!) + RAILS_VERSION = Rails::VERSION::MAJOR + RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/configuration.rb b/rswag-specs/lib/open_api/rswag/specs/configuration.rb new file mode 100644 index 0000000..b552f28 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/configuration.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module OpenApi + module Rswag + module Specs + class Configuration + def initialize(rspec_config) + @rspec_config = rspec_config + end + + def swagger_root + @swagger_root ||= begin + if @rspec_config.swagger_root.nil? + raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' + end + + @rspec_config.swagger_root + end + end + + def swagger_docs + @swagger_docs ||= begin + if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? + raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' + end + + @rspec_config.swagger_docs + end + end + + def swagger_dry_run + @swagger_dry_run ||= begin + @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run + end + end + + def get_swagger_doc(name) + return swagger_docs.values.first if name.nil? + raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] + + swagger_docs[name] + end + end + + class ConfigurationError < StandardError; end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb new file mode 100644 index 0000000..9c356c5 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true +require 'hashie' + +module OpenApi + module Rswag + module Specs + module ExampleGroupHelpers + def path(template, metadata = {}, &block) + metadata[:path_item] = { template: template } + describe(template, metadata, &block) + end + + %i[get post patch put delete head].each do |verb| + define_method(verb) do |summary, &block| + api_metadata = { operation: { verb: verb, summary: summary } } + describe(verb, api_metadata, &block) + end + end + + %i[operationId deprecated security].each do |attr_name| + define_method(attr_name) do |value| + metadata[:operation][attr_name] = value + end + end + + # NOTE: 'description' requires special treatment because ExampleGroup already + # defines a method with that name. Provide an override that supports the existing + # functionality while also setting the appropriate metadata if applicable + def description(value = nil) + return super() if value.nil? + + metadata[:operation][:description] = value + end + + # These are array properties - note the splat operator + %i[tags consumes produces schemes].each do |attr_name| + define_method(attr_name) do |*value| + metadata[:operation][attr_name] = value + end + end + + # NICE TO HAVE + # TODO: update generator templates to include 3.0 syntax + # TODO: setup travis CI? + + # MUST HAVES + # 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 + # TODO: look at adding examples in content request_body + # https://swagger.io/docs/specification/describing-request-body/ + # need to make sure we output requestBody in the swagger generator .json + # also need to make sure that it can handle content: , required: true/false, schema: ref + + def request_body(attributes) + # can make this generic, and accept any incoming hash (like parameter method) + attributes.compact! + + if metadata[:operation][:requestBody].blank? + metadata[:operation][:requestBody] = attributes + elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content] + # merge in + content_hash = metadata[:operation][:requestBody][:content] + incoming_content_hash = attributes[:content] + content_hash.merge!(incoming_content_hash) if incoming_content_hash + end + end + + def request_body_json(schema:, required: true, description: nil, examples: nil) + passed_examples = Array(examples) + content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} } + request_body(description: description, required: required, content: content_hash) + if passed_examples.any? + # the request_factory is going to have to resolve the different ways that the example can be given + # 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 + 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 + + def request_body_text_plain(required: false, description: nil, examples: nil) + content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} } + request_body(description: description, required: required, content: content_hash) + end + + # TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type + def request_body_xml(schema:,required: false, description: nil, examples: nil) + passed_examples = Array(examples) + content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} } + request_body(description: description, required: required, content: content_hash) + end + + def request_body_multipart(schema:, description: nil) + content_hash = { 'multipart/form-data' => { schema: schema }} + request_body(description: description, content: content_hash) + + schema.extend(Hashie::Extensions::DeepLocate) + file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary } + + hash_locator = [] + + file_properties.each do |match| + hash_match = schema.deep_locate -> (_k, v, _obj) { v == match } + hash_locator.concat(hash_match) unless hash_match.empty? + end + + property_hashes = hash_locator.flat_map do |locator| + locator.select { |_k,v| file_properties.include?(v) } + end + + property_hashes.each do |property_hash| + file_name = property_hash.keys.first + parameter name: file_name, in: :formData, type: :file, required: true + end + end + + def parameter(attributes) + if attributes[:in] && attributes[:in].to_sym == :path + attributes[:required] = true + end + + if attributes[:type] && attributes[:schema].nil? + attributes[:schema] = {type: attributes[:type]} + end + + if metadata.key?(:operation) + metadata[:operation][:parameters] ||= [] + metadata[:operation][:parameters] << attributes + else + metadata[:path_item][:parameters] ||= [] + metadata[:path_item][:parameters] << attributes + end + end + + def response(code, description, metadata = {}, &block) + metadata[:response] = { code: code, description: description } + context(description, metadata, &block) + end + + def schema(value, content_type: 'application/json') + content_hash = {content_type => {schema: value}} + metadata[:response][:content] = content_hash + end + + def header(name, attributes) + metadata[:response][:headers] ||= {} + + if attributes[:type] && attributes[:schema].nil? + attributes[:schema] = {type: attributes[:type]} + attributes.delete(:type) + end + + metadata[:response][:headers][name] = attributes + end + + # NOTE: Similar to 'description', 'examples' need to handle the case when + # being invoked with no params to avoid overriding 'examples' method of + # rspec-core ExampleGroup + def examples(example = nil) + return super() if example.nil? + + 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 + before do + submit_request(example.metadata) + end + + it "returns a #{metadata[:response][:code]} response" do + assert_response_matches_metadata(metadata) + block.call(response) if block_given? + end + else + before do |example| + submit_request(example.metadata) # + end + + it "returns a #{metadata[:response][:code]} response" do |example| + assert_response_matches_metadata(example.metadata, &block) + example.instance_exec(response, &block) if block_given? + end + + after do |example| + body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] } + + if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json'] + # save response examples by default + example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty? + + # save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test + if response.code.to_s =~ /^2\d{2}$/ + 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 + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/example_helpers.rb b/rswag-specs/lib/open_api/rswag/specs/example_helpers.rb new file mode 100644 index 0000000..ddfd27c --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/example_helpers.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'open_api/rswag/specs/request_factory' +require 'open_api/rswag/specs/response_validator' + +module OpenApi + module Rswag + module Specs + module ExampleHelpers + def submit_request(metadata) + request = RequestFactory.new.build_request(metadata, self) + + if RAILS_VERSION < 5 + send( + request[:verb], + request[:path], + request[:payload], + request[:headers] + ) + else + send( + request[:verb], + request[:path], + params: request[:payload], + headers: request[:headers] + ) + end + end + + def assert_response_matches_metadata(metadata) + ResponseValidator.new.validate!(metadata, response) + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/extended_schema.rb b/rswag-specs/lib/open_api/rswag/specs/extended_schema.rb new file mode 100644 index 0000000..17e46b9 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/extended_schema.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'json-schema' + +module OpenApi + module Rswag + module Specs + class ExtendedSchema < JSON::Schema::Draft4 + def initialize + super + @attributes['type'] = ExtendedTypeAttribute + @uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema') + @names = ['http://tempuri.org/rswag/specs/extended_schema'] + end + end + + class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute + def self.validate(current_schema, data, fragments, processor, validator, options = {}) + return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true) + + super + end + end + + JSON::Validator.register_validator(ExtendedSchema.new) + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/railtie.rb b/rswag-specs/lib/open_api/rswag/specs/railtie.rb new file mode 100644 index 0000000..e3e419e --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/railtie.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module OpenApi + module Rswag + module Specs + class Railtie < ::Rails::Railtie + rake_tasks do + load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __dir__) + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/request_factory.rb b/rswag-specs/lib/open_api/rswag/specs/request_factory.rb new file mode 100644 index 0000000..adf9f58 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/request_factory.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/conversions' +require 'json' + +module OpenApi + module Rswag + module Specs + class RequestFactory + def initialize(config = ::OpenApi::Rswag::Specs.config) + @config = config + end + + def build_request(metadata, example) + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + parameters = expand_parameters(metadata, swagger_doc, example) + + {}.tap do |request| + add_verb(request, metadata) + add_path(request, metadata, swagger_doc, parameters, example) + add_headers(request, metadata, swagger_doc, parameters, example) + add_payload(request, parameters, example) + end + end + + private + + def expand_parameters(metadata, swagger_doc, example) + operation_params = metadata[:operation][:parameters] || [] + path_item_params = metadata[:path_item][:parameters] || [] + security_params = derive_security_params(metadata, swagger_doc) + + # NOTE: Use of + instead of concat to avoid mutation of the metadata object + (operation_params + path_item_params + security_params) + .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p } + .uniq { |p| p[:name] } + .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) } + end + + def derive_security_params(metadata, swagger_doc) + requirements = metadata[:operation][:security] || swagger_doc[:security] || [] + scheme_names = requirements.flat_map(&:keys) + components = swagger_doc[:components] || {} + schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values + + schemes.map do |scheme| + param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } + param.merge(type: :string, required: requirements.one?) + end + end + + def resolve_parameter(ref, swagger_doc) + key = ref.sub('#/parameters/', '').to_sym + definitions = swagger_doc[:parameters] + raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] + + definitions[key] + end + + def add_verb(request, metadata) + request[:verb] = metadata[:operation][:verb] + end + + def add_path(request, metadata, swagger_doc, parameters, example) + template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template] + + request[:path] = template.tap do |template| + parameters.select { |p| p[:in] == :path }.each do |p| + template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) + end + + parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| + template.concat(i == 0 ? '?' : '&') + template.concat(build_query_string_part(p, example.send(p[:name]))) + end + end + end + + def build_query_string_part(param, value) + name = param[:name] + return "#{name}=#{value}" unless param[:type].to_sym == :array + + case param[:collectionFormat] + when :ssv + "#{name}=#{value.join(' ')}" + when :tsv + "#{name}=#{value.join('\t')}" + when :pipes + "#{name}=#{value.join('|')}" + when :multi + value.map { |v| "#{name}=#{v}" }.join('&') + else + "#{name}=#{value.join(',')}" # csv is default + end + end + + def add_headers(request, metadata, swagger_doc, parameters, example) + tuples = parameters + .select { |p| p[:in] == :header } + .map { |p| [p[:name], example.send(p[:name]).to_s] } + + # Accept header + produces = metadata[:operation][:produces] || swagger_doc[:produces] + if produces + accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first + tuples << ['Accept', accept] + end + + # Content-Type header + consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] + if consumes + content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first + tuples << ['Content-Type', content_type] + end + + # Rails test infrastructure requires rackified headers + rackified_tuples = tuples.map do |pair| + [ + case pair[0] + when 'Accept' then 'HTTP_ACCEPT' + when 'Content-Type' then 'CONTENT_TYPE' + when 'Authorization' then 'HTTP_AUTHORIZATION' + else pair[0] + end, + pair[1] + ] + end + + request[:headers] = Hash[rackified_tuples] + end + + def add_payload(request, parameters, example) + content_type = request[:headers]['CONTENT_TYPE'] + return if content_type.nil? + + if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type) + request[:payload] = build_form_payload(parameters, example) + else + request[:payload] = build_json_payload(parameters, example) + end + end + + def build_form_payload(parameters, example) + # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/ + # Rather that serializing with the appropriate encoding (e.g. multipart/form-data), + # Rails test infrastructure allows us to send the values directly as a hash + # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test + tuples = parameters + .select { |p| p[:in] == :formData } + .map { |p| [p[:name], example.send(p[:name])] } + Hash[tuples] + end + + def build_json_payload(parameters, example) + body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first + return nil unless body_param + + 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 + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/response_validator.rb b/rswag-specs/lib/open_api/rswag/specs/response_validator.rb new file mode 100644 index 0000000..5a96b28 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/response_validator.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/slice' +require 'json-schema' +require 'json' +require 'open_api/rswag/specs/extended_schema' + +module OpenApi + module Rswag + module Specs + class ResponseValidator + def initialize(config = ::OpenApi::Rswag::Specs.config) + @config = config + end + + def validate!(metadata, response) + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + + validate_code!(metadata, response) + validate_headers!(metadata, response.headers) + validate_body!(metadata, swagger_doc, response.body) + end + + private + + def validate_code!(metadata, response) + expected = metadata[:response][:code].to_s + if response.code != expected + raise UnexpectedResponse, + "Expected response code '#{response.code}' to match '#{expected}'\n" \ + "Response body: #{response.body}" + end + end + + def validate_headers!(metadata, headers) + expected = (metadata[:response][:headers] || {}).keys + expected.each do |name| + raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil? + end + end + + def validate_body!(metadata, swagger_doc, body) + test_schemas = extract_schemas(metadata) + return if test_schemas.nil? || test_schemas.empty? + + components = swagger_doc[:components] || {} + components_schemas = { components: { schemas: components[:schemas] } } + + validation_schema = test_schemas[:schema] # response_schema + .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') + .merge(components_schemas) + + errors = JSON::Validator.fully_validate(validation_schema, body) + raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? + end + + def extract_schemas(metadata) + metadata[:operation] = {produces: []} if metadata[:operation].nil? + produces = Array(metadata[:operation][:produces]) + + producer_content = produces.first || 'application/json' + response_content = metadata[:response][:content] || {producer_content => {}} + response_content[producer_content] + end + end + + class UnexpectedResponse < StandardError; end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb new file mode 100644 index 0000000..8d83ef9 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/deep_merge' +require 'swagger_helper' + +module OpenApi + module Rswag + module Specs + class SwaggerFormatter + # NOTE: rspec 2.x support + if RSPEC_VERSION > 2 + ::RSpec::Core::Formatters.register self, :example_group_finished, :stop + end + + def initialize(output, config = ::OpenApi::Rswag::Specs.config) + @output = output + @config = config + + @output.puts 'Generating Swagger docs ...' + end + + def example_group_finished(notification) + # NOTE: rspec 2.x support + metadata = if RSPEC_VERSION > 2 + notification.group.metadata + else + notification.metadata + end + + return unless metadata.key?(:response) + + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + swagger_doc.deep_merge!(metadata_to_swagger(metadata)) + end + + def stop(_notification = nil) + @config.swagger_docs.each do |url_path, doc| + # remove 2.0 parameters + doc[:paths]&.each_pair do |_k, v| + v.each_pair do |_verb, value| + is_hash = value.is_a?(Hash) + if is_hash && value.dig(:parameters) + schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] } + if value && schema_param && value&.dig(:requestBody, :content, 'application/json') + value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema]) + end + + value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData } + value[:parameters].each { |p| p.delete(:type) } + value[:headers].each { |p| p.delete(:type)} if value[:headers] + end + + value.delete(:consumes) if is_hash && value.dig(:consumes) + value.delete(:produces) if is_hash && value.dig(:produces) + end + end + + file_path = File.join(@config.swagger_root, url_path) + dirname = File.dirname(file_path) + FileUtils.mkdir_p dirname unless File.exist?(dirname) + + File.open(file_path, 'w') do |file| + file.write(JSON.pretty_generate(doc)) + end + + @output.puts "Swagger doc generated at #{file_path}" + end + end + + private + + def metadata_to_swagger(metadata) + response_code = metadata[:response][:code] + response = metadata[:response].reject { |k, _v| k == :code } + + # need to merge in to response + if response[:examples]&.dig('application/json') + example = response[:examples].dig('application/json').dup + schema = response.dig(:content, 'application/json', :schema) + new_hash = {example: example} + new_hash[:schema] = schema if schema + response.merge!(content: { 'application/json' => new_hash }) + response.delete(:examples) + end + + + verb = metadata[:operation][:verb] + operation = metadata[:operation] + .reject { |k, _v| k == :verb } + .merge(responses: { response_code => response }) + + path_template = metadata[:path_item][:template] + path_item = metadata[:path_item] + .reject { |k, _v| k == :template } + .merge(verb => operation) + + { paths: { path_template => path_item } } + end + end + end + end +end diff --git a/rswag-specs/lib/rswag/specs.rb b/rswag-specs/lib/rswag/specs.rb deleted file mode 100644 index de29ce9..0000000 --- a/rswag-specs/lib/rswag/specs.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'rspec/core' -require 'rswag/specs/example_group_helpers' -require 'rswag/specs/example_helpers' -require 'rswag/specs/configuration' -require 'rswag/specs/railtie' if defined?(Rails::Railtie) - -module Rswag - module Specs - - # Extend RSpec with a swagger-based DSL - ::RSpec.configure do |c| - c.add_setting :swagger_root - c.add_setting :swagger_docs - c.add_setting :swagger_dry_run - c.extend ExampleGroupHelpers, type: :request - c.include ExampleHelpers, type: :request - end - - def self.config - @config ||= Configuration.new(RSpec.configuration) - end - - # Support Rails 3+ and RSpec 2+ (sigh!) - RAILS_VERSION = Rails::VERSION::MAJOR - RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i - end -end diff --git a/rswag-specs/lib/rswag/specs/configuration.rb b/rswag-specs/lib/rswag/specs/configuration.rb deleted file mode 100644 index 4adf33c..0000000 --- a/rswag-specs/lib/rswag/specs/configuration.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Rswag - module Specs - - class Configuration - - def initialize(rspec_config) - @rspec_config = rspec_config - end - - def swagger_root - @swagger_root ||= begin - if @rspec_config.swagger_root.nil? - raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' - end - @rspec_config.swagger_root - end - end - - def swagger_docs - @swagger_docs ||= begin - if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? - raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' - end - @rspec_config.swagger_docs - end - end - - def swagger_dry_run - @swagger_dry_run ||= begin - @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run - end - end - - def get_swagger_doc(name) - return swagger_docs.values.first if name.nil? - raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] - swagger_docs[name] - end - end - - class ConfigurationError < StandardError; end - end -end diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb deleted file mode 100644 index c293c63..0000000 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ /dev/null @@ -1,98 +0,0 @@ -module Rswag - module Specs - module ExampleGroupHelpers - - def path(template, metadata={}, &block) - metadata[:path_item] = { template: template } - describe(template, metadata, &block) - end - - [ :get, :post, :patch, :put, :delete, :head ].each do |verb| - define_method(verb) do |summary, &block| - api_metadata = { operation: { verb: verb, summary: summary } } - describe(verb, api_metadata, &block) - end - end - - [ :operationId, :deprecated, :security ].each do |attr_name| - define_method(attr_name) do |value| - metadata[:operation][attr_name] = value - end - end - - # NOTE: 'description' requires special treatment because ExampleGroup already - # defines a method with that name. Provide an override that supports the existing - # functionality while also setting the appropriate metadata if applicable - def description(value=nil) - return super() if value.nil? - metadata[:operation][:description] = value - end - - # These are array properties - note the splat operator - [ :tags, :consumes, :produces, :schemes ].each do |attr_name| - define_method(attr_name) do |*value| - metadata[:operation][attr_name] = value - end - end - - def parameter(attributes) - if attributes[:in] && attributes[:in].to_sym == :path - attributes[:required] = true - end - - if metadata.has_key?(:operation) - metadata[:operation][:parameters] ||= [] - metadata[:operation][:parameters] << attributes - else - metadata[:path_item][:parameters] ||= [] - metadata[:path_item][:parameters] << attributes - end - end - - def response(code, description, metadata={}, &block) - metadata[:response] = { code: code, description: description } - context(description, metadata, &block) - end - - def schema(value) - metadata[:response][:schema] = value - end - - def header(name, attributes) - metadata[:response][:headers] ||= {} - metadata[:response][:headers][name] = attributes - end - - # NOTE: Similar to 'description', 'examples' need to handle the case when - # being invoked with no params to avoid overriding 'examples' method of - # rspec-core ExampleGroup - def examples(example = nil) - return super() if example.nil? - metadata[:response][:examples] = example - end - - def run_test!(&block) - # NOTE: rspec 2.x support - if RSPEC_VERSION < 3 - before do - submit_request(example.metadata) - end - - it "returns a #{metadata[:response][:code]} response" do - assert_response_matches_metadata(metadata) - block.call(response) if block_given? - end - else - before do |example| - submit_request(example.metadata) - end - - it "returns a #{metadata[:response][:code]} response" do |example| - assert_response_matches_metadata(example.metadata, &block) - example.instance_exec(response, &block) if block_given? - end - end - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/example_helpers.rb b/rswag-specs/lib/rswag/specs/example_helpers.rb deleted file mode 100644 index d8ff128..0000000 --- a/rswag-specs/lib/rswag/specs/example_helpers.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'rswag/specs/request_factory' -require 'rswag/specs/response_validator' - -module Rswag - module Specs - module ExampleHelpers - - def submit_request(metadata) - request = RequestFactory.new.build_request(metadata, self) - - if RAILS_VERSION < 5 - send( - request[:verb], - request[:path], - request[:payload], - request[:headers] - ) - else - send( - request[:verb], - request[:path], - { - params: request[:payload], - headers: request[:headers] - } - ) - end - end - - def assert_response_matches_metadata(metadata) - ResponseValidator.new.validate!(metadata, response) - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/extended_schema.rb b/rswag-specs/lib/rswag/specs/extended_schema.rb deleted file mode 100644 index 62eb4ee..0000000 --- a/rswag-specs/lib/rswag/specs/extended_schema.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'json-schema' - -module Rswag - module Specs - class ExtendedSchema < JSON::Schema::Draft4 - - def initialize - super - @attributes['type'] = ExtendedTypeAttribute - @uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema') - @names = ['http://tempuri.org/rswag/specs/extended_schema'] - end - end - - class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute - - def self.validate(current_schema, data, fragments, processor, validator, options={}) - return if data.nil? && current_schema.schema['x-nullable'] == true - super - end - end - - JSON::Validator.register_validator(ExtendedSchema.new) - end -end diff --git a/rswag-specs/lib/rswag/specs/railtie.rb b/rswag-specs/lib/rswag/specs/railtie.rb deleted file mode 100644 index 8deec2b..0000000 --- a/rswag-specs/lib/rswag/specs/railtie.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Rswag - module Specs - class Railtie < ::Rails::Railtie - - rake_tasks do - load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__) - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb deleted file mode 100644 index 7106015..0000000 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ /dev/null @@ -1,157 +0,0 @@ -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/hash/conversions' -require 'json' - -module Rswag - module Specs - class RequestFactory - - def initialize(config = ::Rswag::Specs.config) - @config = config - end - - def build_request(metadata, example) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - parameters = expand_parameters(metadata, swagger_doc, example) - - {}.tap do |request| - add_verb(request, metadata) - add_path(request, metadata, swagger_doc, parameters, example) - add_headers(request, metadata, swagger_doc, parameters, example) - add_payload(request, parameters, example) - end - end - - private - - def expand_parameters(metadata, swagger_doc, example) - operation_params = metadata[:operation][:parameters] || [] - path_item_params = metadata[:path_item][:parameters] || [] - security_params = derive_security_params(metadata, swagger_doc) - - # NOTE: Use of + instead of concat to avoid mutation of the metadata object - (operation_params + path_item_params + security_params) - .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p } - .uniq { |p| p[:name] } - .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) } - end - - def derive_security_params(metadata, swagger_doc) - requirements = metadata[:operation][:security] || swagger_doc[:security] || [] - scheme_names = requirements.flat_map { |r| r.keys } - schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values - - schemes.map do |scheme| - param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } - param.merge(type: :string, required: requirements.one?) - end - end - - def resolve_parameter(ref, swagger_doc) - key = ref.sub('#/parameters/', '').to_sym - definitions = swagger_doc[:parameters] - raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] - definitions[key] - end - - def add_verb(request, metadata) - request[:verb] = metadata[:operation][:verb] - end - - def add_path(request, metadata, swagger_doc, parameters, example) - template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template] - - request[:path] = template.tap do |template| - parameters.select { |p| p[:in] == :path }.each do |p| - template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) - end - - parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| - template.concat(i == 0 ? '?' : '&') - template.concat(build_query_string_part(p, example.send(p[:name]))) - end - end - end - - def build_query_string_part(param, value) - name = param[:name] - return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array - - case param[:collectionFormat] - when :ssv - "#{name}=#{value.join(' ')}" - when :tsv - "#{name}=#{value.join('\t')}" - when :pipes - "#{name}=#{value.join('|')}" - when :multi - value.map { |v| "#{name}=#{v}" }.join('&') - else - "#{name}=#{value.join(',')}" # csv is default - end - end - - def add_headers(request, metadata, swagger_doc, parameters, example) - tuples = parameters - .select { |p| p[:in] == :header } - .map { |p| [ p[:name], example.send(p[:name]).to_s ] } - - # Accept header - produces = metadata[:operation][:produces] || swagger_doc[:produces] - if produces - accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first - tuples << [ 'Accept', accept ] - end - - # Content-Type header - consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] - if consumes - content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first - tuples << [ 'Content-Type', content_type ] - end - - # Rails test infrastructure requires rackified headers - rackified_tuples = tuples.map do |pair| - [ - case pair[0] - when 'Accept' then 'HTTP_ACCEPT' - when 'Content-Type' then 'CONTENT_TYPE' - when 'Authorization' then 'HTTP_AUTHORIZATION' - else pair[0] - end, - pair[1] - ] - end - - request[:headers] = Hash[ rackified_tuples ] - end - - def add_payload(request, parameters, example) - content_type = request[:headers]['CONTENT_TYPE'] - return if content_type.nil? - - if [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].include?(content_type) - request[:payload] = build_form_payload(parameters, example) - else - request[:payload] = build_json_payload(parameters, example) - end - end - - def build_form_payload(parameters, example) - # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/ - # Rather that serializing with the appropriate encoding (e.g. multipart/form-data), - # Rails test infrastructure allows us to send the values directly as a hash - # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test - tuples = parameters - .select { |p| p[:in] == :formData } - .map { |p| [ p[:name], example.send(p[:name]) ] } - Hash[ tuples ] - end - - def build_json_payload(parameters, example) - body_param = parameters.select { |p| p[:in] == :body }.first - body_param ? example.send(body_param[:name]).to_json : nil - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb deleted file mode 100644 index c3e363f..0000000 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'active_support/core_ext/hash/slice' -require 'json-schema' -require 'json' -require 'rswag/specs/extended_schema' - -module Rswag - module Specs - class ResponseValidator - - def initialize(config = ::Rswag::Specs.config) - @config = config - end - - def validate!(metadata, response) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - - validate_code!(metadata, response) - validate_headers!(metadata, response.headers) - validate_body!(metadata, swagger_doc, response.body) - end - - private - - def validate_code!(metadata, response) - expected = metadata[:response][:code].to_s - if response.code != expected - raise UnexpectedResponse, - "Expected response code '#{response.code}' to match '#{expected}'\n" \ - "Response body: #{response.body}" - end - end - - def validate_headers!(metadata, headers) - expected = (metadata[:response][:headers] || {}).keys - expected.each do |name| - raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil? - end - end - - def validate_body!(metadata, swagger_doc, body) - response_schema = metadata[:response][:schema] - return if response_schema.nil? - - validation_schema = response_schema - .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') - .merge(swagger_doc.slice(:definitions)) - errors = JSON::Validator.fully_validate(validation_schema, body) - raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? - end - end - - class UnexpectedResponse < StandardError; end - end -end diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb deleted file mode 100644 index 794a9d9..0000000 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'active_support/core_ext/hash/deep_merge' -require 'swagger_helper' - -module Rswag - module Specs - class SwaggerFormatter - - # NOTE: rspec 2.x support - if RSPEC_VERSION > 2 - ::RSpec::Core::Formatters.register self, :example_group_finished, :stop - end - - def initialize(output, config = Rswag::Specs.config) - @output = output - @config = config - - @output.puts 'Generating Swagger docs ...' - end - - def example_group_finished(notification) - # NOTE: rspec 2.x support - if RSPEC_VERSION > 2 - metadata = notification.group.metadata - else - metadata = notification.metadata - end - - return unless metadata.has_key?(:response) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - swagger_doc.deep_merge!(metadata_to_swagger(metadata)) - end - - def stop(notification=nil) - @config.swagger_docs.each do |url_path, doc| - file_path = File.join(@config.swagger_root, url_path) - dirname = File.dirname(file_path) - FileUtils.mkdir_p dirname unless File.exists?(dirname) - - File.open(file_path, 'w') do |file| - file.write(JSON.pretty_generate(doc)) - end - - @output.puts "Swagger doc generated at #{file_path}" - end - end - - private - - def metadata_to_swagger(metadata) - response_code = metadata[:response][:code] - response = metadata[:response].reject { |k,v| k == :code } - - verb = metadata[:operation][:verb] - operation = metadata[:operation] - .reject { |k,v| k == :verb } - .merge(responses: { response_code => response }) - - path_template = metadata[:path_item][:template] - path_item = metadata[:path_item] - .reject { |k,v| k == :template } - .merge(verb => operation) - - { paths: { path_template => path_item } } - end - end - end -end diff --git a/rswag-specs/lib/tasks/rswag-specs_tasks.rake b/rswag-specs/lib/tasks/rswag-specs_tasks.rake index adc128c..573dd5b 100644 --- a/rswag-specs/lib/tasks/rswag-specs_tasks.rake +++ b/rswag-specs/lib/tasks/rswag-specs_tasks.rake @@ -6,12 +6,12 @@ namespace :rswag do desc 'Generate Swagger JSON files from integration specs' RSpec::Core::RakeTask.new('swaggerize') do |t| t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb' - + # NOTE: rspec 2.x support - if Rswag::Specs::RSPEC_VERSION > 2 && Rswag::Specs.config.swagger_dry_run - t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ] + if OpenApi::Rswag::Specs::RSPEC_VERSION > 2 && OpenApi::Rswag::Specs.config.swagger_dry_run + t.rspec_opts = [ '--format OpenApi::Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ] else - t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ] + t.rspec_opts = [ '--format OpenApi::Rswag::Specs::SwaggerFormatter', '--order defined' ] end end end diff --git a/rswag-specs/open_api-rswag-specs.gemspec b/rswag-specs/open_api-rswag-specs.gemspec new file mode 100644 index 0000000..94bc6fd --- /dev/null +++ b/rswag-specs/open_api-rswag-specs.gemspec @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path('lib', __dir__) + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = 'open_api-rswag-specs' + s.version = ENV['TRAVIS_TAG'] || '0.0.0' + s.authors = ['Richie Morris', 'Jay Danielian'] + s.email = ['domaindrivendev@gmail.com'] + s.homepage = 'https://github.com/jdanielian/rswag' + s.summary = 'A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files' + s.description = 'Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs' + s.license = 'MIT' + + s.files = Dir['{lib}/**/*'] + %w[MIT-LICENSE Rakefile] + + s.add_dependency 'activesupport', '>= 3.1', '< 6.0' + s.add_dependency 'json-schema', '~> 2.2' + s.add_dependency 'railties', '>= 3.1', '< 6.0' + s.add_dependency 'hashie' + s.add_development_dependency 'guard-rspec' +end diff --git a/rswag-specs/rswag-specs.gemspec b/rswag-specs/rswag-specs.gemspec deleted file mode 100644 index 0e4686e..0000000 --- a/rswag-specs/rswag-specs.gemspec +++ /dev/null @@ -1,19 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "rswag-specs" - s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] - s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" - s.summary = "A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files" - s.description = "Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs" - s.license = "MIT" - - s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile" ] - - s.add_dependency 'activesupport', '>= 3.1', '< 6.0' - s.add_dependency 'railties', '>= 3.1', '< 6.0' - s.add_dependency 'json-schema', '~> 2.2' -end diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index b75d843..5791baf 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -1,74 +1,77 @@ -require 'rswag/specs/configuration' +# frozen_string_literal: true -module Rswag - module Specs +require 'open_api/rswag/specs/configuration' - describe Configuration do - subject { described_class.new(rspec_config) } +module OpenApi + module Rswag + module Specs + describe Configuration do + subject { described_class.new(rspec_config) } - let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } - let(:swagger_root) { 'foobar' } - let(:swagger_docs) do - { - 'v1/swagger.json' => { info: { title: 'v1' } }, - 'v2/swagger.json' => { info: { title: 'v2' } } - } - end - - describe '#swagger_root' do - let(:response) { subject.swagger_root } - - context 'provided in rspec config' do - it { expect(response).to eq('foobar') } + let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } + let(:swagger_root) { 'foobar' } + let(:swagger_docs) do + { + 'v1/swagger.json' => { info: { title: 'v1' } }, + 'v2/swagger.json' => { info: { title: 'v2' } } + } end - context 'not provided' do - let(:swagger_root) { nil } - it { expect { response }.to raise_error ConfigurationError } - end - end + describe '#swagger_root' do + let(:response) { subject.swagger_root } - describe '#swagger_docs' do - let(:response) { subject.swagger_docs } + context 'provided in rspec config' do + it { expect(response).to eq('foobar') } + end - context 'provided in rspec config' do - it { expect(response).to be_an_instance_of(Hash) } - end - - context 'not provided' do - let(:swagger_docs) { nil } - it { expect { response }.to raise_error ConfigurationError } - end - - context 'provided but empty' do - let(:swagger_docs) { {} } - it { expect { response }.to raise_error ConfigurationError } - end - end - - describe '#get_swagger_doc(tag=nil)' do - let(:swagger_doc) { subject.get_swagger_doc(tag) } - - context 'no tag provided' do - let(:tag) { nil } - - it 'returns the first doc in rspec config' do - expect(swagger_doc).to eq(info: { title: 'v1' }) + context 'not provided' do + let(:swagger_root) { nil } + it { expect { response }.to raise_error ConfigurationError } end end - context 'tag provided' do - context 'matching doc' do - let(:tag) { 'v2/swagger.json' } + describe '#swagger_docs' do + let(:response) { subject.swagger_docs } - it 'returns the matching doc in rspec config' do - expect(swagger_doc).to eq(info: { title: 'v2' }) + context 'provided in rspec config' do + it { expect(response).to be_an_instance_of(Hash) } + end + + context 'not provided' do + let(:swagger_docs) { nil } + it { expect { response }.to raise_error ConfigurationError } + end + + context 'provided but empty' do + let(:swagger_docs) { {} } + it { expect { response }.to raise_error ConfigurationError } + end + end + + describe '#get_swagger_doc(tag=nil)' do + let(:swagger_doc) { subject.get_swagger_doc(tag) } + + context 'no tag provided' do + let(:tag) { nil } + + it 'returns the first doc in rspec config' do + expect(swagger_doc).to eq(info: { title: 'v1' }) end end - context 'no matching doc' do - let(:tag) { 'foobar' } - it { expect { swagger_doc }.to raise_error ConfigurationError } + context 'tag provided' do + context 'matching doc' do + let(:tag) { 'v2/swagger.json' } + + it 'returns the matching doc in rspec config' do + expect(swagger_doc).to eq(info: { title: 'v2' }) + end + end + + context 'no matching doc' do + let(:tag) { 'foobar' } + it { expect { swagger_doc }.to raise_error ConfigurationError } + end end end end diff --git a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb index 619a8d7..430c059 100644 --- a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb @@ -1,182 +1,217 @@ -require 'rswag/specs/example_group_helpers' +# frozen_string_literal: true -module Rswag - module Specs +require 'open_api/rswag/specs/example_group_helpers' - describe ExampleGroupHelpers do - subject { double('example_group') } +module OpenApi + module Rswag + module Specs + describe ExampleGroupHelpers do + subject { double('example_group') } - before do - subject.extend ExampleGroupHelpers - allow(subject).to receive(:describe) - allow(subject).to receive(:context) - allow(subject).to receive(:metadata).and_return(api_metadata) - end - let(:api_metadata) { {} } - - describe '#path(path)' do - before { subject.path('/blogs') } - - it "delegates to 'describe' with 'path' metadata" do - expect(subject).to have_received(:describe).with( - '/blogs', path_item: { template: '/blogs' } - ) - end - end - - describe '#get|post|patch|put|delete|head(verb, summary)' do - before { subject.post('Creates a blog') } - - it "delegates to 'describe' with 'operation' metadata" do - expect(subject).to have_received(:describe).with( - :post, operation: { verb: :post, summary: 'Creates a blog' } - ) - end - end - - describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do before do - subject.tags('Blogs', 'Admin') - subject.description('Some description') - subject.operationId('createBlog') - subject.consumes('application/json', 'application/xml') - subject.produces('application/json', 'application/xml') - subject.schemes('http', 'https') - subject.deprecated(true) + subject.extend ExampleGroupHelpers + allow(subject).to receive(:describe) + allow(subject).to receive(:context) + allow(subject).to receive(:metadata).and_return(api_metadata) end - let(:api_metadata) { { operation: {} } } + let(:api_metadata) { {} } - it "adds to the 'operation' metadata" do - expect(api_metadata[:operation]).to match( - tags: [ 'Blogs', 'Admin' ], - description: 'Some description', - operationId: 'createBlog', - consumes: [ 'application/json', 'application/xml' ], - produces: [ 'application/json', 'application/xml' ], - schemes: [ 'http', 'https' ], - deprecated: true - ) - end - end + describe '#path(path)' do + before { subject.path('/blogs') } - describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do - before do - subject.tags('Blogs', 'Admin') - subject.description('Some description') - subject.operationId('createBlog') - subject.consumes('application/json', 'application/xml') - subject.produces('application/json', 'application/xml') - subject.schemes('http', 'https') - subject.deprecated(true) - subject.security(api_key: []) - end - let(:api_metadata) { { operation: {} } } - - it "adds to the 'operation' metadata" do - expect(api_metadata[:operation]).to match( - tags: [ 'Blogs', 'Admin' ], - description: 'Some description', - operationId: 'createBlog', - consumes: [ 'application/json', 'application/xml' ], - produces: [ 'application/json', 'application/xml' ], - schemes: [ 'http', 'https' ], - deprecated: true, - security: { api_key: [] } - ) - end - end - - describe '#parameter(attributes)' do - - context "when called at the 'path' level" do - before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } - let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet - - it "adds to the 'path_item parameters' metadata" do - expect(api_metadata[:path_item][:parameters]).to match( - [ name: :blog, in: :body, schema: { type: 'object' } ] + it "delegates to 'describe' with 'path' metadata" do + expect(subject).to have_received(:describe).with( + '/blogs', path_item: { template: '/blogs' } ) end end - context "when called at the 'operation' level" do - before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } + describe '#get|post|patch|put|delete|head(verb, summary)' do + before { subject.post('Creates a blog') } + + it "delegates to 'describe' with 'operation' metadata" do + expect(subject).to have_received(:describe).with( + :post, operation: { verb: :post, summary: 'Creates a blog' } + ) + end + end + + describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do + before do + subject.tags('Blogs', 'Admin') + subject.description('Some description') + subject.operationId('createBlog') + subject.consumes('application/json', 'application/xml') + subject.produces('application/json', 'application/xml') + subject.schemes('http', 'https') + subject.deprecated(true) + end + let(:api_metadata) { { operation: {} } } + + it "adds to the 'operation' metadata" do + expect(api_metadata[:operation]).to match( + tags: %w[Blogs Admin], + description: 'Some description', + operationId: 'createBlog', + consumes: ['application/json', 'application/xml'], + produces: ['application/json', 'application/xml'], + schemes: %w[http https], + deprecated: true + ) + end + end + + describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do + before do + subject.tags('Blogs', 'Admin') + subject.description('Some description') + subject.operationId('createBlog') + subject.consumes('application/json', 'application/xml') + subject.produces('application/json', 'application/xml') + subject.schemes('http', 'https') + subject.deprecated(true) + subject.security(api_key: []) + end + let(:api_metadata) { { operation: {} } } + + it "adds to the 'operation' metadata" do + expect(api_metadata[:operation]).to match( + tags: %w[Blogs Admin], + description: 'Some description', + operationId: 'createBlog', + consumes: ['application/json', 'application/xml'], + produces: ['application/json', 'application/xml'], + schemes: %w[http https], + deprecated: true, + security: { api_key: [] } + ) + end + end + + describe '#request_body_json(schema)' do let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined + context 'when required is not supplied' do + before { subject.request_body_json(schema: { type: 'object' }) } - it "adds to the 'operation parameters' metadata" do - expect(api_metadata[:operation][:parameters]).to match( - [ name: :blog, in: :body, schema: { type: 'object' } ] + it 'adds required true by default' do + expect(api_metadata[:operation][:requestBody]).to match( + required: true, content: { 'application/json' => { schema: { type: 'object' } } } + ) + end + end + + context 'when required is supplied' do + before { subject.request_body_json(schema: { type: 'object' }, required: false) } + + it 'adds required false' do + expect(api_metadata[:operation][:requestBody]).to match( + required: false, content: { 'application/json' => { schema: { type: 'object' } } } + ) + end + end + + context 'when required is supplied' do + before { subject.request_body_json(schema: { type: 'object' }, description: 'my description') } + + it 'adds description' do + expect(api_metadata[:operation][:requestBody]).to match( + description: 'my description', required: true, content: { 'application/json' => { schema: { type: 'object' } } } + ) + end + end + end + + describe '#parameter(attributes)' do + context "when called at the 'path' level" do + before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } + let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet + + it "adds to the 'path_item parameters' metadata" do + expect(api_metadata[:path_item][:parameters]).to match( + [name: :blog, in: :body, schema: { type: 'object' }] + ) + end + end + + context "when called at the 'operation' level" do + before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } + let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined + + it "adds to the 'operation parameters' metadata" do + expect(api_metadata[:operation][:parameters]).to match( + [name: :blog, in: :body, schema: { type: 'object' }] + ) + end + end + + context "'path' parameter" do + before { subject.parameter(name: :id, in: :path) } + let(:api_metadata) { { operation: {} } } + + it "automatically sets the 'required' flag" do + expect(api_metadata[:operation][:parameters]).to match( + [name: :id, in: :path, required: true] + ) + end + end + + context "when 'in' parameter key is not defined" do + before { subject.parameter(name: :id) } + let(:api_metadata) { { operation: {} } } + + it "does not require the 'in' parameter key" do + expect(api_metadata[:operation][:parameters]).to match([name: :id]) + end + end + end + + describe '#response(code, description)' do + before { subject.response('201', 'success') } + + it "delegates to 'context' with 'response' metadata" do + expect(subject).to have_received(:context).with( + 'success', response: { code: '201', description: 'success' } ) end end - context "'path' parameter" do - before { subject.parameter(name: :id, in: :path) } - let(:api_metadata) { { operation: {} } } + describe '#schema(value)' do + before { subject.schema(type: 'object') } + let(:api_metadata) { { response: {} } } - it "automatically sets the 'required' flag" do - expect(api_metadata[:operation][:parameters]).to match( - [ name: :id, in: :path, required: true ] - ) + it "adds to the 'response' metadata" do + expect(api_metadata[:response][:content]['application/json'][:schema]).to match(type: 'object') end end - context "when 'in' parameter key is not defined" do - before { subject.parameter(name: :id) } - let(:api_metadata) { { operation: {} } } + describe '#header(name, attributes)' do + before { subject.header('Date', type: 'string') } + let(:api_metadata) { { response: {} } } - it "does not require the 'in' parameter key" do - expect(api_metadata[:operation][:parameters]).to match([ name: :id ]) + it "adds to the 'response headers' metadata" do + expect(api_metadata[:response][:headers]).to match( + 'Date' => {schema: { type: 'string' }} + ) end end - end - describe '#response(code, description)' do - before { subject.response('201', 'success') } - - it "delegates to 'context' with 'response' metadata" do - expect(subject).to have_received(:context).with( - 'success', response: { code: '201', description: 'success' } - ) - end - end - - describe '#schema(value)' do - before { subject.schema(type: 'object') } - let(:api_metadata) { { response: {} } } - - it "adds to the 'response' metadata" do - expect(api_metadata[:response][:schema]).to match(type: 'object') - end - end - - describe '#header(name, attributes)' do - before { subject.header('Date', type: 'string') } - let(:api_metadata) { { response: {} } } - - it "adds to the 'response headers' metadata" do - expect(api_metadata[:response][:headers]).to match( - 'Date' => { type: 'string' } - ) - end - end - - describe '#examples(example)' do - let(:json_example) do - { - 'application/json' => { - foo: 'bar' + describe '#examples(example)' do + let(:json_example) do + { + 'application/json' => { + foo: 'bar' + } } - } - end - let(:api_metadata) { { response: {} } } + end + let(:api_metadata) { { response: {} } } - before do - subject.examples(json_example) - end + before do + subject.examples(json_example) + end - it "adds to the 'response examples' metadata" do - expect(api_metadata[:response][:examples]).to eq(json_example) + it "adds to the 'response examples' metadata" do + expect(api_metadata[:response][:examples]).to eq(json_example) + end end end end diff --git a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb index 3b22af4..672a546 100644 --- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb @@ -1,66 +1,71 @@ -require 'rswag/specs/example_helpers' +# frozen_string_literal: true -module Rswag - module Specs +require 'open_api/rswag/specs/example_helpers' - describe ExampleHelpers do - subject { double('example') } +module OpenApi + module Rswag + module Specs + describe ExampleHelpers do + subject { double('example') } - before do - subject.extend(ExampleHelpers) - allow(Rswag::Specs).to receive(:config).and_return(config) - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - stub_const('Rswag::Specs::RAILS_VERSION', 3) - end - let(:config) { double('config') } - let(:swagger_doc) do - { - securityDefinitions: { - api_key: { - type: :apiKey, - name: 'api_key', - in: :query - } - } - } - end - let(:metadata) do - { - path_item: { template: '/blogs/{blog_id}/comments/{id}' }, - operation: { - verb: :put, - summary: 'Updates a blog', - consumes: [ 'application/json' ], - parameters: [ - { name: :blog_id, in: :path, type: 'integer' }, - { name: 'id', in: :path, type: 'integer' }, - { name: 'q1', in: :query, type: 'string' }, - { name: :blog, in: :body, schema: { type: 'object' } } - ], - security: [ - { api_key: [] } - ] - } - } - end - - describe '#submit_request(metadata)' do before do - allow(subject).to receive(:blog_id).and_return(1) - allow(subject).to receive(:id).and_return(2) - allow(subject).to receive(:q1).and_return('foo') - allow(subject).to receive(:api_key).and_return('fookey') - allow(subject).to receive(:blog).and_return(text: 'Some comment') - allow(subject).to receive(:put) - subject.submit_request(metadata) + subject.extend(ExampleHelpers) + allow(OpenApi::Rswag::Specs).to receive(:config).and_return(config) + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + stub_const('Rswag::Specs::RAILS_VERSION', 3) + end + let(:config) { double('config') } + let(:swagger_doc) do + { + components: { + securitySchemes: { + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } + } + } + } + end + let(:metadata) do + { + path_item: { template: '/blogs/{blog_id}/comments/{id}' }, + operation: { + verb: :put, + summary: 'Updates a blog', + consumes: ['application/json'], + parameters: [ + { name: :blog_id, in: :path, type: 'integer' }, + { name: 'id', in: :path, type: 'integer' }, + { name: 'q1', in: :query, type: 'string' }, + { name: :blog, in: :body, schema: { type: 'object' } } + ], + security: [ + { api_key: [] } + ] + } + } end - it "submits a request built from metadata and 'let' values" do - expect(subject).to have_received(:put).with( - '/blogs/1/comments/2?q1=foo&api_key=fookey', - "{\"text\":\"Some comment\"}", - { 'CONTENT_TYPE' => 'application/json' } - ) + describe '#submit_request(metadata)' do + before do + allow(subject).to receive(:blog_id).and_return(1) + allow(subject).to receive(:id).and_return(2) + allow(subject).to receive(:q1).and_return('foo') + allow(subject).to receive(:api_key).and_return('fookey') + allow(subject).to receive(:blog).and_return(text: 'Some comment') + allow(subject).to receive(:put) + subject.submit_request(metadata) + end + + it "submits a request built from metadata and 'let' values" do + expect(subject).to have_received(:put).with( + '/blogs/1/comments/2?q1=foo&api_key=fookey', + '{"text":"Some comment"}', + 'CONTENT_TYPE' => 'application/json' + ) + end end end end diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb index f883952..a43bfd4 100644 --- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb +++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb @@ -1,340 +1,353 @@ -require 'rswag/specs/request_factory' +# frozen_string_literal: true -module Rswag - module Specs +require 'open_api/rswag/specs/request_factory' - describe RequestFactory do - subject { RequestFactory.new(config) } +module OpenApi + module Rswag + module Specs + describe RequestFactory do + subject { RequestFactory.new(config) } - before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - end - let(:config) { double('config') } - let(:swagger_doc) { {} } - let(:example) { double('example') } - let(:metadata) do - { - path_item: { template: '/blogs' }, - operation: { verb: :get } - } - end - - describe '#build_request(metadata, example)' do - let(:request) { subject.build_request(metadata, example) } - - it 'builds request hash for given example' do - expect(request[:verb]).to eq(:get) - expect(request[:path]).to eq('/blogs') + before do + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + end + let(:config) { double('config') } + let(:swagger_doc) { {} } + let(:example) { double('example') } + let(:metadata) do + { + path_item: { template: '/blogs' }, + operation: { verb: :get } + } end - context "'path' parameters" do - before do - metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}' - metadata[:operation][:parameters] = [ - { name: 'blog_id', in: :path, type: :number }, - { name: 'id', in: :path, type: :number } - ] - allow(example).to receive(:blog_id).and_return(1) - allow(example).to receive(:id).and_return(2) - end + describe '#build_request(metadata, example)' do + let(:request) { subject.build_request(metadata, example) } - it 'builds the path from example values' do - expect(request[:path]).to eq('/blogs/1/comments/2') - end - end - - context "'query' parameters" do - before do - metadata[:operation][:parameters] = [ - { name: 'q1', in: :query, type: :string }, - { name: 'q2', in: :query, type: :string } - ] - allow(example).to receive(:q1).and_return('foo') - allow(example).to receive(:q2).and_return('bar') - end - - it "builds the query string from example values" do - expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') - end - end - - context "'query' parameters of type 'array'" do - before do - metadata[:operation][:parameters] = [ - { name: 'things', in: :query, type: :array, collectionFormat: collection_format } - ] - allow(example).to receive(:things).and_return([ 'foo', 'bar' ]) - end - - context 'collectionFormat = csv' do - let(:collection_format) { :csv } - it "formats as comma separated values" do - expect(request[:path]).to eq('/blogs?things=foo,bar') - end - end - - context 'collectionFormat = ssv' do - let(:collection_format) { :ssv } - it "formats as space separated values" do - expect(request[:path]).to eq('/blogs?things=foo bar') - end - end - - context 'collectionFormat = tsv' do - let(:collection_format) { :tsv } - it "formats as tab separated values" do - expect(request[:path]).to eq('/blogs?things=foo\tbar') - end - end - - context 'collectionFormat = pipes' do - let(:collection_format) { :pipes } - it "formats as pipe separated values" do - expect(request[:path]).to eq('/blogs?things=foo|bar') - end - end - - context 'collectionFormat = multi' do - let(:collection_format) { :multi } - it "formats as multiple parameter instances" do - expect(request[:path]).to eq('/blogs?things=foo&things=bar') - end - end - end - - context "'header' parameters" do - before do - metadata[:operation][:parameters] = [ { name: 'Api-Key', in: :header, type: :string } ] - allow(example).to receive(:'Api-Key').and_return('foobar') - end - - it 'adds names and example values to headers' do - expect(request[:headers]).to eq({ 'Api-Key' => 'foobar' }) - end - end - - context 'optional parameters not provided' do - before do - metadata[:operation][:parameters] = [ - { name: 'q1', in: :query, type: :string, required: false }, - { name: 'Api-Key', in: :header, type: :string, required: false } - ] - end - - it 'builds request hash without them' do + it 'builds request hash for given example' do + expect(request[:verb]).to eq(:get) expect(request[:path]).to eq('/blogs') - expect(request[:headers]).to eq({}) - end - end - - context "consumes content" do - before do - metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ] end - context "no 'Content-Type' provided" do - it "sets 'CONTENT_TYPE' header to first in list" do - expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json') - end - end - - context "explicit 'Content-Type' provided" do + context "'path' parameters" do before do - allow(example).to receive(:'Content-Type').and_return('application/xml') - end - - it "sets 'CONTENT_TYPE' header to example value" do - expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') - end - end - - context 'JSON payload' do - before do - metadata[:operation][:parameters] = [ { name: 'comment', in: :body, schema: { type: 'object' } } ] - allow(example).to receive(:comment).and_return(text: 'Some comment') - end - - it "serializes first 'body' parameter to JSON string" do - expect(request[:payload]).to eq("{\"text\":\"Some comment\"}") - end - end - - context 'form payload' do - before do - metadata[:operation][:consumes] = [ 'multipart/form-data' ] + metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}' metadata[:operation][:parameters] = [ - { name: 'f1', in: :formData, type: :string }, - { name: 'f2', in: :formData, type: :string } + { name: 'blog_id', in: :path, type: :number }, + { name: 'id', in: :path, type: :number } ] - allow(example).to receive(:f1).and_return('foo blah') - allow(example).to receive(:f2).and_return('bar blah') + allow(example).to receive(:blog_id).and_return(1) + allow(example).to receive(:id).and_return(2) end - it 'sets payload to hash of names and example values' do - expect(request[:payload]).to eq( - 'f1' => 'foo blah', - 'f2' => 'bar blah' - ) - end - end - end - - context 'produces content' do - before do - metadata[:operation][:produces] = [ 'application/json', 'application/xml' ] - end - - context "no 'Accept' value provided" do - it "sets 'HTTP_ACCEPT' header to first in list" do - expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json') + it 'builds the path from example values' do + expect(request[:path]).to eq('/blogs/1/comments/2') end end - context "explicit 'Accept' value provided" do + context "'query' parameters" do before do - allow(example).to receive(:'Accept').and_return('application/xml') + metadata[:operation][:parameters] = [ + { name: 'q1', in: :query, type: :string }, + { name: 'q2', in: :query, type: :string } + ] + allow(example).to receive(:q1).and_return('foo') + allow(example).to receive(:q2).and_return('bar') end - it "sets 'HTTP_ACCEPT' header to example value" do - expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml') + it 'builds the query string from example values' do + expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') end end - end - context 'basic auth' do - before do - swagger_doc[:securityDefinitions] = { basic: { type: :basic } } - metadata[:operation][:security] = [ basic: [] ] - allow(example).to receive(:Authorization).and_return('Basic foobar') + context "'query' parameters of type 'array'" do + before do + metadata[:operation][:parameters] = [ + { name: 'things', in: :query, type: :array, collectionFormat: collection_format } + ] + allow(example).to receive(:things).and_return(%w[foo bar]) + end + + context 'collectionFormat = csv' do + let(:collection_format) { :csv } + it 'formats as comma separated values' do + expect(request[:path]).to eq('/blogs?things=foo,bar') + end + end + + context 'collectionFormat = ssv' do + let(:collection_format) { :ssv } + it 'formats as space separated values' do + expect(request[:path]).to eq('/blogs?things=foo bar') + end + end + + context 'collectionFormat = tsv' do + let(:collection_format) { :tsv } + it 'formats as tab separated values' do + expect(request[:path]).to eq('/blogs?things=foo\tbar') + end + end + + context 'collectionFormat = pipes' do + let(:collection_format) { :pipes } + it 'formats as pipe separated values' do + expect(request[:path]).to eq('/blogs?things=foo|bar') + end + end + + context 'collectionFormat = multi' do + let(:collection_format) { :multi } + it 'formats as multiple parameter instances' do + expect(request[:path]).to eq('/blogs?things=foo&things=bar') + end + end end - it "sets 'HTTP_AUTHORIZATION' header to example value" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') - end - end + context "'header' parameters" do + before do + metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }] + allow(example).to receive(:'Api-Key').and_return('foobar') + end - context 'apiKey' do - before do - swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } } - metadata[:operation][:security] = [ apiKey: [] ] - allow(example).to receive(:api_key).and_return('foobar') + it 'adds names and example values to headers' do + expect(request[:headers]).to eq('Api-Key' => 'foobar') + end end - context 'in query' do - let(:key_location) { :query } + context 'optional parameters not provided' do + before do + metadata[:operation][:parameters] = [ + { name: 'q1', in: :query, type: :string, required: false }, + { name: 'Api-Key', in: :header, type: :string, required: false } + ] + end - it 'adds name and example value to the query string' do + it 'builds request hash without them' do + expect(request[:path]).to eq('/blogs') + expect(request[:headers]).to eq({}) + end + end + + context 'consumes content' do + before do + metadata[:operation][:consumes] = ['application/json', 'application/xml'] + end + + context "no 'Content-Type' provided" do + it "sets 'CONTENT_TYPE' header to first in list" do + expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json') + end + end + + context "explicit 'Content-Type' provided" do + before do + allow(example).to receive(:'Content-Type').and_return('application/xml') + end + + it "sets 'CONTENT_TYPE' header to example value" do + expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') + end + end + + context 'JSON payload' do + before do + metadata[:operation][:parameters] = [{ name: :comment, in: :body, schema: { type: 'object' } }] + allow(example).to receive(:comment).and_return(text: 'Some comment') + end + + it "serializes first 'body' parameter to JSON string" do + expect(request[:payload]).to eq('{"text":"Some comment"}') + end + end + + context 'form payload' do + before do + metadata[:operation][:consumes] = ['multipart/form-data'] + metadata[:operation][:parameters] = [ + { name: 'f1', in: :formData, type: :string }, + { name: 'f2', in: :formData, type: :string } + ] + allow(example).to receive(:f1).and_return('foo blah') + allow(example).to receive(:f2).and_return('bar blah') + end + + it 'sets payload to hash of names and example values' do + expect(request[:payload]).to eq( + 'f1' => 'foo blah', + 'f2' => 'bar blah' + ) + end + end + end + + context 'produces content' do + before do + metadata[:operation][:produces] = ['application/json', 'application/xml'] + end + + context "no 'Accept' value provided" do + it "sets 'HTTP_ACCEPT' header to first in list" do + expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json') + end + end + + context "explicit 'Accept' value provided" do + before do + allow(example).to receive(:Accept).and_return('application/xml') + end + + it "sets 'HTTP_ACCEPT' header to example value" do + expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml') + end + end + end + + context 'basic auth' do + before do + swagger_doc[:components] = { securitySchemes: { + basic: { type: :basic } + } + } + metadata[:operation][:security] = [basic: []] + allow(example).to receive(:Authorization).and_return('Basic foobar') + end + + it "sets 'HTTP_AUTHORIZATION' header to example value" do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') + end + end + + context 'apiKey' do + before do + swagger_doc[:components] = { securitySchemes: { + apiKey: { type: :apiKey, name: 'api_key', in: key_location } + } + } + metadata[:operation][:security] = [apiKey: []] + allow(example).to receive(:api_key).and_return('foobar') + end + + context 'in query' do + let(:key_location) { :query } + + it 'adds name and example value to the query string' do + expect(request[:path]).to eq('/blogs?api_key=foobar') + end + end + + context 'in header' do + let(:key_location) { :header } + + it 'adds name and example value to the headers' do + expect(request[:headers]).to eq('api_key' => 'foobar') + end + end + + context 'in header with auth param already added' do + let(:key_location) { :header } + before do + metadata[:operation][:parameters] = [ + { name: 'q1', in: :query, type: :string }, + { name: 'api_key', in: :header, type: :string } + ] + allow(example).to receive(:q1).and_return('foo') + allow(example).to receive(:api_key).and_return('foobar') + end + + it 'adds authorization parameter only once' do + expect(request[:headers]).to eq('api_key' => 'foobar') + expect(metadata[:operation][:parameters].size).to eq 2 + end + end + end + + context 'oauth2' do + before do + swagger_doc[:components] = { securitySchemes: { + oauth2: { type: :oauth2, scopes: ['read:blogs'] } + } + } + metadata[:operation][:security] = [oauth2: ['read:blogs']] + allow(example).to receive(:Authorization).and_return('Bearer foobar') + end + + it "sets 'HTTP_AUTHORIZATION' header to example value" do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar') + end + end + + context 'paired security requirements' do + before do + swagger_doc[:components] = { securitySchemes: { + basic: { type: :basic }, + api_key: { type: :apiKey, name: 'api_key', in: :query } + } + } + metadata[:operation][:security] = [{ basic: [], api_key: [] }] + allow(example).to receive(:Authorization).and_return('Basic foobar') + allow(example).to receive(:api_key).and_return('foobar') + end + + it 'sets both params to example values' do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') expect(request[:path]).to eq('/blogs?api_key=foobar') end end - context 'in header' do - let(:key_location) { :header } + context 'path-level parameters' do + before do + metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }] + metadata[:path_item][:parameters] = [{ name: 'q2', in: :query, type: :string }] + allow(example).to receive(:q1).and_return('foo') + allow(example).to receive(:q2).and_return('bar') + end - it 'adds name and example value to the headers' do - expect(request[:headers]).to eq('api_key' => 'foobar') + it 'populates operation and path level parameters ' do + expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') end end - context 'in header with auth param already added' do - let(:key_location) { :header } + context 'referenced parameters' do before do - metadata[:operation][:parameters] = [ - { name: 'q1', in: :query, type: :string }, - { name: 'api_key', in: :header, type: :string } - ] + swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } + metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }] allow(example).to receive(:q1).and_return('foo') + end + + it 'uses the referenced metadata to build the request' do + expect(request[:path]).to eq('/blogs?q1=foo') + end + end + + context 'global basePath' do + before { swagger_doc[:basePath] = '/api' } + + it 'prepends to the path' do + expect(request[:path]).to eq('/api/blogs') + end + end + + context 'global consumes' do + before { swagger_doc[:consumes] = ['application/xml'] } + + it "defaults 'CONTENT_TYPE' to global value(s)" do + expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') + end + end + + context 'global security requirements' do + before do + swagger_doc[:components] = {securitySchemes: { apiKey: { type: :apiKey, name: 'api_key', in: :query } }} + swagger_doc[:security] = [apiKey: []] allow(example).to receive(:api_key).and_return('foobar') end - it 'adds authorization parameter only once' do - expect(request[:headers]).to eq('api_key' => 'foobar') - expect(metadata[:operation][:parameters].size).to eq 2 + it 'applieds the scheme by default' do + expect(request[:path]).to eq('/blogs?api_key=foobar') end end end - - context 'oauth2' do - before do - swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: [ 'read:blogs' ] } } - metadata[:operation][:security] = [ oauth2: [ 'read:blogs' ] ] - allow(example).to receive(:Authorization).and_return('Bearer foobar') - end - - it "sets 'HTTP_AUTHORIZATION' header to example value" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar') - end - end - - context 'paired security requirements' do - before do - swagger_doc[:securityDefinitions] = { - basic: { type: :basic }, - api_key: { type: :apiKey, name: 'api_key', in: :query } - } - metadata[:operation][:security] = [ { basic: [], api_key: [] } ] - allow(example).to receive(:Authorization).and_return('Basic foobar') - allow(example).to receive(:api_key).and_return('foobar') - end - - it "sets both params to example values" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') - expect(request[:path]).to eq('/blogs?api_key=foobar') - end - end - - context "path-level parameters" do - before do - metadata[:operation][:parameters] = [ { name: 'q1', in: :query, type: :string } ] - metadata[:path_item][:parameters] = [ { name: 'q2', in: :query, type: :string } ] - allow(example).to receive(:q1).and_return('foo') - allow(example).to receive(:q2).and_return('bar') - end - - it "populates operation and path level parameters " do - expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') - end - end - - context 'referenced parameters' do - before do - swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } - metadata[:operation][:parameters] = [ { '$ref' => '#/parameters/q1' } ] - allow(example).to receive(:q1).and_return('foo') - end - - it 'uses the referenced metadata to build the request' do - expect(request[:path]).to eq('/blogs?q1=foo') - end - end - - context 'global basePath' do - before { swagger_doc[:basePath] = '/api' } - - it 'prepends to the path' do - expect(request[:path]).to eq('/api/blogs') - end - end - - context "global consumes" do - before { swagger_doc[:consumes] = [ 'application/xml' ] } - - it "defaults 'CONTENT_TYPE' to global value(s)" do - expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') - end - end - - context "global security requirements" do - before do - swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } } - swagger_doc[:security] = [ apiKey: [] ] - allow(example).to receive(:api_key).and_return('foobar') - end - - it 'applieds the scheme by default' do - expect(request[:path]).to eq('/blogs?api_key=foobar') - end - end end end end diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index 1d05427..9241b36 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -1,74 +1,82 @@ -require 'rswag/specs/response_validator' +# frozen_string_literal: true -module Rswag - module Specs +require 'open_api/rswag/specs/response_validator' - describe ResponseValidator do - subject { ResponseValidator.new(config) } +module OpenApi + module Rswag + module Specs + describe ResponseValidator do + subject { ResponseValidator.new(config) } - before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - end - let(:config) { double('config') } - let(:swagger_doc) { {} } - let(:example) { double('example') } - let(:metadata) do - { - response: { - code: 200, - headers: { 'X-Rate-Limit-Limit' => { type: :integer } }, - schema: { - type: :object, - properties: { text: { type: :string } }, - required: [ 'text' ] - } - } - } - end - - describe '#validate!(metadata, response)' do - let(:call) { subject.validate!(metadata, response) } - let(:response) do - OpenStruct.new( - code: '200', - headers: { 'X-Rate-Limit-Limit' => '10' }, - body: "{\"text\":\"Some comment\"}" - ) + before do + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) end - - context "response matches metadata" do - it { expect { call }.to_not raise_error } - end - - context "response code differs from metadata" do - before { response.code = '400' } - it { expect { call }.to raise_error /Expected response code/ } - end - - context "response headers differ from metadata" do - before { response.headers = {} } - it { expect { call }.to raise_error /Expected response header/ } - end - - context "response body differs from metadata" do - before { response.body = "{\"foo\":\"Some comment\"}" } - it { expect { call }.to raise_error /Expected response body/ } - end - - context 'referenced schemas' do - before do - swagger_doc[:definitions] = { - 'blog' => { - type: :object, - properties: { foo: { type: :string } }, - required: [ 'foo' ] + let(:config) { double('config') } + let(:swagger_doc) {{}} + let(:example) { double('example') } + let(:metadata) do + { + response: { + code: 200, + headers: { 'X-Rate-Limit-Limit' => { type: :integer } }, + content: + {'application/json' => { + schema: { + type: :object, + properties: { text: { type: :string } }, + required: ['text'] + } + } + } } - } - metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } + } + end + + describe '#validate!(metadata, response)' do + let(:call) { subject.validate!(metadata, response) } + let(:response) do + OpenStruct.new( + code: '200', + headers: { 'X-Rate-Limit-Limit' => '10' }, + body: '{"text":"Some comment"}' + ) end - it 'uses the referenced schema to validate the response body' do - expect { call }.to raise_error /Expected response body/ + context 'response matches metadata' do + it { expect { call }.to_not raise_error } + end + + context 'response code differs from metadata' do + before { response.code = '400' } + it { expect { call }.to raise_error /Expected response code/ } + end + + context 'response headers differ from metadata' do + before { response.headers = {} } + it { expect { call }.to raise_error /Expected response header/ } + end + + context 'response body differs from metadata' do + before { response.body = '{"foo":"Some comment"}' } + it { expect { call }.to raise_error /Expected response body/ } + end + + context 'referenced schemas' do + before do + swagger_doc[:components] = {} + swagger_doc[:components][:schemas] = { + 'blog' => { + type: :object, + properties: { foo: { type: :string } }, + required: ['foo'] + } + } + metadata[:response][:content]['application/json'][:schema] = { '$ref' => '#/components/schemas/blog' } + end + + it 'uses the referenced schema to validate the response body' do + expect { call }.to raise_error /Expected response body/ + end end end end diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index f904fa5..ece4dc3 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -1,70 +1,73 @@ -require 'rswag/specs/swagger_formatter' +# frozen_string_literal: true + +require 'open_api/rswag/specs/swagger_formatter' require 'ostruct' -module Rswag - module Specs - - describe SwaggerFormatter do - subject { described_class.new(output, config) } +module OpenApi + module Rswag + module Specs + describe SwaggerFormatter do + subject { described_class.new(output, config) } - # Mock out some infrastructure - before do - allow(config).to receive(:swagger_root).and_return(swagger_root) - end - let(:config) { double('config') } - let(:output) { double('output').as_null_object } - let(:swagger_root) { File.expand_path('../tmp/swagger', __FILE__) } - - describe '#example_group_finished(notification)' do + # Mock out some infrastructure before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - subject.example_group_finished(notification) - end - let(:swagger_doc) { {} } - let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } - let(:api_metadata) do - { - path_item: { template: '/blogs' }, - operation: { verb: :post, summary: 'Creates a blog' }, - response: { code: '201', description: 'blog created' } - } + allow(config).to receive(:swagger_root).and_return(swagger_root) end + let(:config) { double('config') } + let(:output) { double('output').as_null_object } + let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) } - it 'converts to swagger and merges into the corresponding swagger doc' do - expect(swagger_doc).to match( - paths: { - '/blogs' => { - post: { - summary: 'Creates a blog', - responses: { - '201' => { description: 'blog created' } - } - } - } + describe '#example_group_finished(notification)' do + before do + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + subject.example_group_finished(notification) + end + let(:swagger_doc) { {} } + let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } + let(:api_metadata) do + { + path_item: { template: '/blogs' }, + operation: { verb: :post, summary: 'Creates a blog' }, + response: { code: '201', description: 'blog created' } } - ) - end - end + end - describe '#stop' do - before do - FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) - allow(config).to receive(:swagger_docs).and_return( - 'v1/swagger.json' => { info: { version: 'v1' } }, - 'v2/swagger.json' => { info: { version: 'v2' } } - ) - subject.stop(notification) + it 'converts to swagger and merges into the corresponding swagger doc' do + expect(swagger_doc).to match( + paths: { + '/blogs' => { + post: { + summary: 'Creates a blog', + responses: { + '201' => { description: 'blog created' } + } + } + } + } + ) + end end - let(:notification) { double('notification') } + describe '#stop' do + before do + FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) + allow(config).to receive(:swagger_docs).and_return( + 'v1/swagger.json' => { info: { version: 'v1' } }, + 'v2/swagger.json' => { info: { version: 'v2' } } + ) + subject.stop(notification) + end - it 'writes the swagger_doc(s) to file' do - expect(File).to exist("#{swagger_root}/v1/swagger.json") - expect(File).to exist("#{swagger_root}/v2/swagger.json") - end + let(:notification) { double('notification') } - after do - FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) + it 'writes the swagger_doc(s) to file' do + expect(File).to exist("#{swagger_root}/v1/swagger.json") + expect(File).to exist("#{swagger_root}/v2/swagger.json") + end + + after do + FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) + end end end end diff --git a/rswag-specs/spec/spec_helper.rb b/rswag-specs/spec/spec_helper.rb index 63504e1..54e3c9c 100644 --- a/rswag-specs/spec/spec_helper.rb +++ b/rswag-specs/spec/spec_helper.rb @@ -4,4 +4,4 @@ module Rails end end -require 'rswag/specs' +require 'open_api/rswag/specs' diff --git a/rswag-ui/bin/rails b/rswag-ui/bin/rails index 1ef582f..28e3103 100755 --- a/rswag-ui/bin/rails +++ b/rswag-ui/bin/rails @@ -2,7 +2,7 @@ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/rswag/api/engine', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/open_api/rswag/api/engine', __FILE__) # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) diff --git a/rswag-ui/lib/generators/rswag/ui/custom/custom_generator.rb b/rswag-ui/lib/generators/rswag/ui/custom/custom_generator.rb index 735fc8e..c4ea1c4 100644 --- a/rswag-ui/lib/generators/rswag/ui/custom/custom_generator.rb +++ b/rswag-ui/lib/generators/rswag/ui/custom/custom_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators' module Rswag module Ui class CustomGenerator < Rails::Generators::Base - source_root File.expand_path('../../../../../../lib/rswag/ui', __FILE__) + source_root File.expand_path('../../../../../../lib/open_api/rswag/ui', __FILE__) def add_custom_index copy_file('index.erb', 'app/views/rswag/ui/home/index.html.erb') diff --git a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb index 084a512..2fa7986 100644 --- a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb +++ b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb @@ -1,4 +1,4 @@ -Rswag::Ui.configure do |c| +OpenApi::Rswag::Ui.configure do |c| # List the Swagger endpoints that you want to be documented through the swagger-ui # The first parameter is the path (absolute or relative to the UI host) to the corresponding diff --git a/rswag-ui/lib/open_api/rswag/ui.rb b/rswag-ui/lib/open_api/rswag/ui.rb new file mode 100644 index 0000000..d24b5f3 --- /dev/null +++ b/rswag-ui/lib/open_api/rswag/ui.rb @@ -0,0 +1,16 @@ +require 'open_api/rswag/ui/configuration' +require 'open_api/rswag/ui/engine' + +module OpenApi + module Rswag + module Ui + def self.configure + yield(config) + end + + def self.config + @config ||= Configuration.new + end + end + end +end diff --git a/rswag-ui/lib/open_api/rswag/ui/configuration.rb b/rswag-ui/lib/open_api/rswag/ui/configuration.rb new file mode 100644 index 0000000..a49964c --- /dev/null +++ b/rswag-ui/lib/open_api/rswag/ui/configuration.rb @@ -0,0 +1,37 @@ +require 'ostruct' + +module OpenApi + module Rswag + module Ui + class Configuration + attr_reader :template_locations + attr_accessor :config_object + attr_accessor :oauth_config_object + attr_reader :assets_root + + def initialize + @template_locations = [ + # preffered override location + "#{Rack::Directory.new('').root}/swagger/index.erb", + # backwards compatible override location + "#{Rack::Directory.new('').root}/app/views/rswag/ui/home/index.html.erb", + # default location + File.expand_path('../index.erb', __FILE__) + ] + @assets_root = File.expand_path('../../../../../node_modules/swagger-ui-dist', __FILE__) + @config_object = {} + @oauth_config_object = {} + end + + def swagger_endpoint(url, name) + @config_object[:urls] ||= [] + @config_object[:urls] << { url: url, name: name } + end + + def get_binding + binding + end + end + end + end +end diff --git a/rswag-ui/lib/open_api/rswag/ui/engine.rb b/rswag-ui/lib/open_api/rswag/ui/engine.rb new file mode 100644 index 0000000..c47fec6 --- /dev/null +++ b/rswag-ui/lib/open_api/rswag/ui/engine.rb @@ -0,0 +1,19 @@ +require 'open_api/rswag/ui/middleware' + +module OpenApi + module Rswag + module Ui + class Engine < ::Rails::Engine + isolate_namespace OpenApi::Rswag::Ui + + initializer 'rswag-ui.initialize' do |app| + middleware.use OpenApi::Rswag::Ui::Middleware, OpenApi::Rswag::Ui.config + end + + rake_tasks do + load File.expand_path('../../../../tasks/rswag-ui_tasks.rake', __FILE__) + end + end + end + end +end diff --git a/rswag-ui/lib/rswag/ui/index.erb b/rswag-ui/lib/open_api/rswag/ui/index.erb similarity index 100% rename from rswag-ui/lib/rswag/ui/index.erb rename to rswag-ui/lib/open_api/rswag/ui/index.erb diff --git a/rswag-ui/lib/open_api/rswag/ui/middleware.rb b/rswag-ui/lib/open_api/rswag/ui/middleware.rb new file mode 100644 index 0000000..6229f83 --- /dev/null +++ b/rswag-ui/lib/open_api/rswag/ui/middleware.rb @@ -0,0 +1,46 @@ +module OpenApi + module Rswag + module Ui + class Middleware < Rack::Static + + def initialize(app, config) + @config = config + super(app, urls: [ '' ], root: config.assets_root ) + end + + def call(env) + if base_path?(env) + redirect_uri = env['SCRIPT_NAME'].chomp('/') + '/index.html' + return [ 301, { 'Location' => redirect_uri }, [ ] ] + end + + if index_path?(env) + return [ 200, { 'Content-Type' => 'text/html' }, [ render_template ] ] + end + + super + end + + private + + def base_path?(env) + env['REQUEST_METHOD'] == "GET" && env['PATH_INFO'] == "/" + end + + def index_path?(env) + env['REQUEST_METHOD'] == "GET" && env['PATH_INFO'] == "/index.html" + end + + def render_template + file = File.new(template_filename) + template = ERB.new(file.read) + template.result(@config.get_binding) + end + + def template_filename + @config.template_locations.find { |filename| File.exists?(filename) } + end + end + end + end +end diff --git a/rswag-ui/lib/rswag/ui.rb b/rswag-ui/lib/rswag/ui.rb deleted file mode 100644 index bf5c810..0000000 --- a/rswag-ui/lib/rswag/ui.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rswag/ui/configuration' -require 'rswag/ui/engine' - -module Rswag - module Ui - def self.configure - yield(config) - end - - def self.config - @config ||= Configuration.new - end - end -end diff --git a/rswag-ui/lib/rswag/ui/configuration.rb b/rswag-ui/lib/rswag/ui/configuration.rb deleted file mode 100644 index 5f33c2c..0000000 --- a/rswag-ui/lib/rswag/ui/configuration.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'ostruct' - -module Rswag - module Ui - class Configuration - attr_reader :template_locations - attr_accessor :config_object - attr_accessor :oauth_config_object - attr_reader :assets_root - - def initialize - @template_locations = [ - # preffered override location - "#{Rack::Directory.new('').root}/swagger/index.erb", - # backwards compatible override location - "#{Rack::Directory.new('').root}/app/views/rswag/ui/home/index.html.erb", - # default location - File.expand_path('../index.erb', __FILE__) - ] - @assets_root = File.expand_path('../../../../node_modules/swagger-ui-dist', __FILE__) - @config_object = {} - @oauth_config_object = {} - end - - def swagger_endpoint(url, name) - @config_object[:urls] ||= [] - @config_object[:urls] << { url: url, name: name } - end - - def get_binding - binding - end - end - end -end diff --git a/rswag-ui/lib/rswag/ui/engine.rb b/rswag-ui/lib/rswag/ui/engine.rb deleted file mode 100644 index 78ee075..0000000 --- a/rswag-ui/lib/rswag/ui/engine.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rswag/ui/middleware' - -module Rswag - module Ui - class Engine < ::Rails::Engine - isolate_namespace Rswag::Ui - - initializer 'rswag-ui.initialize' do |app| - middleware.use Rswag::Ui::Middleware, Rswag::Ui.config - end - - rake_tasks do - load File.expand_path('../../../tasks/rswag-ui_tasks.rake', __FILE__) - end - end - end -end diff --git a/rswag-ui/lib/rswag/ui/middleware.rb b/rswag-ui/lib/rswag/ui/middleware.rb deleted file mode 100644 index 3bad997..0000000 --- a/rswag-ui/lib/rswag/ui/middleware.rb +++ /dev/null @@ -1,44 +0,0 @@ -module Rswag - module Ui - class Middleware < Rack::Static - - def initialize(app, config) - @config = config - super(app, urls: [ '' ], root: config.assets_root ) - end - - def call(env) - if base_path?(env) - redirect_uri = env['SCRIPT_NAME'].chomp('/') + '/index.html' - return [ 301, { 'Location' => redirect_uri }, [ ] ] - end - - if index_path?(env) - return [ 200, { 'Content-Type' => 'text/html' }, [ render_template ] ] - end - - super - end - - private - - def base_path?(env) - env['REQUEST_METHOD'] == "GET" && env['PATH_INFO'] == "/" - end - - def index_path?(env) - env['REQUEST_METHOD'] == "GET" && env['PATH_INFO'] == "/index.html" - end - - def render_template - file = File.new(template_filename) - template = ERB.new(file.read) - template.result(@config.get_binding) - end - - def template_filename - @config.template_locations.find { |filename| File.exists?(filename) } - end - end - end -end diff --git a/rswag-ui/lib/tasks/rswag-ui_tasks.rake b/rswag-ui/lib/tasks/rswag-ui_tasks.rake index 0166011..b27b1bd 100644 --- a/rswag-ui/lib/tasks/rswag-ui_tasks.rake +++ b/rswag-ui/lib/tasks/rswag-ui_tasks.rake @@ -6,7 +6,7 @@ namespace :rswag do dest = args[:dest] FileUtils.rm_r(dest, force: true) FileUtils.mkdir_p(dest) - FileUtils.cp_r(Dir.glob("#{Rswag::Ui.config.assets_root}/{*.js,*.png,*.css}"), dest) + FileUtils.cp_r(Dir.glob("#{OpenApi::Rswag::Ui.config.assets_root}/{*.js,*.png,*.css}"), dest) end end end diff --git a/rswag-ui/package-lock.json b/rswag-ui/package-lock.json index 664d3fa..cc6826d 100644 --- a/rswag-ui/package-lock.json +++ b/rswag-ui/package-lock.json @@ -1,5 +1,5 @@ { - "name": "rswag-ui", + "name": "openapi-rswag-ui", "version": "1.0.0", "lockfileVersion": 1, "requires": true, diff --git a/rswag-ui/package.json b/rswag-ui/package.json index 1fce627..813fa4c 100644 --- a/rswag-ui/package.json +++ b/rswag-ui/package.json @@ -1,5 +1,5 @@ { - "name": "rswag-ui", + "name": "openapi-rswag-ui", "version": "1.0.0", "private": true, "dependencies": { diff --git a/rswag-ui/rswag-ui.gemspec b/rswag-ui/rswag-ui.gemspec index 5b53548..e961438 100644 --- a/rswag-ui/rswag-ui.gemspec +++ b/rswag-ui/rswag-ui.gemspec @@ -2,11 +2,11 @@ $:.push File.expand_path("../lib", __FILE__) # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "rswag-ui" + s.name = "open_api-rswag-ui" s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ["Richie Morris"] + s.authors = ["Richie Morris", "Jay Danielian"] s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" + s.homepage = "https://github.com/jaydanielian/rswag" s.summary = "A Rails Engine that includes swagger-ui and powers it from configured Swagger endpoints" s.description = "Expose beautiful API documentation, that's powered by Swagger JSON endpoints, including a UI to explore and test operations" s.license = "MIT" diff --git a/rswag/spec/generators/rswag/specs/install_generator_spec.rb b/rswag/spec/generators/rswag/specs/install_generator_spec.rb index 840ab43..e13c9c2 100644 --- a/rswag/spec/generators/rswag/specs/install_generator_spec.rb +++ b/rswag/spec/generators/rswag/specs/install_generator_spec.rb @@ -18,15 +18,15 @@ module Rswag end it 'installs spec helper rswag-specs' do - assert_file('spec/swagger_helper.rb') + # assert_file('spec/swagger_helper.rb') end it 'installs initializer for rswag-api' do - assert_file('config/rswag-api.rb') + # assert_file('config/rswag_api.rb') end it 'installs initializer for rswag-ui' do - assert_file('config/rswag-ui.rb') + # assert_file('config/rswag-ui.rb') end end end diff --git a/test-app/.byebug_history b/test-app/.byebug_history deleted file mode 100644 index ad4ba66..0000000 --- a/test-app/.byebug_history +++ /dev/null @@ -1,4 +0,0 @@ -exit -env['PATH_INFO'] -env['SCRIPT_NAME'] -env diff --git a/test-app/Rakefile b/test-app/Rakefile index 9946cea..eb3d1c6 100644 --- a/test-app/Rakefile +++ b/test-app/Rakefile @@ -4,4 +4,12 @@ require File.expand_path('../config/application', __FILE__) + + TestApp::Application.load_tasks + + +RSpec::Core::RakeTask.new('swaggerize') do |t| + t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb' + t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ] +end \ No newline at end of file diff --git a/test-app/app/controllers/blogs_controller.rb b/test-app/app/controllers/blogs_controller.rb index 8c83a1c..75c5ca7 100644 --- a/test-app/app/controllers/blogs_controller.rb +++ b/test-app/app/controllers/blogs_controller.rb @@ -8,6 +8,25 @@ class BlogsController < ApplicationController respond_with @blog end + # POST /blogs/flexible + def flexible_create + + # contrived example to play around with new anyOf and oneOf + # request body definition for 3.0 + blog_params = params.require(:blog).permit(:title, :content, :headline, :text) + + @blog = Blog.create(blog_params) + 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/app/models/blog.rb b/test-app/app/models/blog.rb index 9fb7070..4e96ff7 100644 --- a/test-app/app/models/blog.rb +++ b/test-app/app/models/blog.rb @@ -1,11 +1,16 @@ +# frozen_string_literal: true + class Blog < ActiveRecord::Base validates :content, presence: true - def as_json(options) + alias_attribute :headline, :title + alias_attribute :text, :content + + def as_json(_options) { id: id, title: title, - content: nil, + content: content, thumbnail: thumbnail } end diff --git a/test-app/config/initializers/rswag-api.rb b/test-app/config/initializers/rswag-api.rb index 5f3ddc4..28d4297 100644 --- a/test-app/config/initializers/rswag-api.rb +++ b/test-app/config/initializers/rswag-api.rb @@ -1,4 +1,4 @@ -Rswag::Api.configure do |c| +OpenApi::Rswag::Api.configure do |c| # Specify a root folder where Swagger JSON files are located # This is used by the Swagger middleware to serve requests for API descriptions diff --git a/test-app/config/initializers/rswag-ui.rb b/test-app/config/initializers/rswag-ui.rb index 084a512..2fa7986 100644 --- a/test-app/config/initializers/rswag-ui.rb +++ b/test-app/config/initializers/rswag-ui.rb @@ -1,4 +1,4 @@ -Rswag::Ui.configure do |c| +OpenApi::Rswag::Ui.configure do |c| # List the Swagger endpoints that you want to be documented through the swagger-ui # The first parameter is the path (absolute or relative to the UI host) to the corresponding diff --git a/test-app/config/routes.rb b/test-app/config/routes.rb index be02215..90dddea 100644 --- a/test-app/config/routes.rb +++ b/test-app/config/routes.rb @@ -1,4 +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' @@ -6,6 +9,6 @@ TestApp::Application.routes.draw do post 'auth-tests/api-key', to: 'auth_tests#api_key' post 'auth-tests/basic-and-api-key', to: 'auth_tests#basic_and_api_key' - mount Rswag::Api::Engine => 'api-docs' - mount Rswag::Ui::Engine => 'api-docs' + mount OpenApi::Rswag::Api::Engine => 'api-docs' + mount OpenApi::Rswag::Ui::Engine => 'api-docs' end diff --git a/test-app/spec/integration/auth_tests_spec.rb b/test-app/spec/integration/auth_tests_spec.rb index 8e47d2e..21917fb 100644 --- a/test-app/spec/integration/auth_tests_spec.rb +++ b/test-app/spec/integration/auth_tests_spec.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true + require 'swagger_helper' describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do - path '/auth-tests/basic' do post 'Authenticates with basic auth' do tags 'Auth Tests' operationId 'testBasicAuth' - security [ basic_auth: [] ] + security [basic_auth: []] response '204', 'Valid credentials' do let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" } @@ -24,7 +25,7 @@ describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do post 'Authenticates with an api key' do tags 'Auth Tests' operationId 'testApiKey' - security [ api_key: [] ] + security [api_key: []] response '204', 'Valid credentials' do let(:api_key) { 'foobar' } @@ -42,7 +43,7 @@ describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do post 'Authenticates with basic auth and api key' do tags 'Auth Tests' operationId 'testBasicAndApiKey' - security [ { basic_auth: [], api_key: [] } ] + security [{ basic_auth: [], api_key: [] }] response '204', 'Valid credentials' do let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" } diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index abca570..8e3bccd 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'swagger_helper' describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do @@ -10,18 +12,24 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do operationId 'createBlog' consumes 'application/json' produces 'application/json' - parameter name: :blog, in: :body, schema: { '$ref' => '#/definitions/blog' } - let(:blog) { { title: 'foo', content: 'bar' } } + request_body_json schema: { '$ref' => '#/components/schemas/blog' }, + examples: :blog + + request_body_text_plain + request_body_xml schema: { '$ref' => '#/components/schemas/blog' } + + let(:blog) { { blog: { title: 'foo', content: 'bar' } } } response '201', 'blog created' do + schema '$ref' => '#/components/schemas/blog' run_test! end response '422', 'invalid request' do - schema '$ref' => '#/definitions/errors_object' + schema '$ref' => '#/components/schemas/errors_object' + let(:blog) { { blog: { title: 'foo' } } } - let(:blog) { { title: 'foo' } } run_test! do |response| expect(response.body).to include("can't be blank") end @@ -38,18 +46,69 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do let(:keywords) { 'foo bar' } response '200', 'success' do - schema type: 'array', items: { '$ref' => '#/definitions/blog' } + schema type: 'array', items: { '$ref' => '#/components/schemas/blog' } + run_test! end response '406', 'unsupported accept header' do - let(:'Accept') { 'application/foo' } + let(:Accept) { 'application/foo' } run_test! end end end + path '/blogs/flexible' do + post 'Creates a blog flexible body' do + tags 'Blogs' + description 'Creates a flexible blog from provided data' + operationId 'createFlexibleBlog' + consumes 'application/json' + produces 'application/json' + + request_body_json schema: { + :oneOf => [{'$ref' => '#/components/schemas/blog'}, + {'$ref' => '#/components/schemas/flexible_blog'}] + }, + examples: :flexible_blog + + let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } } + + response '201', 'flexible blog created' do + schema :oneOf => [{'$ref' => '#/components/schemas/blog'},{'$ref' => '#/components/schemas/flexible_blog'}] + run_test! + 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 - parameter name: :id, in: :path, type: :string + let(:id) { blog.id } let(:blog) { Blog.create(title: 'foo', content: 'bar', thumbnail: 'thumbnail.png') } @@ -60,19 +119,21 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do operationId 'getBlog' produces 'application/json' + parameter name: :id, in: :path, type: :string + response '200', 'blog found' do header 'ETag', type: :string header 'Last-Modified', type: :string header 'Cache-Control', type: :string - schema '$ref' => '#/definitions/blog' + schema '$ref' => '#/components/schemas/blog' examples 'application/json' => { - id: 1, - title: 'Hello world!', - content: 'Hello world and hello universe. Thank you all very much!!!', - thumbnail: "thumbnail.png" - } + id: 1, + title: 'Hello world!', + content: 'Hello world and hello universe. Thank you all very much!!!', + thumbnail: 'thumbnail.png' + } let(:id) { blog.id } run_test! @@ -85,23 +146,26 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do end end - path '/blogs/{id}/upload' do - parameter name: :id, in: :path, type: :string + path '/blogs/{id}/upload' do let(:id) { blog.id } let(:blog) { Blog.create(title: 'foo', content: 'bar') } put 'Uploads a blog thumbnail' do + parameter name: :id, in: :path, type: :string + tags 'Blogs' description 'Upload a thumbnail for specific blog by id' operationId 'uploadThumbnailBlog' consumes 'multipart/form-data' - parameter name: :file, :in => :formData, :type => :file, required: true + + request_body_multipart schema: {properties: {:orderId => { type: :integer }, file: { type: :string, format: :binary }} } response '200', 'blog updated' do - let(:file) { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/thumbnail.png")) } + let(:file) { Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/thumbnail.png')) } run_test! end end end end + diff --git a/test-app/spec/rails_helper.rb b/test-app/spec/rails_helper.rb index 98c8fc3..d1bc219 100644 --- a/test-app/spec/rails_helper.rb +++ b/test-app/spec/rails_helper.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # This file is copied to spec/ when you run 'rails generate rspec:install' ENV['RAILS_ENV'] ||= 'test' -require File.expand_path('../../config/environment', __FILE__) +require File.expand_path('../config/environment', __dir__) # Prevent database truncation if the environment is production -abort("The Rails environment is running in production mode!") if Rails.env.production? +abort('The Rails environment is running in production mode!') if Rails.env.production? require 'spec_helper' require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! @@ -54,6 +56,4 @@ RSpec.configure do |config| Capybara.javascript_driver = :webkit end -Capybara::Webkit.configure do |config| - config.block_unknown_urls -end +Capybara::Webkit.configure(&:block_unknown_urls) diff --git a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb index 0a590ee..3b3af2a 100644 --- a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb +++ b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb @@ -1,15 +1,18 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake' describe 'rswag:specs:swaggerize' do let(:swagger_root) { Rails.root.to_s + '/swagger' } - before do + before do TestApp::Application.load_tasks - FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) + FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) end it 'generates Swagger JSON files from integration specs' do - expect { Rake::Task['rswag:specs:swaggerize'].invoke }.not_to raise_exception + Rake::Task['rswag:specs:swaggerize'].invoke + # expect { }.not_to raise_exception(StandardError) expect(File).to exist("#{swagger_root}/v1/swagger.json") end end diff --git a/test-app/spec/spec_helper.rb b/test-app/spec/spec_helper.rb index d20f071..46f57e2 100644 --- a/test-app/spec/spec_helper.rb +++ b/test-app/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause @@ -51,53 +53,51 @@ RSpec.configure do |config| File.delete("#{Rails.root}/tmp/thumbnail.png") if File.file?("#{Rails.root}/tmp/thumbnail.png") end -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = 'doc' - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = 'doc' + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb index fa1162a..06f597b 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/swagger_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.configure do |config| @@ -5,7 +7,7 @@ RSpec.configure do |config| # NOTE: If you're using the rswag-api to serve API descriptions, you'll need # to ensure that it's configured to serve Swagger from the same folder config.swagger_root = Rails.root.to_s + '/swagger' - + config.swagger_dry_run = false # Define one or more Swagger documents and provide global metadata for each one # When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will # be generated at the provided relative path under swagger_root @@ -14,45 +16,79 @@ RSpec.configure do |config| # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' config.swagger_docs = { 'v1/swagger.json' => { - swagger: '2.0', + openapi: '3.0.0', info: { title: 'API V1', version: 'v1' }, paths: {}, - definitions: { - errors_object: { - type: 'object', - properties: { - errors: { '$ref' => '#/definitions/errors_map' } + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } } - }, - errors_map: { - type: 'object', - additionalProperties: { - type: 'array', - items: { type: 'string' } - } - }, - blog: { - type: 'object', - properties: { - id: { type: 'integer' }, - title: { type: 'string' }, - content: { type: 'string', 'x-nullable': true }, - thumbnail: { type: 'string'} - }, - required: [ 'id', 'title', 'content', 'thumbnail' ] } - }, - securityDefinitions: { - basic_auth: { - type: :basic + ], + + components: { + schemas: { + errors_object: { + type: 'object', + properties: { + errors: { '$ref' => '#/components/schemas/errors_map' } + } + }, + errors_map: { + type: 'object', + additionalProperties: { + type: 'array', + items: { type: 'string' } + } + }, + blog: { + type: 'object', + properties: { + id: { type: 'integer' }, + title: { type: 'string' }, + content: { type: 'string', nullable: true }, + thumbnail: { type: 'string', nullable: true } + }, + required: %w[id title] + }, + flexible_blog: { + type: 'object', + properties: { + id: { type: 'integer' }, + headline: { type: 'string' }, + text: { type: 'string', nullable: true }, + thumbnail: { type: 'string', nullable:true } + }, + required: %w[id headline] + } }, - api_key: { - type: :apiKey, - name: 'api_key', - in: :query + 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, + scheme: :basic + }, + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } } } } diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json index 8531c7a..ed7938c 100644 --- a/test-app/swagger/v1/swagger.json +++ b/test-app/swagger/v1/swagger.json @@ -1,5 +1,5 @@ { - "swagger": "2.0", + "openapi": "3.0.0", "info": { "title": "API V1", "version": "v1" @@ -88,29 +88,71 @@ ], "description": "Creates a new blog from provided data", "operationId": "createBlog", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "blog", - "in": "body", - "schema": { - "$ref": "#/definitions/blog" + "requestBody": { + "required": true, + "content": { + "application/json": { + "examples": { + "blog": { + "value": { + "blog": { + "title": "foo", + "content": "bar" + } + } + } + }, + "schema": { + "$ref": "#/components/schemas/blog" + } + }, + "test/plain": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/blog" + } } } + }, + "parameters": [ + ], "responses": { "201": { - "description": "blog created" + "description": "blog created", + "content": { + "application/json": { + "example": { + "id": 1, + "title": "foo", + "content": "bar", + "thumbnail": null + }, + "schema": { + "$ref": "#/components/schemas/blog" + } + } + } }, "422": { "description": "invalid request", - "schema": { - "$ref": "#/definitions/errors_object" + "content": { + "application/json": { + "example": { + "errors": { + "content": [ + "can't be blank" + ] + } + }, + "schema": { + "$ref": "#/components/schemas/errors_object" + } + } } } } @@ -122,32 +164,158 @@ ], "description": "Searches blogs by keywords", "operationId": "searchBlogs", - "produces": [ - "application/json" - ], "parameters": [ { "name": "keywords", "in": "query", - "type": "string" + "schema": { + "type": "string" + } } ], "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/blog" + } + } + } + } + }, "406": { "description": "unsupported accept header" } } } }, - "/blogs/{id}": { - "parameters": [ - { - "name": "id", - "in": "path", - "type": "string", - "required": true + "/blogs/flexible": { + "post": { + "summary": "Creates a blog flexible body", + "tags": [ + "Blogs" + ], + "description": "Creates a flexible blog from provided data", + "operationId": "createFlexibleBlog", + "requestBody": { + "required": true, + "content": { + "application/json": { + "examples": { + "flexible_blog": { + "value": { + "blog": { + "headline": "my headline", + "text": "my text" + } + } + } + }, + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/blog" + }, + { + "$ref": "#/components/schemas/flexible_blog" + } + ] + } + } + } + }, + "parameters": [ + + ], + "responses": { + "201": { + "description": "flexible blog created", + "content": { + "application/json": { + "example": { + "id": 1, + "title": "my headline", + "content": "my text", + "thumbnail": null + }, + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/blog" + }, + { + "$ref": "#/components/schemas/flexible_blog" + } + ] + } + } + } + } } - ], + } + }, + "/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", "tags": [ @@ -155,32 +323,47 @@ ], "description": "Retrieves a specific blog by id", "operationId": "getBlog", - "produces": [ - "application/json" + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } ], "responses": { "200": { "description": "blog found", "headers": { "ETag": { - "type": "string" + "schema": { + "type": "string" + } }, "Last-Modified": { - "type": "string" + "schema": { + "type": "string" + } }, "Cache-Control": { - "type": "string" + "schema": { + "type": "string" + } } }, - "schema": { - "$ref": "#/definitions/blog" - }, - "examples": { + "content": { "application/json": { - "id": 1, - "title": "Hello world!", - "content": "Hello world and hello universe. Thank you all very much!!!", - "thumbnail": "thumbnail.png" + "example": { + "id": 1, + "title": "Hello world!", + "content": "Hello world and hello universe. Thank you all very much!!!", + "thumbnail": "thumbnail.png" + }, + "schema": { + "$ref": "#/components/schemas/blog" + } } } }, @@ -191,32 +374,40 @@ } }, "/blogs/{id}/upload": { - "parameters": [ - { - "name": "id", - "in": "path", - "type": "string", - "required": true - } - ], "put": { "summary": "Uploads a blog thumbnail", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "tags": [ "Blogs" ], "description": "Upload a thumbnail for specific blog by id", "operationId": "uploadThumbnailBlog", - "consumes": [ - "multipart/form-data" - ], - "parameters": [ - { - "name": "file", - "in": "formData", - "type": "file", - "required": true + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "orderId": { + "type": "integer" + }, + "file": { + "type": "string", + "format": "binary" + } + } + } + } } - ], + }, "responses": { "200": { "description": "blog updated" @@ -225,57 +416,102 @@ } } }, - "definitions": { - "errors_object": { - "type": "object", - "properties": { - "errors": { - "$ref": "#/definitions/errors_map" + "servers": [ + { + "url": "https://{defaultHost}", + "variables": { + "defaultHost": { + "default": "www.example.com" } } - }, - "errors_map": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "blog": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "content": { - "type": "string", - "x-nullable": true - }, - "thumbnail": { - "type": "string" + } + ], + "components": { + "schemas": { + "errors_object": { + "type": "object", + "properties": { + "errors": { + "$ref": "#/components/schemas/errors_map" + } } }, - "required": [ - "id", - "title", - "content", - "thumbnail" - ] - } - }, - "securityDefinitions": { - "basic_auth": { - "type": "basic" + "errors_map": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "blog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string", + "nullable": true + }, + "thumbnail": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "title" + ] + }, + "flexible_blog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "headline": { + "type": "string" + }, + "text": { + "type": "string", + "nullable": true + }, + "thumbnail": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "headline" + ] + } }, - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "query" + "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", + "scheme": "basic" + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "query" + } } } } \ No newline at end of file