Skip to content

Commit cf14fc7

Browse files
committed
Replaces APIs with RemountableAPI
Fixes all remaining issues
1 parent 37cce04 commit cf14fc7

File tree

8 files changed

+295
-295
lines changed

8 files changed

+295
-295
lines changed

.rubocop_todo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Naming/HeredocDelimiterNaming:
8787
# Configuration parameters: AutoCorrect.
8888
Performance/HashEachMethods:
8989
Exclude:
90-
- 'lib/grape/api.rb'
90+
- 'lib/grape/api_instance.rb'
9191
- 'lib/grape/middleware/versioner/header.rb'
9292

9393
# Offense count: 1

lib/grape.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ module Grape
2828
extend ::ActiveSupport::Autoload
2929

3030
eager_autoload do
31+
autoload :APIInstance
3132
autoload :API
32-
autoload :RemountableAPI
3333
autoload :Endpoint
3434

3535
autoload :Namespace

lib/grape/api.rb

Lines changed: 54 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,233 +1,84 @@
11
require 'grape/router'
22

33
module Grape
4-
# The API class is the primary entry point for creating Grape APIs. Users
4+
# The RemountableAPI class can replace most API classes, except for the base one that is to be mounted in rack.
55
# should subclass this class in order to build an API.
66
class API
7-
include Grape::DSL::API
7+
# Class methods that we want to call on the RemountableAPI rather than on the API object
8+
NON_OVERRIDABLE = %I[define_singleton_method instance_variable_set inspect class is_a? ! kind_of? respond_to? byebug].freeze
89

910
class << self
10-
attr_reader :instance
11-
12-
# A class-level lock to ensure the API is not compiled by multiple
13-
# threads simultaneously within the same process.
14-
LOCK = Mutex.new
15-
16-
# Clears all defined routes, endpoints, etc., on this API.
17-
def reset!
18-
reset_endpoints!
19-
reset_routes!
20-
reset_validations!
21-
end
22-
23-
# Parses the API's definition and compiles it into an instance of
24-
# Grape::API.
25-
def compile
26-
@instance ||= new
11+
attr_accessor :base_instance
12+
# When inherited, will create a list of all instances (times the API was mounted)
13+
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
14+
def inherited(remountable_class, base_instance_parent = Grape::APIInstance)
15+
remountable_class.initial_setup(base_instance_parent)
16+
remountable_class.override_all_methods
17+
remountable_class.make_inheritable
2718
end
2819

29-
# Wipe the compiled API so we can recompile after changes were made.
30-
def change!
31-
@instance = nil
20+
# Initialize the instance variables on the remountable class, and the base_instance
21+
# an instance that will be used to create the set up but will not be mounted
22+
def initial_setup(base_instance_parent)
23+
@instances = []
24+
@setup = []
25+
@base_parent = base_instance_parent
26+
@base_instance = mount_instance
3227
end
3328

34-
# This is the interface point between Rack and Grape; it accepts a request
35-
# from Rack and ultimately returns an array of three values: the status,
36-
# the headers, and the body. See [the rack specification]
37-
# (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
38-
def call(env)
39-
LOCK.synchronize { compile } unless instance
40-
call!(env)
41-
end
42-
43-
# A non-synchronized version of ::call.
44-
def call!(env)
45-
instance.call(env)
46-
end
47-
48-
# (see #cascade?)
49-
def cascade(value = nil)
50-
if value.nil?
51-
inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
52-
else
53-
namespace_inheritable(:cascade, value)
29+
# Redefines all methods so that are forwarded to add_setup and recorded
30+
def override_all_methods
31+
(base_instance.methods - NON_OVERRIDABLE).each do |method_override|
32+
define_singleton_method(method_override) do |*args, &block|
33+
add_setup(method_override, *args, &block)
34+
end
5435
end
5536
end
5637

57-
# see Grape::Router#recognize_path
58-
def recognize_path(path)
59-
LOCK.synchronize { compile } unless instance
60-
instance.router.recognize_path(path)
61-
end
62-
63-
protected
64-
65-
def prepare_routes
66-
endpoints.map(&:routes).flatten
67-
end
68-
69-
# Execute first the provided block, then each of the
70-
# block passed in. Allows for simple 'before' setups
71-
# of settings stack pushes.
72-
def nest(*blocks, &block)
73-
blocks.reject!(&:nil?)
74-
if blocks.any?
75-
instance_eval(&block) if block_given?
76-
blocks.each { |b| instance_eval(&b) }
77-
reset_validations!
78-
else
79-
instance_eval(&block)
38+
# When classes inheriting from this RemountableAPI child, we also want the instances to inherit from our instance
39+
def make_inheritable
40+
define_singleton_method(:inherited) do |sub_remountable|
41+
Grape::API.inherited(sub_remountable, base_instance)
8042
end
8143
end
8244

83-
def inherited(subclass)
84-
subclass.reset!
85-
subclass.logger = logger.clone
45+
# The remountable class can have a configuration hash to provide some dynamic class-level variables.
46+
# For instance, a descripcion could be done using: `desc configuration[:description]` if it may vary
47+
# depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
48+
# too much, you may actually want to provide a new API rather than remount it.
49+
def mount_instance(configuration: {})
50+
instance = Class.new(@base_parent)
51+
instance.instance_variable_set(:@configuration, configuration)
52+
instance.define_singleton_method(:configuration) { @configuration }
53+
replay_setup_on(instance)
54+
@instances << instance
55+
instance
8656
end
8757

88-
def inherit_settings(other_settings)
89-
top_level_setting.inherit_from other_settings.point_in_time_copy
90-
91-
# Propagate any inherited params down to our endpoints, and reset any
92-
# compiled routes.
93-
endpoints.each do |e|
94-
e.inherit_settings(top_level_setting.namespace_stackable)
95-
e.reset_routes!
58+
# Replays the set up to produce an API as defined in this class, can be called
59+
# on classes that inherit from Grape::API
60+
def replay_setup_on(instance)
61+
@setup.each do |setup_stage|
62+
instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
9663
end
97-
98-
reset_routes!
9964
end
100-
end
101-
102-
attr_reader :router
10365

104-
# Builds the routes from the defined endpoints, effectively compiling
105-
# this API into a usable form.
106-
def initialize
107-
@router = Router.new
108-
add_head_not_allowed_methods_and_options_methods
109-
self.class.endpoints.each do |endpoint|
110-
endpoint.mount_in(@router)
66+
def respond_to?(method, include_private = false)
67+
super(method, include_private) || base_instance.respond_to?(method, include_private)
11168
end
11269

113-
@router.compile!
114-
@router.freeze
115-
end
116-
117-
# Handle a request. See Rack documentation for what `env` is.
118-
def call(env)
119-
result = @router.call(env)
120-
result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade?
121-
result
122-
end
123-
124-
# Some requests may return a HTTP 404 error if grape cannot find a matching
125-
# route. In this case, Grape::Router adds a X-Cascade header to the response
126-
# and sets it to 'pass', indicating to grape's parents they should keep
127-
# looking for a matching route on other resources.
128-
#
129-
# In some applications (e.g. mounting grape on rails), one might need to trap
130-
# errors from reaching upstream. This is effectivelly done by unsetting
131-
# X-Cascade. Default :cascade is true.
132-
def cascade?
133-
return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.keys.include?(:cascade)
134-
return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.namespace_inheritable(:version_options).key?(:cascade)
135-
true
136-
end
137-
138-
reset!
139-
140-
private
70+
private
14171

142-
# For every resource add a 'OPTIONS' route that returns an HTTP 204 response
143-
# with a list of HTTP methods that can be called. Also add a route that
144-
# will return an HTTP 405 response for any HTTP method that the resource
145-
# cannot handle.
146-
def add_head_not_allowed_methods_and_options_methods
147-
routes_map = {}
148-
149-
self.class.endpoints.each do |endpoint|
150-
routes = endpoint.routes
151-
routes.each do |route|
152-
# using the :any shorthand produces [nil] for route methods, substitute all manually
153-
route_key = route.pattern.to_regexp
154-
routes_map[route_key] ||= {}
155-
route_settings = routes_map[route_key]
156-
route_settings[:pattern] = route.pattern
157-
route_settings[:requirements] = route.requirements
158-
route_settings[:path] = route.origin
159-
route_settings[:methods] ||= []
160-
route_settings[:methods] << route.request_method
161-
route_settings[:endpoint] = route.app
162-
163-
# using the :any shorthand produces [nil] for route methods, substitute all manually
164-
route_settings[:methods] = %w[GET PUT POST DELETE PATCH HEAD OPTIONS] if route_settings[:methods].include?('*')
165-
end
166-
end
167-
168-
# The paths we collected are prepared (cf. Path#prepare), so they
169-
# contain already versioning information when using path versioning.
170-
# Disable versioning so adding a route won't prepend versioning
171-
# informations again.
172-
without_root_prefix do
173-
without_versioning do
174-
routes_map.each do |_, config|
175-
methods = config[:methods]
176-
allowed_methods = methods.dup
177-
178-
unless self.class.namespace_inheritable(:do_not_route_head)
179-
allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
180-
end
181-
182-
allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ')
183-
184-
unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
185-
config[:endpoint].options[:options_route_enabled] = true
186-
end
187-
188-
attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
189-
generate_not_allowed_method(config[:pattern], attributes)
190-
end
72+
# Adds a new stage to the set up require to get a Grape::API up and running
73+
def add_setup(method, *args, &block)
74+
setup_stage = { method: method, args: args, block: block }
75+
@setup << setup_stage
76+
last_response = nil
77+
@instances.each do |instance|
78+
last_response = instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
19179
end
80+
last_response
19281
end
19382
end
194-
195-
# Generate a route that returns an HTTP 405 response for a user defined
196-
# path on methods not specified
197-
def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
198-
not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods
199-
not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)
200-
201-
return if not_allowed_methods.empty?
202-
203-
@router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
204-
end
205-
206-
# Allows definition of endpoints that ignore the versioning configuration
207-
# used by the rest of your API.
208-
def without_versioning(&_block)
209-
old_version = self.class.namespace_inheritable(:version)
210-
old_version_options = self.class.namespace_inheritable(:version_options)
211-
212-
self.class.namespace_inheritable_to_nil(:version)
213-
self.class.namespace_inheritable_to_nil(:version_options)
214-
215-
yield
216-
217-
self.class.namespace_inheritable(:version, old_version)
218-
self.class.namespace_inheritable(:version_options, old_version_options)
219-
end
220-
221-
# Allows definition of endpoints that ignore the root prefix used by the
222-
# rest of your API.
223-
def without_root_prefix(&_block)
224-
old_prefix = self.class.namespace_inheritable(:root_prefix)
225-
226-
self.class.namespace_inheritable_to_nil(:root_prefix)
227-
228-
yield
229-
230-
self.class.namespace_inheritable(:root_prefix, old_prefix)
231-
end
23283
end
23384
end

0 commit comments

Comments
 (0)