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.

@@ -16,10 +16,13 @@ Gem::Specification.new do |s|
16
16
 
17
17
  s.add_runtime_dependency 'rack'
18
18
  s.add_runtime_dependency 'rack-mount'
19
+ s.add_runtime_dependency 'rack-accept'
20
+ s.add_runtime_dependency 'activesupport'
19
21
  # s.add_runtime_dependency 'rack-jsonp'
20
- s.add_runtime_dependency 'multi_json'
21
- s.add_runtime_dependency 'multi_xml', '>= 0.5.2'
22
+ s.add_runtime_dependency 'multi_json', '>= 1.3.2'
23
+ s.add_runtime_dependency 'multi_xml'
22
24
  s.add_runtime_dependency 'hashie', '~> 1.2'
25
+ s.add_runtime_dependency 'virtus'
23
26
 
24
27
  s.add_development_dependency 'rake'
25
28
  s.add_development_dependency 'maruku'
@@ -9,6 +9,12 @@ module Grape
9
9
  autoload :Route, 'grape/route'
10
10
  autoload :Entity, 'grape/entity'
11
11
  autoload :Cookies, 'grape/cookies'
12
+ autoload :Validations, 'grape/validations'
13
+
14
+ module Exceptions
15
+ autoload :Base, 'grape/exceptions/base'
16
+ end
17
+ autoload :ValidationError, 'grape/exceptions/validation_error'
12
18
 
13
19
  module Middleware
14
20
  autoload :Base, 'grape/middleware/base'
@@ -2,12 +2,15 @@ require 'rack/mount'
2
2
  require 'rack/auth/basic'
3
3
  require 'rack/auth/digest/md5'
4
4
  require 'logger'
5
+ require 'grape/util/deep_merge'
5
6
 
6
7
  module Grape
7
8
  # The API class is the primary entry point for
8
9
  # creating Grape APIs.Users should subclass this
9
10
  # class in order to build an API.
10
11
  class API
12
+ extend Validations::ClassMethods
13
+
11
14
  class << self
12
15
  attr_reader :route_set
13
16
  attr_reader :versions
@@ -32,6 +35,7 @@ module Grape
32
35
  @endpoints = []
33
36
  @mountings = []
34
37
  @routes = nil
38
+ reset_validations!
35
39
  end
36
40
 
37
41
  def compile
@@ -96,12 +100,17 @@ module Grape
96
100
  options = args.pop if args.last.is_a? Hash
97
101
  options ||= {}
98
102
  options = {:using => :path}.merge!(options)
103
+
104
+ raise ArgumentError, "Must specify :vendor option." if options[:using] == :header && !options.has_key?(:vendor)
105
+
99
106
  @versions = versions | args
100
107
  nest(block) do
101
108
  set(:version, args)
102
109
  set(:version_options, options)
103
110
  end
104
111
  end
112
+
113
+ @versions.last unless @versions.nil?
105
114
  end
106
115
 
107
116
  # Add a description to the next namespace or function.
@@ -120,7 +129,7 @@ module Grape
120
129
  def format(new_format = nil)
121
130
  new_format ? set(:format, new_format.to_sym) : settings[:format]
122
131
  end
123
-
132
+
124
133
  # Specify the format for error messages.
125
134
  # May be `:json` or `:txt` (default).
126
135
  def error_format(new_format = nil)
@@ -284,16 +293,22 @@ module Grape
284
293
  endpoint_options = {
285
294
  :method => methods,
286
295
  :path => paths,
287
- :route_options => (route_options || {}).merge(@last_description || {})
296
+ :route_options => (@namespace_description || {}).deep_merge(@last_description || {}).deep_merge(route_options || {})
288
297
  }
289
298
  endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)
299
+
290
300
  @last_description = nil
301
+ reset_validations!
291
302
  end
292
303
 
293
304
  def before(&block)
294
305
  imbue(:befores, [block])
295
306
  end
296
307
 
308
+ def after_validation(&block)
309
+ imbue(:after_validations, [block])
310
+ end
311
+
297
312
  def after(&block)
298
313
  imbue(:afters, [block])
299
314
  end
@@ -308,9 +323,13 @@ module Grape
308
323
 
309
324
  def namespace(space = nil, &block)
310
325
  if space || block_given?
326
+ previous_namespace_description = @namespace_description
327
+ @namespace_description = (@namespace_description || {}).deep_merge(@last_description || {})
328
+ @last_description = nil
311
329
  nest(block) do
