Support mount-specific swagger_root and add swagger_filter setting

This commit is contained in:
richie 2016-06-29 16:11:40 -07:00
parent 60f33a5386
commit 7a01babe01
30 changed files with 367 additions and 144 deletions

View File

@ -2,6 +2,7 @@ PATH
remote: . remote: .
specs: specs:
swagger_rails (1.0.0.pre.beta2) swagger_rails (1.0.0.pre.beta2)
rack
rails (>= 3.1, < 5) rails (>= 3.1, < 5)
GEM GEM
@ -111,7 +112,7 @@ GEM
treetop (1.4.15) treetop (1.4.15)
polyglot polyglot
polyglot (>= 0.3.1) polyglot (>= 0.3.1)
tzinfo (0.3.49) tzinfo (0.3.50)
PLATFORMS PLATFORMS
ruby ruby

View File

@ -26,11 +26,9 @@ __NOTE__: It's early days so please be gentle when reporting issues :) As author
3. Create an integration spec to describe and test your API 3. Create an integration spec to describe and test your API
```ruby ```ruby
require 'rails_helper' require 'swagger_helper'
require 'swagger_rails/rspec/dsl'
describe 'Blogs API' do describe 'Blogs API' do
extend SwaggerRails::RSpec::DSL
path '/blogs' do path '/blogs' do
@ -86,33 +84,40 @@ This will wire up routes for the swagger docs and swagger-ui assets, all prefixe
If you'd like your swagger resources to appear under a different base path, you can change the Engine mount point from "/api-docs" to something else. If you'd like your swagger resources to appear under a different base path, you can change the Engine mount point from "/api-docs" to something else.
By default, the generator will create all operation descriptions in a single swagger.json file. You can customize this by defining additional documents in the swagger_rails initializer (also created by the install generator) ... ## Multiple Swagger Documents ##
By default, the generator will create all operation descriptions in a single swagger.json file. You can customize this by defining additional documents in the swagger_helper (installed under your spec folder) ...
```ruby ```ruby
SwaggerRails.configure do |c| RSpec.configure do |config|
...
c.swagger_doc 'v1/swagger.json' do config.swagger_docs = {
{ 'v1/swagger.json' => {
info: { title: 'API V1', version: 'v1' } swagger: '2.0',
} info: {
end title: 'API V1',
version: 'v1'
}
},
c.swagger_doc 'v2/swagger.json' do 'v2/swagger.json' => {
{ swagger: '2.0',
info: { title: 'API V2', version: 'v2' } info: {
title: 'API V2',
version: 'v2'
}
} }
end }
end end
``` ```
And then tagging your spec's with the target swagger_doc: And then tagging your spec's with the target swagger_doc:
```ruby ```ruby
require 'rails_helper' require 'swagger_helper'
require 'swagger_rails/rspec/dsl'
describe 'Blogs API V2', swagger_doc: 'v2/swagger.json' do describe 'Blogs API V2', swagger_doc: 'v2/swagger.json' do
extend SwaggerRails::RSpec::DSL
path '/blogs' do path '/blogs' do
... ...
@ -123,6 +128,20 @@ And then tagging your spec's with the target swagger_doc:
Then, when you run the generator and spin up the swagger-ui, you'll see a select box in the top right allowing your audience to switch between the different API versions. Then, when you run the generator and spin up the swagger-ui, you'll see a select box in the top right allowing your audience to switch between the different API versions.
## Tweaking the Swagger Document with Request Context ##
You can provide global metadata for Swagger documents in the swagger_helper file and this will be included in the resulting Swagger JSON when you run the "swaggerize" rake task. For the most part, this is sufficient. However, you may want to make some changes that require the current request context. This is possible by applying an optional swagger_filter in the swagger_rails initializer (installed into config/initializers):
```ruby
SwaggerRails.configure do |c|
...
c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end
```
This function will get called prior to serialization of any Swagger file and is passed the rack env for the current request. This provides a lot of flexibilty. For example, you could dynamically assign the "host" property (as shown above) or you could inspect session information or Authoriation header and remove operations based on user permissions.
## Customizing the UI ## ## Customizing the UI ##
The swagger-ui provides several options for customizing it's behavior, all of which are documented here https://github.com/swagger-api/swagger-ui#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator. The swagger-ui provides several options for customizing it's behavior, all of which are documented here https://github.com/swagger-api/swagger-ui#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator.

