grape 0.2.1.1 → 0.2.2

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.

@@ -0,0 +1,10 @@
1
+ require 'grape/exceptions/base'
2
+
3
+ class ValidationError < Grape::Exceptions::Base
4
+ attr_accessor :param
5
+
6
+ def initialize(args = {})
7
+ @param = args[:param].to_s if args.has_key? :param
8
+ super
9
+ end
10
+ end
@@ -1,3 +1,4 @@
1
+ require 'active_support/ordered_hash'
1
2
  require 'multi_json'
2
3
  require 'multi_xml'
3
4
 
@@ -44,14 +45,14 @@ module Grape
44
45
 
45
46
 
46
47
  module Formats
47
-
48
- CONTENT_TYPES = {
49
- :xml => 'application/xml',
50
- :json => 'application/json',
51
- :atom => 'application/atom+xml',
52
- :rss => 'application/rss+xml',
53
- :txt => 'text/plain'
54
- }
48
+ # Content types are listed in order of preference.
49
+ CONTENT_TYPES = ActiveSupport::OrderedHash[
50
+ :xml, 'application/xml',
51
+ :json, 'application/json',
52
+ :atom, 'application/atom+xml',
53
+ :rss, 'application/rss+xml',
54
+ :txt, 'text/plain',
55
+ ]
55
56
  FORMATTERS = {
56
57
  :json => :encode_json,
57
58
  :txt => :encode_txt,
@@ -70,10 +71,6 @@ module Grape
70
71
  PARSERS.merge(options[:parsers] || {})
71
72
  end
72
73
 
73
- def content_type_for(format)
74
- Hash.new(content_types)[format.to_sym]
75
- end
76
-
77
74
  def content_types
78
75
  CONTENT_TYPES.merge(options[:content_types] || {})
79
76
  end
@@ -114,20 +111,32 @@ module Grape
114
111
  MultiJson.load(object)
115
112
  end
116
113
 
117
- def encode_json(object)
118
- return object if object.is_a?(String)
114
+ def serializable?(object)
115
+ object.respond_to?(:serializable_hash) ||
116
+ object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false) ||
117
+ object.kind_of?(Hash)
118
+ end
119
119
 
120
+ def serialize(object)
120
121
  if object.respond_to? :serializable_hash
121
- MultiJson.dump(object.serializable_hash)
122
+ object.serializable_hash
122
123
  elsif object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false)
123
- MultiJson.dump(object.map {|o| o.serializable_hash })
124
- elsif object.respond_to? :to_json
125
- object.to_json
124
+ object.map {|o| o.serializable_hash }
125
+ elsif object.kind_of?(Hash)
126
+ object.inject({}) { |h,(k,v)| h[k] = serialize(v); h }
126
127
  else
127
- MultiJson.dump(object)
128
+ object
128
129
  end
129
130
  end
130
131
 
132
+ def encode_json(object)
133
+ return object if object.is_a?(String)
134
+ return MultiJson.dump(serialize(object)) if serializable?(object)
135
+ return object.to_json if object.respond_to?(:to_json)
136
+
137
+ MultiJson.dump(object)
138
+ end
139
+
131
140
  def encode_txt(object)
132
141
  object.respond_to?(:to_txt) ? object.to_txt : object.to_s
133
142
  end
@@ -44,11 +44,19 @@ module Grape
44
44
  return @app.call(@env)
45
45
  })
46
46
  rescue Exception => e
47
- raise unless options[:rescue_all] || (options[:rescued_errors] || []).include?(e.class)
48
- handler = options[:rescue_handlers][e.class] || options[:rescue_handlers][:all]
47
+ is_rescuable = rescuable?(e.class)
48
+ if e.is_a?(Grape::Exceptions::Base) && !is_rescuable
49
+ handler = lambda {|e| error_response(e) }
50
+ else
51
+ raise unless is_rescuable
52
+ handler = options[:rescue_handlers][e.class] || options[:rescue_handlers][:all]
53
+ end
49
54
  handler.nil? ? handle_error(e) : self.instance_exec(e, &handler)
