Skip to content

Commit

Permalink
Fix StubApp/RequestParamsParser to play nice together.
Browse files Browse the repository at this point in the history
There was a naming conflict on `interpol_config`, and both were
using the sinatra app to store state.  This was problematic so
I pulled the state-based stuff into a separate class (w/ its own
scope) so it's no longer a problem.
  • Loading branch information
myronmarston committed Oct 2, 2012
1 parent 7e35214 commit 41080da
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 102 deletions.
124 changes: 73 additions & 51 deletions lib/interpol/sinatra/request_params_parser.rb
@@ -1,4 +1,5 @@
require 'interpol/request_params_parser'
require 'forwardable'

module Interpol
module Sinatra
Expand All @@ -11,100 +12,110 @@ module Sinatra
# it can take a config block.
class RequestParamsParser
def initialize(app, &block)
@app = app
hook_into(app, &block)
@original_app_instance = app
hook_into_app(&block)
end

def call(env)
@app.call(env)
@original_app_instance.call(env)
end

ConfigurationError = Class.new(StandardError)

# Sinatra dups the app before each request, so we need to
# receive the app instance as an argument here.
def validate_and_parse_params(app)
return unless app.settings.parse_params?
SingleRequestParamsParser.parse_params(config, app)
end

private

def hook_into(app, &block)
return if defined?(app.settings.interpol_config)
check_configuration_validity(app)
attr_reader :config, :original_app_instance

def hook_into_app(&block)
return if defined?(@config)
check_configuration_validity

config = Configuration.default.customized_duplicate(&block)
@config = Configuration.default.customized_duplicate(&block)
parser = self

app.class.class_eval do
original_app_instance.class.class_eval do
alias unparsed_params params
helpers SinatraHelpers
set :interpol_config, config
set :request_params_parser, parser
enable :parse_params unless settings.respond_to?(:parse_params)
include SinatraOverriddes
end
end

def check_configuration_validity(app)
return if app.class.ancestors.include?(::Sinatra::Base)
def check_configuration_validity
return if original_app_instance.class.ancestors.include?(::Sinatra::Base)

raise ConfigurationError, "#{self.class} must come last in the Sinatra " +
"middleware list but #{app.class} currently comes after."
"middleware list but #{original_app_instance.class} " +
"currently comes after."
end

module SinatraHelpers
# Make the config available at the instance level for convenience.
def interpol_config
self.class.interpol_config
# Handles finding parsing request params for a single request.
class SingleRequestParamsParser
def self.parse_params(config, app)
new(config, app).parse_params
end

def endpoint_definition
@endpoint_definition ||= begin
version = available_versions = nil

definition = interpol_config.endpoints.find_definition \
env.fetch('REQUEST_METHOD'), request.path, 'request', nil do |endpoint|
available_versions ||= endpoint.available_versions
interpol_config.api_version_for(env, endpoint).tap do |_version|
version ||= _version
end
end

if definition == DefinitionFinder::NoDefinitionFound
interpol_config.request_version_unavailable(self, version, available_versions)
end

definition
end
def initialize(config, app)
@config = config
@app = app
end

def params
(@_use_parsed_params && @_parsed_params) || super
end

def validate_params
@_parsed_params ||= endpoint_definition.parse_request_params(params_to_parse)
def parse_params
endpoint_definition.parse_request_params(params_to_parse)
rescue Interpol::ValidationError => error
request_params_invalid(error)
end

def request_params_invalid(error)
interpol_config.sinatra_request_params_invalid(self, error)
end
private

def with_parsed_params
@_use_parsed_params = true
validate_params if settings.parse_params?
yield
ensure
@_use_parsed_params = false
attr_reader :app, :config
extend Forwardable
def_delegators :app, :request

def endpoint_definition
version = available_versions = nil

definition = config.endpoints.find_definition \
request.env.fetch('REQUEST_METHOD'), request.path, 'request', nil do |endpoint|
available_versions ||= endpoint.available_versions
config.api_version_for(request.env, endpoint).tap do |_version|
version ||= _version
end
end

if definition == DefinitionFinder::NoDefinitionFound
config.request_version_unavailable(app, version, available_versions)
end

definition
end

# Sinatra includes a couple of "meta" params that are always
# present in the params hash even though they are not declared
# as params: splat and captures.
def params_to_parse
unparsed_params.dup.tap do |p|
app.unparsed_params.dup.tap do |p|
p.delete('splat')
p.delete('captures')
end
end

def request_params_invalid(error)
config.sinatra_request_params_invalid(app, error)
end
end

module SinatraOverriddes
extend Forwardable
def_delegators :settings, :request_params_parser

# We cannot access the full params (w/ path params) in a before hook,
# due to the order that sinatra runs the hooks in relation to route
# matching.
Expand All @@ -126,6 +137,17 @@ def self.being_processed_by_sinatra?(block)
return true unless block.respond_to?(:source_location)
block.source_location.first.end_with?('sinatra/base.rb')
end

def params
@_parsed_params || super
end

def with_parsed_params
@_parsed_params = request_params_parser.validate_and_parse_params(self)
yield
ensure
@_parsed_params = nil
end
end
end
end
Expand Down
40 changes: 19 additions & 21 deletions lib/interpol/stub_app.rb
Expand Up @@ -13,29 +13,17 @@ def build(&block)
builder.app
end

module Helpers
def interpol_config
self.class.interpol_config
end

def example_for(endpoint, version, message_type)
example = endpoint.find_example_for!(version, message_type)
rescue NoEndpointDefinitionFoundError
interpol_config.request_version_unavailable(self, version, endpoint.available_versions)
else
example.apply_filters(interpol_config.filter_example_data_blocks, request.env)
end
end