View File

@ -1,8 +0,0 @@
module SwaggerRails
class ApplicationController < ActionController::Base
def redirect_to_swagger_ui
redirect_to swagger_ui_path
end
end
end

View File

@ -1,14 +1,32 @@
require 'json'
module SwaggerRails module SwaggerRails
class SwaggerUiController < ApplicationController class SwaggerUiController < ApplicationController
def root
redirect_to action: 'index'
end
def index def index
swagger_root = SwaggerRails.config.resolve_swagger_root(request.env)
swagger_filenames = Dir["#{swagger_root}/**/*.json"]
@discovery_paths = Hash[ @discovery_paths = Hash[
SwaggerRails.config.swagger_docs.map do |path, doc| swagger_filenames.map do |filename|
[ "#{root_path}#{path}", doc[:info][:title] ] [
"#{root_path.chomp('/')}#{filename.sub(swagger_root, '')}",
load_json(filename)["info"]["title"]
]
end end
] ]
render :index, layout: false render :index, layout: false
end end
private
def load_json(filename)
JSON.parse(File.read(filename))
end
end end
end end

View File

@ -1,4 +0,0 @@
module SwaggerRails
module ApplicationHelper
end
end

View File

@ -1,4 +1,4 @@
SwaggerRails::Engine.routes.draw do SwaggerRails::Engine.routes.draw do
root to: 'application#redirect_to_swagger_ui' root to: 'swagger_ui#root'
get '/index.html', to: 'swagger_ui#index', as: :swagger_ui get '/index.html', to: 'swagger_ui#index'
end end

View File

@ -13,6 +13,10 @@ module SwaggerRails
template('swagger_rails.rb', 'config/initializers/swagger_rails.rb') template('swagger_rails.rb', 'config/initializers/swagger_rails.rb')
end end
def add_rspec_helper
template('swagger_helper.rb', 'spec/swagger_helper.rb')
end
def add_routes def add_routes
route("mount SwaggerRails::Engine => '/api-docs'") route("mount SwaggerRails::Engine => '/api-docs'")
end end

View File

@ -0,0 +1,30 @@
require 'rails_helper'
require 'swagger_rails/rspec/dsl'
RSpec.configure do |config|
# NOTE: Should be no need to modify these 3 lines
config.add_setting :swagger_root
config.add_setting :swagger_docs
config.extend SwaggerRails::RSpec::DSL
# Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the Swagger JSON middleware to serve API descriptions, you'll need
# to ensure that the same folder is also specified in the swagger_rails initializer
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and provide global metadata for each one
# When you run the "swaggerize" rake task, the complete Swagger will be generated
# at the provided relative path under swagger_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
}
}
end

View File

@ -1,16 +1,14 @@
SwaggerRails.configure do |c| SwaggerRails.configure do |c|
# Define your swagger documents and provide any global metadata here # Specify a root folder where Swagger JSON files are located
# (Individual operations are generated from your spec/test files) # This is used by the Swagger middleware to serve requests for API descriptions
c.swagger_doc 'v1/swagger.json', # NOTE: If you're using the rspec DSL to generate Swagger, you'll need to ensure
{ # that the same folder is also specified in spec/swagger_helper.rb
swagger: '2.0', c.swagger_root = Rails.root.to_s + '/swagger'
info: {
title: 'API V1',
version: 'v1'
}
}
# Specify a location to output generated swagger files # Inject a lamda function to alter the returned Swagger prior to serialization
c.swagger_dir File.expand_path('../../../swagger', __FILE__) # The function will have access to the rack env for the current request
# For example, you could leverage this to dynamically assign the "host" property
#
#c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end end

View File