50
55
  end
51
-
56
+ end
57
+
58
+ def rescuable?(klass)
59
+ options[:rescue_all] || (options[:rescued_errors] || []).include?(klass)
52
60
  end
53
61
 
54
62
  def handle_error(e)
@@ -19,29 +19,17 @@ module Grape
19
19
  end
20
20
 
21
21
  def before
22
- fmt = format_from_extension || options[:format] || format_from_header || options[:default_format]
22
+ fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
23
23
  if content_types.key?(fmt)
24
24
  if !env['rack.input'].nil? and (body = env['rack.input'].read).strip.length != 0
25
25
  parser = parser_for fmt
26
26
  unless parser.nil?
27
27
  begin
28
- fmt = mime_types[request.media_type] if request.media_type
29
- if content_type_for(fmt)
30
- parser = parser_for fmt
31
- unless parser.nil?
32
- begin
33
- body = parser.call body
34
- env['rack.request.form_hash'] = !env['rack.request.form_hash'].nil? ? env['rack.request.form_hash'].merge(body) : body
35
- env['rack.request.form_input'] = env['rack.input']
36
- rescue
37
- # It's possible that it's just regular POST content -- just back off
38
- end
39
- end
40
- else
41
- throw :error, :status => 406, :message => 'The requested content-type is not supported.'
42
- end
43
- ensure
44
- env['rack.input'].rewind
28
+ body = parser.call(body)
29
+ env['rack.request.form_hash'] = !env['rack.request.form_hash'].nil? ? env['rack.request.form_hash'].merge(body) : body
30
+ env['rack.request.form_input'] = env['rack.input']
31
+ rescue
32
+ # It's possible that it's just regular POST content -- just back off
45
33
  end
46
34
  end
47
35
  env['rack.input'].rewind
@@ -62,6 +50,11 @@ module Grape
62
50
  nil
63
51
  end
64
52
 
53
+ def format_from_params
54
+ fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
55
+ fmt ? fmt.to_sym : nil
56
+ end
57
+
65
58
  def format_from_header
66
59
  mime_array.each do |t|
67
60
  if mime_types.key?(t)
@@ -1,4 +1,5 @@
1
1
  require 'grape/middleware/base'
2
+ require 'rack/accept'
2
3
 
3
4
  module Grape
4
5
  module Middleware
@@ -21,37 +22,95 @@ module Grape
21
22
  # If version does not match this route, then a 406 is throw with
22
23
  # X-Cascade header to alert Rack::Mount to attempt the next matched
23
24
  # route.
25
+ #
26
+ # @throws [RuntimeError] if Accept header is invalid
24
27
  class Header < Base
28
+ include Formats
29
+
25
30
  def before
26
- accept = env['HTTP_ACCEPT'] || ""
31
+ header = Rack::Accept::MediaType.new env['HTTP_ACCEPT']
27
32
 
28
- if options[:version_options] && options[:version_options].keys.include?(:strict) && options[:version_options][:strict]
29
- if (is_accept_header_valid?(accept)) && options[:version_options][:using] == :header
30
- throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => "406 API Version Not Found"
33
+ if strict?
34
+ # If no Accept header:
35
+ if header.qvalues.empty?
36
+ throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'Accept header must be set'
37
+ end
38
+ # Remove any acceptable content types with ranges.
39
+ header.qvalues.reject! do |media_type,_|
40
+ Rack::Accept::Header.parse_media_type(media_type).find{|s| s == '*'}
41
+ end
42
+ # If all Accept headers included a range:
43
+ if header.qvalues.empty?
44
+ throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'Accept header must not contain ranges ("*")'
31
45
  end
32
46
  end