312
330
  set(:namespace, space.to_s) if space
313
331
  end
332
+ @namespace_description = previous_namespace_description
314
333
  else
315
334
  Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
316
335
  end
@@ -376,6 +395,7 @@ module Grape
376
395
  instance_eval &block if block_given?
377
396
  blocks.each{|b| instance_eval &b}
378
397
  settings.pop # when finished, we pop the context
398
+ reset_validations!
379
399
  else
380
400
  instance_eval &block
381
401
  end
@@ -397,6 +417,7 @@ module Grape
397
417
  self.class.endpoints.each do |endpoint|
398
418
  endpoint.mount_in(@route_set)
399
419
  end
420
+ add_head_not_allowed_methods
400
421
  @route_set.freeze
401
422
  end
402
423
 
@@ -405,5 +426,41 @@ module Grape
405
426
  end
406
427
 
407
428
  reset!
429
+
430
+ private
431
+
432
+ # For every resource add a 'OPTIONS' route that returns an HTTP 204 response
433
+ # with a list of HTTP methods that can be called. Also add a route that
434
+ # will return an HTTP 405 response for any HTTP method that the resource
435
+ # cannot handle.
436
+ def add_head_not_allowed_methods
437
+ allowed_methods = Hash.new{|h,k| h[k] = [] }
438
+ resources = self.class.endpoints.map do |endpoint|
439
+ endpoint.options[:app] && endpoint.options[:app].respond_to?(:endpoints) ?
440
+ endpoint.options[:app].endpoints.map(&:routes) :
441
+ endpoint.routes
442
+ end
443
+ resources.flatten.each do |route|
444
+ allowed_methods[route.route_compiled] << route.route_method
445
+ end
446
+
447
+ allowed_methods.each do |path_info, methods|
448
+ allow_header = (["OPTIONS"] | methods).join(", ")
449
+ unless methods.include?("OPTIONS")
450
+ @route_set.add_route( proc { [204, { 'Allow' => allow_header }, []]}, {
451
+ :path_info => path_info,
452
+ :request_method => "OPTIONS"
453
+ })
454
+ end
455
+ not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - methods
456
+ not_allowed_methods.each do |bad_method|
457
+ @route_set.add_route( proc { [405, { 'Allow' => allow_header }, []]}, {
458
+ :path_info => path_info,
459
+ :request_method => bad_method
460
+ })
461
+ end
462
+ end
463
+ end
464
+
408
465
  end
409
466
  end
@@ -123,9 +123,31 @@ module Grape
123
123
  deep_merge(self.body_params)
124
124
  end
125
125
 
126
+ # A filtering method that will return a hash
127
+ # consisting only of keys that have been declared by a
128
+ # `params` statement.
129
+ #
130
+ # @param params [Hash] The initial hash to filter. Usually this will just be `params`
131
+ # @param options [Hash] Can pass `:include_missing` and `:stringify` options.
132
+ def declared(params, options = {})
133
+ options[:include_missing] = true unless options.key?(:include_missing)
134
+
135
+ unless settings[:declared_params]
136
+ raise ArgumentError, "Tried to filter for declared parameters but none exist."
137
+ end
138
+
139
+ settings[:declared_params].inject({}){|h,k|
140
+ output_key = options[:stringify] ? k.to_s : k.to_sym
141
+ if params.key?(output_key) || options[:include_missing]
142
+ h[output_key] = params[k]
143
+ end
144
+ h
145
+ }
146
+ end
147
+
126
148
  # Pull out request body params if the content type matches and we're on a POST or PUT
127
149
  def body_params
128
- if ['POST', 'PUT'].include?(request.request_method.to_s.upcase)
150
+ if ['POST', 'PUT'].include?(request.request_method.to_s.upcase) && request.content_length.to_i > 0
129
151
  return case env['CONTENT_TYPE']
130
152
  when 'application/json'
131
153
  MultiJson.decode(request.body.read)
@@ -159,7 +181,7 @@ module Grape
159
181
  def redirect(url, options = {})
160
182
  merged_options = {:permanent => false }.merge(options)
161
183
  if merged_options[:permanent]
162
- status 304
184
+ status 301
163
185
  else
164
186
  if env['HTTP_VERSION'] == 'HTTP/1.1' && request.request_method.to_s.upcase != "GET"
165
187
  status 303
@@ -197,12 +219,12 @@ module Grape
197
219
  @header
198
220
  end
