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: .
specs:
swagger_rails (1.0.0.pre.beta2)
rack
rails (>= 3.1, < 5)
GEM
@ -111,7 +112,7 @@ GEM
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.49)
tzinfo (0.3.50)
PLATFORMS
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
```ruby
require 'rails_helper'
require 'swagger_rails/rspec/dsl'
require 'swagger_helper'
describe 'Blogs API' do
extend SwaggerRails::RSpec::DSL
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.
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
SwaggerRails.configure do |c|
RSpec.configure do |config|
...
c.swagger_doc 'v1/swagger.json' do
{
info: { title: 'API V1', version: 'v1' }
}
end
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
},
c.swagger_doc 'v2/swagger.json' do
{
info: { title: 'API V2', version: 'v2' }
'v2/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V2',
version: 'v2'
}
}
end
}
end
```
And then tagging your spec's with the target swagger_doc:
```ruby
require 'rails_helper'
require 'swagger_rails/rspec/dsl'
require 'swagger_helper'
describe 'Blogs API V2', swagger_doc: 'v2/swagger.json' do
extend SwaggerRails::RSpec::DSL
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.
## 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 ##
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
class SwaggerUiController < ApplicationController
def root
redirect_to action: 'index'
end
def index
swagger_root = SwaggerRails.config.resolve_swagger_root(request.env)
swagger_filenames = Dir["#{swagger_root}/**/*.json"]
@discovery_paths = Hash[
SwaggerRails.config.swagger_docs.map do |path, doc|
[ "#{root_path}#{path}", doc[:info][:title] ]
swagger_filenames.map do |filename|
[
"#{root_path.chomp('/')}#{filename.sub(swagger_root, '')}",
load_json(filename)["info"]["title"]
]
end
]
render :index, layout: false
end
private
def load_json(filename)
JSON.parse(File.read(filename))
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
root to: 'application#redirect_to_swagger_ui'
get '/index.html', to: 'swagger_ui#index', as: :swagger_ui
root to: 'swagger_ui#root'
get '/index.html', to: 'swagger_ui#index'
end

View File

@ -13,6 +13,10 @@ module SwaggerRails
template('swagger_rails.rb', 'config/initializers/swagger_rails.rb')
end
def add_rspec_helper
template('swagger_helper.rb', 'spec/swagger_helper.rb')
end
def add_routes
route("mount SwaggerRails::Engine => '/api-docs'")
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|
# Define your swagger documents and provide any global metadata here
# (Individual operations are generated from your spec/test files)
c.swagger_doc 'v1/swagger.json',
{
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
}
# Specify a root folder where Swagger JSON files are located
# This is used by the Swagger middleware to serve requests for API descriptions
# 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
c.swagger_root = Rails.root.to_s + '/swagger'
# Specify a location to output generated swagger files
c.swagger_dir File.expand_path('../../../swagger', __FILE__)
# Inject a lamda function to alter the returned Swagger prior to serialization
# 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

View File

@ -1,33 +1,15 @@
require "swagger_rails/engine"
require 'swagger_rails/version'
require 'swagger_rails/configuration'
module SwaggerRails
class Configuration
attr_reader :swagger_docs, :swagger_dir_string
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
def self.configure
yield(config)
end
class << self
attr_reader :config
def configure
yield config
end
def config
@config ||= Configuration.new
end
def self.config
@config ||= Configuration.new
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'
module SwaggerRails
@ -6,8 +6,8 @@ module SwaggerRails
isolate_namespace SwaggerRails
initializer 'swagger_rails.initialize' do |app|
middleware.use SwaggerDocs, SwaggerRails.config.swagger_dir_string
middleware.use SwaggerUi, "#{root}/bower_components/swagger-ui/dist"
middleware.use SwaggerJson, SwaggerRails.config
middleware.use SwaggerUi
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
class SwaggerUi < ActionDispatch::Static
IGNORE_PATHS = [ '', '/', '/index.html' ]
class SwaggerUi < Rack::Static
def call(env)
# Serve index.html via swagger_ui_controller
if IGNORE_PATHS.include?(env['PATH_INFO'])
@app.call(env)
else
super(env)
end
def initialize(app)
options = {
root: File.expand_path('../../../../bower_components/swagger-ui/dist', __FILE__),
urls: %w(/css /fonts /images /lang /lib /oc2.html /swagger-ui.js)
}
# NOTE: /index.html is excluded as it is servered dynamically (via conrtoller)
super(app, options)
end
end
end

View File

@ -50,9 +50,9 @@ module SwaggerRails
def run_test!
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
swagger_doc = SwaggerRails.config.swagger_docs.values.first
swagger_doc = ::RSpec.configuration.swagger_docs.values.first
end
test_visitor = SwaggerRails::TestVisitor.new(swagger_doc)

View File

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

View File

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

View File

@ -1,16 +1,14 @@
SwaggerRails.configure do |c|
# Define your swagger documents and provide any global metadata here
# (Individual operations are generated from your spec/test files)
c.swagger_doc 'v1/swagger.json',
{
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
}
# Specify a root folder where Swagger JSON files are located
# This is used by the Swagger middleware to serve requests for API descriptions
# 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
c.swagger_root = Rails.root.to_s + '/swagger'
# Specify a location to output generated swagger files
c.swagger_dir File.expand_path('../../../swagger', __FILE__)
# Inject a lamda function to alter the returned Swagger prior to serialization
# 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

View File

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

View File

@ -1,26 +1,34 @@
require 'rails_helper'
require 'generators/swagger_rails/install/install_generator'
describe SwaggerRails::InstallGenerator do
include GeneratorSpec::TestCase
destination File.expand_path('../tmp', __FILE__)
module SwaggerRails
before(:all) do
prepare_destination
config_dir = File.expand_path('../../fixtures/config', __FILE__)
FileUtils.cp_r(config_dir, destination_root)
describe InstallGenerator do
include GeneratorSpec::TestCase
destination File.expand_path('../tmp', __FILE__)
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
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

View File

@ -89,7 +89,4 @@ RSpec.configure do |config|
# as the one that triggered the failure.
Kernel.srand config.seed
=end
require 'swagger_rails/rspec/dsl'
config.extend SwaggerRails::RSpec::DSL
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.add_dependency 'rack'
s.add_dependency "rails", ">= 3.1", "< 5"
s.add_development_dependency "rspec-rails", "~> 3.0"