@ -1,33 +1,15 @@
require "swagger_rails/engine" require 'swagger_rails/version'
require 'swagger_rails/configuration'
module SwaggerRails module SwaggerRails
class Configuration def self.configure
attr_reader :swagger_docs, :swagger_dir_string yield(config)
def initialize
@swagger_docs = {}
@swagger_dir_string = nil
end
def swagger_doc(path, doc)
@swagger_docs[path] = doc
end
def swagger_dir(dir_string)
@swagger_dir_string = dir_string
end
end end
class << self def self.config
attr_reader :config @config ||= Configuration.new
def configure
yield config
end
def config
@config ||= Configuration.new
end
end end
end end
require 'swagger_rails/engine' if defined?(Rails)

View File

@ -0,0 +1,10 @@
module SwaggerRails
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

View File

@ -1,4 +1,4 @@
require 'swagger_rails/middleware/swagger_docs' require 'swagger_rails/middleware/swagger_json'
require 'swagger_rails/middleware/swagger_ui' require 'swagger_rails/middleware/swagger_ui'
module SwaggerRails module SwaggerRails
@ -6,8 +6,8 @@ module SwaggerRails
isolate_namespace SwaggerRails isolate_namespace SwaggerRails
initializer 'swagger_rails.initialize' do |app| initializer 'swagger_rails.initialize' do |app|
middleware.use SwaggerDocs, SwaggerRails.config.swagger_dir_string middleware.use SwaggerJson, SwaggerRails.config
middleware.use SwaggerUi, "#{root}/bower_components/swagger-ui/dist" middleware.use SwaggerUi
end end
end end
end end

View File

@ -1,4 +0,0 @@
module SwaggerRails
class SwaggerDocs < ActionDispatch::Static
end
end

View File

@ -0,0 +1,35 @@
require 'json'
module SwaggerRails
class SwaggerJson
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

View File

@ -1,14 +1,13 @@
module SwaggerRails module SwaggerRails
class SwaggerUi < ActionDispatch::Static class SwaggerUi < Rack::Static
IGNORE_PATHS = [ '', '/', '/index.html' ]
def call(env) def initialize(app)
# Serve index.html via swagger_ui_controller options = {
if IGNORE_PATHS.include?(env['PATH_INFO']) root: File.expand_path('../../../../bower_components/swagger-ui/dist', __FILE__),
@app.call(env) urls: %w(/css /fonts /images /lang /lib /oc2.html /swagger-ui.js)
else }
super(env) # NOTE: /index.html is excluded as it is servered dynamically (via conrtoller)
end super(app, options)
end end
end end
end end

View File

@ -50,9 +50,9 @@ module SwaggerRails
def run_test! def run_test!
if metadata.has_key?(:swagger_doc) if metadata.has_key?(:swagger_doc)
swagger_doc = SwaggerRails.config.swagger_docs[metadata[:swagger_doc]] swagger_doc = ::RSpec.configuration.swagger_docs[metadata[:swagger_doc]]
else else
swagger_doc = SwaggerRails.config.swagger_docs.values.first swagger_doc = ::RSpec.configuration.swagger_docs.values.first
end end
test_visitor = SwaggerRails::TestVisitor.new(swagger_doc) test_visitor = SwaggerRails::TestVisitor.new(swagger_doc)

View File

@ -1,18 +1,17 @@
require 'rspec/core/formatters/base_text_formatter' require 'rspec/core/formatters'
require 'rails_helper' require 'swagger_helper'
module SwaggerRails module SwaggerRails
module RSpec module RSpec
class Formatter class Formatter
::RSpec::Core::Formatters.register self, ::RSpec::Core::Formatters.register self,
:example_group_finished, :example_group_finished,
:stop :stop
def initialize(output, config=SwaggerRails.config) def initialize(output)
@output = output @output = output
@swagger_docs = config.swagger_docs @swagger_root = ::RSpec.configuration.swagger_root
@swagger_dir_string = config.swagger_dir_string @swagger_docs = ::RSpec.configuration.swagger_docs
@output.puts 'Generating Swagger Docs ...' @output.puts 'Generating Swagger Docs ...'
end end
@ -27,8 +26,8 @@ module SwaggerRails
end end
def stop(notification) def stop(notification)
@swagger_docs.each do |path, doc| @swagger_docs.each do |url_path, doc|
file_path = File.join(@swagger_dir_string, path) file_path = File.join(@swagger_root, url_path)
File.open(file_path, 'w') do |file| File.open(file_path, 'w') do |file|
file.write(JSON.pretty_generate(doc)) file.write(JSON.pretty_generate(doc))