199
221
  end
200
-
222
+
201
223
  # Set response content-type
202
224
  def content_type(val)
203
225
  header('Content-Type', val)
204
226
  end
205
-
227
+
206
228
  # Set or get a cookie
207
229
  #
208
230
  # @example
@@ -255,6 +277,8 @@ module Grape
255
277
  entity_class ||= (settings[:representations] || {})[potential]
256
278
  end
257
279
 
280
+ entity_class ||= object.class.const_get(:Entity) if object.class.const_defined?(:Entity)
281
+
258
282
  root = options.delete(:root)
259
283
 
260
284
  representation = if entity_class
@@ -268,7 +292,7 @@ module Grape
268
292
  representation = { root => representation } if root
269
293
  body representation
270
294
  end
271
-
295
+
272
296
  # Returns route information for the current request.
273
297
  #
274
298
  # @example
@@ -290,11 +314,20 @@ module Grape
290
314
 
291
315
  self.extend helpers
292
316
  cookies.read(@request)
317
+
293
318
  run_filters befores
319
+
320
+ # Retieve validations from this namespace and all parent namespaces.
321
+ settings.gather(:validations).each do |validator|
322
+ validator.validate!(params)
323
+ end
324
+
325
+ run_filters after_validations
326
+
294
327
  response_text = instance_eval &self.block
295
328
  run_filters afters
296
329
  cookies.write(header)
297
-
330
+
298
331
  [status, header, [body || response_text]]
299
332
  end
300
333
 
@@ -305,10 +338,10 @@ module Grape
305
338
  b.use Grape::Middleware::Error,
306
339
  :default_status => settings[:default_error_status] || 403,
307
340
  :rescue_all => settings[:rescue_all],
308
- :rescued_errors => settings[:rescued_errors],
341
+ :rescued_errors => aggregate_setting(:rescued_errors),
309
342
  :format => settings[:error_format] || :txt,
310
343
  :rescue_options => settings[:rescue_options],
311
- :rescue_handlers => settings[:rescue_handlers] || {}
344
+ :rescue_handlers => merged_setting(:rescue_handlers)
312
345
 
313
346
  b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
314
347
  b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
@@ -320,7 +353,7 @@ module Grape
320
353
  :version_options => settings[:version_options]
321
354
  }
322
355
  end
323
-
356
+
324
357
  b.use Grape::Middleware::Formatter,
325
358
  :format => settings[:format],
326
359
  :default_format => settings[:default_format] || :txt,
@@ -351,6 +384,12 @@ module Grape
351
384
  end
352
385
  end
353
386
 
387
+ def merged_setting(key)
388
+ settings.stack.inject({}) do |merged, frame|
389
+ merged.merge(frame[key] || {})
390
+ end
391
+ end
392
+
354
393
  def run_filters(filters)
355
394
  (filters || []).each do |filter|
356
395
  instance_eval &filter
@@ -358,6 +397,7 @@ module Grape
358
397
  end
359
398
 
360
399
  def befores; aggregate_setting(:befores) end
400
+ def after_validations; aggregate_setting(:after_validations) end
361
401
  def afters; aggregate_setting(:afters) end
362
402
  end
363
403
  end
@@ -43,6 +43,60 @@ module Grape
43
43
  class Entity
44
44
  attr_reader :object, :options
45
45
 
46
+ # The Entity DSL allows you to mix entity functionality into
47
+ # your existing classes.
48
+ module DSL
49
+ def self.included(base)
50
+ base.extend ClassMethods
51
+ ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)}
52
+ base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity)
53
+ end
54
+
55
+ module ClassMethods
56
+ # Returns the automatically-created entity class for this
57
+ # Class.
58
+ def entity_class(search_ancestors=true)
59
+ klass = const_get(:Entity) if const_defined?(:Entity)
60
+ klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
61
+ klass
62
+ end
63
+
64
+ # Call this to make exposures to the entity for this Class.
65
+ # Can be called with symbols for the attributes to expose,
66
+ # a block that yields the full Entity DSL (See Grape::Entity),
67
+ # or both.
68
+ #
69
+ # @example Symbols only.
70
+ #
71
+ # class User
72
+ # include Grape::Entity::DSL
73
+ #
74
+ # entity :name, :email
75
+ # end
76
+ #
77
+ # @example Mixed.
78
+ #
79
+ # class User
80
+ # include Grape::Entity::DSL
81
+ #
82
+ # entity :name, :email do
83
+ # expose :latest_status, using: Status::Entity, if: :include_status
84
+ # expose :new_attribute, :if => {:version => 'v2'}
85
+ # end
86
+ # end
87
+ def entity(*exposures, &block)
88
+ entity_class.expose *exposures if exposures.any?
89
+ entity_class.class_eval(&block) if block_given?
90
+ entity_class
91
+ end
92
+ end
93
+
94
+ # Instantiates an entity version of this object.
95
+ def entity
96
+ self.class.entity_class.new(self)
97
+ end
98
+ end
99
+
46
100
  # This method is the primary means by which you will declare what attributes
