grape 0.15.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/Gemfile.lock +16 -15
  4. data/README.md +41 -47
  5. data/UPGRADING.md +62 -0
  6. data/gemfiles/rails_3.gemfile.lock +225 -0
  7. data/grape.gemspec +2 -2
  8. data/lib/grape.rb +31 -26
  9. data/lib/grape/api.rb +39 -23
  10. data/lib/grape/dsl/inside_route.rb +8 -4
  11. data/lib/grape/dsl/routing.rb +2 -1
  12. data/lib/grape/endpoint.rb +43 -62
  13. data/lib/grape/error_formatter.rb +4 -2
  14. data/lib/grape/error_formatter/base.rb +10 -6
  15. data/lib/grape/formatter.rb +4 -2
  16. data/lib/grape/http/headers.rb +1 -0
  17. data/lib/grape/middleware/formatter.rb +2 -2
  18. data/lib/grape/middleware/versioner/accept_version_header.rb +2 -2
  19. data/lib/grape/middleware/versioner/header.rb +2 -2
  20. data/lib/grape/middleware/versioner/path.rb +2 -2
  21. data/lib/grape/namespace.rb +1 -1
  22. data/lib/grape/parser.rb +4 -2
  23. data/lib/grape/path.rb +3 -3
  24. data/lib/grape/request.rb +2 -2
  25. data/lib/grape/router.rb +156 -0
  26. data/lib/grape/router/attribute_translator.rb +40 -0
  27. data/lib/grape/router/pattern.rb +55 -0
  28. data/lib/grape/router/route.rb +105 -0
  29. data/lib/grape/serve_file/file_body.rb +34 -0
  30. data/lib/grape/{util → serve_file}/file_response.rb +1 -1
  31. data/lib/grape/{util → serve_file}/sendfile_response.rb +1 -1
  32. data/lib/grape/util/env.rb +1 -1
  33. data/lib/grape/util/registrable.rb +13 -0
  34. data/lib/grape/validations/types/custom_type_coercer.rb +2 -0
  35. data/lib/grape/version.rb +1 -1
  36. data/spec/grape/api/invalid_format_spec.rb +43 -0
  37. data/spec/grape/api/recognize_path_spec.rb +21 -0
  38. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +26 -0
  39. data/spec/grape/api_spec.rb +110 -38
  40. data/spec/grape/dsl/inside_route_spec.rb +267 -240
  41. data/spec/grape/endpoint_spec.rb +10 -0
  42. data/spec/grape/entity_spec.rb +2 -2
  43. data/spec/grape/middleware/formatter_spec.rb +23 -4
  44. data/spec/grape/middleware/versioner/header_spec.rb +1 -1
  45. data/spec/grape/middleware/versioner/path_spec.rb +1 -1
  46. data/spec/grape/parser_spec.rb +82 -0
  47. data/spec/grape/request_spec.rb +2 -2
  48. data/spec/grape/validations/params_scope_spec.rb +2 -2
  49. data/spec/grape/validations/validators/coerce_spec.rb +51 -0
  50. data/spec/grape/validations_spec.rb +1 -1
  51. data/tmp/Gemfile.lock +63 -0
  52. metadata +70 -55
  53. data/lib/grape/route.rb +0 -32
@@ -1,8 +1,10 @@
1
1
  module Grape
2
2
  module ErrorFormatter
3
+ extend Util::Registrable
4
+
3
5
  class << self
4
6
  def builtin_formatters
