Skip to content

Commit 9821470

Browse files
committed
1 parent d4244ab commit 9821470

File tree

10 files changed

+194
-126
lines changed

10 files changed

+194
-126
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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).
67
* Your contribution here.
78

89
#### Fixes

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/dsl/routing.rb

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ module DSL
55
module Routing
66
attr_reader :endpoints
77

8+
def given(conditional_option, &block)
9+
return unless conditional_option
10+
11+
mounted(&block)
12+
end
13+
14+
def mounted(&block)
15+
evaluate_as_instance_with_configuration(block, lazy: true)
16+
end
17+
18+
def cascade(value = nil)
19+
return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable(:cascade).nil? : true if value.nil?
20+
21+
inheritable_setting.namespace_inheritable[:cascade] = value
22+
end
23+
824
# Specify an API version.
925
#
1026
# @example API with legacy support.
@@ -202,16 +218,6 @@ def routes
202218
@routes ||= endpoints.map(&:routes).flatten
203219
end
204220

205-
# Remove all defined routes.
206-
def reset_routes!
207-
endpoints.each(&:reset_routes!)
208-
@routes = nil
209-
end
210-
211-
def reset_endpoints!
212-
@endpoints = []
213-
end
214-
215221
# This method allows you to quickly define a parameter route segment
216222
# in your API.
217223
#
@@ -238,6 +244,16 @@ def versions
238244

239245
private
240246

247+
# Remove all defined routes.
248+
def reset_routes!
249+
endpoints.each(&:reset_routes!)
250+
@routes = nil
251+
end
252+
253+
def reset_endpoints!
254+
@endpoints = []
255+
end
256+
241257
def refresh_mounted_api(mounts, *opts)
242258
opts << { refresh_already_mounted: true }
243259
mount(mounts, *opts)
@@ -265,7 +281,7 @@ def evaluate_as_instance_with_configuration(block, lazy: false)
265281
self.configuration = value_for_configuration
266282
response
267283
end
268-
if base && base_instance? && lazy
284+
if @base && base_instance? && lazy
269285
lazy_block
270286
else
271287
lazy_block.evaluate_from(configuration)

lib/grape/dsl/settings.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module DSL
77
# matter where they're defined, and inheritable settings which apply only
88
# in the current scope and scopes nested under it.
99
module Settings
10-
attr_writer :inheritable_setting, :top_level_setting
10+
attr_writer :inheritable_setting
1111

1212
# Fetch our top-level settings, which apply to all endpoints in the API.
1313
def top_level_setting

lib/grape/dsl/validations.rb

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
module Grape
44
module DSL
55
module Validations
6+
# Opens a root-level ParamsScope, defining parameter coercions and
7+
# validations for the endpoint.
8+
# @yield instance context of the new scope
9+
def params(&block)
10+
Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
11+
end
12+
13+
# Declare the contract to be used for the endpoint's parameters.
14+
# @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
15+
# The contract or schema to be used for validation. Optional.
16+
# @yield a block yielding a new instance of Dry::Schema::Params
17+
# subclass, allowing to define the schema inline. When the
18+
# +contract+ parameter is a schema, it will be used as a parent. Optional.
19+
def contract(contract = nil, &block)
20+
raise ArgumentError, 'Either contract or block must be provided' unless contract || block
21+
raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
22+
23+
Grape::Validations::ContractScope.new(self, contract, &block)
24+
end
25+
26+
private
27+
628
# Clears all defined parameters and validations. The main purpose of it is to clean up
729
# settings, so next endpoint won't interfere with previous one.
830
#
@@ -24,26 +46,6 @@ module Validations
2446
def reset_validations!
2547
inheritable_setting.namespace_stackable.delete(:declared_params, :params, :validations)
2648
end
27-
28-
# Opens a root-level ParamsScope, defining parameter coercions and
29-
# validations for the endpoint.
30-
# @yield instance context of the new scope
31-
def params(&block)
32-
Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
33-
end
34-
35-
# Declare the contract to be used for the endpoint's parameters.
36-
# @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
37-
# The contract or schema to be used for validation. Optional.
38-
# @yield a block yielding a new instance of Dry::Schema::Params
39-
# subclass, allowing to define the schema inline. When the
40-
# +contract+ parameter is a schema, it will be used as a parent. Optional.
41-
def contract(contract = nil, &block)
42-
raise ArgumentError, 'Either contract or block must be provided' unless contract || block
43-
raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
44-
45-
Grape::Validations::ContractScope.new(self, contract, &block)
46-
end
4749
end
4850
end
4951
end

