• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ruby-grape / grape / 13501493749

24 Feb 2025 03:21PM UTC coverage: 98.389% (+0.02%) from 98.367%
13501493749

Pull #2538

github

web-flow
Merge ab35c7e0b into 6e6958f35
Pull Request #2538: Handle json array

7 of 7 new or added lines in 2 files covered. (100.0%)

4 existing lines in 3 files now uncovered.

3542 of 3600 relevant lines covered (98.39%)

76444.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.5
/lib/grape/endpoint.rb
1
# frozen_string_literal: true
2

3
module Grape
60✔
4
  # An Endpoint is the proxy scope in which all routing
5
  # blocks are executed. In other words, any methods
6
  # on the instance level of this class may be called
7
  # from inside a `get`, `post`, etc.
8
  class Endpoint
60✔
9
    extend Forwardable
60✔
10
    include Grape::DSL::Settings
60✔
11
    include Grape::DSL::InsideRoute
60✔
12

13
    attr_accessor :block, :source, :options
60✔
14
    attr_reader :env, :request
60✔
15

16
    def_delegators :request, :params, :headers
60✔
17

18
    class << self
60✔
19
      def new(...)
60✔
20
        self == Endpoint ? Class.new(Endpoint).new(...) : super
393,302✔
21
      end
22

23
      def before_each(new_setup = false, &block)
60✔
24
        @before_each ||= []
159,123✔
25
        if new_setup == false
159,123✔
26
          return @before_each unless block
158,976✔
27

28
          @before_each << block
196✔
29
        else
30
          @before_each = [new_setup]
147✔
31
        end
32
      end
33

34
      def run_before_each(endpoint)
60✔
35
        superclass.run_before_each(endpoint) unless self == Endpoint
158,486✔
36
        before_each.each { |blk| blk.try(:call, endpoint) }
158,731✔
37
      end
38

39
      # @api private
40
      #
41
      # Create an UnboundMethod that is appropriate for executing an endpoint
42
      # route.
43
      #
44
      # The unbound method allows explicit calls to +return+ without raising a
45
      # +LocalJumpError+. The method will be removed, but a +Proc+ reference to
46
      # it will be returned. The returned +Proc+ expects a single argument: the
47
      # instance of +Endpoint+ to bind to the method during the call.
48
      #
49
      # @param [String, Symbol] method_name
50
      # @return [Proc]
51
      # @raise [NameError] an instance method with the same name already exists
52
      def generate_api_method(method_name, &block)
60✔
53
        raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") if method_defined?(method_name)
150,471✔
54

55
        define_method(method_name, &block)
150,422✔
56
        method = instance_method(method_name)
150,373✔
57
        remove_method(method_name)
150,373✔
58

59
        proc do |endpoint_instance|
150,373✔
60
          ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
54,632✔
61
            method.bind_call(endpoint_instance)
54,632✔
62
          end
63
        end
64
      end
65
    end
66

67
    # Create a new endpoint.
68
    # @param new_settings [InheritableSetting] settings to determine the params,
69
    #   validations, and other properties from.
70
    # @param options [Hash] attributes of this endpoint
71
    # @option options path [String or Array] the path to this endpoint, within
72
    #   the current scope.
73
    # @option options method [String or Array] which HTTP method(s) can be used
74
    #   to reach this endpoint.
75
    # @option options route_options [Hash]
76
    # @note This happens at the time of API definition, so in this context the
77
    # endpoint does not know if it will be mounted under a different endpoint.
78
    # @yield a block defining what your API should do when this endpoint is hit
79
    def initialize(new_settings, options = {}, &block)
60✔
80
      require_option(options, :path)
196,651✔
81
      require_option(options, :method)
196,651✔
82

83
      self.inheritable_setting = new_settings.point_in_time_copy
196,651✔
84

85
      # now +namespace_stackable(:declared_params)+ contains all params defined for
86
      # this endpoint and its parents, but later it will be cleaned up,
87
      # see +reset_validations!+ in lib/grape/dsl/validations.rb
