Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove rack-accept dependency #2389

Merged
merged 11 commits into from
Mar 24, 2024
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ RSpec/Capybara/FeatureMethods:

RSpec/ExampleLength:
Max: 60

RSpec/NestedGroups:
Max: 4
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* [#2406](https://github.com/ruby-grape/grape/pull/2406): Remove mime-types dependency in specs - [@ericproulx](https://github.com/ericproulx).
* [#2408](https://github.com/ruby-grape/grape/pull/2408): Fix params method redefined warnings - [@ericproulx](https://github.com/ericproulx).
* [#2410](https://github.com/ruby-grape/grape/pull/2410): Gem deprecations will raise a DeprecationWarning in specs - [@ericproulx](https://github.com/ericproulx).
* [#2389](https://github.com/ruby-grape/grape/pull/2389): Remove rack-accept dependency - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
1 change: 0 additions & 1 deletion grape.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'dry-types', '>= 1.1'
s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0'
s.add_runtime_dependency 'rack', '>= 1.3.0'
s.add_runtime_dependency 'rack-accept'

s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec']
s.require_paths = ['lib']
Expand Down
1 change: 0 additions & 1 deletion lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
require 'logger'
require 'rack'
require 'rack/builder'
require 'rack/accept'
require 'rack/auth/basic'
require 'set'
require 'bigdecimal'
Expand Down
178 changes: 17 additions & 161 deletions lib/grape/middleware/versioner/header.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# frozen_string_literal: true

require 'grape/middleware/base'
require 'grape/middleware/versioner/parse_media_type_patch'
require 'grape/util/media_type'
require 'grape/util/accept_header_handler'

module Grape
module Middleware
Expand All @@ -25,169 +26,24 @@ module Versioner
# X-Cascade header to alert Grape::Router to attempt the next matched
# route.
class Header < Base
VENDOR_VERSION_HEADER_REGEX =
/\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze

HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#{Regexp.last_match(0)}\^]+/.freeze
HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))+/.freeze

def before
strict_header_checks if strict?

if media_type || env[Grape::Env::GRAPE_ALLOWED_METHODS]
media_type_header_handler
elsif headers_contain_wrong_vendor?
fail_with_invalid_accept_header!('API vendor not found.')
elsif headers_contain_wrong_version?
fail_with_invalid_version_header!('API version not found.')
end
end

private

def strict_header_checks
strict_accept_header_presence_check
strict_version_vendor_accept_header_presence_check
end

def strict_accept_header_presence_check
return unless header.qvalues.empty?

fail_with_invalid_accept_header!('Accept header must be set.')
end

def strict_version_vendor_accept_header_presence_check
return if versions.blank? || an_accept_header_with_version_and_vendor_is_present?

fail_with_invalid_accept_header!('API vendor or version not found.')
end

def an_accept_header_with_version_and_vendor_is_present?
header.qvalues.keys.any? do |h|
VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', ''))
end
end

def header
@header ||= rack_accept_header
end

def media_type
@media_type ||= header.best_of(available_media_types)
end

def media_type_header_handler
type, subtype = Rack::Accept::Header.parse_media_type(media_type)
env[Grape::Env::API_TYPE] = type
env[Grape::Env::API_SUBTYPE] = subtype

return unless VENDOR_VERSION_HEADER_REGEX =~ subtype

env[Grape::Env::API_VENDOR] = Regexp.last_match[1]
env[Grape::Env::API_VERSION] = Regexp.last_match[2]
# weird that Grape::Middleware::Formatter also does this
env[Grape::Env::API_FORMAT] = Regexp.last_match[3]
end

def fail_with_invalid_accept_header!(message)
raise Grape::Exceptions::InvalidAcceptHeader
.new(message, error_headers)
end

def fail_with_invalid_version_header!(message)
raise Grape::Exceptions::InvalidVersionHeader
.new(message, error_headers)
end

def available_media_types
[].tap do |available_media_types|
content_types.each_key do |extension|
versions.reverse_each do |version|
available_media_types << "application/vnd.#{vendor}-#{version}+#{extension}"
available_media_types << "application/vnd.#{vendor}-#{version}"
end
available_media_types << "application/vnd.#{vendor}+#{extension}"
end

available_media_types << "application/vnd.#{vendor}"
available_media_types.concat(content_types.values.flatten)
end
end

def headers_contain_wrong_vendor?
header.values.all? do |header_value|
vendor?(header_value) && request_vendor(header_value) != vendor
end
end

def headers_contain_wrong_version?
header.values.all? do |header_value|
version?(header_value) && versions.exclude?(request_version(header_value))
end
end

def rack_accept_header
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
rescue RuntimeError => e
fail_with_invalid_accept_header!(e.message)
end

def versions
options[:versions] || []
end

def vendor
version_options && version_options[:vendor]
end

def strict?
version_options && version_options[:strict]
end

def version_options
options[:version_options]
end

# By default those errors contain an `X-Cascade` header set to `pass`,
# which allows nesting and stacking of routes
# (see Grape::Router for more
# information). To prevent # this behavior, and not add the `X-Cascade`
# header, one can set the `:cascade` option to `false`.
def cascade?
if version_options&.key?(:cascade)
version_options[:cascade]
else
true
handler = Grape::Util::AcceptHeaderHandler.new(
accept_header: env[Grape::Http::Headers::HTTP_ACCEPT],
versions: options[:versions],
**options.fetch(:version_options) { {} }
)

handler.match_best_quality_media_type!(
content_types: content_types,
allowed_methods: env[Grape::Env::GRAPE_ALLOWED_METHODS]
) do |media_type|
env[Grape::Env::API_TYPE] = media_type.type
env[Grape::Env::API_SUBTYPE] = media_type.subtype
env[Grape::Env::API_VENDOR] = media_type.vendor
env[Grape::Env::API_VERSION] = media_type.version
env[Grape::Env::API_FORMAT] = media_type.format
end
end

def error_headers
cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
end

# @param [String] media_type a content type
# @return [Boolean] whether the content type sets a vendor
def vendor?(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.present? && subtype[HAS_VENDOR_REGEX]
end

def request_vendor(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
end

def request_version(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
end

# @param [String] media_type a content type
# @return [Boolean] whether the content type sets an API version
def version?(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.present? && subtype[HAS_VERSION_REGEX]
end
end
end
end
Expand Down
24 changes: 0 additions & 24 deletions lib/grape/middleware/versioner/parse_media_type_patch.rb

This file was deleted.

107 changes: 107 additions & 0 deletions lib/grape/util/accept_header_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require 'grape/util/media_type'

module Grape
module Util
class AcceptHeaderHandler
attr_reader :accept_header, :versions, :vendor, :strict, :cascade

def initialize(accept_header:, versions:, **options)
@accept_header = accept_header
@versions = versions
@vendor = options.fetch(:vendor, nil)
@strict = options.fetch(:strict, false)
@cascade = options.fetch(:cascade, true)
end

def match_best_quality_media_type!(content_types: Grape::ContentTypes::CONTENT_TYPES, allowed_methods: nil)
return unless vendor

strict_header_checks!
media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types(content_types))
if media_type
yield media_type
else
fail!(allowed_methods)
end
end

private

def strict_header_checks!
return unless strict

accept_header_check!
version_and_vendor_check!
end

def accept_header_check!
return if accept_header.present?

invalid_accept_header!('Accept header must be set.')
end

def version_and_vendor_check!
return if versions.blank? || version_and_vendor?

invalid_accept_header!('API vendor or version not found.')
end

def q_values_mime_types
@q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
end

def version_and_vendor?
q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
end

def invalid_accept_header!(message)
raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
end

def invalid_version_header!(message)
raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
end

def fail!(grape_allowed_methods)
return grape_allowed_methods if grape_allowed_methods.present?

media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
vendor_not_found!(media_types) || version_not_found!(media_types)
end

def vendor_not_found!(media_types)
return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }

invalid_accept_header!('API vendor not found.')
end

def version_not_found!(media_types)
return unless media_types.all? { |media_type| media_type&.version && versions.exclude?(media_type.version) }

invalid_version_header!('API version not found.')
end

def error_headers
cascade ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
end

def available_media_types(content_types)
[].tap do |available_media_types|
base_media_type = "application/vnd.#{vendor}"
content_types.each_key do |extension|
versions&.reverse_each do |version|
available_media_types << "#{base_media_type}-#{version}+#{extension}"
available_media_types << "#{base_media_type}-#{version}"
end
available_media_types << "#{base_media_type}+#{extension}"
end

available_media_types << base_media_type
available_media_types.concat(content_types.values.flatten)
end
end
end
end
end
Loading
Loading