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.
- data/.gitignore +1 -0
- data/CHANGELOG.markdown +23 -2
- data/Gemfile +2 -0
- data/README.markdown +402 -227
- data/grape.gemspec +5 -2
- data/lib/grape.rb +6 -0
- data/lib/grape/api.rb +59 -2
- data/lib/grape/endpoint.rb +49 -9
- data/lib/grape/entity.rb +75 -8
- data/lib/grape/exceptions/base.rb +17 -0
- data/lib/grape/exceptions/validation_error.rb +10 -0
- data/lib/grape/middleware/base.rb +28 -19
- data/lib/grape/middleware/error.rb +11 -3
- data/lib/grape/middleware/formatter.rb +11 -18
- data/lib/grape/middleware/versioner/header.rb +76 -17
- data/lib/grape/util/deep_merge.rb +23 -0
- data/lib/grape/util/hash_stack.rb +12 -3
- data/lib/grape/validations.rb +202 -0
- data/lib/grape/validations/coerce.rb +61 -0
- data/lib/grape/validations/presence.rb +11 -0
- data/lib/grape/validations/regexp.rb +13 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +281 -123
- data/spec/grape/endpoint_spec.rb +69 -4
- data/spec/grape/entity_spec.rb +204 -16
- data/spec/grape/middleware/exception_spec.rb +21 -0
- data/spec/grape/middleware/formatter_spec.rb +19 -0
- data/spec/grape/middleware/versioner/header_spec.rb +159 -88
- data/spec/grape/validations/coerce_spec.rb +129 -0
- data/spec/grape/validations/presence_spec.rb +138 -0
- data/spec/grape/validations/regexp_spec.rb +33 -0
- data/spec/grape/validations_spec.rb +185 -0
- metadata +65 -74
- data/spec/grape_spec.rb +0 -1
data/grape.gemspec
CHANGED
@@ -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'
|
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'
|
data/lib/grape.rb
CHANGED
@@ -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'
|
data/lib/grape/api.rb
CHANGED
@@ -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 => (
|
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
|
data/lib/grape/endpoint.rb
CHANGED
@@ -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
|
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 =>
|
341
|
+
:rescued_errors => aggregate_setting(:rescued_errors),
|
309
342
|
:format => settings[:error_format] || :txt,
|
310
343
|
:rescue_options => settings[:rescue_options],
|
311
|
-
: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
|
data/lib/grape/entity.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|