88
      route_setting(:declared_params, namespace_stackable(:declared_params).flatten)
196,651✔
89
      route_setting(:saved_validations, namespace_stackable(:validations))
196,651✔
90

91
      namespace_stackable(:representations, []) unless namespace_stackable(:representations)
196,651✔
92
      namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status)
196,651✔
93

94
      @options = options
196,651✔
95

96
      @options[:path] = Array(options[:path])
196,651✔
97
      @options[:path] << '/' if options[:path].empty?
196,651✔
98

99
      @options[:method] = Array(options[:method])
196,651✔
100
      @options[:route_options] ||= {}
196,651✔
101

102
      @lazy_initialize_lock = Mutex.new
196,651✔
103
      @lazy_initialized = nil
196,651✔
104
      @block = nil
196,651✔
105

106
      @status = nil
196,651✔
107
      @stream = nil
196,651✔
108
      @body = nil
196,651✔
109
      @proc = nil
196,651✔
110

111
      return unless block
196,651✔
112

113
      @source = block
150,324✔
114
      @block = self.class.generate_api_method(method_name, &block)
150,324✔
115
    end
116

117
    # Update our settings from a given set of stackable parameters. Used when
118
    # the endpoint's API is mounted under another one.
119
    def inherit_settings(namespace_stackable)
60✔
120
      parent_validations = namespace_stackable[:validations]
12,054✔
121
      inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any?
12,054✔
122
      parent_declared_params = namespace_stackable[:declared_params]
12,054✔
123
      inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any?
12,054✔
124

125
      endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
12,642✔
126
    end
127

128
    def require_option(options, key)
60✔
129
      raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
393,302✔
130
    end
131

132
    def method_name
60✔
133
      [options[:method],
150,324✔
134
       Namespace.joined_space(namespace_stackable(:namespace)),
135
       (namespace_stackable(:mount_path) || []).join('/'),
150,324✔
136
       options[:path].join('/')]
137
        .join(' ')
138
    end
139

140
    def routes
60✔
141
      @routes ||= endpoints&.collect(&:routes)&.flatten || to_routes
354,374✔
142
    end
143

144
    def reset_routes!
60✔
145
      endpoints&.each(&:reset_routes!)
196,101✔
146
      @namespace = nil
196,101✔
147
      @routes = nil
196,101✔
148
    end
149

150
    def mount_in(router)
60✔
151
      return endpoints.each { |e| e.mount_in(router) } if endpoints
187,967✔
152

153
      reset_routes!
171,993✔
154
      routes.each do |route|
171,993✔
155
        router.append(route.apply(self))
172,238✔
156
        next unless !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET
172,238✔
157

158
        route.dup.then do |head_route|
151,304✔
159
          head_route.convert_to_head_request!
151,304✔
160
          router.append(head_route.apply(self))
151,304✔
161
        end
162
      end
163
    end
164

165
    def to_routes
60✔
166
      default_route_options = prepare_default_route_attributes
345,995✔
167
      default_path_settings = prepare_default_path_settings
345,995✔
168

169
      map_routes do |method, raw_path|
345,995✔
170
        prepared_path = Path.new(raw_path, namespace, default_path_settings)
346,485✔
171
        params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
346,485✔
172
        route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
346,485✔
173
        route.apply(self)
346,485✔
174
      end.flatten
175
    end
176

177
    def prepare_routes_requirements
60✔
178
      {}.merge!(*namespace_stackable(:namespace).map(&:requirements)).tap do |requirements|
345,995✔
179
        endpoint_requirements = options.dig(:route_options, :requirements)
345,995✔
180
        requirements.merge!(endpoint_requirements) if endpoint_requirements
345,995✔
181
      end
182
    end
183

184
    def prepare_default_route_attributes
60✔
185
      {
186
        namespace: namespace,
345,995✔
187
        version: prepare_version,
188
        requirements: prepare_routes_requirements,
189
        prefix: namespace_inheritable(:root_prefix),
190
        anchor: options[:route_options].fetch(:anchor, true),
191
        settings: inheritable_setting.route.except(:declared_params, :saved_validations),
192
        forward_match: options[:forward_match]
193
      }