View File

@ -5,7 +5,6 @@
if defined?(RSpec) if defined?(RSpec)
require 'rspec/core/rake_task' require 'rspec/core/rake_task'
require 'swagger_rails'
desc 'Generate Swagger JSON files from integration specs' desc 'Generate Swagger JSON files from integration specs'
RSpec::Core::RakeTask.new('swaggerize') do |t| RSpec::Core::RakeTask.new('swaggerize') do |t|

View File

@ -1,16 +1,14 @@
SwaggerRails.configure do |c| SwaggerRails.configure do |c|
# Define your swagger documents and provide any global metadata here # Specify a root folder where Swagger JSON files are located
# (Individual operations are generated from your spec/test files) # This is used by the Swagger middleware to serve requests for API descriptions
c.swagger_doc 'v1/swagger.json', # NOTE: If you're using the rspec DSL to generate Swagger, you'll need to ensure
{ # that the same folder is also specified in spec/swagger_helper.rb
swagger: '2.0', c.swagger_root = Rails.root.to_s + '/swagger'
info: {
title: 'API V1',
version: 'v1'
}
}
# Specify a location to output generated swagger files # Inject a lamda function to alter the returned Swagger prior to serialization
c.swagger_dir File.expand_path('../../../swagger', __FILE__) # The function will have access to the rack env for the current request
# For example, you could leverage this to dynamically assign the "host" property
#
#c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end end

View File

@ -1,4 +1,4 @@
require 'rails_helper' require 'swagger_helper'
describe 'Blogs API', swagger_doc: 'v1/swagger.json' do describe 'Blogs API', swagger_doc: 'v1/swagger.json' do

View File

@ -0,0 +1,30 @@
require 'rails_helper'
require 'swagger_rails/rspec/dsl'
RSpec.configure do |config|
# NOTE: Should be no need to modify these 3 lines
config.add_setting :swagger_root
config.add_setting :swagger_docs
config.extend SwaggerRails::RSpec::DSL
# Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the Swagger JSON middleware to serve API descriptions, you'll need
# to ensure that the same folder is also specified in the swagger_rails initializer
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and provide global metadata for each one
# When you run the "swaggerize" rake task, the complete Swagger will be generated
# at the provided relative path under swagger_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
}
}
end

View File

@ -87,4 +87,4 @@
} }
} }
} }
} }

View File

@ -1,16 +1,19 @@
require 'rails_helper' require 'rails_helper'
require 'generators/swagger_rails/custom_ui/custom_ui_generator' require 'generators/swagger_rails/custom_ui/custom_ui_generator'
describe SwaggerRails::CustomUiGenerator do module SwaggerRails
include GeneratorSpec::TestCase
destination File.expand_path('../tmp', __FILE__)
before(:all) do describe CustomUiGenerator do
prepare_destination include GeneratorSpec::TestCase
run_generator destination File.expand_path('../tmp', __FILE__)
end
it 'creates a local version of index.html.erb' do before(:all) do
assert_file('app/views/swagger_rails/swagger_ui/index.html.erb') prepare_destination
run_generator
end
it 'creates a local version of index.html.erb' do
assert_file('app/views/swagger_rails/swagger_ui/index.html.erb')
end
end end
end end

View File

