Skip to content

Commit 83a0487

Browse files
authored
Merge branch 'ruby-grape:master' into fix_danger_workflow
2 parents 8c59d3e + baab208 commit 83a0487

25 files changed

+424
-369
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@
33
#### Features
44

55
* [#2629](https://github.com/ruby-grape/grape/pull/2629): Refactor Router Architecture - [@ericproulx](https://github.com/ericproulx).
6+
* [#2633](https://github.com/ruby-grape/grape/pull/2633): Refactor API::Instance and reorganize DSL modules - [@ericproulx](https://github.com/ericproulx).
7+
* [#2636](https://github.com/ruby-grape/grape/pull/2636): Refactor router to simplify method signatures and reduce duplication - [@ericproulx](https://github.com/ericproulx).
8+
* [#2640](https://github.com/ruby-grape/grape/pull/2640): Compute available_media_types once - [@ericproulx](https://github.com/ericproulx).
9+
* [#2637](https://github.com/ruby-grape/grape/pull/2637): Refactor declared method - [@ericproulx](https://github.com/ericproulx).
10+
* [#2639](https://github.com/ruby-grape/grape/pull/2639): Refactor mime_types_for - [@ericproulx](https://github.com/ericproulx).
11+
* [#2638](https://github.com/ruby-grape/grape/pull/2638): Remove unnecessary path string duplication - [@ericproulx](https://github.com/ericproulx).
612
* Your contribution here.
713

814
#### Fixes
915

16+
* [#2633](https://github.com/ruby-grape/grape/pull/2633): Fix cascade reading - [@ericproulx](https://github.com/ericproulx).
17+
* [#2642](https://github.com/ruby-grape/grape/pull/2642): Fix array allocation in base_route.rb - [@ericproulx](https://github.com/ericproulx).
1018
* Your contribution here.
1119

1220
### 3.0.1 (2025-11-24)

lib/grape/api.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Grape
55
# should subclass this class in order to build an API.
66
class API
77
# Class methods that we want to call on the API rather than on the API object
8-
NON_OVERRIDABLE = %i[call call! configuration compile! inherited recognize_path routes].freeze
8+
NON_OVERRIDABLE = %i[base= base_instance? call change! configuration compile! inherit_settings recognize_path reset! routes top_level_setting= top_level_setting].freeze
99

1010
Helpers = Grape::DSL::Helpers::BaseHelper
1111

@@ -29,7 +29,7 @@ class << self
2929
# the headers, and the body. See [the rack specification]
3030
# (https://github.com/rack/rack/blob/main/SPEC.rdoc) for more.
3131
# NOTE: This will only be called on an API directly mounted on RACK
32-
def_delegators :base_instance, :new, :configuration, :call, :compile!
32+
def_delegators :base_instance, :new, :configuration, :call, :change!, :compile!, :recognize_path, :routes
3333

3434
# Initialize the instance variables on the remountable class, and the base_instance
3535
# an instance that will be used to create the set up but will not be mounted

lib/grape/api/instance.rb

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,17 @@ class Instance
2121
class << self
2222
extend Forwardable
2323

24-
attr_reader :instance, :base
2524
attr_accessor :configuration
2625

27-
def_delegators :base, :to_s
28-
29-
def given(conditional_option, &block)
30-
return unless conditional_option
31-
32-
mounted(&block)
33-
end
34-
35-
def mounted(&block)
36-
evaluate_as_instance_with_configuration(block, lazy: true)
37-
end
26+
def_delegators :@base, :to_s
3827

3928
def base=(grape_api)
4029
@base = grape_api
4130
grape_api.instances << self
4231
end
4332

4433
def base_instance?
45-
self == base.base_instance
34+
self == @base.base_instance
4635
end
4736

4837
# A class-level lock to ensure the API is not compiled by multiple
@@ -56,43 +45,30 @@ def reset!
5645
reset_validations!
5746
end
5847

59-
# Parses the API's definition and compiles it into an instance of
60-
# Grape::API.
61-
def compile
62-
@instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
63-
end
64-
6548
# This is the interface point between Rack and Grape; it accepts a request
6649
# from Rack and ultimately returns an array of three values: the status,
6750
# the headers, and the body. See [the rack specification]
6851
# (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
6952
def call(env)
7053
compile!
71-
call!(env)
72-
end
73-
74-
# A non-synchronized version of ::call.
75-
def call!(env)
76-
instance.call(env)
77-
end
78-
79-
# (see #cascade?)
80-
def cascade(value = nil)
81-
return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable(:cascade).nil? : true if value.nil?
82-
83-
inheritable_setting.namespace_inheritable[:cascade] = value
54+
@instance.call(env)
8455
end
8556

8657
def compile!
87-
return if instance
58+
return if @instance
8859

89-
LOCK.synchronize { compile unless instance }
60+
LOCK.synchronize { @instance ||= new }
9061
end
9162

9263
# see Grape::Router#recognize_path
9364
def recognize_path(path)
9465
compile!
95-
instance.router.recognize_path(path)
66+
@instance.router.recognize_path(path)
67+
end
68+
69+
# Wipe the compiled API so we can recompile after changes were made.
70+
def change!
71+
@instance = nil
9672
end
9773

9874
protected
@@ -110,11 +86,6 @@ def inherit_settings(other_settings)
11086
reset_routes!
11187
end
11288

113-
# Wipe the compiled API so we can recompile after changes were made.
114-
def change!
115-
@instance = nil
116-
end
117-
11889
private
11990

12091
def inherited(subclass)

lib/grape/content_types.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ def content_types_for(from_settings)
2222
def mime_types_for(from_settings)
2323
return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS
2424

25-
from_settings.each_with_object({}) do |(k, v), types_without_params|
26-
# remove optional parameter
27-
types_without_params[v.split(';', 2).first] = k
28-
end
25+
from_settings.invert.transform_keys! { |k| k.include?(';') ? k.split(';', 2).first : k }
2926
end
3027
end
3128
end

lib/grape/dsl/declared.rb

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module DSL
5+
module Declared
6+
# Denotes a situation where a DSL method has been invoked in a
7+
# filter which it should not yet be available in
8+
class MethodNotYetAvailable < StandardError
9+
def initialize(msg = '#declared is not available prior to parameter validation')
10+
super
11+
end
12+
end
13+
14+
# A filtering method that will return a hash
15+
# consisting only of keys that have been declared by a
16+
# `params` statement against the current/target endpoint or parent
17+
# namespaces.
18+
# @param params [Hash] The initial hash to filter. Usually this will just be `params`
19+
# @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
20+
# options. `:include_parent_namespaces` defaults to true, hence must be set to false if
21+
# you want only to return params declared against the current/target endpoint.
22+
def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
23+
raise MethodNotYetAvailable unless before_filter_passed
24+
25+
options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
26+
declared_params ||= optioned_declared_params(options[:include_parent_namespaces])
27+
28+
res = if passed_params.is_a?(Array)
29+
declared_array(passed_params, options, declared_params, params_nested_path)
30+
else
31+
declared_hash(passed_params, options, declared_params, params_nested_path)
32+
end
33+
34+
if (key_maps = inheritable_setting.namespace_stackable[:contract_key_map])
35+
key_maps.each { |key_map| key_map.write(passed_params, res) }
36+
end
37+
38+
res
39+
end
40+
41+
private
42+
43+
def declared_array(passed_params, options, declared_params, params_nested_path)
44+
passed_params.map do |passed_param|
45+
declared(passed_param || {}, options, declared_params, params_nested_path)
46+
end
47+
end
48+
49+
def declared_hash(passed_params, options, declared_params, params_nested_path)
50+
declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
51+
next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
52+
53+
declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo)
54+
end
55+
end
56+
57+
def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo)
58+
renamed_params = inheritable_setting.route[:renamed_params] || {}
59+
if declared_param.is_a?(Hash)
60+
declared_param.each_pair do |declared_parent_param, declared_children_params|
61+
params_nested_path_dup = params_nested_path.dup
62+
params_nested_path_dup << declared_parent_param.to_s
63+
next unless options[:include_missing] || passed_params.key?(declared_parent_param)
64+
65+
rename_path = params_nested_path + [declared_parent_param.to_s]
66+
renamed_param_name = renamed_params[rename_path]
67+
68+
memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
69+
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
70+
71+
memo[memo_key] = handle_passed_param(params_nested_path_dup, has_passed_children: passed_children_params.any?) do
72+
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
73+
end
74+
end
75+
else
76+
# If it is not a Hash then it does not have children.
77+
# Find its value or set it to nil.
78+
return unless options[:include_missing] || passed_params.try(:key?, declared_param)
79+
80+
rename_path = params_nested_path + [declared_param.to_s]
81+
renamed_param_name = renamed_params[rename_path]
82+
83+
memo_key = optioned_param_key(renamed_param_name || declared_param, options)
84+
passed_param = passed_params[declared_param]
85+
86+
params_nested_path_dup = params_nested_path.dup
87+
params_nested_path_dup << declared_param.to_s
88+
memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
89+
passed_param
90+
end
91+
end
92+
end
93+
94+
def handle_passed_param(params_nested_path, has_passed_children: false, &_block)
95+
return yield if has_passed_children
96+
97+
key = params_nested_path[0]
98+
key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
99+
100+
route_options_params = options[:route_options][:params] || {}
101+
type = route_options_params.dig(key, :type)
102+
has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103+
104+
if type == 'Hash' && !has_children
105+
{}
106+
elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
107+
[]
108+
elsif type == 'Set' || type&.start_with?('#<Set')
109+
Set.new
110+
else
111+
yield
112+
end
113+
end
114+
115+
def optioned_param_key(declared_param, options)
116+
options[:stringify] ? declared_param.to_s : declared_param.to_sym
117+
end
118+
119+
def optioned_declared_params(include_parent_namespaces)
120+
declared_params = if include_parent_namespaces
121+
# Declared params including parent namespaces
122+
inheritable_setting.route[:declared_params]
123+
else
124+
# Declared params at current namespace
125+
inheritable_setting.namespace_stackable[:declared_params].last || []
126+
end
127+
128+
raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params
129+
130+
declared_params
131+
end
132+
end
133+
end
134+
end

0 commit comments

Comments
 (0)