194
    end
195

196
    def prepare_version
60✔
197
      version = namespace_inheritable(:version)
345,995✔
198
      return if version.blank?
345,995✔
199

200
      version.length == 1 ? version.first : version
17,691✔
201
    end
202

203
    def map_routes
60✔
204
      options[:method].map { |method| options[:path].map { |path| yield method, path } }
1,038,867✔
205
    end
206

207
    def prepare_default_path_settings
60✔
208
      namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
345,995✔
209
      namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
345,995✔
210
      namespace_stackable_hash.merge!(namespace_inheritable_hash)
345,995✔
211
    end
212

213
    def namespace
60✔
214
      @namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
692,480✔
215
    end
216

217
    def call(env)
60✔
218
      lazy_initialize!
83,016✔
219
      dup.call!(env)
83,016✔
220
    end
221

222
    def call!(env)
60✔
223
      env[Grape::Env::API_ENDPOINT] = self
83,016✔
224
      @env = env
83,016✔
225
      @app.call(env)
83,016✔
226
    end
227

228
    # Return the collection of endpoints within this endpoint.
229
    # This is the case when an Grape::API mounts another Grape::API.
230
    def endpoints
60✔
231
      @endpoints ||= options[:app].try(:endpoints)
749,516✔
232
    end
233

234
    def equals?(endpoint)
60✔
235
      (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash)
1,618,177✔
236
    end
237

238
    # The purpose of this override is solely for stripping internals when an error occurs while calling
239
    # an endpoint through an api. See https://github.com/ruby-grape/grape/issues/2398
240
    # Otherwise, it calls super.
241
    def inspect
60✔
242
      return super unless env
80✔
243

244
      "#{self.class} in '#{route.origin}' endpoint"
31✔
245
    end
246

247
    protected
60✔
248

249
    def run
60✔
250
      ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
79,243✔
251
        @header = Grape::Util::Header.new
79,243✔
252
        @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
79,243✔
253
        begin
254
          cookies.read(@request)
79,243✔
255
          self.class.run_before_each(self)
79,243✔
256
          run_filters befores, :before
79,243✔
257

258
          if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
78,998✔
259
            allow_header_value = allowed_methods.join(', ')
4,313✔
260
            raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allow_header_value)) unless options?
4,313✔
261

262
            header Grape::Http::Headers::ALLOW, allow_header_value
1,618✔
263
            response_object = ''
1,618✔
264
            status 204
1,618✔
265
          else
266
            run_filters before_validations, :before_validation
74,685✔
267
            run_validators validations, request
74,685✔
268
            run_filters after_validations, :after_validation
57,235✔
269
            response_object = execute
57,186✔
270
          end
271

272
          run_filters afters, :after
53,264✔
273
          cookies.write(header)
53,166✔
274

275
          # status verifies body presence when DELETE
276
          @body ||= response_object
53,166✔
277

278
          # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
279
          response_object = stream || [body]
53,166✔
280

281
          [status, header, response_object]
53,166✔
282
        ensure
283
          run_filters finallies, :finally
79,243✔
284
        end
285
      end
286
    end
287

288
    def execute
60✔
289
      @block&.call(self)
57,186✔
290
    end
291

292
    def helpers
60✔
UNCOV
293
      lazy_initialize! && @helpers
×
294
    end
295

296
    def lazy_initialize!
60✔
297
      return true if @lazy_initialized
83,016✔
298

299
      @lazy_initialize_lock.synchronize do
65,662✔
300
        return true if @lazy_initialized
65,662✔
301

302
        @helpers = build_helpers&.tap { |mod| self.class.include mod }
67,622✔
303
        @app = options[:app] || build_stack(@helpers)
65,662✔
304

305
        @lazy_initialized = true
65,662✔
306
      end
307
    end
308

309
    def run_validators(validators, request)
60✔
310
      validation_errors = []
74,685✔
311

312
      ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
74,685✔
313
        validators.each do |validator|
