-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathapi.rb
636 lines (555 loc) · 20.1 KB
/
api.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
module Grape
# The API class is the primary entry point for
# creating Grape APIs.Users should subclass this
# class in order to build an API.
class API
extend Validations::ClassMethods
class << self
attr_reader :endpoints, :instance, :routes, :route_set, :settings, :versions
attr_writer :logger
LOCK = Mutex.new
def logger(logger = nil)
if logger
@logger = logger
else
@logger ||= Logger.new($stdout)
end
end
def reset!
@settings = Grape::Util::HashStack.new
@route_set = Rack::Mount::RouteSet.new
@endpoints = []
@routes = nil
reset_validations!
end
def compile
@instance ||= new
end
def change!
@instance = nil
end
def call(env)
LOCK.synchronize { compile } unless instance
call!(env)
end
def call!(env)
instance.call(env)
end
# Set a configuration value for this namespace.
#
# @param key [Symbol] The key of the configuration variable.
# @param value [Object] The value to which to set the configuration variable.
def set(key, value)
settings[key.to_sym] = value
end
# Add to a configuration value for this
# namespace.
#
# @param key [Symbol] The key of the configuration variable.
# @param value [Object] The value to which to set the configuration variable.
def imbue(key, value)
settings.imbue(key, value)
end
# Define a root URL prefix for your entire API.
def prefix(prefix = nil)
prefix ? set(:root_prefix, prefix) : settings[:root_prefix]
end
# Do not route HEAD requests to GET requests automatically
def do_not_route_head!
set(:do_not_route_head, true)
end
# Do not automatically route OPTIONS
def do_not_route_options!
set(:do_not_route_options, true)
end
# Specify an API version.
#
# @example API with legacy support.
# class MyAPI < Grape::API
# version 'v2'
#
# get '/main' do
# {some: 'data'}
# end
#
# version 'v1' do
# get '/main' do
# {legacy: 'data'}
# end
# end
# end
#
def version(*args, &block)
if args.any?
options = args.pop if args.last.is_a? Hash
options ||= {}
options = { using: :path }.merge(options)
raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
@versions = versions | args
nest(block) do
set(:version, args)
set(:version_options, options)
end
end
@versions.last unless @versions.nil?
end
# Add a description to the next namespace or function.
def desc(description, options = {})
@last_description = options.merge(description: description)
end
# Specify the default format for the API's serializers.
# May be `:json` or `:txt` (default).
def default_format(new_format = nil)
new_format ? set(:default_format, new_format.to_sym) : settings[:default_format]
end
# Specify the format for the API's serializers.
# May be `:json`, `:xml`, `:txt`, etc.
def format(new_format = nil)
if new_format
set(:format, new_format.to_sym)
# define the default error formatters
set(:default_error_formatter, Grape::ErrorFormatter::Base.formatter_for(new_format, {}))
# define a single mime type
mime_type = content_types[new_format.to_sym]
raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type
settings.imbue(:content_types, new_format.to_sym => mime_type)
else
settings[:format]
end
end
# Specify a custom formatter for a content-type.
def formatter(content_type, new_formatter)
settings.imbue(:formatters, content_type.to_sym => new_formatter)
end
# Specify a custom parser for a content-type.
def parser(content_type, new_parser)
settings.imbue(:parsers, content_type.to_sym => new_parser)
end
# Specify a default error formatter.
def default_error_formatter(new_formatter_name = nil)
if new_formatter_name
new_formatter = Grape::ErrorFormatter::Base.formatter_for(new_formatter_name, {})
set(:default_error_formatter, new_formatter)
else
settings[:default_error_formatter]
end
end
def error_formatter(format, options)
if options.is_a?(Hash) && options.key?(:with)
formatter = options[:with]
else
formatter = options
end
settings.imbue(:error_formatters, format.to_sym => formatter)
end
# Specify additional content-types, e.g.:
# content_type :xls, 'application/vnd.ms-excel'
def content_type(key, val)
settings.imbue(:content_types, key.to_sym => val)
end
# All available content types.
def content_types
Grape::ContentTypes.content_types_for(settings[:content_types])
end
# Specify the default status code for errors.
def default_error_status(new_status = nil)
new_status ? set(:default_error_status, new_status) : settings[:default_error_status]
end
# Allows you to rescue certain exceptions that occur to return
# a grape error rather than raising all the way to the
# server level.
#
# @example Rescue from custom exceptions
# class ExampleAPI < Grape::API
# class CustomError < StandardError; end
#
# rescue_from CustomError
# end
#
# @overload rescue_from(*exception_classes, options = {})
# @param [Array] exception_classes A list of classes that you want to rescue, or
# the symbol :all to rescue from all exceptions.
# @param [Block] block Execution block to handle the given exception.
# @param [Hash] options Options for the rescue usage.
# @option options [Boolean] :backtrace Include a backtrace in the rescue response.
# @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
# @param [Proc] handler Execution proc to handle the given exception as an
# alternative to passing a block
def rescue_from(*args, &block)
if args.last.is_a?(Proc)
handler = args.pop
elsif block_given?
handler = block
end
options = args.last.is_a?(Hash) ? args.pop : {}
handler ||= proc { options[:with] } if options.key?(:with)
handler_type = !!options[:rescue_subclasses] ? :rescue_handlers : :base_only_rescue_handlers
imbue handler_type, Hash[args.map { |arg| [arg, handler] }]
imbue(:rescue_options, options)
set(:rescue_all, true) if args.include?(:all)
end
# Allows you to specify a default representation entity for a
# class. This allows you to map your models to their respective
# entities once and then simply call `present` with the model.
#
# @example
# class ExampleAPI < Grape::API
# represent User, with: Entity::User
#
# get '/me' do
# present current_user # with: Entity::User is assumed
# end
# end
#
# Note that Grape will automatically go up the class ancestry to
# try to find a representing entity, so if you, for example, define
# an entity to represent `Object` then all presented objects will
# bubble up and utilize the entity provided on that `represent` call.
#
# @param model_class [Class] The model class that will be represented.
# @option options [Class] :with The entity class that will represent the model.
def represent(model_class, options)
raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with] && options[:with].is_a?(Class)
imbue(:representations, model_class => options[:with])
end
# Add helper methods that will be accessible from any
# endpoint within this namespace (and child namespaces).
#
# When called without a block, all known helpers within this scope
# are included.
#
# @param [Module] new_mod optional module of methods to include
# @param [Block] block optional block of methods to include
#
# @example Define some helpers.
#
# class ExampleAPI < Grape::API
# helpers do
# def current_user
# User.find_by_id(params[:token])
# end
# end
# end
#
def helpers(new_mod = nil, &block)
if block_given? || new_mod
mod = settings.peek[:helpers] || Module.new
if new_mod
inject_api_helpers_to_mod(new_mod) if new_mod.is_a?(Helpers)
mod.class_eval do
include new_mod
end
end
if block_given?
inject_api_helpers_to_mod(mod) do
mod.class_eval(&block)
end
end
set(:helpers, mod)
else
mod = Module.new
settings.stack.each do |s|
mod.send :include, s[:helpers] if s[:helpers]
end
change!
mod
end
end
# Add an authentication type to the API. Currently
# only `:http_basic`, `:http_digest` and `:oauth2` are supported.
def auth(type = nil, options = {}, &block)
if type
set(:auth, { type: type.to_sym, proc: block }.merge(options))
else
settings[:auth]
end
end
# Add HTTP Basic authorization to the API.
#
# @param [Hash] options A hash of options.
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
def http_basic(options = {}, &block)
options[:realm] ||= "API Authorization"
auth :http_basic, options, &block
end
def http_digest(options = {}, &block)
options[:realm] ||= "API Authorization"
options[:opaque] ||= "secret"
auth :http_digest, options, &block
end
def mount(mounts)
mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
mounts.each_pair do |app, path|
if app.respond_to?(:inherit_settings, true)
app_settings = settings.clone
mount_path = Rack::Mount::Utils.normalize_path([settings[:mount_path], path].compact.join("/"))
app_settings.set :mount_path, mount_path
app.inherit_settings(app_settings)
end
endpoints << Grape::Endpoint.new(
settings.clone,
method: :any,
path: path,
app: app
)
end
end
# Defines a route that will be recognized
# by the Grape API.
#
# @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
# @param paths [String] One or more strings representing the URL segment(s) for this route.
#
# @example Defining a basic route.
# class MyAPI < Grape::API
# route(:any, '/hello') do
# {hello: 'world'}
# end
# end
def route(methods, paths = ['/'], route_options = {}, &block)
endpoint_options = {
method: methods,
path: paths,
route_options: (@namespace_description || {}).deep_merge(@last_description || {}).deep_merge(route_options || {})
}
endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)
@last_description = nil
reset_validations!
end
def before(&block)
imbue(:befores, [block])
end
def before_validation(&block)
imbue(:before_validations, [block])
end
def after_validation(&block)
imbue(:after_validations, [block])
end
def after(&block)
imbue(:afters, [block])
end
def get(paths = ['/'], options = {}, &block)
route('GET', paths, options, &block)
end
def post(paths = ['/'], options = {}, &block)
route('POST', paths, options, &block)
end
def put(paths = ['/'], options = {}, &block)
route('PUT', paths, options, &block)
end
def head(paths = ['/'], options = {}, &block)
route('HEAD', paths, options, &block)
end
def delete(paths = ['/'], options = {}, &block)
route('DELETE', paths, options, &block)
end
def options(paths = ['/'], options = {}, &block)
route('OPTIONS', paths, options, &block)
end
def patch(paths = ['/'], options = {}, &block)
route('PATCH', paths, options, &block)
end
def namespace(space = nil, options = {}, &block)
if space || block_given?
previous_namespace_description = @namespace_description
@namespace_description = (@namespace_description || {}).deep_merge(@last_description || {})
@last_description = nil
nest(block) do
set(:namespace, Namespace.new(space, options)) if space
end
@namespace_description = previous_namespace_description
else
Namespace.joined_space_path(settings)
end
end
# Thie method allows you to quickly define a parameter route segment
# in your API.
#
# @param param [Symbol] The name of the parameter you wish to declare.
# @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
def route_param(param, options = {}, &block)
options = options.dup
options[:requirements] = { param.to_sym => options[:requirements] } if options[:requirements].is_a?(Regexp)
namespace(":#{param}", options, &block)
end
alias_method :group, :namespace
alias_method :resource, :namespace
alias_method :resources, :namespace
alias_method :segment, :namespace
# Create a scope without affecting the URL.
#
# @param name [Symbol] Purely placebo, just allows to to name the scope to make the code more readable.
def scope(name = nil, &block)
nest(block)
end
# Apply a custom middleware to the API. Applies
# to the current namespace and any children, but
# not parents.
#
# @param middleware_class [Class] The class of the middleware you'd like
# to inject.
def use(middleware_class, *args, &block)
arr = [middleware_class, *args]
arr << block if block_given?
imbue(:middleware, [arr])
end
# Retrieve an array of the middleware classes
# and arguments that are currently applied to the
# application.
def middleware
settings.stack.inject([]) do |a, s|
a += s[:middleware] if s[:middleware]
a
end
end
# An array of API routes.
def routes
@routes ||= prepare_routes
end
def versions
@versions ||= []
end
def cascade(value = nil)
if value.nil?
settings.key?(:cascade) ? !!settings[:cascade] : true
else
set(:cascade, value)
end
end
protected
def prepare_routes
routes = []
endpoints.each do |endpoint|
routes.concat(endpoint.routes)
end
routes
end
# Execute first the provided block, then each of the
# block passed in. Allows for simple 'before' setups
# of settings stack pushes.
def nest(*blocks, &block)
blocks.reject! { |b| b.nil? }
if blocks.any?
settings.push # create a new context to eval the follow
instance_eval(&block) if block_given?
blocks.each { |b| instance_eval(&b) }
settings.pop # when finished, we pop the context
reset_validations!
else
instance_eval(&block)
end
end
def inherited(subclass)
subclass.reset!
subclass.logger = logger.clone
end
def inherit_settings(other_stack)
settings.prepend other_stack
endpoints.each do |e|
e.settings.prepend(other_stack)
e.options[:app].inherit_settings(other_stack) if e.options[:app].respond_to?(:inherit_settings, true)
end
end
def inject_api_helpers_to_mod(mod, &block)
mod.extend(Helpers)
yield if block_given?
mod.api_changed(self)
end
end
def initialize
@route_set = Rack::Mount::RouteSet.new
add_head_not_allowed_methods_and_options_methods
self.class.endpoints.each do |endpoint|
endpoint.mount_in(@route_set)
end
@route_set.freeze
end
def call(env)
status, headers, body = @route_set.call(env)
headers.delete('X-Cascade') unless cascade?
[status, headers, body]
end
# Some requests may return a HTTP 404 error if grape cannot find a matching
# route. In this case, Rack::Mount adds a X-Cascade header to the response
# and sets it to 'pass', indicating to grape's parents they should keep
# looking for a matching route on other resources.
#
# In some applications (e.g. mounting grape on rails), one might need to trap
# errors from reaching upstream. This is effectivelly done by unsetting
# X-Cascade. Default :cascade is true.
def cascade?
return !!self.class.settings[:cascade] if self.class.settings.key?(:cascade)
return !!self.class.settings[:version_options][:cascade] if self.class.settings[:version_options] && self.class.settings[:version_options].key?(:cascade)
true
end
reset!
private
# For every resource add a 'OPTIONS' route that returns an HTTP 204 response
# with a list of HTTP methods that can be called. Also add a route that
# will return an HTTP 405 response for any HTTP method that the resource
# cannot handle.
def add_head_not_allowed_methods_and_options_methods
methods_per_path = {}
self.class.endpoints.each do |endpoint|
routes = endpoint.routes
routes.each do |route|
methods_per_path[route.route_path] ||= []
methods_per_path[route.route_path] << route.route_method
end
end
# The paths we collected are prepared (cf. Path#prepare), so they
# contain already versioning information when using path versioning.
# Disable versioning so adding a route won't prepend versioning
# informations again.
without_versioning do
methods_per_path.each do |path, methods|
allowed_methods = methods.dup
unless self.class.settings[:do_not_route_head]
allowed_methods |= ['HEAD'] if allowed_methods.include?('GET')
end
allow_header = (['OPTIONS'] | allowed_methods).join(', ')
unless self.class.settings[:do_not_route_options]
unless allowed_methods.include?('OPTIONS')
self.class.options(path, {}) do
header 'Allow', allow_header
status 204
''
end
end
end
not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - allowed_methods
not_allowed_methods << 'OPTIONS' if self.class.settings[:do_not_route_options]
self.class.route(not_allowed_methods, path) do
header 'Allow', allow_header
status 405
''
end
end
end
end
def without_versioning(&block)
self.class.settings.push(version: nil, version_options: nil)
yield
self.class.settings.pop
end
# This module extends user defined helpers
# to provide some API-specific functionality
module Helpers
attr_accessor :api
def params(name, &block)
@named_params ||= {}
@named_params.merge! name => block
end
def api_changed(new_api)
@api = new_api
process_named_params
end
protected
def process_named_params
if @named_params && @named_params.any?
api.imbue(:named_params, @named_params)
end
end
end
end
end