@ -1,26 +1,34 @@
require 'rails_helper' require 'rails_helper'
require 'generators/swagger_rails/install/install_generator' require 'generators/swagger_rails/install/install_generator'
describe SwaggerRails::InstallGenerator do module SwaggerRails
include GeneratorSpec::TestCase
destination File.expand_path('../tmp', __FILE__)
before(:all) do describe InstallGenerator do
prepare_destination include GeneratorSpec::TestCase
config_dir = File.expand_path('../../fixtures/config', __FILE__) destination File.expand_path('../tmp', __FILE__)
FileUtils.cp_r(config_dir, destination_root)
run_generator before(:all) do
prepare_destination
fixtures_dir = File.expand_path('../fixtures', __FILE__)
FileUtils.cp_r("#{fixtures_dir}/config", destination_root)
FileUtils.cp_r("#{fixtures_dir}/spec", destination_root)
run_generator
end
it 'creates a default swagger directory' do
assert_directory('swagger/v1')
end
it 'installs swagger_rails initializer' do
assert_file('config/initializers/swagger_rails.rb')
end
it 'installs the swagger_helper for rspec' do
assert_file('spec/swagger_helper.rb')
end
it 'wires up the swagger routes'
# Not sure how to test this
end end
it 'creates a default swagger directory' do
assert_directory('swagger/v1')
end
it 'creates a swagger_rails initializer' do
assert_file('config/initializers/swagger_rails.rb')
end
it 'wires up the swagger routes'
# Not sure how to test this
end end

View File

@ -89,7 +89,4 @@ RSpec.configure do |config|
# as the one that triggered the failure. # as the one that triggered the failure.
Kernel.srand config.seed Kernel.srand config.seed
=end =end
require 'swagger_rails/rspec/dsl'
config.extend SwaggerRails::RSpec::DSL
end end

30
spec/swagger_helper.rb Normal file
View File

@ -0,0 +1,30 @@
require 'rails_helper'
require 'swagger_rails/rspec/dsl'
RSpec.configure do |config|
# NOTE: Should be no need to modify these 3 lines
config.add_setting :swagger_root
config.add_setting :swagger_docs
config.extend SwaggerRails::RSpec::DSL
# Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the Swagger JSON middleware to serve API descriptions, you'll need
# to ensure that the same folder is also specified in the swagger_rails initializer
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and provide global metadata for each one
# When you run the "swaggerize" rake task, the complete Swagger will be generated
# at the provided relative path under swagger_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
}
}
end

View File

@ -0,0 +1,78 @@
require 'rails_helper'
module SwaggerRails
describe SwaggerJson do
let(:app) { double('app') }
let(:config) do
Configuration.new.tap { |c| c.swagger_root = (Rails.root + 'swagger').to_s }
end
subject { described_class.new(app, config) }
describe '#call(env)' do
let(:response) { subject.call(env) }
let(:env_defaults) do
{
'HTTP_HOST' => 'tempuri.org',
'REQUEST_METHOD' => 'GET',
}
end
context 'given a path that maps to an existing swagger file' do
let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
it 'returns a 200 status' do
expect(response.length).to eql(3)
expect(response.first).to eql('200')
end
it 'returns contents of the swagger file' do
expect(response.length).to eql(3)
expect(response.second).to include( 'Content-Type' => 'application/json')
expect(response.third.join).to include('"title":"API V1"')
end
end
context "given a path that doesn't map to any swagger file" do
let(:env) { env_defaults.merge('PATH_INFO' => 'foobar.json') }
before do
allow(app).to receive(:call).and_return([ '500', {}, [] ])
end
it 'delegates to the next middleware' do
expect(response).to include('500')
end
end
context 'when the env contains a specific swagger_root' do
let(:env) do
env_defaults.merge(
'PATH_INFO' => 'swagger.json',
'action_dispatch.request.path_parameters' => {
swagger_root: (Rails.root + 'swagger/v1').to_s
}
)
end
it 'locates files at the provided swagger_root' do
expect(response.length).to eql(3)
expect(response.second).to include( 'Content-Type' => 'application/json')
expect(response.third.join).to include('"swagger":"2.0"')
end
end
context 'when a swagger_filter is configured' do
before do
config.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end
let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
it 'applies the filter prior to serialization' do
expect(response.length).to eql(3)
expect(response.third.join).to include('"host":"tempuri.org"')
end
end
end
end
end

View File

@ -16,6 +16,7 @@ Gem::Specification.new do |s|
s.files = Dir["{app,bower_components/swagger-ui/dist,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] s.files = Dir["{app,bower_components/swagger-ui/dist,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
s.add_dependency 'rack'
s.add_dependency "rails", ">= 3.1", "< 5" s.add_dependency "rails", ">= 3.1", "< 5"
s.add_development_dependency "rspec-rails", "~> 3.0" s.add_development_dependency "rspec-rails", "~> 3.0"