spec/grape/api_spec.rb

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3938,19 +3938,12 @@ def my_method
39383938
end
39393939
end
39403940

3941-
describe '.compile' do
3942-
it 'sets the instance' do
3943-
expect(subject.instance).to be_nil
3944-
subject.compile
3945-
expect(subject.instance).to be_a(subject.base_instance)
3946-
end
3947-
end
3948-
39493941
describe '.change!' do
39503942
it 'invalidates any compiled instance' do
3951-
subject.compile
3943+
expect(Grape::API::Instance.singleton_class::LOCK).to receive(:synchronize).and_call_original.twice
3944+
subject.compile!
39523945
subject.change!
3953-
expect(subject.instance).to be_nil
3946+
subject.compile!
39543947
end
39553948
end
39563949

spec/grape/dsl/routing_spec.rb

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,53 @@ class << self
7272
describe '.mount' do
7373
it 'mounts on a nested path' do
7474
subject = Class.new(Grape::API)
75-
app1 = Class.new(Grape::API)
76-
app2 = Class.new(Grape::API)
77-
app2.get '/nice' do
78-
'play'
75+
app1 = Class.new(Grape::API) do
76+
get '/' do
77+
return_no_content
78+
end
79+
end
80+
app2 = Class.new(Grape::API) do
81+
get '/' do
82+
return_no_content
83+
end
7984
end
80-
8185
subject.mount app1 => '/app1'
8286
app1.mount app2 => '/app2'
8387

84-
expect(subject.inheritable_setting.to_hash[:namespace]).to eq({})
85-
expect(subject.inheritable_setting.to_hash[:namespace_inheritable]).to eq({})
86-
expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1'])
88+
env = Rack::MockRequest.env_for('/app1', method: Rack::GET)
89+
response = Rack::MockResponse[*subject.call(env)]
8790

88-
expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1', '/app2'])
91+
expect(response).to be_no_content
92+
93+
env = Rack::MockRequest.env_for('/app1/app2', method: Rack::GET)
94+
response = Rack::MockResponse[*subject.call(env)]
95+
96+
expect(response).to be_no_content
8997
end
9098

9199
it 'mounts multiple routes at once' do
92100
base_app = Class.new(Grape::API)
93-
app1 = Class.new(Grape::API)
94-
app2 = Class.new(Grape::API)
101+
app1 = Class.new(Grape::API) do
102+
get '/' do
103+
return_no_content
104+
end
105+
end
106+
app2 = Class.new(Grape::API) do
107+
get '/' do
108+
return_no_content
109+
end
110+
end
95111
base_app.mount(app1 => '/app1', app2 => '/app2')
96112

97-
expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1'])
98-
expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app2'])
113+
env = Rack::MockRequest.env_for('/app1', method: Rack::GET)
114+
response = Rack::MockResponse[*base_app.call(env)]
115+
116+
expect(response).to be_no_content
117+
118+
env = Rack::MockRequest.env_for('/app2', method: Rack::GET)
119+
response = Rack::MockResponse[*base_app.call(env)]
120+
121+
expect(response).to be_no_content
99122
end
100123
end
101124

spec/grape/dsl/validations_spec.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,6 @@
1010
end
1111
end
1212

13-
describe '.reset_validations' do
14-
subject { dummy_class.reset_validations! }
15-
16-
before do
17-
%i[declared_params params validations other].each do |key|
18-
dummy_class.inheritable_setting.namespace_stackable[key] = key
19-
end
20-
end
21-
22-
it 'calls unset_namespace_stackable properly' do
23-
subject
24-
expect(dummy_class.inheritable_setting.namespace_stackable.to_hash).to eq(other: [:other])
25-
end
26-
end
27-
2813
describe '.params' do
2914
subject { dummy_class.params { :my_block } }
3015

0 commit comments

Comments
 (0)