rackr 0.0.64 → 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: 7b2ba418e1314ee479ecaa958fe3b88b8a59f43360f3411011cc24c70576ed57
4
- data.tar.gz: fefd0f17b2d1b935b80606f9c12a99b4d866e1ed9b57038a74e3b157ee2933fa
3
+ metadata.gz: a2c53919b2365343fd55636d5d3a29b41e93975a60ef3d2af74416e12808f27a
4
+ data.tar.gz: 32f5c5da65e14bf318c07427fd5bb0f9c978c04d75e94877ddab0793da4a8005
5
5
  SHA512:
6
- metadata.gz: 4dd390660b33e36ee89c535433a6ffaadc8e669fb9f4a3d05cd1edecf85f62d4fec4d0c6c130959225d7b08e37f0621878b2d92df54ec9dee777cc2e153be0fc
7
- data.tar.gz: 8b7eb721d13f69adac0a46d98a21e4159fd69265ecdea1c5c3400f1ecc8e90dadbf91996e4add09b5d0a8b66635ffb9d6b1d69f5bd6dc9580b84dd35380a9bb4
6
+ metadata.gz: 8e0decd638c32afe394950c7b45bf44539704b136bde9d9e9dfc2a698e37c427317e16fb310bb6515d0f4a339fd224228d9d5ee424ddbaca7f191ec8f40f4b27
7
+ data.tar.gz: 04117f475a16165a813cac337f72880e6d461ffe41e3ce085ab06690fbc4b21a85f1f22d343fa4a4b070f4a7d9898f26eec29d231df7c8b9469b4d2d60f34848
data/lib/rackr/action.rb CHANGED
@@ -3,76 +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
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|
87
+ {
88
+ 'content-type' => content_type,
89
+ 'content-length' => content.bytesize.to_s
90
+ }.merge(headers)
91
+ end).freeze
92
+
9
93
  RENDER = {
10
- html: lambda do |val, status: 200, headers: {}, html: nil|
11
- [status, { 'content-type' => 'text/html', 'content-length' => val.bytesize.to_s }.merge(headers), [val]]
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]]
12
97
  end,
13
- text: lambda do |val, status: 200, headers: {}, text: nil|
14
- [status, { 'content-type' => 'text/plain', 'content-length' => val.bytesize.to_s }.merge(headers), [val]]
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]]
15
101
  end,
16
- json: lambda do |val, status: 200, headers: {}, json: nil|
17
- val = Oj.dump(val, mode: :compat) unless val.is_a?(String)
18
- [status, { 'content-type' => 'application/json', 'content-length' => val.bytesize.to_s }.merge(headers), [val]]
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
19
110
  end,
20
- res: lambda do |val, status: nil, headers: nil, res: nil|
21
- val.status = status if status
22
- headers.each { |h, v| val.set_header(h, v) } if headers
23
- 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))
24
131
  end,
25
- response: lambda do |val, status: nil, headers: nil, response: nil|
26
- val.status = status if status
27
- headers.each { |h, v| val.set_header(h, v) } if headers
28
- 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
+ )
29
141
  end
30
142
  }.freeze
31
143
 
32
144
  def self.included(base)
33
145
  base.class_eval do
34
- 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')
35
154
 
36
155
  def initialize(routes: nil, config: nil)
37
156
  @routes = routes
38
157
  @config = config
39
- @deps = config[:deps]
40
- @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)
41
170
  end
42
171
 
43
172
  def render(**opts)
44
173
  type = opts.keys.first
45
174
  content = opts[type]
46
175
 
47
- Rackr::Action::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
+ )
48
207
  end
49
208
 
50
- def view_response(
51
- paths,
52
- status: 200,
53
- headers: {},
54
- layout_path: 'layout'
55
- )
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
+
56
235
  _render_view(
57
- paths,
58
- status: status,
59
- headers: headers,
60
- layout_path: layout_path,
61
- 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'
62
243
  )
63
244
  end
64
245
 
65
246
  def _render_view(
66
247
  paths,
67
- status: 200,
68
- headers: {},
69
- layout_path: 'layout',
70
- response_instance: false,
71
- view: nil
248
+ status:,
249
+ headers:,
250
+ layout_path:,
251
+ response_instance:,
252
+ view:,
253
+ charset:
72
254
  )
73
255
  base_path = config.dig(:views, :path) || 'views'
256
+ headers['content-security-policy'] ||= content_security_policy
74
257
 