33
- accept.strip.scan(/^(.+?)\/(.+?)$/) do |type, subtype|
47
+
48
+ media_type = header.best_of available_media_types
49
+
50
+ if media_type
51
+ type, subtype = Rack::Accept::Header.parse_media_type media_type
34
52
  env['api.type'] = type
35
53
  env['api.subtype'] = subtype
36
54
 
37
- subtype.scan(/vnd\.(.+)?-(.+)?\+(.*)?/) do |vendor, version, format|
38
- is_vendored = options[:version_options] && options[:version_options][:vendor]
39
- is_vendored_match = is_vendored ? options[:version_options][:vendor] == vendor : true
55
+ if /\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/ =~ subtype
56
+ env['api.vendor'] = $1
57
+ env['api.version'] = $2
58
+ env['api.format'] = $3 # weird that Grape::Middleware::Formatter also does this
59
+ end
60
+ # If none of the available content types are acceptable:
61
+ elsif strict?
62
+ throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => '406 Not Acceptable'
63
+ # If all acceptable content types specify a vendor or version that doesn't exist:
64
+ elsif header.values.all?{|media_type| has_vendor?(media_type) || has_version?(media_type)}
65
+ throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'API vendor or version not found'
66
+ end
67
+ end
40
68
 
41
- if (options[:versions] && !options[:versions].include?(version)) || !is_vendored_match
42
- throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => "406 API Version Not Found"
43
- end
69
+ private
44
70
 
45
- env['api.version'] = version
46
- env['api.vendor'] = vendor
47
- env['api.format'] = format # weird that Grape::Middleware::Formatter also does this
71
+ def available_media_types
72
+ available_media_types = []
73
+
74
+ content_types.each do |extension,media_type|
75
+ versions.reverse.each do |version|
76
+ available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
48
77
  end
78
+ available_media_types << "application/vnd.#{vendor}+#{extension}"
79
+ end
80
+
81
+ available_media_types << "application/vnd.#{vendor}"
82
+
83
+ content_types.each do |_,media_type|
84
+ available_media_types << media_type
49
85
  end
86
+
87
+ available_media_types = available_media_types.flatten
88
+ end
89
+
90
+ def versions
91
+ options[:versions] || []
92
+ end
93
+
94
+ def vendor
95
+ options[:version_options] && options[:version_options][:vendor]
96
+ end
97
+
98
+ def strict?
99
+ options[:version_options] && options[:version_options][:strict]
100
+ end
101
+
102
+ # @param [String] media_type a content type
103
+ # @return [Boolean] whether the content type sets a vendor
104
+ def has_vendor?(media_type)
105
+ type, subtype = Rack::Accept::Header.parse_media_type media_type
106
+ subtype[/\Avnd\.[a-z0-9*.]+/]
50
107
  end
51
108
 
52
- protected
53
- def is_accept_header_valid?(header)
54
- (header.strip =~ /application\/vnd\.(.+?)-(.+?)\+(.+?)/).nil?
109
+ # @param [String] media_type a content type
110
+ # @return [Boolean] whether the content type sets an API version
111
+ def has_version?(media_type)
112
+ type, subtype = Rack::Accept::Header.parse_media_type media_type
113
+ subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
55
114
  end
56
115
  end
57
116
  end
@@ -0,0 +1,23 @@
1
+ class Hash
2
+ # deep_merge from rails
3
+ # activesupport/lib/active_support/core_ext/hash/deep_merge.rb
4
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
5
+ #
6
+ # h1 = {:x => {:y => [4,5,6]}, :z => [7,8,9]}
7
+ # h2 = {:x => {:y => [7,8,9]}, :z => "xyz"}
8
+ #
9
+ # h1.deep_merge(h2) #=> { :x => {:y => [7, 8, 9]}, :z => "xyz" }
10
+ # h2.deep_merge(h1) #=> { :x => {:y => [4, 5, 6]}, :z => [7, 8, 9] }
11
+ def deep_merge(other_hash)
12
+ dup.deep_merge!(other_hash)
13
+ end
14
+
15
+ # Same as +deep_merge+, but modifies +self+.
16
+ def deep_merge!(other_hash)
17
+ other_hash.each_pair do |k,v|
18
+ tv = self[k]
19
+ self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
20
+ end
21
+ self
22
+ end
23
+ end
@@ -18,7 +18,7 @@ module Grape
18
18
  end