# Private: Builds a stub sinatra app for the given interpol
# configuration.
class Builder
attr_reader :app
attr_reader :app, :config

def initialize(config)
builder = self
@config = config

@app = ::Sinatra.new do
set :interpol_config, config
helpers Helpers
set :stub_app_builder, builder
not_found { JSON.dump(:error => "The requested resource could not be found") }
before { content_type "application/json;charset=utf-8" }
get('/__ping') { JSON.dump(:message => "Interpol stub app running.") }
Expand All @@ -47,21 +35,31 @@ def self.name
end

def build
@app.interpol_config.endpoints.each do |endpoint|
config.endpoints.each do |endpoint|
app.send(endpoint.method, endpoint.route, &endpoint_definition(endpoint))
end
end

def endpoint_definition(endpoint)
lambda do
version = interpol_config.api_version_for(request.env, endpoint)
message_type = 'response'
example = example_for(endpoint, version, message_type)
example, version = settings.
stub_app_builder.
example_and_version_for(endpoint, self)
example.validate!
status endpoint.find_example_status_code_for!(version)
JSON.dump(example.data)
end
end

def example_and_version_for(endpoint, app)
version = config.api_version_for(app.request.env, endpoint)
example = endpoint.find_example_for!(version, 'response')
rescue NoEndpointDefinitionFoundError
config.request_version_unavailable(app, version, endpoint.available_versions)
else
example.apply_filters(config.filter_example_data_blocks, app.request.env)
return example, version
end
end
end
end
Expand Down
22 changes: 0 additions & 22 deletions spec/unit/interpol/sinatra/request_params_parser_spec.rb
Expand Up @@ -56,14 +56,6 @@ def sinatra_overrides(&block)
end
end

it 'makes the endpoint definition available as `endpoint_definition`' do
on_get { endpoint_definition.endpoint_name }

get '/users/23.12/projects/ruby'
last_response.status.should eq(200)
last_response.body.should eq(endpoint.name)
end

it 'makes the original unparsed params available as `unparsed_params`' do
on_get { JSON.dump(unparsed_params) }

Expand Down Expand Up @@ -140,20 +132,6 @@ def sinatra_overrides(&block)
available_versions.should eq(['1.0'])
end

it 'provides a means to add additional validations' do
configure_parser do |config|
config.on_invalid_sinatra_request_params do |error|
halt 412, error
end
end

on_get { request_params_invalid("bad") }

get '/users/12.23/projects/ruby'
last_response.status.should eq(412)
last_response.body.should eq("bad")
end

it 'allows unmatched routes to 404 as normal' do
get '/some/invalid/route'
last_response.status.should eq(404)
Expand Down
44 changes: 36 additions & 8 deletions spec/unit/interpol/stub_app_spec.rb
@@ -1,5 +1,6 @@
require 'fast_spec_helper'
require 'interpol/stub_app'
require 'interpol/sinatra/request_params_parser'
require 'rack/test'

module Interpol
Expand All @@ -13,26 +14,41 @@ module Interpol
method: GET
definitions:
- versions: ["1.0"]
message_type: response
schema:
type: object
properties:
name:
type: string
examples:
- name: "some project"
- versions: ["1.0"]
message_type: request
path_params:
type: object
properties:
user_id:
type: integer
schema: {}
examples: []
EOF
end

let(:endpoint) { Endpoint.new(YAML.load endpoint_definition_yml) }

let(:app) do
StubApp.build do |config|
config.stub(:endpoints => [endpoint])
let(:default_config) do
lambda do |config|
config.endpoints = [endpoint]

unless api_version_configured?(config) # allow default config to take precedence
config.api_version { |env, _| env.fetch('HTTP_API_VERSION') }
end
end.tap do |a|
end
end

let(:config) { app.stub_app_builder.config }

let(:app) do
StubApp.build(&default_config).tap do |a|
a.set :raise_errors, true
a.set :show_exceptions, false
end
Expand Down Expand Up @@ -77,7 +93,7 @@ def parsed_body
end

it 'uses any provided filters to modify the example data' do
app.interpol_config.filter_example_data do |example, request_env|
app.settings.stub_app_builder.config.filter_example_data do |example, request_env|
example.data["name"] << " for #{request_env["REQUEST_METHOD"]}"
end

Expand All @@ -89,14 +105,14 @@ def parsed_body
end

it 'allows errors in filters to bubble up' do
app.interpol_config.filter_example_data { raise ArgumentError }
config.filter_example_data { raise ArgumentError }

header 'API-Version', '1.0'
expect { get '/users/3/projects' }.to raise_error(ArgumentError)
end

it 'uses the unavailable_request_version hook when an invalid version is requested' do
app.interpol_config.on_unavailable_request_version do |requested_version, available_versions|
config.on_unavailable_request_version do |requested_version, available_versions|
halt 405, JSON.dump(:requested => requested_version, :available => available_versions)
end

Expand Down Expand Up @@ -141,6 +157,18 @@ def parsed_body
get '/__ping'
parsed_body.should eq("message" => "Interpol stub app running.")
end

it 'can be used together with the RequestParamsParser' do
app.use Interpol::Sinatra::RequestParamsParser, &default_config

header 'API-Version', '1.0'
get '/users/3/projects'
last_response.status.should eq(200)

get '/users/not-a-number/projects'
last_response.body.should include('user_id')
last_response.status.should eq(400)
end
end
end

0 comments on commit 41080da

Please sign in to comment.