|
| 1 | +require 'grape/router' |
| 2 | + |
| 3 | +module Grape |
| 4 | + class API |
| 5 | + # The API Instance class, is the engine behind Grape::API. Each class that inherits |
| 6 | + # from this will represent a different API instance |
| 7 | + class Instance |
| 8 | + include Grape::DSL::API |
| 9 | + |
| 10 | + class << self |
| 11 | + attr_reader :instance |
| 12 | + |
| 13 | + # A class-level lock to ensure the API is not compiled by multiple |
| 14 | + # threads simultaneously within the same process. |
| 15 | + LOCK = Mutex.new |
| 16 | + |
| 17 | + # Clears all defined routes, endpoints, etc., on this API. |
| 18 | + def reset! |
| 19 | + reset_endpoints! |
| 20 | + reset_routes! |
| 21 | + reset_validations! |
| 22 | + end |
| 23 | + |
| 24 | + # Parses the API's definition and compiles it into an instance of |
| 25 | + # Grape::API. |
| 26 | + def compile |
| 27 | + @instance ||= new |
| 28 | + end |
| 29 | + |
| 30 | + # Wipe the compiled API so we can recompile after changes were made. |
| 31 | + def change! |
| 32 | + @instance = nil |
| 33 | + end |
| 34 | + |
| 35 | + # This is the interface point between Rack and Grape; it accepts a request |
| 36 | + # from Rack and ultimately returns an array of three values: the status, |
| 37 | + # the headers, and the body. See [the rack specification] |
| 38 | + # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more. |
| 39 | + def call(env) |
| 40 | + LOCK.synchronize { compile } unless instance |
| 41 | + call!(env) |
| 42 | + end |
| 43 | + |
| 44 | + # A non-synchronized version of ::call. |
| 45 | + def call!(env) |
| 46 | + instance.call(env) |
| 47 | + end |
| 48 | + |
| 49 | + # (see #cascade?) |
| 50 | + def cascade(value = nil) |
| 51 | + if value.nil? |
| 52 | + inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !namespace_inheritable(:cascade).nil? : true |
| 53 | + else |
| 54 | + namespace_inheritable(:cascade, value) |
| 55 | + end |
| 56 | + end |
| 57 | + |
| 58 | + # see Grape::Router#recognize_path |
| 59 | + def recognize_path(path) |
| 60 | + LOCK.synchronize { compile } unless instance |
| 61 | + instance.router.recognize_path(path) |
| 62 | + end |
| 63 | + |
| 64 | + protected |
| 65 | + |
| 66 | + def prepare_routes |
| 67 | + endpoints.map(&:routes).flatten |
| 68 | + end |
| 69 | + |
| 70 | + # Execute first the provided block, then each of the |
| 71 | + # block passed in. Allows for simple 'before' setups |
| 72 | + # of settings stack pushes. |
| 73 | + def nest(*blocks, &block) |
| 74 | + blocks.reject!(&:nil?) |
| 75 | + if blocks.any? |
| 76 | + instance_eval(&block) if block_given? |
| 77 | + blocks.each { |b| instance_eval(&b) } |
| 78 | + reset_validations! |
| 79 | + else |
| 80 | + instance_eval(&block) |
| 81 | + end |
| 82 | + end |
| 83 | + |
| 84 | + def inherited(subclass) |
| 85 | + subclass.reset! |
| 86 | + subclass.logger = logger.clone |
| 87 | + end |
| 88 | + |
| 89 | + def inherit_settings(other_settings) |
| 90 | + top_level_setting.inherit_from other_settings.point_in_time_copy |
| 91 | + |
| 92 | + # Propagate any inherited params down to our endpoints, and reset any |
| 93 | + # compiled routes. |
| 94 | + endpoints.each do |e| |
| 95 | + e.inherit_settings(top_level_setting.namespace_stackable) |
| 96 | + e.reset_routes! |
| 97 | + end |
| 98 | + |
| 99 | + reset_routes! |
| 100 | + end |
| 101 | + end |
| 102 | + |
| 103 | + attr_reader :router |
| 104 | + |
| 105 | + # Builds the routes from the defined endpoints, effectively compiling |
| 106 | + # this API into a usable form. |
| 107 | + def initialize |
| 108 | + @router = Router.new |
| 109 | + add_head_not_allowed_methods_and_options_methods |
| 110 | + self.class.endpoints.each do |endpoint| |
| 111 | + endpoint.mount_in(@router) |
| 112 | + end |
| 113 | + |
| 114 | + @router.compile! |
| 115 | + @router.freeze |
| 116 | + end |
| 117 | + |
| 118 | + # Handle a request. See Rack documentation for what `env` is. |
| 119 | + def call(env) |
| 120 | + result = @router.call(env) |
| 121 | + result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade? |
| 122 | + result |
| 123 | + end |
| 124 | + |
| 125 | + # Some requests may return a HTTP 404 error if grape cannot find a matching |
| 126 | + # route. In this case, Grape::Router adds a X-Cascade header to the response |
| 127 | + # and sets it to 'pass', indicating to grape's parents they should keep |
| 128 | + # looking for a matching route on other resources. |
| 129 | + # |
| 130 | + # In some applications (e.g. mounting grape on rails), one might need to trap |
| 131 | + # errors from reaching upstream. This is effectivelly done by unsetting |
| 132 | + # X-Cascade. Default :cascade is true. |
| 133 | + def cascade? |
| 134 | + return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.keys.include?(:cascade) |
| 135 | + return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.namespace_inheritable(:version_options).key?(:cascade) |
| 136 | + true |
| 137 | + end |
| 138 | + |
| 139 | + reset! |
| 140 | + |
| 141 | + private |
| 142 | + |
| 143 | + # For every resource add a 'OPTIONS' route that returns an HTTP 204 response |
| 144 | + # with a list of HTTP methods that can be called. Also add a route that |
| 145 | + # will return an HTTP 405 response for any HTTP method that the resource |
| 146 | + # cannot handle. |
| 147 | + def add_head_not_allowed_methods_and_options_methods |
| 148 | + routes_map = {} |
| 149 | + |
| 150 | + self.class.endpoints.each do |endpoint| |
| 151 | + routes = endpoint.routes |
| 152 | + routes.each do |route| |
| 153 | + # using the :any shorthand produces [nil] for route methods, substitute all manually |
| 154 | + route_key = route.pattern.to_regexp |
| 155 | + routes_map[route_key] ||= {} |
| 156 | + route_settings = routes_map[route_key] |
| 157 | + route_settings[:pattern] = route.pattern |
| 158 | + route_settings[:requirements] = route.requirements |
| 159 | + route_settings[:path] = route.origin |
| 160 | + route_settings[:methods] ||= [] |
| 161 | + route_settings[:methods] << route.request_method |
| 162 | + route_settings[:endpoint] = route.app |
| 163 | + |
| 164 | + # using the :any shorthand produces [nil] for route methods, substitute all manually |
| 165 | + route_settings[:methods] = %w[GET PUT POST DELETE PATCH HEAD OPTIONS] if route_settings[:methods].include?('*') |
| 166 | + end |
| 167 | + end |
| 168 | + |
| 169 | + # The paths we collected are prepared (cf. Path#prepare), so they |
| 170 | + # contain already versioning information when using path versioning. |
| 171 | + # Disable versioning so adding a route won't prepend versioning |
| 172 | + # informations again. |
| 173 | + without_root_prefix do |
| 174 | + without_versioning do |
| 175 | + routes_map.each do |_, config| |
| 176 | + methods = config[:methods] |
| 177 | + allowed_methods = methods.dup |
| 178 | + |
| 179 | + unless self.class.namespace_inheritable(:do_not_route_head) |
| 180 | + allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET) |
| 181 | + end |
| 182 | + |
| 183 | + allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ') |
| 184 | + |
| 185 | + unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS) |
| 186 | + config[:endpoint].options[:options_route_enabled] = true |
| 187 | + end |
| 188 | + |
| 189 | + attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) |
| 190 | + generate_not_allowed_method(config[:pattern], attributes) |
| 191 | + end |
| 192 | + end |
| 193 | + end |
| 194 | + end |
| 195 | + |
| 196 | + # Generate a route that returns an HTTP 405 response for a user defined |
| 197 | + # path on methods not specified |
| 198 | + def generate_not_allowed_method(pattern, allowed_methods: [], **attributes) |
| 199 | + not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods |
| 200 | + not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options) |
| 201 | + |
| 202 | + return if not_allowed_methods.empty? |
| 203 | + |
| 204 | + @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes) |
| 205 | + end |
| 206 | + |
| 207 | + # Allows definition of endpoints that ignore the versioning configuration |
| 208 | + # used by the rest of your API. |
| 209 | + def without_versioning(&_block) |
| 210 | + old_version = self.class.namespace_inheritable(:version) |
| 211 | + old_version_options = self.class.namespace_inheritable(:version_options) |
| 212 | + |
| 213 | + self.class.namespace_inheritable_to_nil(:version) |
| 214 | + self.class.namespace_inheritable_to_nil(:version_options) |
| 215 | + |
| 216 | + yield |
| 217 | + |
| 218 | + self.class.namespace_inheritable(:version, old_version) |
| 219 | + self.class.namespace_inheritable(:version_options, old_version_options) |
| 220 | + end |
| 221 | + |
| 222 | + # Allows definition of endpoints that ignore the root prefix used by the |
| 223 | + # rest of your API. |
| 224 | + def without_root_prefix(&_block) |
| 225 | + old_prefix = self.class.namespace_inheritable(:root_prefix) |
| 226 | + |
| 227 | + self.class.namespace_inheritable_to_nil(:root_prefix) |
| 228 | + |
| 229 | + yield |
| 230 | + |
| 231 | + self.class.namespace_inheritable(:root_prefix, old_prefix) |
| 232 | + end |
| 233 | + end |
| 234 | + end |
| 235 | +end |
0 commit comments