19
19
 
20
20
  # Add a new hash to the top of the stack.
21
- #
21
+ #
22
22
  # @param hash [Hash] optional hash to be pushed. Defaults to empty hash
23
23
  # @return [HashStack]
24
24
  def push(hash = {})
@@ -31,7 +31,7 @@ module Grape
31
31
  end
32
32
 
33
33
  # Looks through the stack for the first frame that matches :key
34
- #
34
+ #
35
35
  # @param key [Symbol] key to look for in hash frames
36
36
  # @return value of given key after merging the stack
37
37
  def get(key)
@@ -52,7 +52,7 @@ module Grape
52
52
  alias_method :[]=, :set
53
53
 
54
54
  # Replace multiple values on the top hash of the stack.
55
- #
55
+ #
56
56
  # @param hash [Hash] Hash of values to be merged in.
57
57
  def update(hash)
58
58
  peek.merge!(hash)
@@ -83,6 +83,15 @@ module Grape
83
83
  self
84
84
  end
85
85
 
86
+ # Looks through the stack for all instances of a given key and returns
87
+ # them as a flat Array.
88
+ #
89
+ # @param key [Symbol] The key to gather
90
+ # @return [Array]
91
+ def gather(key)
92
+ stack.map{|s| s[key] }.flatten.compact.uniq
93
+ end
94
+
86
95
  def to_s
87
96
  @stack.to_s
88
97
  end