74,685✔
314
          validator.validate(request)
173,154✔
315
        rescue Grape::Exceptions::Validation => e
316
          validation_errors << e
98✔
317
          break if validator.fail_fast?
98✔
318
        rescue Grape::Exceptions::ValidationArrayErrors => e
319
          validation_errors.concat e.errors
21,566✔
320
          break if validator.fail_fast?
21,566✔
321
        end
322
      end
323

324
      validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header))
74,293✔
325
    end
326

327
    def run_filters(filters, type = :other)
60✔
328
      ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
343,670✔
329
        filters&.each { |filter| instance_eval(&filter) }
349,851✔
330
      end
331
      post_extension = DSL::InsideRoute.post_filter_methods(type)
343,278✔
332
      extend post_extension if post_extension
343,278✔
333
    end
334

335
    def befores
60✔
336
      namespace_stackable(:befores)
79,243✔
337
    end
338

339
    def before_validations
60✔
340
      namespace_stackable(:before_validations)
74,685✔
341
    end
342

343
    def after_validations
60✔
344
      namespace_stackable(:after_validations)
57,235✔
345
    end
346

347
    def afters
60✔
348
      namespace_stackable(:afters)
53,264✔
349
    end
350

351
    def finallies
60✔
352
      namespace_stackable(:finallies)
79,243✔
353
    end
354

355
    def validations
60✔
356
      return enum_for(:validations) unless block_given?
149,468✔
357

358
      route_setting(:saved_validations)&.each do |saved_validation|
74,783✔
359
        yield Grape::Validations::ValidatorFactory.create_validator(saved_validation)
173,497✔
360
      end
361
    end
362

363
    def options?
60✔
364
      options[:options_route_enabled] &&
4,313✔
365
        env[Rack::REQUEST_METHOD] == Rack::OPTIONS
366
    end
367

368
    private
60✔
369

370
    def build_stack(helpers)
60✔
371
      stack = Grape::Middleware::Stack.new
65,417✔
372

373
      content_types = namespace_stackable_with_hash(:content_types)
65,417✔
374
      format = namespace_inheritable(:format)
65,417✔
375

376
      stack.use Rack::Head
65,417✔
377
      stack.use Class.new(Grape::Middleware::Error),
65,417✔
378
                helpers: helpers,
379
                format: format,
380
                content_types: content_types,
381
                default_status: namespace_inheritable(:default_error_status),
382
                rescue_all: namespace_inheritable(:rescue_all),
383
                rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions),
384
                default_error_formatter: namespace_inheritable(:default_error_formatter),
385
                error_formatters: namespace_stackable_with_hash(:error_formatters),
386
                rescue_options: namespace_stackable_with_hash(:rescue_options),
387
                rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers),
388
                base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers),
389
                all_rescue_handler: namespace_inheritable(:all_rescue_handler),
390
                grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler)
391

392
      stack.concat namespace_stackable(:middleware)
65,417✔
393

394
      if namespace_inheritable(:version).present?
65,417✔
395
        stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
7,007✔
396
                  versions: namespace_inheritable(:version).flatten,
397
                  version_options: namespace_inheritable(:version_options),
398
                  prefix: namespace_inheritable(:root_prefix),
399
                  mount_path: namespace_stackable(:mount_path).first
400
      end
401

402
      stack.use Grape::Middleware::Formatter,
65,417✔
403
                format: format,
404
                default_format: namespace_inheritable(:default_format) || :txt,
405
                content_types: content_types,
406
                formatters: namespace_stackable_with_hash(:formatters),
407
                parsers: namespace_stackable_with_hash(:parsers)
408

409
      builder = stack.build
65,417✔
410
      builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
144,660✔
411
      builder.to_app
65,417✔
412
    end
413

414
    def build_helpers
60✔
415
      helpers = namespace_stackable(:helpers)
65,662✔
416
      return if helpers.empty?
65,662✔
417

418
      Module.new { helpers.each { |mod_to_include| include mod_to_include } }
6,419✔
419
    end
420
  end
421
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc