grape 0.1.5 → 0.2.0
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 +13 -1
- data/.rspec +1 -1
- data/.travis.yml +1 -0
- data/Gemfile +10 -0
- data/Guardfile +15 -0
- data/README.markdown +472 -67
- data/grape.gemspec +4 -4
- data/lib/grape.rb +17 -5
- data/lib/grape/api.rb +227 -124
- data/lib/grape/cookies.rb +41 -0
- data/lib/grape/endpoint.rb +256 -27
- data/lib/grape/entity.rb +227 -0
- data/lib/grape/middleware/auth/oauth2.rb +1 -2
- data/lib/grape/middleware/base.rb +59 -6
- data/lib/grape/middleware/error.rb +15 -6
- data/lib/grape/middleware/filter.rb +17 -0
- data/lib/grape/middleware/formatter.rb +25 -31
- data/lib/grape/middleware/versioner.rb +20 -20
- data/lib/grape/middleware/versioner/header.rb +59 -0
- data/lib/grape/middleware/versioner/path.rb +42 -0
- data/lib/grape/route.rb +23 -0
- data/lib/grape/util/hash_stack.rb +100 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +651 -162
- data/spec/grape/endpoint_spec.rb +216 -18
- data/spec/grape/entity_spec.rb +320 -0
- data/spec/grape/middleware/auth/basic_spec.rb +2 -2
- data/spec/grape/middleware/auth/digest_spec.rb +4 -6
- data/spec/grape/middleware/exception_spec.rb +1 -0
- data/spec/grape/middleware/formatter_spec.rb +81 -27
- data/spec/grape/middleware/versioner/header_spec.rb +148 -0
- data/spec/grape/middleware/versioner/path_spec.rb +40 -0
- data/spec/grape/middleware/versioner_spec.rb +6 -34
- data/spec/grape/util/hash_stack_spec.rb +133 -0
- data/spec/shared/versioning_examples.rb +77 -0
- data/spec/spec_helper.rb +11 -3
- data/spec/support/basic_auth_encode_helpers.rb +4 -0
- data/spec/support/rack_patch.rb +25 -0
- data/spec/support/versioned_helpers.rb +34 -0
- metadata +140 -241
- data/.yardoc/checksums +0 -13
- data/.yardoc/objects/Grape.dat +0 -0
- data/.yardoc/objects/Grape/API.dat +0 -0
- data/.yardoc/objects/Grape/API/auth_c.dat +0 -0
- data/.yardoc/objects/Grape/API/build_endpoint_c.dat +0 -0
- data/.yardoc/objects/Grape/API/call_c.dat +0 -0
- data/.yardoc/objects/Grape/API/compile_path_c.dat +0 -0
- data/.yardoc/objects/Grape/API/default_format_c.dat +0 -0
- data/.yardoc/objects/Grape/API/delete_c.dat +0 -0
- data/.yardoc/objects/Grape/API/get_c.dat +0 -0
- data/.yardoc/objects/Grape/API/group_c.dat +0 -0
- data/.yardoc/objects/Grape/API/head_c.dat +0 -0
- data/.yardoc/objects/Grape/API/helpers_c.dat +0 -0
- data/.yardoc/objects/Grape/API/http_basic_c.dat +0 -0
- data/.yardoc/objects/Grape/API/inherited_c.dat +0 -0
- data/.yardoc/objects/Grape/API/logger_c.dat +0 -0
- data/.yardoc/objects/Grape/API/namespace_c.dat +0 -0
- data/.yardoc/objects/Grape/API/nest_c.dat +0 -0
- data/.yardoc/objects/Grape/API/post_c.dat +0 -0
- data/.yardoc/objects/Grape/API/prefix_c.dat +0 -0
- data/.yardoc/objects/Grape/API/put_c.dat +0 -0
- data/.yardoc/objects/Grape/API/reset_21_c.dat +0 -0
- data/.yardoc/objects/Grape/API/resource_c.dat +0 -0
- data/.yardoc/objects/Grape/API/resources_c.dat +0 -0
- data/.yardoc/objects/Grape/API/route_c.dat +0 -0
- data/.yardoc/objects/Grape/API/route_set_c.dat +0 -0
- data/.yardoc/objects/Grape/API/scope_c.dat +0 -0
- data/.yardoc/objects/Grape/API/set_c.dat +0 -0
- data/.yardoc/objects/Grape/API/settings_c.dat +0 -0
- data/.yardoc/objects/Grape/API/settings_stack_c.dat +0 -0
- data/.yardoc/objects/Grape/API/version_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/block_3D_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/block_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/call_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/call_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/env_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/error_21_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/generate_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/header_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/params_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/request_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/status_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/version_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/authenticator_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/basic_request_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/credentials_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/initialize_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/error_out_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/parse_authorization_header_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/token_class_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/verify_token_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/after_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/app_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/call_21_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/call_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/env_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/initialize_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/request_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/response_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Error.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Error/call_21_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Error/error_response_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/CONTENT_TYPES.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/after_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/content_types_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/encode_json_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/encode_txt_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/format_from_extension_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/format_from_header_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/headers_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/mime_array_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/mime_types_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Prefixer.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Prefixer/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Prefixer/prefix_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Versioner.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Versioner/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Versioner/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/initialize_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/stack_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/to_app_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/use_i.dat +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -2
- data/Gemfile.lock +0 -52
- data/autotest/discover.rb +0 -1
@@ -0,0 +1,41 @@
|
|
1
|
+
module Grape
|
2
|
+
class Cookies
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@cookies = {}
|
6
|
+
@send_cookies = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def read(request)
|
10
|
+
request.cookies.each do |name, value|
|
11
|
+
@cookies[name.to_sym] = value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(header)
|
16
|
+
@cookies.select { |key, value|
|
17
|
+
@send_cookies[key.to_sym] == true
|
18
|
+
}.each { |name, value|
|
19
|
+
Rack::Utils.set_cookie_header!(
|
20
|
+
header, name, value.instance_of?(Hash) ? value : { :value => value })
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](name)
|
25
|
+
@cookies[name]
|
26
|
+
end
|
27
|
+
|
28
|
+
def []=(name, value)
|
29
|
+
@cookies[name.to_sym] = value
|
30
|
+
@send_cookies[name.to_sym] = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def each(&block)
|
34
|
+
@cookies.each(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(name)
|
38
|
+
self.[]=(name, { :value => 'deleted', :expires => Time.at(0) })
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/grape/endpoint.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rack'
|
2
2
|
require 'grape'
|
3
|
+
require 'hashie'
|
3
4
|
|
4
5
|
module Grape
|
5
6
|
# An Endpoint is the proxy scope in which all routing
|
@@ -7,37 +8,119 @@ module Grape
|
|
7
8
|
# on the instance level of this class may be called
|
8
9
|
# from inside a `get`, `post`, etc. block.
|
9
10
|
class Endpoint
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
attr_accessor :block, :options, :settings
|
12
|
+
attr_reader :env, :request
|
13
|
+
|
14
|
+
def initialize(settings, options = {}, &block)
|
15
|
+
@settings = settings
|
16
|
+
@block = block
|
17
|
+
@options = options
|
18
|
+
|
19
|
+
raise ArgumentError, "Must specify :path option." unless options.key?(:path)
|
20
|
+
options[:path] = Array(options[:path])
|
21
|
+
options[:path] = ['/'] if options[:path].empty?
|
22
|
+
|
23
|
+
raise ArgumentError, "Must specify :method option." unless options.key?(:method)
|
24
|
+
options[:method] = Array(options[:method])
|
25
|
+
|
26
|
+
options[:route_options] ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def routes
|
30
|
+
@routes ||= prepare_routes
|
31
|
+
end
|
32
|
+
|
33
|
+
def mount_in(route_set)
|
34
|
+
if options[:app] && options[:app].respond_to?(:endpoints)
|
35
|
+
options[:app].endpoints.each{|e| e.mount_in(route_set)}
|
36
|
+
else
|
37
|
+
routes.each do |route|
|
38
|
+
route_set.add_route(self, {
|
39
|
+
:path_info => route.route_compiled,
|
40
|
+
:request_method => route.route_method,
|
41
|
+
}, { :route_info => route })
|
42
|
+
end
|
14
43
|
end
|
15
|
-
c
|
16
44
|
end
|
17
|
-
|
18
|
-
|
19
|
-
|
45
|
+
|
46
|
+
def prepare_routes
|
47
|
+
routes = []
|
48
|
+
options[:method].each do |method|
|
49
|
+
options[:path].each do |path|
|
50
|
+
prepared_path = prepare_path(path)
|
51
|
+
|
52
|
+
anchor = options[:route_options][:anchor]
|
53
|
+
anchor = anchor.nil? ? true : anchor
|
54
|
+
|
55
|
+
path = compile_path(prepared_path, anchor && !options[:app])
|
56
|
+
regex = Rack::Mount::RegexpWithNamedGroups.new(path)
|
57
|
+
path_params = {}
|
58
|
+
# named parameters in the api path
|
59
|
+
named_params = regex.named_captures.map { |nc| nc[0] } - [ 'version', 'format' ]
|
60
|
+
named_params.each { |named_param| path_params[named_param] = "" }
|
61
|
+
# route parameters declared via desc or appended to the api declaration
|
62
|
+
route_params = (options[:route_options][:params] || {})
|
63
|
+
path_params.merge!(route_params)
|
64
|
+
request_method = (method.to_s.upcase unless method == :any)
|
65
|
+
routes << Route.new(options[:route_options].clone.merge({
|
66
|
+
:prefix => settings[:root_prefix],
|
67
|
+
:version => settings[:version] ? settings[:version].join('|') : nil,
|
68
|
+
:namespace => namespace,
|
69
|
+
:method => request_method,
|
70
|
+
:path => prepared_path,
|
71
|
+
:params => path_params,
|
72
|
+
:compiled => path,
|
73
|
+
})
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
routes
|
20
78
|
end
|
21
|
-
|
22
|
-
def
|
23
|
-
|
79
|
+
|
80
|
+
def prepare_path(path)
|
81
|
+
parts = []
|
82
|
+
parts << settings[:root_prefix] if settings[:root_prefix]
|
83
|
+
parts << ':version' if settings[:version] && settings[:version_options][:using] == :path
|
84
|
+
parts << namespace.to_s if namespace
|
85
|
+
parts << path.to_s if path && '/' != path
|
86
|
+
parts.last << '(.:format)'
|
87
|
+
Rack::Mount::Utils.normalize_path(parts.join('/'))
|
24
88
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
89
|
+
|
90
|
+
def namespace
|
91
|
+
Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
|
92
|
+
end
|
93
|
+
|
94
|
+
def compile_path(prepared_path, anchor = true)
|
95
|
+
endpoint_options = {}
|
96
|
+
endpoint_options[:version] = /#{settings[:version].join('|')}/ if settings[:version]
|
97
|
+
Rack::Mount::Strexp.compile(prepared_path, endpoint_options, %w( / . ? ), anchor)
|
98
|
+
end
|
99
|
+
|
100
|
+
def call(env)
|
101
|
+
dup.call!(env)
|
102
|
+
end
|
103
|
+
|
104
|
+
def call!(env)
|
105
|
+
env['api.endpoint'] = self
|
106
|
+
if options[:app]
|
107
|
+
options[:app].call(env)
|
108
|
+
else
|
109
|
+
builder = build_middleware
|
110
|
+
builder.run options[:app] || lambda{|env| self.run(env) }
|
111
|
+
builder.call(env)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
28
115
|
# The parameters passed into the request as
|
29
116
|
# well as parsed from URL segments.
|
30
117
|
def params
|
31
|
-
@params ||= request.params.
|
32
|
-
h[k.to_s] = v
|
33
|
-
h[k.to_sym] = v
|
34
|
-
h
|
35
|
-
end
|
118
|
+
@params ||= Hashie::Mash.new.deep_merge(request.params).deep_merge(env['rack.routing_args'] || {})
|
36
119
|
end
|
37
|
-
|
120
|
+
|
38
121
|
# The API version as specified in the URL.
|
39
122
|
def version; env['api.version'] end
|
40
|
-
|
123
|
+
|
41
124
|
# End the request and display an error to the
|
42
125
|
# end user with the specified message.
|
43
126
|
#
|
@@ -46,7 +129,7 @@ module Grape
|
|
46
129
|
def error!(message, status=403)
|
47
130
|
throw :error, :message => message, :status => status
|
48
131
|
end
|
49
|
-
|
132
|
+
|
50
133
|
# Set or retrieve the HTTP status code.
|
51
134
|
#
|
52
135
|
# @param status [Integer] The HTTP Status Code to return for this request.
|
@@ -61,9 +144,9 @@ module Grape
|
|
61
144
|
else
|
62
145
|
200
|
63
146
|
end
|
64
|
-
end
|
147
|
+
end
|
65
148
|
end
|
66
|
-
|
149
|
+
|
67
150
|
# Set an individual header or retrieve
|
68
151
|
# all headers that have been set.
|
69
152
|
def header(key = nil, val = nil)
|
@@ -73,15 +156,161 @@ module Grape
|
|
73
156
|
@header
|
74
157
|
end
|
75
158
|
end
|
159
|
+
|
160
|
+
# Set or get a cookie
|
161
|
+
#
|
162
|
+
# @example
|
163
|
+
# cookies[:mycookie] = 'mycookie val'
|
164
|
+
# cookies['mycookie-string'] = 'mycookie string val'
|
165
|
+
# cookies[:more] = { :value => '123', :expires => Time.at(0) }
|
166
|
+
# cookies.delete :more
|
167
|
+
#
|
168
|
+
def cookies
|
169
|
+
@cookies ||= Cookies.new
|
170
|
+
end
|
171
|
+
|
172
|
+
# Allows you to define the response body as something other than the
|
173
|
+
# return value.
|
174
|
+
#
|
175
|
+
# @example
|
176
|
+
# get '/body' do
|
177
|
+
# body "Body"
|
178
|
+
# "Not the Body"
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# GET /body # => "Body"
|
182
|
+
def body(value = nil)
|
183
|
+
if value
|
184
|
+
@body = value
|
185
|
+
else
|
186
|
+
@body
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Allows you to make use of Grape Entities by setting
|
191
|
+
# the response body to the serializable hash of the
|
192
|
+
# entity provided in the `:with` option. This has the
|
193
|
+
# added benefit of automatically passing along environment
|
194
|
+
# and version information to the serialization, making it
|
195
|
+
# very easy to do conditional exposures. See Entity docs
|
196
|
+
# for more info.
|
197
|
+
#
|
198
|
+
# @example
|
199
|
+
#
|
200
|
+
# get '/users/:id' do
|
201
|
+
# present User.find(params[:id]),
|
202
|
+
# :with => API::Entities::User,
|
203
|
+
# :admin => current_user.admin?
|
204
|
+
# end
|
205
|
+
def present(object, options = {})
|
206
|
+
entity_class = options.delete(:with)
|
207
|
+
|
208
|
+
object.class.ancestors.each do |potential|
|
209
|
+
entity_class ||= (settings[:representations] || {})[potential]
|
210
|
+
end
|
211
|
+
|
212
|
+
root = options.delete(:root)
|
213
|
+
|
214
|
+
representation = if entity_class
|
215
|
+
embeds = {:env => env}
|
216
|
+
embeds[:version] = env['api.version'] if env['api.version']
|
217
|
+
entity_class.represent(object, embeds.merge(options))
|
218
|
+
else
|
219
|
+
object
|
220
|
+
end
|
221
|
+
|
222
|
+
representation = { root => representation } if root
|
223
|
+
body representation
|
224
|
+
end
|
76
225
|
|
77
|
-
|
226
|
+
# Returns route information for the current request.
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
#
|
230
|
+
# desc "Returns the route description."
|
231
|
+
# get '/' do
|
232
|
+
# route.route_description
|
233
|
+
# end
|
234
|
+
def route
|
235
|
+
env["rack.routing_args"][:route_info]
|
236
|
+
end
|
237
|
+
|
238
|
+
protected
|
239
|
+
|
240
|
+
def run(env)
|
78
241
|
@env = env
|
79
242
|
@header = {}
|
80
243
|
@request = Rack::Request.new(@env)
|
244
|
+
|
245
|
+
self.extend helpers
|
246
|
+
cookies.read(@request)
|
247
|
+
run_filters befores
|
248
|
+
response_text = instance_eval &self.block
|
249
|
+
run_filters afters
|
250
|
+
cookies.write(header)
|
81
251
|
|
82
|
-
|
252
|
+
[status, header, [body || response_text]]
|
253
|
+
end
|
254
|
+
|
255
|
+
def build_middleware
|
256
|
+
b = Rack::Builder.new
|
257
|
+
|
258
|
+
b.use Grape::Middleware::Error,
|
259
|
+
:default_status => settings[:default_error_status] || 403,
|
260
|
+
:rescue_all => settings[:rescue_all],
|
261
|
+
:rescued_errors => settings[:rescued_errors],
|
262
|
+
:format => settings[:error_format] || :txt,
|
263
|
+
:rescue_options => settings[:rescue_options],
|
264
|
+
:rescue_handlers => settings[:rescue_handlers] || {}
|
265
|
+
|
266
|
+
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
|
267
|
+
b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
|
268
|
+
b.use Grape::Middleware::Prefixer, :prefix => settings[:root_prefix] if settings[:root_prefix]
|
269
|
+
|
270
|
+
if settings[:version]
|
271
|
+
b.use Grape::Middleware::Versioner.using(settings[:version_options][:using]), {
|
272
|
+
:versions => settings[:version],
|
273
|
+
:version_options => settings[:version_options]
|
274
|
+
}
|
275
|
+
end
|
83
276
|
|
84
|
-
|
277
|
+
b.use Grape::Middleware::Formatter,
|
278
|
+
:format => settings[:format],
|
279
|
+
:default_format => settings[:default_format] || :txt,
|
280
|
+
:content_types => settings[:content_types]
|
281
|
+
|
282
|
+
aggregate_setting(:middleware).each do |m|
|
283
|
+
m = m.dup
|
284
|
+
block = m.pop if m.last.is_a?(Proc)
|
285
|
+
if block
|
286
|
+
b.use *m, &block
|
287
|
+
else
|
288
|
+
b.use *m
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
b
|
293
|
+
end
|
294
|
+
|
295
|
+
def helpers
|
296
|
+
m = Module.new
|
297
|
+
settings.stack.each{|frame| m.send :include, frame[:helpers] if frame[:helpers]}
|
298
|
+
m
|
85
299
|
end
|
300
|
+
|
301
|
+
def aggregate_setting(key)
|
302
|
+
settings.stack.inject([]) do |aggregate, frame|
|
303
|
+
aggregate += (frame[key] || [])
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def run_filters(filters)
|
308
|
+
(filters || []).each do |filter|
|
309
|
+
instance_eval &filter
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def befores; aggregate_setting(:befores) end
|
314
|
+
def afters; aggregate_setting(:afters) end
|
86
315
|
end
|
87
316
|
end
|
data/lib/grape/entity.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
# An Entity is a lightweight structure that allows you to easily
|
5
|
+
# represent data from your application in a consistent and abstracted
|
6
|
+
# way in your API.
|
7
|
+
#
|
8
|
+
# @example Entity Definition
|
9
|
+
#
|
10
|
+
# module API
|
11
|
+
# module Entities
|
12
|
+
# class User < Grape::Entity
|
13
|
+
# expose :first_name, :last_name, :screen_name, :location
|
14
|
+
# expose :latest_status, :using => API::Status, :as => :status, :unless => {:collection => true}
|
15
|
+
# expose :email, :if => {:type => :full}
|
16
|
+
# expose :new_attribute, :if => {:version => 'v2'}
|
17
|
+
# expose(:name){|model,options| [model.first_name, model.last_name].join(' ')}
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Entities are not independent structures, rather, they create
|
23
|
+
# **representations** of other Ruby objects using a number of methods
|
24
|
+
# that are convenient for use in an API. Once you've defined an Entity,
|
25
|
+
# you can use it in your API like this:
|
26
|
+
#
|
27
|
+
# @example Usage in the API Layer
|
28
|
+
#
|
29
|
+
# module API
|
30
|
+
# class Users < Grape::API
|
31
|
+
# version 'v2'
|
32
|
+
#
|
33
|
+
# get '/users' do
|
34
|
+
# @users = User.all
|
35
|
+
# type = current_user.admin? ? :full : :default
|
36
|
+
# present @users, :with => API::Entities::User, :type => type
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
class Entity
|
41
|
+
attr_reader :object, :options
|
42
|
+
|
43
|
+
# This method is the primary means by which you will declare what attributes
|
44
|
+
# should be exposed by the entity.
|
45
|
+
#
|
46
|
+
# @option options :as Declare an alias for the representation of this attribute.
|
47
|
+
# @option options :if When passed a Hash, the attribute will only be exposed if the
|
48
|
+
# runtime options match all the conditions passed in. When passed a lambda, the
|
49
|
+
# lambda will execute with two arguments: the object being represented and the
|
50
|
+
# options passed into the representation call. Return true if you want the attribute
|
51
|
+
# to be exposed.
|
52
|
+
# @option options :unless When passed a Hash, the attribute will be exposed if the
|
53
|
+
# runtime options fail to match any of the conditions passed in. If passed a lambda,
|
54
|
+
# it will yield the object being represented and the options passed to the
|
55
|
+
# representation call. Return true to prevent exposure, false to allow it.
|
56
|
+
# @option options :using This option allows you to map an attribute to another Grape
|
57
|
+
# Entity. Pass it a Grape::Entity class and the attribute in question will
|
58
|
+
# automatically be transformed into a representation that will receive the same
|
59
|
+
# options as the parent entity when called. Note that arrays are fine here and
|
60
|
+
# will automatically be detected and handled appropriately.
|
61
|
+
# @option options :proc If you pass a Proc into this option, it will
|
62
|
+
# be used directly to determine the value for that attribute. It
|
63
|
+
# will be called with the represented object as well as the
|
64
|
+
# runtime options that were passed in. You can also just supply a
|
65
|
+
# block to the expose call to achieve the same effect.
|
66
|
+
def self.expose(*args, &block)
|
67
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
68
|
+
|
69
|
+
if args.size > 1
|
70
|
+
raise ArgumentError, "You may not use the :as option on multi-attribute exposures." if options[:as]
|
71
|
+
raise ArgumentError, "You may not use block-setting on multi-attribute exposures." if block_given?
|
72
|
+
end
|
73
|
+
|
74
|
+
options[:proc] = block if block_given?
|
75
|
+
|
76
|
+
args.each do |attribute|
|
77
|
+
exposures[attribute.to_sym] = options
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a hash of exposures that have been declared for this Entity or ancestors. The keys
|
82
|
+
# are symbolized references to methods on the containing object, the values are
|
83
|
+
# the options that were passed into expose.
|
84
|
+
def self.exposures
|
85
|
+
@exposures ||= {}
|
86
|
+
|
87
|
+
if superclass.respond_to? :exposures
|
88
|
+
@exposures = superclass.exposures.merge(@exposures)
|
89
|
+
end
|
90
|
+
|
91
|
+
@exposures
|
92
|
+
end
|
93
|
+
|
94
|
+
# This allows you to set a root element name for your representation.
|
95
|
+
#
|
96
|
+
# @param plural [String] the root key to use when representing
|
97
|
+
# a collection of objects. If missing or nil, no root key will be used
|
98
|
+
# when representing collections of objects.
|
99
|
+
# @param singular [String] the root key to use when representing
|
100
|
+
# a single object. If missing or nil, no root key will be used when
|
101
|
+
# representing an individual object.
|
102
|
+
#
|
103
|
+
# @example Entity Definition
|
104
|
+
#
|
105
|
+
# module API
|
106
|
+
# module Entities
|
107
|
+
# class User < Grape::Entity
|
108
|
+
# root 'users', 'user'
|
109
|
+
# expose :id
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# @example Usage in the API Layer
|
115
|
+
#
|
116
|
+
# module API
|
117
|
+
# class Users < Grape::API
|
118
|
+
# version 'v2'
|
119
|
+
#
|
120
|
+
# # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
|
121
|
+
# get '/users' do
|
122
|
+
# @users = User.all
|
123
|
+
# present @users, :with => API::Entities::User
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# # this will render { "user": {"id":"1"} }
|
127
|
+
# get '/users/:id' do
|
128
|
+
# @user = User.find(params[:id])
|
129
|
+
# present @user, :with => API::Entities::User
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
def self.root(plural, singular=nil)
|
134
|
+
@collection_root = plural
|
135
|
+
@root = singular
|
136
|
+
end
|
137
|
+
|
138
|
+
# This convenience method allows you to instantiate one or more entities by
|
139
|
+
# passing either a singular or collection of objects. Each object will be
|
140
|
+
# initialized with the same options. If an array of objects is passed in,
|
141
|
+
# an array of entities will be returned. If a single object is passed in,
|
142
|
+
# a single entity will be returned.
|
143
|
+
#
|
144
|
+
# @param objects [Object or Array] One or more objects to be represented.
|
145
|
+
# @param options [Hash] Options that will be passed through to each entity
|
146
|
+
# representation.
|
147
|
+
#
|
148
|
+
# @option options :root [String] override the default root name set for the
|
149
|
+
# entity. Pass nil or false to represent the object or objects with no
|
150
|
+
# root name even if one is defined for the entity.
|
151
|
+
def self.represent(objects, options = {})
|
152
|
+
inner = if objects.respond_to?(:to_ary)
|
153
|
+
objects.to_ary().map{|o| self.new(o, {:collection => true}.merge(options))}
|
154
|
+
else
|
155
|
+
self.new(objects, options)
|
156
|
+
end
|
157
|
+
|
158
|
+
root_element = if options.has_key?(:root)
|
159
|
+
options[:root]
|
160
|
+
else
|
161
|
+
objects.respond_to?(:to_ary) ? @collection_root : @root
|
162
|
+
end
|
163
|
+
root_element ? { root_element => inner } : inner
|
164
|
+
end
|
165
|
+
|
166
|
+
def initialize(object, options = {})
|
167
|
+
@object, @options = object, options
|
168
|
+
end
|
169
|
+
|
170
|
+
def exposures
|
171
|
+
self.class.exposures
|
172
|
+
end
|
173
|
+
|
174
|
+
# The serializable hash is the Entity's primary output. It is the transformed
|
175
|
+
# hash for the given data model and is used as the basis for serialization to
|
176
|
+
# JSON and other formats.
|
177
|
+
#
|
178
|
+
# @param options [Hash] Any options you pass in here will be known to the entity
|
179
|
+
# representation, this is where you can trigger things from conditional options
|
180
|
+
# etc.
|
181
|
+
def serializable_hash(runtime_options = {})
|
182
|
+
return nil if object.nil?
|
183
|
+
opts = options.merge(runtime_options || {})
|
184
|
+
exposures.inject({}) do |output, (attribute, exposure_options)|
|
185
|
+
output[key_for(attribute)] = value_for(attribute, opts) if conditions_met?(exposure_options, opts)
|
186
|
+
output
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
alias :as_json :serializable_hash
|
191
|
+
|
192
|
+
protected
|
193
|
+
|
194
|
+
def key_for(attribute)
|
195
|
+
exposures[attribute.to_sym][:as] || attribute.to_sym
|
196
|
+
end
|
197
|
+
|
198
|
+
def value_for(attribute, options = {})
|
199
|
+
exposure_options = exposures[attribute.to_sym]
|
200
|
+
|
201
|
+
if exposure_options[:proc]
|
202
|
+
exposure_options[:proc].call(object, options)
|
203
|
+
elsif exposure_options[:using]
|
204
|
+
exposure_options[:using].represent(object.send(attribute), :root => nil)
|
205
|
+
else
|
206
|
+
object.send(attribute)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def conditions_met?(exposure_options, options)
|
211
|
+
if_condition = exposure_options[:if]
|
212
|
+
unless_condition = exposure_options[:unless]
|
213
|
+
|
214
|
+
case if_condition
|
215
|
+
when Hash; if_condition.each_pair{|k,v| return false if options[k.to_sym] != v }
|
216
|
+
when Proc; return false unless if_condition.call(object, options)
|
217
|
+
end
|
218
|
+
|
219
|
+
case unless_condition
|
220
|
+
when Hash; unless_condition.each_pair{|k,v| return false if options[k.to_sym] == v}
|
221
|
+
when Proc; return false if unless_condition.call(object, options)
|
222
|
+
end
|
223
|
+
|
224
|
+
true
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|