75
- file_or_nil = lambda do |path|
258
+ file_or_nil = proc do |path|
76
259
  ::File.read(path)
77
260
  rescue Errno::ENOENT
78
261
  nil
@@ -99,62 +282,31 @@ class Rackr
99
282
  return Rack::Response.new(
100
283
  parsed_erb,
101
284
  status,
102
- { 'content-type' => 'text/html' }.merge(headers)
285
+ DEFAULT_HEADERS.call("text/html; charset=#{charset}", headers, parsed_erb)
103
286
  )
104
287
  end
105
288
 
106
- [status, { 'content-type' => 'text/html', 'content-length' => parsed_erb.bytesize.to_s }.merge(headers),
107
- [parsed_erb]]
289
+ [status, DEFAULT_HEADERS.call("text/html; charset=#{charset}", headers, parsed_erb), [parsed_erb]]
108
290
  end
109
291
 
110
- def load_json(val)
111
- return Oj.load(val.body.read) if val.is_a?(Rack::Request)
112
-
113
- Oj.load(val)
292
+ def d(content)
293
+ raise Rackr::Dump, content
114
294
  end
115
295
 
116
- def html_response(content = '', status: 200, headers: {})
117
- Rack::Response.new(content, status,
118
- { 'content-type' => 'text/html', 'content-length' => content.bytesize.to_s }.merge(headers))
119
- end
120
-
121
- def json_response(content = {}, status: 200, headers: {})
122
- content = Oj.dump(content, mode: :compat) unless content.is_a?(String)
123
- Rack::Response.new(
124
- content,
125
- status,
126
- { 'content-type' => 'application/json', 'content-length' => content.bytesize.to_s }.merge(headers)
127
- )
128
- end
129
-
130
- def text_response(content, status: 200, headers: {})
131
- Rack::Response.new(
132
- content,
133
- status,
134
- { 'content-type' => 'text/plain', 'content-length' => content.bytesize.to_s }.merge(headers)
135
- )
296
+ def not_found!
297
+ raise Rackr::NotFound
136
298
  end
137
299
 
300
+ # rubocop:disable Security/Eval
138
301
  def load_erb(content, binding_context: nil)
139
302
  eval(Erubi::Engine.new(content).src, binding_context)
140
303
  end
304
+ # rubocop:enable Security/Eval
141
305
 
142
306
  def head(status, headers: {})
143
307
  [status, headers, []]
144
308
  end
145
309
 
146
- def head_response(status, headers: {})
147
- Rack::Response.new(nil, status, headers)
148
- end
149
-
150
- def redirect_response(url, headers: {})
151
- Rack::Response.new(
152
- nil,
153
- 302,
154
- { 'location' => url }.merge(headers)
155
- )
156
- end
157
-
158
310
  def redirect_to(url, headers: {})
159
311
  [302, { 'location' => url }.merge(headers), []]
160
312
  end
@@ -162,7 +314,17 @@ class Rackr
162
314
  def response(body = nil, status = 200, headers = {})
163
315
  Rack::Response.new(body, status, headers)
164
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
165
325
  end
166
326
  end
167
327
  end
328
+ # rubocop:enable Metrics/PerceivedComplexity
329
+ # rubocop:enable Metrics/MethodLength
168
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,10 +61,10 @@ 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
- traceback << "<p>#{tail.join("<br>")}</p>"
67
+ traceback << "<p>#{tail.join('<br>')}</p>"
65
68
  traceback
66
69
  end
67
70
 
@@ -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}'")
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rackr
4
+ class Router
5
+ # Path route is a route that has a path value that can be matched, and may have path params
6
+ class PathRoute < Route
7
+ attr_reader :splitted_path,
8
+ :has_params
9
+
10
+ def initialize(path, endpoint, befores: [], afters: [], wildcard: false)
11
+ super(endpoint, befores:, afters:)
12
+
13
+ @path = path
14
+ @splitted_path = @path.split('/')
15
+ @params = fetch_params
16
+ @has_params = @params != []
17
+ @path_regex = /\A#{path.gsub(/(:\w+)/, '([^/]+)')}\z/
18
+ @wildcard = wildcard
19
+ end
20
+
21
+ def match?(path_info)
22
+ return path_info.match?(@path_regex) if @has_params
23
+ return true if @wildcard
24
+
25
+ path_info == @path
26
+ end
27
+
28
+ private
29
+
30
+ def fetch_params
31
+ @splitted_path.select { |value| value.start_with? ':' }
32
+ end
33
+ end
34
+ end
35
+ end