5
- {
7
+ @builtin_formatters ||= {
6
8
  serializable_hash: Grape::ErrorFormatter::Json,
7
9
  json: Grape::ErrorFormatter::Json,
8
10
  jsonapi: Grape::ErrorFormatter::Json,
@@ -12,7 +14,7 @@ module Grape
12
14
  end
13
15
 
14
16
  def formatters(options)
15
- builtin_formatters.merge(options[:error_formatters] || {})
17
+ builtin_formatters.merge(default_elements).merge(options[:error_formatters] || {})
16
18
  end
17
19
 
18
20
  def formatter_for(api_format, options = {})
@@ -3,14 +3,18 @@ module Grape
3
3
  module Base
4
4
  def present(message, env)
5
5
  present_options = {}
6
- present_options[:with] = message.delete(:with) if message.is_a?(Hash)
6
+ presented_message = message
7
+ if presented_message.is_a?(Hash)
8
+ presented_message = presented_message.dup
9
+ present_options[:with] = presented_message.delete(:with)
10
+ end
7
11
 
8
- presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(message, present_options)
12
+ presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
9
13
 
10
- unless presenter || env[Grape::Env::RACK_ROUTING_ARGS].nil?
14
+ unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
11
15
  # env['api.endpoint'].route does not work when the error occurs within a middleware
12
16
  # the Endpoint does not have a valid env at this moment
13
- http_codes = env[Grape::Env::RACK_ROUTING_ARGS][:route_info].route_http_codes || []
17
+ http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
14
18
  found_code = http_codes.find do |http_code|
15
19
  (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
16
20
  end if env[Grape::Env::API_ENDPOINT].request
@@ -21,10 +25,10 @@ module Grape
21
25
  if presenter
22
26
  embeds = { env: env }
23
27
  embeds[:version] = env[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION]
24
- message = presenter.represent(message, embeds).serializable_hash
28
+ presented_message = presenter.represent(presented_message, embeds).serializable_hash
25
29
  end
26
30
 
27
- message
31
+ presented_message
28
32
  end
29
33
  end
30
34
  end
@@ -1,8 +1,10 @@
1
1
  module Grape
2
2
  module Formatter
3
+ extend Util::Registrable
4
+
3
5
  class << self
4
6
  def builtin_formmaters
5
- {
7
+ @builtin_formatters ||= {
6
8
  json: Grape::Formatter::Json,
7
9
  jsonapi: Grape::Formatter::Json,
8
10
  serializable_hash: Grape::Formatter::SerializableHash,
@@ -12,7 +14,7 @@ module Grape
12
14
  end
13
15
 
14
16
  def formatters(options)
15
- builtin_formmaters.merge(options[:formatters] || {})
17
+ builtin_formmaters.merge(default_elements).merge(options[:formatters] || {})
16
18
  end
17
19
 
18
20
  def formatter_for(api_format, options = {})
@@ -4,6 +4,7 @@ module Grape
4
4
  # https://github.com/rack/rack/blob/master/lib/rack.rb
5
5
  HTTP_VERSION = 'HTTP_VERSION'.freeze
6
6
  PATH_INFO = 'PATH_INFO'.freeze
7
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
7
8
  QUERY_STRING = 'QUERY_STRING'.freeze
8
9
  CONTENT_TYPE = 'Content-Type'.freeze
9
10
 
@@ -34,8 +34,8 @@ module Grape
34
34
  def build_formatted_response(status, headers, bodies)
35
35
  headers = ensure_content_type(headers)
36
36
 
37
- if bodies.is_a?(Grape::Util::FileResponse)
38
- Grape::Util::SendfileResponse.new([], status, headers) do |resp|
37
+ if bodies.is_a?(Grape::ServeFile::FileResponse)
38
+ Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
39
39
  resp.body = bodies.file
40
40
  end
41
41
  else
@@ -14,7 +14,7 @@ module Grape
14
14
  # env['api.version'] => 'v1'
15
15
  #
16
16
  # If version does not match this route, then a 406 is raised with
17
- # X-Cascade header to alert Rack::Mount to attempt the next matched
17
+ # X-Cascade header to alert Grape::Router to attempt the next matched
18
18
  # route.
19
19
  class AcceptVersionHeader < Base
20
20
  def before
@@ -46,7 +46,7 @@ module Grape
46
46
  end
47
47
 
48
48
  # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
49
- # of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
49
+ # of routes (see Grape::Router) for more information). To prevent
50
50
  # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
51
51
  def cascade?
52
52
  if options[:version_options] && options[:version_options].key?(:cascade)
@@ -20,7 +20,7 @@ module Grape
20
20
  # env['api.format] => 'json'
21
21
  #
22
22
  # If version does not match this route, then a 406 is raised with
23
- # X-Cascade header to alert Rack::Mount to attempt the next matched
23
+ # X-Cascade header to alert Grape::Router to attempt the next matched
24
24
  # route.
25
25
  class Header < Base
26
26
  VENDOR_VERSION_HEADER_REGEX =
@@ -154,7 +154,7 @@ module Grape
154
154
 
155
155
  # By default those errors contain an `X-Cascade` header set to `pass`,
156
156
  # which allows nesting and stacking of routes
157
- # (see [Rack::Mount](https://github.com/josh/rack-mount) for more
157
+ # (see Grape::Router for more
158
158
  # information). To prevent # this behavior, and not add the `X-Cascade`
159
159
  # header, one can set the `:cascade` option to `false`.
160
160
  def cascade?
@@ -28,7 +28,7 @@ module Grape
28
28
 
29
29
  if prefix && path.index(prefix) == 0
30
30
  path.sub!(prefix, '')
31
- path = Rack::Mount::Utils.normalize_path(path)
31
+ path = Grape::Router.normalize_path(path)
32
32
  end
33
33
 
34
34
  pieces = path.split('/')
@@ -41,7 +41,7 @@ module Grape
41
41
  private
42
42
 
43
43
  def prefix
44
- Rack::Mount::Utils.normalize_path(options[:prefix].to_s) if options[:prefix]
44
+ Grape::Router.normalize_path(options[:prefix].to_s) if options[:prefix]
45
45
  end
46
46
  end
47
47
  end
@@ -29,7 +29,7 @@ module Grape
29
29
  # Join the namespaces from a list of settings to create a path prefix.
30
30
  # @param settings [Array] list of Grape::Util::InheritableSettings.
31
31
  def self.joined_space_path(settings)
32
- Rack::Mount::Utils.normalize_path(joined_space(settings))
32
+ Grape::Router.normalize_path(joined_space(settings))
33
33
  end
34
34
  end
35
35
  end
@@ -1,8 +1,10 @@
1
1
  module Grape
2
2
  module Parser
3
+ extend Util::Registrable
4
+
3
5
  class << self
4
6
  def builtin_parsers
5
- {
7
+ @builtin_parsers ||= {
6
8
  json: Grape::Parser::Json,
7
9
  jsonapi: Grape::Parser::Json,
8
10
  xml: Grape::Parser::Xml
@@ -10,7 +12,7 @@ module Grape
10
12
  end
11
13
 
12
14
  def parsers(options)
13
- builtin_parsers.merge(options[:parsers] || {})
15
+ builtin_parsers.merge(default_elements).merge(options[:parsers] || {})
14
16
  end
15
17
 
16
18
  def parser_for(api_format, options = {})
@@ -2,7 +2,7 @@ module Grape
2
2
  # Represents a path to an endpoint.
3
3
  class Path
4
4
  def self.prepare(raw_path, namespace, settings)
5
- Path.new(raw_path, namespace, settings).path_with_suffix
5
+ Path.new(raw_path, namespace, settings)
6
6
  end
7
7
 
8
8
  attr_reader :raw_path, :namespace, :settings
@@ -22,7 +22,7 @@ module Grape
22
22
  end
23
23
 
24
24
  def uses_specific_format?
25
- !!(settings[:format] && settings[:content_types].size == 1)
25
+ !!(settings[:format] && Array(settings[:content_types]).size == 1)
26
26
  end
27
27
 
28
28
  def uses_path_versioning?
@@ -48,7 +48,7 @@ module Grape
48
48
  end
49
49
 
50
50
  def path
51
- Rack::Mount::Utils.normalize_path(parts.join('/'))
51
+ Grape::Router.normalize_path(parts.join('/'))
52
52
  end
53
53
 
54
54
  def path_with_suffix
@@ -16,8 +16,8 @@ module Grape
16
16
 
17
17
  def build_params
18
18
  params = Hashie::Mash.new(rack_params)
19
- if env[Grape::Env::RACK_ROUTING_ARGS]
20
- args = env[Grape::Env::RACK_ROUTING_ARGS].dup
19
+ if env[Grape::Env::GRAPE_ROUTING_ARGS]
20
+ args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup
21
21
  # preserve version from query string parameters
22
22
  args.delete(:version)
23
23
  args.delete(:route_info)
@@ -0,0 +1,156 @@
1
+ require 'grape/router/route'
2
+
3
+ module Grape
4
+ class Router
5
+ attr_reader :map, :compiled
6
+
7
+ class Any < AttributeTranslator
8
+ def initialize(pattern, attributes = {})
9
+ @pattern = pattern
10
+ super(attributes)
11
+ end
12
+ end
13
+
14
+ def initialize
15
+ @neutral_map = []
16
+ @map = Hash.new { |hash, key| hash[key] = [] }
17
+ @optimized_map = Hash.new { |hash, key| hash[key] = // }
18
+ end
19
+
20
+ def compile!
21
+ return if compiled
22
+ @union = Regexp.union(@neutral_map.map(&:regexp))
23
+ map.each do |method, routes|
24
+ @optimized_map[method] = routes.map.with_index do |route, index|
25
+ route.index = index
26
+ route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/
27
+ end
28
+ @optimized_map[method] = Regexp.union(@optimized_map[method])
29
+ end
30
+ @compiled = true
31
+ end
32
+
33
+ def append(route)
34
+ map[route.request_method.to_s.upcase] << route
35
+ end
36
+
37
+ def associate_routes(pattern, options = {})
38
+ regexp = /(?<_#{@neutral_map.length}>)#{pattern.to_regexp}/
39
+ @neutral_map << Any.new(pattern, options.merge(regexp: regexp, index: @neutral_map.length))
40
+ end
41
+
42
+ def call(env)
43
+ with_optimization do
44
+ identity(env) || rotation(env) { |route| route.exec(env) }
45
+ end
46
+ end
47
+
48
+ def recognize_path(input)
49
+ any = with_optimization { greedy_match?(input) }
50
+ return if any == default_response
51
+ any.endpoint
52
+ end
53
+
54
+ private
55
+
56
+ def identity(env)
57
+ transaction(env) do |input, method, routing_args|
58
+ route = match?(input, method)
59
+ if route
60
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
61
+ route.exec(env)
62
+ end
63
+ end
64
+ end
65
+
66
+ def rotation(env)
67
+ transaction(env) do |input, method, routing_args|
68
+ response = nil
69
+ routes_for(method).each do |route|
70
+ next unless route.match?(input)
71
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
72
+ response = yield(route)
73
+ break unless cascade?(response)
74
+ end
75
+ response
76
+ end
77
+ end
78
+
79
+ def transaction(env)
80
+ input, method, routing_args = *extract_required_args(env)
81
+ response = yield(input, method, routing_args)
82
+
83
+ return response if response && !(cascade = cascade?(response))
84
+ neighbor = greedy_match?(input)
85
+ return unless neighbor
86
+
87
+ (!cascade && neighbor) ? method_not_allowed(env, neighbor.allow_header, neighbor.endpoint) : nil
88
+ end
89
+
90
+ def make_routing_args(default_args, route, input)
91
+ args = default_args || { route_info: route }
92
+ args.merge(route.params(input))
93
+ end
94
+
95
+ def extract_required_args(env)
96
+ input = string_for(env[Grape::Http::Headers::PATH_INFO])
97
+ method = env[Grape::Http::Headers::REQUEST_METHOD]
98
+ routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
99
+ [input, method, routing_args]
100
+ end
101
+
102
+ def with_optimization
103
+ compile! unless compiled
104
+ yield || default_response
105
+ end
106
+
107
+ def default_response
108
+ [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
109
+ end
110
+
111
+ def match?(input, method)
112
+ current_regexp = @optimized_map[method]
113
+ return unless current_regexp.match(input)
114
+ last_match = Regexp.last_match
115
+ @map[method].detect { |route| last_match["_#{route.index}"] }
116
+ end
117
+
118
+ def greedy_match?(input)
119
+ return unless @union.match(input)
120
+ last_match = Regexp.last_match
121
+ @neutral_map.detect { |route| last_match["_#{route.index}"] }
122
+ end
123
+
124
+ def method_not_allowed(env, methods, endpoint)
125
+ current = endpoint.dup
126
+ current.instance_eval do
127
+ run_filters befores, :before
128
+ @method_not_allowed = true
129
+ @block = proc do
130
+ fail Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => methods)
131
+ end
132
+ end
133
+ current.call(env)
134
+ end
135
+
136
+ def cascade?(response)
137
+ response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
138
+ end
139
+
140
+ def routes_for(method)
141
+ map[method] + map['ANY']
142
+ end
143
+
144
+ def string_for(input)
145
+ self.class.normalize_path(input)
146
+ end
147
+
148
+ def self.normalize_path(path)
149
+ path = "/#{path}"
150
+ path.squeeze!('/')
151
+ path.sub!(%r{/+\Z}, '')
152
+ path = '/' if path == ''
153
+ path
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,40 @@
1
+ require 'delegate'
2
+ require 'ostruct'
3
+
4
+ module Grape
5
+ class Router
6
+ class AttributeTranslator < DelegateClass(OpenStruct)
7
+ def self.register(*attributes)
8
+ AttributeTranslator.supported_attributes.concat(attributes)
9
+ end
10
+
11
+ def self.supported_attributes
12
+ @supported_attributes ||= []
13
+ end
14
+
15
+ def initialize(attributes = {})
16
+ ostruct = OpenStruct.new(attributes)
17
+ super ostruct
18
+ @attributes = attributes
19
+ self.class.supported_attributes.each do |name|
20
+ ostruct.send(:"#{name}=", nil) unless ostruct.respond_to?(name)
21
+ self.class.instance_eval do
22
+ define_method(name) { instance_variable_get(:"@#{name}") }
23
+ end if name == :format
24
+ end
25
+ end
26
+
27
+ def to_h
28
+ @attributes.each_with_object({}) do |(key, _), attributes|
29
+ attributes[key.to_sym] = send(:"#{key}")
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def accessor_available?(name)
36
+ respond_to?(name) && respond_to?(:"#{name}=")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ require 'forwardable'
2
+ require 'mustermann/grape'
3
+
4
+ module Grape
5
+ class Router
6
+ class Pattern
7
+ DEFAULT_PATTERN_OPTIONS = { uri_decode: true, type: :grape }.freeze
8
+ DEFAULT_SUPPORTED_CAPTURE = [:format, :version].freeze
9
+
10
+ attr_reader :origin, :path, :capture, :pattern
11
+
12
+ extend Forwardable
13
+ def_delegators :pattern, :named_captures, :params
14
+ def_delegators :@regexp, :===
15
+ alias_method :match?, :===
16
+
17
+ def initialize(pattern, options = {})
18
+ @origin = pattern
19
+ @path = build_path(pattern, options)
20
+ @capture = extract_capture(options)
21
+ @pattern = Mustermann.new(@path, pattern_options)
22
+ @regexp = to_regexp
23
+ end
24
+
25
+ def to_regexp
26
+ @to_regexp ||= @pattern.to_regexp
27
+ end
28
+
29
+ private
30
+
31
+ def pattern_options
32
+ options = DEFAULT_PATTERN_OPTIONS.dup
33
+ options.merge!(capture: capture) if capture.present?
34
+ options
35
+ end
36
+
37
+ def build_path(pattern, options = {})
38
+ pattern << '*path' unless options[:anchor] || pattern.end_with?('*path')
39
+ pattern + options[:suffix].to_s
40
+ end
41
+
42
+ def extract_capture(options = {})
43
+ requirements = {}.merge(options[:requirements])
44
+ supported_capture.each_with_object(requirements) do |field, capture|
45
+ option = Array(options[field])
46
+ capture[field] = option.map(&:to_s) if option.present?
47
+ end
48
+ end
49
+
50
+ def supported_capture
51
+ DEFAULT_SUPPORTED_CAPTURE
52
+ end
53
+ end
54
+ end
55
+ end