|
1 | 1 | require 'grape/router'
|
2 | 2 |
|
3 | 3 | 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. |
5 | 5 | # should subclass this class in order to build an API.
|
6 | 6 | 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 |
8 | 9 |
|
9 | 10 | 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 |
27 | 18 | end
|
28 | 19 |
|
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 |
32 | 27 | end
|
33 | 28 |
|
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 |
54 | 35 | end
|
55 | 36 | end
|
56 | 37 |
|
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) |
80 | 42 | end
|
81 | 43 | end
|
82 | 44 |
|
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 |
86 | 56 | end
|
87 | 57 |
|
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]) |
96 | 63 | end
|
97 |
| - |
98 |
| - reset_routes! |
99 | 64 | end
|
100 |
| - end |
101 |
| - |
102 |
| - attr_reader :router |
103 | 65 |
|
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) |
111 | 68 | end
|
112 | 69 |
|
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 |
141 | 71 |
|
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]) |
191 | 79 | end
|
| 80 | + last_response |
192 | 81 | end
|
193 | 82 | 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 |
232 | 83 | end
|
233 | 84 | end
|
0 commit comments