rackr 0.0.65 → 0.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d146e39fc587acb31017ef9e5b093aba6a8cecadf8fe8beb038ab7804425015
4
- data.tar.gz: '04787fa986d2d6f4033114d5bf6a134bd6451e2e7cb0c957ef063ddf7fbb9cf3'
3
+ metadata.gz: a2c53919b2365343fd55636d5d3a29b41e93975a60ef3d2af74416e12808f27a
4
+ data.tar.gz: 32f5c5da65e14bf318c07427fd5bb0f9c978c04d75e94877ddab0793da4a8005
5
5
  SHA512:
6
- metadata.gz: 57661323fb8e596afd2833f24145716090db3bd1024bf465ae2388f48c5a2c9cbff4384331368d69337a08f7fd977b5f33dcb275421324748eb783f33d6a877e
7
- data.tar.gz: b524b6414b3218cca05f134f81ce03583fac97d38eb4e754c27a754b50fd0d2eaa969b4728304ce6aafe08a45c370ffde59a68d21d76f72634cc43fc74925b9b
6
+ metadata.gz: 8e0decd638c32afe394950c7b45bf44539704b136bde9d9e9dfc2a698e37c427317e16fb310bb6515d0f4a339fd224228d9d5ee424ddbaca7f191ec8f40f4b27
7
+ data.tar.gz: 04117f475a16165a813cac337f72880e6d461ffe41e3ce085ab06690fbc4b21a85f1f22d343fa4a4b070f4a7d9898f26eec29d231df7c8b9469b4d2d60f34848
data/lib/rackr/action.rb CHANGED
@@ -3,95 +3,259 @@
3
3
  require 'erubi'
4
4
  require 'oj'
5
5
  require 'rack'
6
+ require_relative 'action/callbacks'
6
7
 
7
8
  class Rackr
9
+ # This module provides the action functions available inside the routes context or
10
+ # specific action class that included the Rackr::Action.
8
11
  module Action