@@ -0,0 +1,202 @@
1
+ require 'virtus'
2
+
3
+ module Grape
4
+
5
+ module Validations
6
+
7
+ ##
8
+ # All validators must inherit from this class.
9
+ #
10
+ class Validator
11
+ attr_reader :attrs
12
+
13
+ def initialize(attrs, options, required, scope)
14
+ @attrs = Array(attrs)
15
+ @required = required
16
+ @scope = scope
17
+
18
+ if options.is_a?(Hash) && !options.empty?
19
+ raise "unknown options: #{options.keys}"
20
+ end
21
+ end
22
+
23
+ def validate!(params)
24
+ params = @scope.params(params)
25
+
26
+ @attrs.each do |attr_name|
27
+ if @required || params.has_key?(attr_name)
28
+ validate_param!(attr_name, params)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def self.convert_to_short_name(klass)
36
+ ret = klass.name.gsub(/::/, '/').
37
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
38
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
39
+ tr("-", "_").
40
+ downcase
41
+ File.basename(ret, '_validator')
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Base class for all validators taking only one param.
47
+ class SingleOptionValidator < Validator
48
+ def initialize(attrs, options, required, scope)
49
+ @option = options
50
+ super
51
+ end
52
+
53
+ end
54
+
55
+ # We define Validator::inherited here so SingleOptionValidator
56
+ # will not be considered a validator.
57
+ class Validator
58
+ def self.inherited(klass)
59
+ short_name = convert_to_short_name(klass)
60
+ Validations::register_validator(short_name, klass)
61
+ end
62
+ end
63
+
64
+ class << self
65
+ attr_accessor :validators
66
+ end
67
+
68
+ self.validators = {}
69
+
70
+ def self.register_validator(short_name, klass)
71
+ validators[short_name] = klass
72
+ end
73
+
74
+ class ParamsScope
75
+ attr_accessor :element, :parent
76
+
77
+ def initialize(api, element, parent, &block)
78
+ @element = element
79
+ @parent = parent
80
+ @api = api
81
+ instance_eval(&block)
82
+ end
83
+
84
+ def requires(*attrs)
85
+ validations = {:presence => true}
86
+ if attrs.last.is_a?(Hash)
87
+ validations.merge!(attrs.pop)
88
+ end
89
+
90
+ push_declared_params(attrs)
91
+ validates(attrs, validations)
92
+ end
93
+
94
+ def optional(*attrs)
95
+ validations = {}
96
+ if attrs.last.is_a?(Hash)
97
+ validations.merge!(attrs.pop)
98
+ end
99
+
100
+ push_declared_params(attrs)
101
+ validates(attrs, validations)
102
+ end
103
+
104
+ def group(element, &block)
105
+ scope = ParamsScope.new(@api, element, self, &block)
106
+ end
107
+
108
+ def params(params)
109
+ params = @parent.params(params) if @parent
110
+ params = params[@element] || {} if @element
111
+ params
112
+ end
113
+
114
+ def full_name(name)
115
+ return "#{@parent.full_name(@element)}[#{name}]" if @parent
116
+ name.to_s
117
+ end
118
+
119
+ private
120
+ def validates(attrs, validations)
121
+ doc_attrs = { :required => validations.keys.include?(:presence) }
122
+
123
+ # special case (type = coerce)
124
+ if validations[:type]
125
+ validations[:coerce] = validations.delete(:type)
126
+ end
127
+
128
+ if coerce_type = validations[:coerce]
129
+ doc_attrs[:type] = coerce_type.to_s
130
+ end
131
+
132
+ if desc = validations.delete(:desc)
133
+ doc_attrs[:desc] = desc
134
+ end
135
+
136
+ full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
137
+ @api.document_attribute(full_attrs, doc_attrs)
138
+
139
+ # Validate for presence before any other validators
140
+ if validations.has_key?(:presence) && validations[:presence]
141
+ validate('presence', validations[:presence], attrs, doc_attrs)
142
+ end
143
+
144
+ # Before we run the rest of the validators, lets handle
145
+ # whatever coercion so that we are working with correctly
146
+ # type casted values
147
+ if validations.has_key? :coerce
148
+ validate('coerce', validations[:coerce], attrs, doc_attrs)
149
+ validations.delete(:coerce)
150
+ end
151
+
152
+ validations.each do |type, options|
153
+ validate(type, options, attrs, doc_attrs)
154
+ end
155
+ end
156
+
157
+ def validate(type, options, attrs, doc_attrs)
158
+ validator_class = Validations::validators[type.to_s]
159
+
160
+ if validator_class
161
+ (@api.settings.peek[:validations] ||= []) << validator_class.new(attrs, options, doc_attrs[:required], self)
162
+ else
163
+ raise "unknown validator: #{type}"
164
+ end
165
+ end
166
+
167
+ def push_declared_params(attrs)
168
+ @api.settings.peek[:declared_params] ||= []
169
+ @api.settings[:declared_params] += attrs
170
+ end
171
+ end
172
+
173
+ # This module is mixed into the API Class.
174
+ module ClassMethods
175
+ def reset_validations!
176
+ settings.peek[:declared_params] = []
177
+ settings.peek[:validations] = []
178
+ end
179
+
180
+ def params(&block)
181
+ ParamsScope.new(self, nil, nil, &block)
182
+ end
183
+
184
+ def document_attribute(names, opts)
185
+ @last_description ||= {}
186
+ @last_description[:params] ||= {}
187
+
188
+ Array(names).each do |name|
189
+ @last_description[:params][name[:name].to_s] ||= {}
190
+ @last_description[:params][name[:name].to_s].merge!(opts).merge!({:full_name => name[:full_name]})
191
+ end
192
+ end
193
+
194
+ end
195
+
196
+ end
197
+ end
198
+
199
+ # Load all defined validations.
200
+ Dir[File.expand_path('../validations/*.rb', __FILE__)].each do |path|
201
+ require(path)
202
+ end