grape 0.1.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1167 -0
- data/CONTRIBUTING.md +156 -0
- data/LICENSE +1 -1
- data/README.md +4192 -0
- data/UPGRADING.md +1697 -0
- data/grape.gemspec +28 -105
- data/grape.png +0 -0
- data/lib/grape/api/helpers.rb +9 -0
- data/lib/grape/api/instance.rb +247 -0
- data/lib/grape/api.rb +158 -194
- data/lib/grape/content_types.rb +31 -0
- data/lib/grape/cookies.rb +50 -0
- data/lib/grape/dry_types.rb +10 -0
- data/lib/grape/dsl/api.rb +17 -0
- data/lib/grape/dsl/callbacks.rb +69 -0
- data/lib/grape/dsl/configuration.rb +15 -0
- data/lib/grape/dsl/desc.rb +124 -0
- data/lib/grape/dsl/headers.rb +21 -0
- data/lib/grape/dsl/helpers.rb +108 -0
- data/lib/grape/dsl/inside_route.rb +454 -0
- data/lib/grape/dsl/logger.rb +22 -0
- data/lib/grape/dsl/middleware.rb +54 -0
- data/lib/grape/dsl/parameters.rb +266 -0
- data/lib/grape/dsl/request_response.rb +171 -0
- data/lib/grape/dsl/routing.rb +248 -0
- data/lib/grape/dsl/settings.rb +179 -0
- data/lib/grape/dsl/validations.rb +57 -0
- data/lib/grape/endpoint.rb +398 -68
- data/lib/grape/env.rb +20 -0
- data/lib/grape/error_formatter/base.rb +68 -0
- data/lib/grape/error_formatter/json.rb +28 -0
- data/lib/grape/error_formatter/serializable_hash.rb +7 -0
- data/lib/grape/error_formatter/txt.rb +21 -0
- data/lib/grape/error_formatter/xml.rb +11 -0
- data/lib/grape/error_formatter.rb +15 -0
- data/lib/grape/exceptions/base.rb +76 -0
- data/lib/grape/exceptions/conflicting_types.rb +11 -0
- data/lib/grape/exceptions/empty_message_body.rb +11 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +11 -0
- data/lib/grape/exceptions/invalid_accept_header.rb +11 -0
- data/lib/grape/exceptions/invalid_formatter.rb +11 -0
- data/lib/grape/exceptions/invalid_message_body.rb +11 -0
- data/lib/grape/exceptions/invalid_parameters.rb +11 -0
- data/lib/grape/exceptions/invalid_response.rb +11 -0
- data/lib/grape/exceptions/invalid_version_header.rb +11 -0
- data/lib/grape/exceptions/invalid_versioner_option.rb +11 -0
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +11 -0
- data/lib/grape/exceptions/method_not_allowed.rb +11 -0
- data/lib/grape/exceptions/missing_group_type.rb +13 -0
- data/lib/grape/exceptions/missing_mime_type.rb +11 -0
- data/lib/grape/exceptions/missing_option.rb +11 -0
- data/lib/grape/exceptions/missing_vendor_option.rb +11 -0
- data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
- data/lib/grape/exceptions/too_many_multipart_files.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
- data/lib/grape/exceptions/unknown_options.rb +11 -0
- data/lib/grape/exceptions/unknown_parameter.rb +11 -0
- data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
- data/lib/grape/exceptions/unknown_validator.rb +11 -0
- data/lib/grape/exceptions/unsupported_group_type.rb +13 -0
- data/lib/grape/exceptions/validation.rb +25 -0
- data/lib/grape/exceptions/validation_array_errors.rb +14 -0
- data/lib/grape/exceptions/validation_errors.rb +53 -0
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +24 -0
- data/lib/grape/extensions/hash.rb +27 -0
- data/lib/grape/extensions/hashie/mash.rb +24 -0
- data/lib/grape/formatter/base.rb +16 -0
- data/lib/grape/formatter/json.rb +13 -0
- data/lib/grape/formatter/serializable_hash.rb +39 -0
- data/lib/grape/formatter/txt.rb +11 -0
- data/lib/grape/formatter/xml.rb +13 -0
- data/lib/grape/formatter.rb +17 -0
- data/lib/grape/json.rb +10 -0
- data/lib/grape/locale/en.yml +59 -0
- data/lib/grape/middleware/auth/base.rb +23 -0
- data/lib/grape/middleware/auth/dsl.rb +39 -0
- data/lib/grape/middleware/auth/strategies.rb +25 -0
- data/lib/grape/middleware/auth/strategy_info.rb +15 -0
- data/lib/grape/middleware/base.rb +89 -18
- data/lib/grape/middleware/error.rb +129 -10
- data/lib/grape/middleware/filter.rb +19 -0
- data/lib/grape/middleware/formatter.rb +124 -82
- data/lib/grape/middleware/globals.rb +14 -0
- data/lib/grape/middleware/stack.rb +109 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +38 -0
- data/lib/grape/middleware/versioner/base.rb +74 -0
- data/lib/grape/middleware/versioner/header.rb +127 -0
- data/lib/grape/middleware/versioner/param.rb +32 -0
- data/lib/grape/middleware/versioner/path.rb +40 -0
- data/lib/grape/middleware/versioner.rb +21 -21
- data/lib/grape/namespace.rb +46 -0
- data/lib/grape/params_builder/base.rb +18 -0
- data/lib/grape/params_builder/hash.rb +11 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
- data/lib/grape/params_builder/hashie_mash.rb +11 -0
- data/lib/grape/params_builder.rb +32 -0
- data/lib/grape/parser/base.rb +16 -0
- data/lib/grape/parser/json.rb +14 -0
- data/lib/grape/parser/xml.rb +14 -0
- data/lib/grape/parser.rb +15 -0
- data/lib/grape/path.rb +76 -0
- data/lib/grape/presenters/presenter.rb +11 -0
- data/lib/grape/railtie.rb +9 -0
- data/lib/grape/request.rb +190 -0
- data/lib/grape/router/base_route.rb +39 -0
- data/lib/grape/router/greedy_route.rb +20 -0
- data/lib/grape/router/pattern.rb +81 -0
- data/lib/grape/router/route.rb +62 -0
- data/lib/grape/router.rb +190 -0
- data/lib/grape/serve_stream/file_body.rb +36 -0
- data/lib/grape/serve_stream/sendfile_response.rb +21 -0
- data/lib/grape/serve_stream/stream_response.rb +23 -0
- data/lib/grape/types/invalid_value.rb +8 -0
- data/lib/grape/util/base_inheritable.rb +43 -0
- data/lib/grape/util/cache.rb +17 -0
- data/lib/grape/util/endpoint_configuration.rb +8 -0
- data/lib/grape/util/header.rb +13 -0
- data/lib/grape/util/inheritable_setting.rb +100 -0
- data/lib/grape/util/inheritable_values.rb +29 -0
- data/lib/grape/util/lazy/block.rb +29 -0
- data/lib/grape/util/lazy/value.rb +38 -0
- data/lib/grape/util/lazy/value_array.rb +21 -0
- data/lib/grape/util/lazy/value_enumerable.rb +34 -0
- data/lib/grape/util/lazy/value_hash.rb +21 -0
- data/lib/grape/util/media_type.rb +70 -0
- data/lib/grape/util/registry.rb +27 -0
- data/lib/grape/util/reverse_stackable_values.rb +15 -0
- data/lib/grape/util/stackable_values.rb +36 -0
- data/lib/grape/util/strict_hash_configuration.rb +108 -0
- data/lib/grape/validations/attributes_doc.rb +60 -0
- data/lib/grape/validations/attributes_iterator.rb +62 -0
- data/lib/grape/validations/contract_scope.rb +34 -0
- data/lib/grape/validations/multiple_attributes_iterator.rb +13 -0
- data/lib/grape/validations/params_scope.rb +538 -0
- data/lib/grape/validations/single_attribute_iterator.rb +26 -0
- data/lib/grape/validations/types/array_coercer.rb +61 -0
- data/lib/grape/validations/types/custom_type_coercer.rb +164 -0
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +56 -0
- data/lib/grape/validations/types/dry_type_coercer.rb +66 -0
- data/lib/grape/validations/types/file.rb +31 -0
- data/lib/grape/validations/types/invalid_value.rb +17 -0
- data/lib/grape/validations/types/json.rb +71 -0
- data/lib/grape/validations/types/multiple_type_coercer.rb +57 -0
- data/lib/grape/validations/types/primitive_coercer.rb +73 -0
- data/lib/grape/validations/types/set_coercer.rb +35 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +51 -0
- data/lib/grape/validations/types.rb +213 -0
- data/lib/grape/validations/validator_factory.rb +15 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +16 -0
- data/lib/grape/validations/validators/allow_blank_validator.rb +20 -0
- data/lib/grape/validations/validators/as_validator.rb +14 -0
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +15 -0
- data/lib/grape/validations/validators/base.rb +88 -0
- data/lib/grape/validations/validators/coerce_validator.rb +75 -0
- data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
- data/lib/grape/validations/validators/default_validator.rb +37 -0
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +17 -0
- data/lib/grape/validations/validators/except_values_validator.rb +24 -0
- data/lib/grape/validations/validators/length_validator.rb +49 -0
- data/lib/grape/validations/validators/multiple_params_base.rb +34 -0
- data/lib/grape/validations/validators/mutual_exclusion_validator.rb +16 -0
- data/lib/grape/validations/validators/presence_validator.rb +15 -0
- data/lib/grape/validations/validators/regexp_validator.rb +16 -0
- data/lib/grape/validations/validators/same_as_validator.rb +29 -0
- data/lib/grape/validations/validators/values_validator.rb +60 -0
- data/lib/grape/validations.rb +21 -0
- data/lib/grape/version.rb +6 -0
- data/lib/grape/xml.rb +10 -0
- data/lib/grape.rb +81 -17
- metadata +250 -192
- data/.document +0 -5
- data/.gitignore +0 -23
- data/.rspec +0 -2
- data/.rvmrc +0 -1
- data/Gemfile +0 -21
- data/Gemfile.lock +0 -58
- data/README.markdown +0 -75
- data/Rakefile +0 -70
- data/VERSION +0 -1
- data/autotest/discover.rb +0 -1
- data/lib/grape/middleware/auth/basic.rb +0 -30
- data/lib/grape/middleware/auth/oauth2.rb +0 -55
- data/lib/grape/middleware/prefixer.rb +0 -21
- data/lib/grape/middleware_stack.rb +0 -35
- data/spec/grape/api_spec.rb +0 -343
- data/spec/grape/endpoint_spec.rb +0 -104
- data/spec/grape/middleware/auth/basic_spec.rb +0 -31
- data/spec/grape/middleware/auth/oauth2_spec.rb +0 -88
- data/spec/grape/middleware/base_spec.rb +0 -63
- data/spec/grape/middleware/error_spec.rb +0 -49
- data/spec/grape/middleware/formatter_spec.rb +0 -87
- data/spec/grape/middleware/prefixer_spec.rb +0 -30
- data/spec/grape/middleware/versioner_spec.rb +0 -40
- data/spec/grape/middleware_stack_spec.rb +0 -47
- data/spec/grape_spec.rb +0 -1
- data/spec/spec_helper.rb +0 -20
|
@@ -1,106 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
require 'multi_json'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module Grape
|
|
5
4
|
module Middleware
|
|
6
5
|
class Formatter < Base
|
|
7
|
-
|
|
8
|
-
:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
:rss => 'application/rss+xml',
|
|
12
|
-
:txt => 'text/plain'
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
def default_options
|
|
16
|
-
{
|
|
17
|
-
:default_format => :txt,
|
|
18
|
-
:content_types => {}
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def content_types
|
|
23
|
-
CONTENT_TYPES.merge(options[:content_types])
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def mime_types
|
|
27
|
-
content_types.invert
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def headers
|
|
31
|
-
env.dup.inject({}){|h,(k,v)| h[k.downcase] = v; h}
|
|
32
|
-
end
|
|
33
|
-
|
|
6
|
+
DEFAULT_OPTIONS = {
|
|
7
|
+
default_format: :txt
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
34
10
|
def before
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if content_types.key?(fmt)
|
|
38
|
-
env['api.format'] = fmt
|
|
39
|
-
else
|
|
40
|
-
throw :error, :status => 406, :message => 'The requested format is not supported.'
|
|
41
|
-
end
|
|
11
|
+
negotiate_content_type
|
|
12
|
+
read_body_input
|
|
42
13
|
end
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
14
|
+
|
|
15
|
+
def after
|
|
16
|
+
return unless @app_response
|
|
17
|
+
|
|
18
|
+
status, headers, bodies = *@app_response
|
|
19
|
+
|
|
20
|
+
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
|
21
|
+
[status, headers, []]
|
|
50
22
|
else
|
|
51
|
-
|
|
23
|
+
build_formatted_response(status, headers, bodies)
|
|
52
24
|
end
|
|
53
25
|
end
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_formatted_response(status, headers, bodies)
|
|
30
|
+
headers = ensure_content_type(headers)
|
|
31
|
+
|
|
32
|
+
if bodies.is_a?(Grape::ServeStream::StreamResponse)
|
|
33
|
+
Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
|
|
34
|
+
resp.body = bodies.stream
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
# Allow content-type to be explicitly overwritten
|
|
38
|
+
formatter = fetch_formatter(headers, options)
|
|
39
|
+
bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do
|
|
40
|
+
bodies.collect { |body| formatter.call(body, env) }
|
|
59
41
|
end
|
|
42
|
+
Rack::Response.new(bodymap, status, headers)
|
|
60
43
|
end
|
|
61
|
-
|
|
44
|
+
rescue Grape::Exceptions::InvalidFormatter => e
|
|
45
|
+
throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch_formatter(headers, options)
|
|
49
|
+
api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
|
|
50
|
+
Grape::Formatter.formatter_for(api_format, options[:formatters])
|
|
62
51
|
end
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
|
|
53
|
+
# Set the content type header for the API format if it is not already present.
|
|
54
|
+
#
|
|
55
|
+
# @param headers [Hash]
|
|
56
|
+
# @return [Hash]
|
|
57
|
+
def ensure_content_type(headers)
|
|
58
|
+
if headers[Rack::CONTENT_TYPE]
|
|
59
|
+
headers
|
|
71
60
|
else
|
|
72
|
-
[]
|
|
61
|
+
headers.merge(Rack::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
|
|
73
62
|
end
|
|
74
63
|
end
|
|
75
|
-
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
64
|
+
|
|
65
|
+
def read_body_input
|
|
66
|
+
input = rack_request.body # reads RACK_INPUT
|
|
67
|
+
return if input.nil?
|
|
68
|
+
return unless read_body_input?
|
|
69
|
+
|
|
70
|
+
input.try(:rewind)
|
|
71
|
+
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
|
|
72
|
+
begin
|
|
73
|
+
read_rack_input(body)
|
|
74
|
+
ensure
|
|
75
|
+
input.try(:rewind)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def read_rack_input(body)
|
|
80
|
+
return if body.empty?
|
|
81
|
+
|
|
82
|
+
media_type = rack_request.media_type
|
|
83
|
+
fmt = media_type ? mime_types[media_type] : options[:default_format]
|
|
84
|
+
|
|
85
|
+
throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
|
|
86
|
+
parser = Grape::Parser.parser_for fmt, options[:parsers]
|
|
87
|
+
if parser
|
|
88
|
+
begin
|
|
89
|
+
body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
|
|
90
|
+
if body.is_a?(Hash)
|
|
91
|
+
env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH)
|
|
92
|
+
env[Rack::RACK_REQUEST_FORM_HASH].merge(body)
|
|
93
|
+
else
|
|
94
|
+
body
|
|
95
|
+
end
|
|
96
|
+
env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
|
|
97
|
+
end
|
|
98
|
+
rescue Grape::Exceptions::Base => e
|
|
99
|
+
raise e
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
|
|
85
102
|
end
|
|
103
|
+
else
|
|
104
|
+
env[Grape::Env::API_REQUEST_BODY] = body
|
|
86
105
|
end
|
|
87
|
-
headers['Content-Type'] = 'application/json'
|
|
88
|
-
Rack::Response.new(bodymap, status, headers).to_a
|
|
89
106
|
end
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
|
|
108
|
+
# this middleware will not try to format the following content-types since Rack already handles them
|
|
109
|
+
# when calling Rack's `params` function
|
|
110
|
+
# - application/x-www-form-urlencoded
|
|
111
|
+
# - multipart/form-data
|
|
112
|
+
# - multipart/related
|
|
113
|
+
# - multipart/mixed
|
|
114
|
+
def read_body_input?
|
|
115
|
+
(rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
|
|
116
|
+
!(rack_request.form_data? && rack_request.content_type) &&
|
|
117
|
+
!rack_request.parseable_data? &&
|
|
118
|
+
(rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def negotiate_content_type
|
|
122
|
+
fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
|
|
123
|
+
if content_type_for(fmt)
|
|
124
|
+
env[Grape::Env::API_FORMAT] = fmt.to_sym
|
|
96
125
|
else
|
|
97
|
-
|
|
126
|
+
throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
|
|
98
127
|
end
|
|
99
128
|
end
|
|
100
|
-
|
|
101
|
-
def
|
|
102
|
-
|
|
129
|
+
|
|
130
|
+
def format_from_extension
|
|
131
|
+
request_path = rack_request.path.try(:scrub)
|
|
132
|
+
dot_pos = request_path.rindex('.')
|
|
133
|
+
return unless dot_pos
|
|
134
|
+
|
|
135
|
+
extension = request_path[dot_pos + 1..]
|
|
136
|
+
extension if content_type_for(extension)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def format_from_header
|
|
140
|
+
accept_header = env['HTTP_ACCEPT'].try(:scrub)
|
|
141
|
+
return if accept_header.blank?
|
|
142
|
+
|
|
143
|
+
media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
|
|
144
|
+
mime_types[media_type] if media_type
|
|
103
145
|
end
|
|
104
146
|
end
|
|
105
147
|
end
|
|
106
|
-
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
class Globals < Base
|
|
6
|
+
def before
|
|
7
|
+
request = Grape::Request.new(@env, build_params_with: @options[:build_params_with])
|
|
8
|
+
@env[Grape::Env::GRAPE_REQUEST] = request
|
|
9
|
+
@env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers
|
|
10
|
+
@env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
# Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
|
|
6
|
+
# It allows to insert and insert after
|
|
7
|
+
class Stack
|
|
8
|
+
extend Forwardable
|
|
9
|
+
class Middleware
|
|
10
|
+
attr_reader :args, :block, :klass
|
|
11
|
+
|
|
12
|
+
def initialize(klass, args, block)
|
|
13
|
+
@klass = klass
|
|
14
|
+
@args = args
|
|
15
|
+
@block = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
klass.name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
case other
|
|
24
|
+
when Middleware
|
|
25
|
+
klass == other.klass
|
|
26
|
+
when Class
|
|
27
|
+
klass == other || (name.nil? && klass.superclass == other)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inspect
|
|
32
|
+
klass.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build(builder)
|
|
36
|
+
# we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords
|
|
37
|
+
# like ActionDispatch::RequestId since middleware arguments are serialized
|
|
38
|
+
# https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash
|
|
39
|
+
args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash)
|
|
40
|
+
builder.use(klass, *args, &block)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
include Enumerable
|
|
45
|
+
|
|
46
|
+
attr_accessor :middlewares, :others
|
|
47
|
+
|
|
48
|
+
def_delegators :middlewares, :each, :size, :last, :[]
|
|
49
|
+
|
|
50
|
+
def initialize
|
|
51
|
+
@middlewares = []
|
|
52
|
+
@others = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def insert(index, klass, *args, &block)
|
|
56
|
+
index = assert_index(index, :before)
|
|
57
|
+
middlewares.insert(index, self.class::Middleware.new(klass, args, block))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
alias insert_before insert
|
|
61
|
+
|
|
62
|
+
def insert_after(index, *args, &block)
|
|
63
|
+
index = assert_index(index, :after)
|
|
64
|
+
insert(index + 1, *args, &block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def use(klass, *args, &block)
|
|
68
|
+
middleware = self.class::Middleware.new(klass, args, block)
|
|
69
|
+
middlewares.push(middleware)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def merge_with(middleware_specs)
|
|
73
|
+
middleware_specs.each do |operation, klass, *args|
|
|
74
|
+
if args.last.is_a?(Proc)
|
|
75
|
+
last_proc = args.pop
|
|
76
|
+
public_send(operation, klass, *args, &last_proc)
|
|
77
|
+
else
|
|
78
|
+
public_send(operation, klass, *args)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Rack::Builder] the builder object with our middlewares applied
|
|
84
|
+
def build
|
|
85
|
+
Rack::Builder.new.tap do |builder|
|
|
86
|
+
others.shift(others.size).each { |m| merge_with(m) }
|
|
87
|
+
middlewares.each do |m|
|
|
88
|
+
m.build(builder)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
|
|
94
|
+
# @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
|
|
95
|
+
def concat(other_specs)
|
|
96
|
+
use, not_use = other_specs.partition { |o| o.first == :use }
|
|
97
|
+
others << not_use
|
|
98
|
+
merge_with(use)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
protected
|
|
102
|
+
|
|
103
|
+
def assert_index(index, where)
|
|
104
|
+
i = index.is_a?(Integer) ? index : middlewares.index(index)
|
|
105
|
+
i || raise("No such middleware to insert #{where}: #{index.inspect}")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
module Versioner
|
|
6
|
+
# This middleware sets various version related rack environment variables
|
|
7
|
+
# based on the HTTP Accept-Version header
|
|
8
|
+
#
|
|
9
|
+
# Example: For request header
|
|
10
|
+
# Accept-Version: v1
|
|
11
|
+
#
|
|
12
|
+
# The following rack env variables are set:
|
|
13
|
+
#
|
|
14
|
+
# env['api.version'] => 'v1'
|
|
15
|
+
#
|
|
16
|
+
# If version does not match this route, then a 406 is raised with
|
|
17
|
+
# X-Cascade header to alert Grape::Router to attempt the next matched
|
|
18
|
+
# route.
|
|
19
|
+
class AcceptVersionHeader < Base
|
|
20
|
+
def before
|
|
21
|
+
potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub)
|
|
22
|
+
not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
|
|
23
|
+
|
|
24
|
+
return if potential_version.blank?
|
|
25
|
+
|
|
26
|
+
not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version)
|
|
27
|
+
env[Grape::Env::API_VERSION] = potential_version
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def not_acceptable!(message)
|
|
33
|
+
throw :error, status: 406, headers: error_headers, message: message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
module Versioner
|
|
6
|
+
class Base < Grape::Middleware::Base
|
|
7
|
+
DEFAULT_OPTIONS = {
|
|
8
|
+
pattern: /.*/i.freeze,
|
|
9
|
+
version_options: {
|
|
10
|
+
strict: false,
|
|
11
|
+
cascade: true,
|
|
12
|
+
parameter: 'apiver'
|
|
13
|
+
}.freeze
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.inherited(klass)
|
|
17
|
+
super
|
|
18
|
+
Versioner.register(klass)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def versions
|
|
22
|
+
options[:versions]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def prefix
|
|
26
|
+
options[:prefix]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mount_path
|
|
30
|
+
options[:mount_path]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pattern
|
|
34
|
+
options[:pattern]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def version_options
|
|
38
|
+
options[:version_options]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def strict?
|
|
42
|
+
version_options[:strict]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
|
|
46
|
+
# of routes (see Grape::Router) for more information). To prevent
|
|
47
|
+
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
|
48
|
+
def cascade?
|
|
49
|
+
version_options[:cascade]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parameter_key
|
|
53
|
+
version_options[:parameter]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def vendor
|
|
57
|
+
version_options[:vendor]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def error_headers
|
|
61
|
+
cascade? ? { 'X-Cascade' => 'pass' } : {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def potential_version_match?(potential_version)
|
|
65
|
+
versions.blank? || versions.any? { |v| v.to_s == potential_version }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def version_not_found!
|
|
69
|
+
throw :error, status: 404, message: '404 API Version Not Found', headers: { 'X-Cascade' => 'pass' }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
module Versioner
|
|
6
|
+
# This middleware sets various version related rack environment variables
|
|
7
|
+
# based on the HTTP Accept header with the pattern:
|
|
8
|
+
# application/vnd.:vendor-:version+:format
|
|
9
|
+
#
|
|
10
|
+
# Example: For request header
|
|
11
|
+
# Accept: application/vnd.mycompany.a-cool-resource-v1+json
|
|
12
|
+
#
|
|
13
|
+
# The following rack env variables are set:
|
|
14
|
+
#
|
|
15
|
+
# env['api.type'] => 'application'
|
|
16
|
+
# env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json'
|
|
17
|
+
# env['api.vendor] => 'mycompany.a-cool-resource'
|
|
18
|
+
# env['api.version] => 'v1'
|
|
19
|
+
# env['api.format] => 'json'
|
|
20
|
+
#
|
|
21
|
+
# If version does not match this route, then a 406 is raised with
|
|
22
|
+
# X-Cascade header to alert Grape::Router to attempt the next matched
|
|
23
|
+
# route.
|
|
24
|
+
class Header < Base
|
|
25
|
+
def before
|
|
26
|
+
match_best_quality_media_type! do |media_type|
|
|
27
|
+
env.update(
|
|
28
|
+
Grape::Env::API_TYPE => media_type.type,
|
|
29
|
+
Grape::Env::API_SUBTYPE => media_type.subtype,
|
|
30
|
+
Grape::Env::API_VENDOR => media_type.vendor,
|
|
31
|
+
Grape::Env::API_VERSION => media_type.version,
|
|
32
|
+
Grape::Env::API_FORMAT => media_type.format
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def match_best_quality_media_type!
|
|
40
|
+
return unless vendor
|
|
41
|
+
|
|
42
|
+
strict_header_checks!
|
|
43
|
+
media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types)
|
|
44
|
+
if media_type
|
|
45
|
+
yield media_type
|
|
46
|
+
else
|
|
47
|
+
fail!
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def accept_header
|
|
52
|
+
env['HTTP_ACCEPT']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def strict_header_checks!
|
|
56
|
+
return unless strict?
|
|
57
|
+
|
|
58
|
+
accept_header_check!
|
|
59
|
+
version_and_vendor_check!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def accept_header_check!
|
|
63
|
+
return if accept_header.present?
|
|
64
|
+
|
|
65
|
+
invalid_accept_header!('Accept header must be set.')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def version_and_vendor_check!
|
|
69
|
+
return if versions.blank? || version_and_vendor?
|
|
70
|
+
|
|
71
|
+
invalid_accept_header!('API vendor or version not found.')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def q_values_mime_types
|
|
75
|
+
@q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def version_and_vendor?
|
|
79
|
+
q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def invalid_accept_header!(message)
|
|
83
|
+
raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def invalid_version_header!(message)
|
|
87
|
+
raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fail!
|
|
91
|
+
return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present?
|
|
92
|
+
|
|
93
|
+
media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
|
|
94
|
+
vendor_not_found!(media_types) || version_not_found!(media_types)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def vendor_not_found!(media_types)
|
|
98
|
+
return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
|
|
99
|
+
|
|
100
|
+
invalid_accept_header!('API vendor not found.')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def version_not_found!(media_types)
|
|
104
|
+
return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
|
|
105
|
+
|
|
106
|
+
invalid_version_header!('API version not found.')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def available_media_types
|
|
110
|
+
[].tap do |available_media_types|
|
|
111
|
+
base_media_type = "application/vnd.#{vendor}"
|
|
112
|
+
content_types.each_key do |extension|
|
|
113
|
+
versions&.reverse_each do |version|
|
|
114
|
+
available_media_types << "#{base_media_type}-#{version}+#{extension}"
|
|
115
|
+
available_media_types << "#{base_media_type}-#{version}"
|
|
116
|
+
end
|
|
117
|
+
available_media_types << "#{base_media_type}+#{extension}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
available_media_types << base_media_type
|
|
121
|
+
available_media_types.concat(content_types.values.flatten)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
module Versioner
|
|
6
|
+
# This middleware sets various version related rack environment variables
|
|
7
|
+
# based on the request parameters and removes that parameter from the
|
|
8
|
+
# request parameters for subsequent middleware and API.
|
|
9
|
+
# If the version substring does not match any potential initialized
|
|
10
|
+
# versions, a 404 error is thrown.
|
|
11
|
+
# If the version substring is not passed the version (highest mounted)
|
|
12
|
+
# version will be used.
|
|
13
|
+
#
|
|
14
|
+
# Example: For a uri path
|
|
15
|
+
# /resource?apiver=v1
|
|
16
|
+
#
|
|
17
|
+
# The following rack env variables are set and path is rewritten to
|
|
18
|
+
# '/resource':
|
|
19
|
+
#
|
|
20
|
+
# env['api.version'] => 'v1'
|
|
21
|
+
class Param < Base
|
|
22
|
+
def before
|
|
23
|
+
potential_version = query_params[parameter_key]
|
|
24
|
+
return if potential_version.blank?
|
|
25
|
+
|
|
26
|
+
version_not_found! unless potential_version_match?(potential_version)
|
|
27
|
+
env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Middleware
|
|
5
|
+
module Versioner
|
|
6
|
+
# This middleware sets various version related rack environment variables
|
|
7
|
+
# based on the uri path and removes the version substring from the uri
|
|
8
|
+
# path. If the version substring does not match any potential initialized
|
|
9
|
+
# versions, a 404 error is thrown.
|
|
10
|
+
#
|
|
11
|
+
# Example: For a uri path
|
|
12
|
+
# /v1/resource
|
|
13
|
+
#
|
|
14
|
+
# The following rack env variables are set and path is rewritten to
|
|
15
|
+
# '/resource':
|
|
16
|
+
#
|
|
17
|
+
# env['api.version'] => 'v1'
|
|
18
|
+
#
|
|
19
|
+
class Path < Base
|
|
20
|
+
def before
|
|
21
|
+
path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
|
|
22
|
+
return if path_info == '/'
|
|
23
|
+
|
|
24
|
+
[mount_path, Grape::Router.normalize_path(prefix)].each do |path|
|
|
25
|
+
path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
slash_position = path_info.index('/', 1) # omit the first one
|
|
29
|
+
return unless slash_position
|
|
30
|
+
|
|
31
|
+
potential_version = path_info[1..slash_position - 1]
|
|
32
|
+
return unless potential_version.match?(pattern)
|
|
33
|
+
|
|
34
|
+
version_not_found! unless potential_version_match?(potential_version)
|
|
35
|
+
env[Grape::Env::API_VERSION] = potential_version
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|