47
101
  # should be exposed by the entity.
48
102
  #
@@ -66,8 +120,8 @@ module Grape
66
120
  # will be called with the represented object as well as the
67
121
  # runtime options that were passed in. You can also just supply a
68
122
  # block to the expose call to achieve the same effect.
69
- # @option options :documentation Define documenation for an exposed
70
- # field, typically the value is a hash with two fields, type and desc.
123
+ # @option options :documentation Define documenation for an exposed
124
+ # field, typically the value is a hash with two fields, type and desc.
71
125
  def self.expose(*args, &block)
72
126
  options = args.last.is_a?(Hash) ? args.pop : {}
73
127
 
@@ -98,13 +152,13 @@ module Grape
98
152
  @exposures
99
153
  end
100
154
 
101
- # Returns a hash, the keys are symbolized references to fields in the entity,
102
- # the values are document keys in the entity's documentation key. When calling
155
+ # Returns a hash, the keys are symbolized references to fields in the entity,
156
+ # the values are document keys in the entity's documentation key. When calling
103
157
  # #docmentation, any exposure without a documentation key will be ignored.
104
158
  def self.documentation
105
159
  @documentation ||= exposures.inject({}) do |memo, value|
106
160
  unless value[1][:documentation].nil? || value[1][:documentation].empty?
107
- memo[value[0]] = value[1][:documentation]
161
+ memo[value[0]] = value[1][:documentation]
108
162
  end
109
163
  memo
110
164
  end
@@ -118,7 +172,7 @@ module Grape
118
172
 
119
173
  # This allows you to declare a Proc in which exposures can be formatted with.
120
174
  # It take a block with an arity of 1 which is passed as the value of the exposed attribute.
121
- #
175
+ #
122
176
  # @param name [Symbol] the name of the formatter
123
177
  # @param block [Proc] the block that will interpret the exposed attribute
124
178
  #
@@ -259,7 +313,17 @@ module Grape
259
313
  return nil if object.nil?
260
314
  opts = options.merge(runtime_options || {})
261
315
  exposures.inject({}) do |output, (attribute, exposure_options)|
262
- output[key_for(attribute)] = value_for(attribute, opts) if conditions_met?(exposure_options, opts)
316
+ if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts)
317
+ partial_output = value_for(attribute, opts)
318
+ output[key_for(attribute)] =
319
+ if partial_output.respond_to? :serializable_hash
320
+ partial_output.serializable_hash(runtime_options)
321
+ elsif partial_output.kind_of?(Array) && !partial_output.map {|o| o.respond_to? :serializable_hash}.include?(false)
322
+ partial_output.map {|o| o.serializable_hash}
323
+ else
324
+ partial_output
325
+ end
326
+ end
263
327
  output
264
328
  end
265
329
  end
@@ -278,7 +342,10 @@ module Grape
278
342
  if exposure_options[:proc]
279
343
  exposure_options[:proc].call(object, options)
280
344
  elsif exposure_options[:using]
281
- exposure_options[:using].represent(object.send(attribute), :root => nil)
345
+ using_options = options.dup
346
+ using_options.delete(:collection)
347
+ using_options[:root] = nil
348
+ exposure_options[:using].represent(object.send(attribute), using_options)
282
349
  elsif exposure_options[:format_with]
283
350
  format_with = exposure_options[:format_with]
284
351
 
@@ -0,0 +1,17 @@
1
+ module Grape
2
+ module Exceptions
3
+ class Base < StandardError
4
+ attr_reader :status, :message, :headers
5
+
6
+ def initialize(args = {})
7
+ @status = args[:status] || nil
8
+ @message = args[:message] || nil
9
+ @headers = args[:headers] || nil
10
+ end
11
+
12
+ def [](index)
13
+ self.send(index)
14
+ end
15
+ end
16
+ end
17
+ end