9
- @@default_headers_for = lambda { |content_type, headers, content|
12
+ MIME_TYPES = {
13
+ text: 'text/plain',
14
+ html: 'text/html',
15
+ json: 'application/json',
16
+ manifest: 'text/cache-manifest',
17
+ atom: 'application/atom+xml',
18
+ avi: 'video/x-msvideo',
19
+ bmp: 'image/bmp',
20
+ bz: 'application/x-bzip',
21
+ bz2: 'application/x-bzip2',
22
+ chm: 'application/vnd.ms-htmlhelp',
23
+ css: 'text/css',
24
+ csv: 'text/csv',
25
+ flv: 'video/x-flv',
26
+ gif: 'image/gif',
27
+ gz: 'application/x-gzip',
28
+ h264: 'video/h264',
29
+ ico: 'image/vnd.microsoft.icon',
30
+ ics: 'text/calendar',
31
+ jpg: 'image/jpeg',
32
+ js: 'application/javascript',
33
+ mp4: 'video/mp4',
34
+ mov: 'video/quicktime',
35
+ mp3: 'audio/mpeg',
36
+ mp4a: 'audio/mp4',
37
+ mpg: 'video/mpeg',
38
+ oga: 'audio/ogg',
39
+ ogg: 'application/ogg',
40
+ ogv: 'video/ogg',
41
+ pdf: 'application/pdf',
42
+ pgp: 'application/pgp-encrypted',
43
+ png: 'image/png',
44
+ psd: 'image/vnd.adobe.photoshop',
45
+ rss: 'application/rss+xml',
46
+ rtf: 'application/rtf',
47
+ sh: 'application/x-sh',
48
+ svg: 'image/svg+xml',
49
+ swf: 'application/x-shockwave-flash',
50
+ tar: 'application/x-tar',
51
+ torrent: 'application/x-bittorrent',
52
+ tsv: 'text/tab-separated-values',
53
+ uri: 'text/uri-list',
54
+ vcs: 'text/x-vcalendar',
55
+ wav: 'audio/x-wav',
56
+ webm: 'video/webm',
57
+ wmv: 'video/x-ms-wmv',
58
+ woff: 'application/font-woff',
59
+ woff2: 'application/font-woff2',
60
+ wsdl: 'application/wsdl+xml',
61
+ xhtml: 'application/xhtml+xml',
62
+ xml: 'application/xml',
63
+ xslt: 'application/xslt+xml',
64
+ yml: 'text/yaml',
65
+ zip: 'application/zip'
66
+ }.freeze
67
+
68
+ DEFAULT_CSP_HEADERS = {
69
+ base_uri: "'self'",
70
+ child_src: "'self'",
71
+ connect_src: "'self'",
72
+ default_src: "'none'",
73
+ font_src: "'self'",
74
+ form_action: "'self'",
75
+ frame_ancestors: "'self'",
76
+ frame_src: "'self'",
77
+ img_src: "'self' https: data:",
78
+ media_src: "'self'",
79
+ object_src: "'none'",
80
+ script_src: "'self'",
81
+ style_src: "'self' 'unsafe-inline' https:"
82
+ }.freeze
83
+
84
+ # These are constant (not methods) for better performance
85
+
86
+ DEFAULT_HEADERS = (proc do |content_type, headers, content|
10
87
  {
11
88
  'content-type' => content_type,
12
89
  'content-length' => content.bytesize.to_s
13
90
  }.merge(headers)
14
- }
15
-
16
- @@render = {
17
- html: lambda do |val, status: 200, headers: {}, html: nil|
18
- [
19
- status,
20
- @@default_headers_for.call('text/html; charset=utf-8', headers, val),
21
- [val]
22
- ]
91
+ end).freeze
92
+
93
+ RENDER = {
94
+ json: proc do |content, status, headers, charset|
95
+ content = Oj.dump(content, mode: :compat) unless content.is_a?(String)
96
+ [status || 200, DEFAULT_HEADERS.call("application/json; charset=#{charset}", headers, content), [content]]
23
97
  end,
24
- text: lambda do |val, status: 200, headers: {}, text: nil|
25
- [
26
- status,
27
- @@default_headers_for.call('text/plain', headers, val),
28
- [val]
29
- ]
98
+ html: proc do |content, status, headers, charset, content_security_policy|
99
+ headers['content-security-policy'] = content_security_policy
100
+ [status || 200, DEFAULT_HEADERS.call("text/html; charset=#{charset}", headers, content), [content]]
30
101
  end,
31
- json: lambda do |val, status: 200, headers: {}, json: nil|
32
- val = Oj.dump(val, mode: :compat) unless val.is_a?(String)
33
- [
34
- status,
35
- @@default_headers_for.call('application/json', headers, val),
36
- [val]
37
- ]
102
+ res: proc do |content, status, _headers, charset|
103
+ content.status = status if status
104
+ if charset
105
+ content.content_type =
106
+ (content.content_type || 'charset=utf-8').sub(/charset=\S+/, "charset=#{charset}")
107
+ end
108
+ content.headers['content-length'] ||= content.body.join.bytesize.to_s
109
+ content.finish
38
110
  end,
39
- res: lambda do |val, status: nil, headers: nil, res: nil|
40
- val.status = status if status
41
- headers.each { |h, v| val.set_header(h, v) } if headers
42
- val.finish
111
+ response: proc do |content, status, _headers, charset|
112
+ content.status = status if status
113
+ if charset
114
+ content.content_type =
115
+ (content.content_type || 'charset=utf-8').sub(/charset=\S+/, "charset=#{charset}")
116
+ end
117
+ content.headers['content-length'] ||= content.body.join.bytesize.to_s
118
+ content.finish
119
+ end
120
+ }.freeze
121
+
122
+ BUILD_RESPONSE = {
123
+ json: proc do |content, status, headers, charset|
124
+ content = Oj.dump(content, mode: :compat) unless content.is_a?(String)
125
+ Rack::Response.new(content, status,
126
+ DEFAULT_HEADERS.call("application/json; charset=#{charset}", headers, content))
127
+ end,
128
+ html: proc do |content, status, headers, charset, content_security_policy|
129
+ headers['content-security-policy'] = content_security_policy
130
+ Rack::Response.new(content, status, DEFAULT_HEADERS.call("text/html; charset=#{charset}", headers, content))
43
131
  end,
44
- response: lambda do |val, status: nil, headers: nil, response: nil|
45
- val.status = status if status
46
- headers.each { |h, v| val.set_header(h, v) } if headers
47
- val.finish
132
+ head: proc do |status, _empty, headers|
133
+ Rack::Response.new(nil, status, headers)
134
+ end,
135
+ redirect_to: proc do |content, _status, headers|
136
+ Rack::Response.new(
137
+ nil,
138
+ 302,
139
+ { 'location' => content }.merge(headers)
140
+ )
48
141
  end
49
142
  }.freeze
50
143
 
51
144
  def self.included(base)
52
145
  base.class_eval do
53
- attr_reader :routes, :config, :deps, :db if self != Rackr
146
+ if self != Rackr
147
+ attr_reader :routes, :config, :deps, :db, :log, :cache
148
+
149
+ include Callbacks unless included_modules.include?(Rackr::Callback)
150
+ end
151
+
152
+ include HtmlSlice if Object.const_defined?('HtmlSlice')
153
+ include Stimulux if Object.const_defined?('Stimulux')
54
154
 
55
155
  def initialize(routes: nil, config: nil)
56
156
  @routes = routes
57
157
  @config = config
58
- @deps = config[:deps]
59
- @db = config.dig(:deps, :db)
158
+ @deps = config&.dig(:deps)
159
+ @db = config&.dig(:deps, :db)
160
+ @log = config&.dig(:deps, :log)
161
+ @cache = config&.dig(:deps, :cache)
162
+ end
163
+
164
+ def url_for(method, name)
165
+ "#{config&.dig(:host)}#{path_for(method, name)}"
166
+ end
167
+
168
+ def path_for(method, name)
169
+ routes.send(method)&.dig(name)
60
170
  end
61
171
 
62
172
  def render(**opts)
63
173
  type = opts.keys.first
64
174
  content = opts[type]
65
175
 
66
- @@render[type]&.call(content, **opts) || _render_view(content, **opts)
176
+ if (renderer = RENDER[type])
177
+ return renderer.call(
178
+ content,
179
+ opts[:status],
180
+ opts[:headers] || {},
181
+ opts[:charset] || 'utf-8',
182
+ content_security_policy
183
+ )
184
+ end
185
+
186
+ if (mime = MIME_TYPES[type])
187
+ return [
188
+ opts[:status] || 200,
189
+ DEFAULT_HEADERS.call(
190
+ "#{mime}; charset=#{opts[:charset] || 'utf-8'}",
191
+ opts[:headers] || {},
192
+ content
193
+ ),
194
+ [content]
195
+ ]
196
+ end
197
+
198
+ _render_view(
199
+ content,
200
+ status: opts[:status] || 200,
201
+ headers: opts[:headers] || {},
202
+ layout_path: opts[:layout_path] || 'layout',
203
+ view: opts[:view],
204
+ response_instance: opts[:response_instance] || false,
205
+ charset: opts[:charset] || 'utf-8'
206
+ )
67
207
  end
68
208
 
69
- def view_response(
70
- paths,
71
- status: 200,
72
- headers: {},
73
- layout_path: 'layout'
74
- )
209
+ def build_response(**opts)
210
+ type = opts.keys.first
211
+ content = opts[type]
212
+
213
+ if (builder = BUILD_RESPONSE[type])
214
+ return builder.call(
215
+ content,
216
+ opts[:status] || 200,
217
+ opts[:headers] || {},
218
+ opts[:charset] || 'utf-8',
219
+ content_security_policy
220
+ )
221
+ end
222
+
223
+ if (mime = MIME_TYPES[type])
224
+ return Rack::Response.new(
225
+ content,
226
+ opts[:status] || 200,
227
+ DEFAULT_HEADERS.call(
228
+ "#{mime}; charset=#{opts[:charset] || 'utf-8'}",
229
+ opts[:headers] || {},
230
+ content
231
+ )
232
+ )
233
+ end
234
+
75
235
  _render_view(
76
- paths,
77
- status: status,
78
- headers: headers,
79
- layout_path: layout_path,
80
- response_instance: true
236
+ content,
237
+ status: opts[:status] || 200,
238
+ headers: opts[:headers] || {},
239
+ layout_path: opts[:layout_path] || 'layout',
240
+ view: opts[:view],
241
+ response_instance: true,
242
+ charset: opts[:charset] || 'utf-8'
81
243
  )
82
244
  end
83
245
 
84
246
  def _render_view(
85
247
  paths,
86
- status: 200,
87
- headers: {},
88
- layout_path: 'layout',
89
- response_instance: false,
90
- view: nil
248
+ status:,
249
+ headers:,
250
+ layout_path:,
251
+ response_instance:,
252
+ view:,
253
+ charset:
91
254
  )
92
255
  base_path = config.dig(:views, :path) || 'views'
256
+ headers['content-security-policy'] ||= content_security_policy
93
257
 
94
- file_or_nil = lambda do |path|
258
+ file_or_nil = proc do |path|
95
259
  ::File.read(path)
96
260
  rescue Errno::ENOENT
97
261
  nil
@@ -118,56 +282,31 @@ class Rackr
118
282
  return Rack::Response.new(
119
283
  parsed_erb,
120
284
  status,
121
- @@default_headers_for.call('text/html; charset=utf-8', headers, parsed_erb)
285
+ DEFAULT_HEADERS.call("text/html; charset=#{charset}", headers, parsed_erb)
122
286
  )
123
287
  end
124
288
 
125
- [status, @@default_headers_for.call('text/html; charset=utf-8', headers, parsed_erb), [parsed_erb]]
289
+ [status, DEFAULT_HEADERS.call("text/html; charset=#{charset}", headers, parsed_erb), [parsed_erb]]
126
290
  end
127
291
 
128
- def not_found!
129
- raise Rackr::NotFound
292
+ def d(content)
293
+ raise Rackr::Dump, content
130
294
  end
131
295
 
132
- def load_json(val)
133
- return Oj.load(val.body.read) if val.is_a?(Rack::Request)
134
-
135
- Oj.load(val)
136
- end
137
-
138
- def html_response(content = '', status: 200, headers: {})
139
- Rack::Response.new(content, status, @@default_headers_for.call('text/html; charset=utf-8', headers, content))
140
- end
141
-
142
- def json_response(content = {}, status: 200, headers: {})
143
- content = Oj.dump(content, mode: :compat) unless content.is_a?(String)
144
- Rack::Response.new(content, status, @@default_headers_for.call('application/json', headers, content))
145
- end
146
-
147
- def text_response(content, status: 200, headers: {})
148
- Rack::Response.new(content, status, @@default_headers_for.call('text/plain', headers, content))
296
+ def not_found!
297
+ raise Rackr::NotFound
149
298
  end
150
299
 
300
+ # rubocop:disable Security/Eval
151
301
  def load_erb(content, binding_context: nil)
152
302
  eval(Erubi::Engine.new(content).src, binding_context)
153
303
  end
304
+ # rubocop:enable Security/Eval
154
305
 
155
306
  def head(status, headers: {})
156
307
  [status, headers, []]
157
308
  end
158
309
 
159
- def head_response(status, headers: {})
160
- Rack::Response.new(nil, status, headers)
161
- end
162
-
163
- def redirect_response(url, headers: {})
164
- Rack::Response.new(
165
- nil,
166
- 302,
167
- { 'location' => url }.merge(headers)
168
- )
169
- end
170
-
171
310
  def redirect_to(url, headers: {})
172
311
  [302, { 'location' => url }.merge(headers), []]
173
312
  end
@@ -175,7 +314,17 @@ class Rackr
175
314
  def response(body = nil, status = 200, headers = {})
176
315
  Rack::Response.new(body, status, headers)
177
316
  end
317
+
318
+ def content_security_policy
319
+ @content_security_policy ||=
320
+ DEFAULT_CSP_HEADERS
321
+ .merge(config&.dig(:csp_headers) || {})
322
+ .map { |k, v| "#{k.to_s.tr('_', '-')} #{v}" }
323
+ .join('; ')
324
+ end
178
325
  end
179
326
  end
180
327
  end
328
+ # rubocop:enable Metrics/PerceivedComplexity
329
+ # rubocop:enable Metrics/MethodLength
181
330
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rackr
4
+ # A callback is just a Rackr::Action with the assign method
4
5
  module Callback
5
6
  def self.included(base)
6
7
  base.class_eval do
@@ -2,6 +2,7 @@
2
2
 
3
3
  class Rackr
4
4
  class Router
5
+ # This class is responsible for initialize the Rack::Request object and give it the path params
5
6
  class BuildRequest
6
7
  def initialize(env, spplited_request_path)
7
8
  @env = env
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/utils'
4
+
5
+ class Rackr
6
+ class Router
7
+ module DevHtml
8
+ # Pretty print the content of the dump
9
+ class PrettyPrint
10
+ def self.call(content, level = 0)
11
+ content = content.inspect if content.instance_of?(String)
12
+ content = 'nil' if content.instance_of?(nil)
13
+ content = 'false' if content.instance_of?(false)
14
+
15
+ return "<pre>#{Rack::Utils.escape_html(content)}</pre>" if level >= 2
16
+
17
+ case content
18
+ when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
19
+ "<pre>#{Rack::Utils.escape_html(content)}</pre>"
20
+ when Array
21
+ pretty_print_array(content, level)
22
+ when Hash
23
+ pretty_print_hash(content, level)
24
+ else
25
+ pretty_print_object(content, level)
26
+ end
27
+ end
28
+
29
+ def self.pretty_print_array(array, level)
30
+ list_items = array.map do |item|
31
+ "<li>#{call(item, level + 1)}</li>"
32
+ end.join
33
+
34
+ "<ul>#{list_items}</ul>"
35
+ end
36
+
37
+ def self.pretty_print_hash(hash, level)
38
+ list_items = hash.map do |key, value|
39
+ "<li><strong>#{key.inspect} =></strong> #{call(value, level + 1)}</li>"
40
+ end.join
41
+
42
+ "<ul>#{list_items}</ul>"
43
+ end
44
+
45
+ def self.pretty_print_object(content, level)
46
+ instance_vars = content.instance_variables.map do |var|
47
+ value = content.instance_variable_get(var)
48
+ "<li><strong>#{var}:</strong> #{call(value, level + 1)}</li>"
49
+ end.join
50
+
51
+ "<h3>#{content.class}</h3><ul>#{instance_vars}</ul>"
52
+ end
53
+ end
54
+
55
+ # This is the action that is called when a dump is raised
56
+ class Dump
57
+ include Rackr::Action
58
+
59
+ def call(env)
60
+ res = response(<<-HTML
61
+ <!DOCTYPE html>
62
+ <html>
63
+ <head>
64
+ <title>Application dump</title>
65
+ <style>
66
+ body {
67
+ padding: 0px;
68
+ margin: 0px;
69
+ font:small sans-serif;
70
+ background-color: #2b2b2b;
71
+ color: white;
72
+ }
73
+ h2 {
74
+ margin: 0px;
75
+ padding: 0.2em;
76
+ background-color: #353535;
77
+ }
78
+ li {
79
+ padding: 0px;
80
+ }
81
+ div {
82
+ margin: 1em;
83
+ }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <h2>#{env['dump'].content.class}</h2>
88
+ <div>
89
+ <h3>Methods</h3>
90
+ #{env['dump'].content.methods}
91
+ <h3>Content</h3>
92
+ #{PrettyPrint.call(env['dump'].content)}
93
+ </div>
94
+ </body>
95
+ </html>
96
+ HTML
97
+ )
98
+ res.status = 200
99
+ res.headers['content-type'] = 'text/html'
100
+ render res:
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  class Rackr
4
4
  class Router
5
- module Errors
6
- class DevHtml
5
+ module DevHtml
6
+ # This is the action that is called when an error is raised
7
+ class Errors
7
8
  include Rackr::Action
8
9
 
9
10
  def call(env)
10
- render(html: <<-HTML
11
+ res = build_response(html: <<-HTML
11
12
  <!DOCTYPE html>
12
13
  <html>
13
14
  <head>
@@ -20,9 +21,9 @@ class Rackr
20
21
  body>div { border-bottom:1px solid #ddd; }
21
22
  h1 { font-weight:normal; }
22
23
  h2 { margin-bottom:.8em; }
23
- h2 span { font-size:80%; color:#666; font-weight:normal; }
24
+ h2 span { font-size:80%; color:#555; font-weight:normal; }
24
25
  #summary { background: #ffc; }
25
- #summary h2 { font-weight: normal; color: #666; }
26
+ #summary h2 { font-weight: normal; color: #555; }
26
27
  #backtrace { background: #eee; }
27
28
  pre {
28
29
  background: #ddd;
@@ -45,12 +46,14 @@ class Rackr
45
46
  </body>
46
47
  </html>
47
48
  HTML
48
- )
49
+ )
50
+ res.status = 500
51
+ render res:
49
52
  end
50
53
 
51
54
  def backtrace(env)
52
55
  first, *tail = env['error'].backtrace
53
- traceback = String.new('<h2>Traceback <span>(innermost first)</span></h2>')
56
+ traceback = +'<h2>Traceback <span>(innermost first)</span></h2>'
54
57
  traceback << "<p class=\"first-p\">#{first}</p><br/>"
55
58
 
56
59
  line_number = extract_line_number(first)
@@ -58,7 +61,7 @@ class Rackr
58
61
  file_path = (match ? match[1] : nil)
59
62
  unless file_path.nil?
60
63
  lines = File.readlines(file_path).map.with_index { |line, i| "#{i + 1}: #{line}" }
61
- traceback << "<pre>#{slice_around_index(lines, line_number).join('')}</pre>"
64
+ traceback << "<pre>#{slice_around_index(lines, line_number).join}</pre>"
62
65
  end
63
66
 
64
67
  traceback << "<p>#{tail.join('<br>')}</p>"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rackr
4
+ class Router
5
+ # A endpoint in Rackr is all objects that respond do .call, or .new.call
6
+ module Endpoint
7
+ def self.call(endpoint, content, routes = nil, config = nil, error = nil)
8
+ instance =
9
+ if endpoint.respond_to?(:call)
10
+ endpoint
11
+ elsif endpoint < Rackr::Action || endpoint < Rackr::Callback
12
+ endpoint.new(routes:, config:)
13
+ else
14
+ endpoint.new
15
+ end
16
+
17
+ return instance.call(content, error) if error
18
+
19
+ instance.call(content)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  class Rackr
4
4
  class Router
5
+ # Errors for the router
5
6
  module Errors
6
7
  class Error < StandardError; end
7
8
  class InvalidNamedRouteError < Error; end
@@ -10,8 +11,15 @@ class Rackr
10
11
  class InvalidCallbackError < Error; end
11
12
  class InvalidPathError < Error; end
12
13
  class InvalidBranchNameError < Error; end
14
+ class InvalidRackResponseError < StandardError; end
13
15
 
14
16
  class << self
17
+ def check_rack_response(response, where)
18
+ return if response.is_a?(Array)
19
+
20
+ raise(InvalidRackResponseError, "Invalid Rack response in #{where}, received: #{response}")
21
+ end
22
+
15
23
  def check_scope_name(path)
16
24
  return if path.is_a?(String) || path.is_a?(Symbol) || path.nil?
17
25
 
@@ -39,7 +47,7 @@ class Rackr
39
47
 
40
48
  def check_callbacks(callbacks, path)
41
49
  check = lambda { |callback|
42
- unless callback.nil? || callback.respond_to?(:call) || (callback.respond_to?(:new) && callback.instance_methods.include?(:call))
50
+ unless callback.nil? || callback.respond_to?(:call) || (callback.respond_to?(:new) && callback.method_defined?(:call))
43
51
  raise(InvalidCallbackError,
44
52
  "Callbacks must respond to a `call` method or be a class with a `call` instance method, got: '#{callback.inspect}' for '#{path}'")
45
53
  end
@@ -49,9 +57,7 @@ class Rackr
49
57
  end
50
58
 
51
59
  def check_endpoint(endpoint, path)
52
- if endpoint.respond_to?(:call) || (endpoint.respond_to?(:new) && endpoint.instance_methods.include?(:call))
53
- return
54
- end
60
+ return if endpoint.respond_to?(:call) || (endpoint.respond_to?(:new) && endpoint.method_defined?(:call))
55
61
 
56
62
  raise(InvalidEndpointError,
57
63
  "Endpoints must respond to a `call` method or be a class with a `call` instance method, got: '#{endpoint.inspect}' for '#{path}'")