rage-rb 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -5
- data/Gemfile +3 -0
- data/README.md +14 -12
- data/lib/rage/all.rb +27 -0
- data/lib/rage/application.rb +35 -6
- data/lib/rage/cli.rb +43 -44
- data/lib/rage/configuration.rb +22 -6
- data/lib/rage/controller/api.rb +52 -4
- data/lib/rage/errors.rb +4 -0
- data/lib/rage/fiber.rb +58 -10
- data/lib/rage/fiber_scheduler.rb +59 -32
- data/lib/rage/logger/logger.rb +164 -0
- data/lib/rage/logger/text_formatter.rb +46 -0
- data/lib/rage/params_parser.rb +45 -0
- data/lib/rage/request.rb +44 -0
- data/lib/rage/router/backend.rb +37 -4
- data/lib/rage/router/dsl.rb +236 -6
- data/lib/rage/router/handler_storage.rb +8 -5
- data/lib/rage/sidekiq_session.rb +72 -0
- data/lib/rage/templates/Gemfile +5 -1
- data/lib/rage/templates/Rakefile +1 -0
- data/lib/rage/templates/config-application.rb +6 -1
- data/lib/rage/templates/config-environments-development.rb +6 -3
- data/lib/rage/templates/config-environments-production.rb +7 -3
- data/lib/rage/templates/config-environments-test.rb +6 -3
- data/lib/rage/uploaded_file.rb +70 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +7 -16
- data/rage.gemspec +1 -1
- metadata +13 -4
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -3,41 +3,50 @@
|
|
3
3
|
require "resolv"
|
4
4
|
|
5
5
|
class Rage::FiberScheduler
|
6
|
+
MAX_READ = 65536
|
7
|
+
|
6
8
|
def initialize
|
7
9
|
@root_fiber = Fiber.current
|
8
10
|
end
|
9
11
|
|
10
12
|
def io_wait(io, events, timeout = nil)
|
11
13
|
f = Fiber.current
|
12
|
-
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { f.resume }
|
13
|
-
Fiber.yield
|
14
|
+
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { |err| f.resume(err) }
|
14
15
|
|
15
|
-
|
16
|
+
err = Fiber.yield
|
17
|
+
if err == Errno::ETIMEDOUT::Errno
|
18
|
+
0
|
19
|
+
else
|
20
|
+
events
|
21
|
+
end
|
16
22
|
end
|
17
23
|
|
18
|
-
# TODO: this is more synchronous than asynchronous right now
|
19
24
|
def io_read(io, buffer, length, offset = 0)
|
20
|
-
|
21
|
-
|
25
|
+
length_to_read = if length == 0
|
26
|
+
buffer.size > MAX_READ ? MAX_READ : buffer.size
|
27
|
+
else
|
28
|
+
length
|
29
|
+
end
|
30
|
+
|
31
|
+
while true
|
32
|
+
string = ::Iodine::Scheduler.read(io.fileno, length_to_read, offset)
|
22
33
|
|
23
34
|
if string.nil?
|
24
35
|
return offset
|
25
36
|
end
|
26
37
|
|
27
38
|
if string.empty?
|
28
|
-
|
29
|
-
next
|
39
|
+
return -Errno::EAGAIN::Errno
|
30
40
|
end
|
31
41
|
|
32
42
|
buffer.set_string(string, offset)
|
33
|
-
offset += string.bytesize
|
34
43
|
|
35
44
|
size = string.bytesize
|
36
|
-
|
37
|
-
|
38
|
-
end
|
45
|
+
offset += size
|
46
|
+
return offset if size < length_to_read || size >= buffer.size
|
39
47
|
|
40
|
-
|
48
|
+
Fiber.pause
|
49
|
+
end
|
41
50
|
end
|
42
51
|
|
43
52
|
def io_write(io, buffer, length, offset = 0)
|
@@ -46,15 +55,11 @@ class Rage::FiberScheduler
|
|
46
55
|
|
47
56
|
::Iodine::Scheduler.write(io.fileno, buffer.get_string, bytes_to_write, offset)
|
48
57
|
|
49
|
-
|
58
|
+
bytes_to_write - offset
|
50
59
|
end
|
51
60
|
|
52
61
|
def kernel_sleep(duration = nil)
|
53
|
-
|
54
|
-
f = Fiber.current
|
55
|
-
::Iodine.run_after((duration * 1000).to_i) { f.resume }
|
56
|
-
Fiber.yield
|
57
|
-
end
|
62
|
+
block(nil, duration || 0)
|
58
63
|
end
|
59
64
|
|
60
65
|
# TODO: GC works a little strange with this closure;
|
@@ -75,13 +80,22 @@ class Rage::FiberScheduler
|
|
75
80
|
Resolv.getaddresses(hostname)
|
76
81
|
end
|
77
82
|
|
78
|
-
def block(
|
79
|
-
f = Fiber.current
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
+
def block(_blocker, timeout = nil)
|
84
|
+
f, fulfilled, channel = Fiber.current, false, "unblock:#{Fiber.current.object_id}"
|
85
|
+
|
86
|
+
resume_fiber_block = proc do
|
87
|
+
unless fulfilled
|
88
|
+
fulfilled = true
|
89
|
+
::Iodine.defer { ::Iodine.unsubscribe(channel) }
|
90
|
+
f.resume
|
91
|
+
end
|
83
92
|
end
|
84
|
-
|
93
|
+
|
94
|
+
::Iodine.subscribe(channel, &resume_fiber_block)
|
95
|
+
if timeout
|
96
|
+
::Iodine.run_after((timeout * 1000).to_i, &resume_fiber_block)
|
97
|
+
end
|
98
|
+
|
85
99
|
Fiber.yield
|
86
100
|
end
|
87
101
|
|
@@ -90,15 +104,28 @@ class Rage::FiberScheduler
|
|
90
104
|
end
|
91
105
|
|
92
106
|
def fiber(&block)
|
93
|
-
|
94
|
-
inner_schedule = f != @root_fiber
|
107
|
+
parent = Fiber.current
|
95
108
|
|
96
|
-
fiber =
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
109
|
+
fiber = if parent == @root_fiber
|
110
|
+
# the fiber to wrap a request in
|
111
|
+
Fiber.new(blocking: false) do
|
112
|
+
Fiber.current.__set_result(block.call)
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# the fiber was created in the user code
|
116
|
+
logger = Thread.current[:rage_logger]
|
117
|
+
|
118
|
+
Fiber.new(blocking: false) do
|
119
|
+
Thread.current[:rage_logger] = logger
|
120
|
+
Fiber.current.__set_result(block.call)
|
121
|
+
# send a message for `Fiber.await` to work
|
122
|
+
Iodine.publish("await:#{parent.object_id}", "") if parent.alive?
|
123
|
+
rescue Exception => e
|
124
|
+
Fiber.current.__set_err(e)
|
125
|
+
Iodine.publish("await:#{parent.object_id}", Fiber::AWAIT_ERROR_MESSAGE) if parent.alive?
|
126
|
+
end
|
101
127
|
end
|
128
|
+
|
102
129
|
fiber.resume
|
103
130
|
|
104
131
|
fiber
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
##
|
6
|
+
# All logs in `rage` consist of two parts: keys and tags. A sample log entry might look like this:
|
7
|
+
# ```
|
8
|
+
# [fecbba0735355738] timestamp=2023-10-19T11:12:56+00:00 pid=1825 level=info message=hello
|
9
|
+
# ```
|
10
|
+
# In the log entry above, `timestamp`, `pid`, `level`, and `message` are keys, while `fecbba0735355738` is a tag.
|
11
|
+
#
|
12
|
+
# Use {tagged} to add custom tags to an entry:
|
13
|
+
# ```ruby
|
14
|
+
# Rage.logger.tagged("ApiCall") do
|
15
|
+
# perform_api_call
|
16
|
+
# Rage.logger.info "success"
|
17
|
+
# end
|
18
|
+
# # => [fecbba0735355738][ApiCall] timestamp=2023-10-19T11:12:56+00:00 pid=1825 level=info message=success
|
19
|
+
# ```
|
20
|
+
#
|
21
|
+
# {with_context} can be used to add custom keys:
|
22
|
+
# ```ruby
|
23
|
+
# cache_key = "mykey"
|
24
|
+
# Rage.logger.with_context(cache_key: cache_key) do
|
25
|
+
# get_from_cache(cache_key)
|
26
|
+
# Rage.logger.info "cache miss"
|
27
|
+
# end
|
28
|
+
# # => [fecbba0735355738] timestamp=2023-10-19T11:12:56+00:00 pid=1825 level=info cache_key=mykey message=cache miss
|
29
|
+
# ```
|
30
|
+
#
|
31
|
+
# `Rage::Logger` also implements the interface of Ruby's native {https://ruby-doc.org/3.2.2/stdlibs/logger/Logger.html Logger}:
|
32
|
+
# ```ruby
|
33
|
+
# Rage.logger.info("Initializing")
|
34
|
+
# Rage.logger.debug { "This is a " + potentially + " expensive operation" }
|
35
|
+
# ```
|
36
|
+
class Rage::Logger
|
37
|
+
METHODS_MAP = {
|
38
|
+
"debug" => Logger::DEBUG,
|
39
|
+
"info" => Logger::INFO,
|
40
|
+
"warn" => Logger::WARN,
|
41
|
+
"error" => Logger::ERROR,
|
42
|
+
"fatal" => Logger::FATAL,
|
43
|
+
"unknown" => Logger::UNKNOWN
|
44
|
+
}
|
45
|
+
private_constant :METHODS_MAP
|
46
|
+
|
47
|
+
attr_reader :level, :formatter
|
48
|
+
|
49
|
+
# Create a new logger.
|
50
|
+
#
|
51
|
+
# @param log [Object] a filename (`String`), IO object (typically `STDOUT`, `STDERR`, or an open file), `nil` (it writes nothing) or `File::NULL` (same as `nil`)
|
52
|
+
# @param level [Integer] logging severity threshold
|
53
|
+
# @param formatter [#call] logging formatter
|
54
|
+
# @param shift_age [Integer, String] number of old log files to keep, or frequency of rotation (`"daily"`, `"weekly"` or `"monthly"`). Default value is `0`, which disables log file rotation
|
55
|
+
# @param shift_size [Integer] maximum log file size in bytes (only applies when `shift_age` is a positive Integer)
|
56
|
+
# @param shift_period_suffix [String] the log file suffix format for daily, weekly or monthly rotation
|
57
|
+
# @param binmode sets whether the logger writes in binary mode
|
58
|
+
def initialize(log, level: Logger::DEBUG, formatter: Rage::TextFormatter.new, shift_age: 0, shift_size: 104857600, shift_period_suffix: "%Y%m%d", binmode: false)
|
59
|
+
if log && log != File::NULL
|
60
|
+
@logdev = Logger::LogDevice.new(log, shift_age:, shift_size:, shift_period_suffix:, binmode:)
|
61
|
+
end
|
62
|
+
|
63
|
+
@formatter = formatter
|
64
|
+
@level = level
|
65
|
+
define_log_methods
|
66
|
+
end
|
67
|
+
|
68
|
+
def level=(level)
|
69
|
+
@level = level
|
70
|
+
define_log_methods
|
71
|
+
end
|
72
|
+
|
73
|
+
def formatter=(formatter)
|
74
|
+
@formatter = formatter
|
75
|
+
define_log_methods
|
76
|
+
end
|
77
|
+
|
78
|
+
# Add custom keys to an entry.
|
79
|
+
#
|
80
|
+
# @param context [Hash] a hash of custom keys
|
81
|
+
# @example
|
82
|
+
# Rage.logger.with_context(key: "mykey") do
|
83
|
+
# Rage.logger.info "cache miss"
|
84
|
+
# end
|
85
|
+
def with_context(context)
|
86
|
+
old_context = Thread.current[:rage_logger][:context]
|
87
|
+
|
88
|
+
if old_context.empty? # there's nothing in the context yet
|
89
|
+
Thread.current[:rage_logger][:context] = context
|
90
|
+
else # it's not the first `with_context` call in the chain
|
91
|
+
Thread.current[:rage_logger][:context] = old_context.merge(context)
|
92
|
+
end
|
93
|
+
|
94
|
+
yield(self)
|
95
|
+
true
|
96
|
+
|
97
|
+
ensure
|
98
|
+
Thread.current[:rage_logger][:context] = old_context
|
99
|
+
end
|
100
|
+
|
101
|
+
# Add a custom tag to an entry.
|
102
|
+
#
|
103
|
+
# @param tag [String] the tag to add to an entry
|
104
|
+
# @example
|
105
|
+
# Rage.logger.tagged("ApiCall") do
|
106
|
+
# Rage.logger.info "success"
|
107
|
+
# end
|
108
|
+
def tagged(tag)
|
109
|
+
Thread.current[:rage_logger][:tags] << tag
|
110
|
+
|
111
|
+
yield(self)
|
112
|
+
true
|
113
|
+
|
114
|
+
ensure
|
115
|
+
Thread.current[:rage_logger][:tags].pop
|
116
|
+
end
|
117
|
+
|
118
|
+
alias_method :with_tag, :tagged
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def define_log_methods
|
123
|
+
methods = METHODS_MAP.map do |level_name, level_val|
|
124
|
+
if @logdev.nil? || level_val < @level
|
125
|
+
# logging is disabled or the log level is higher than the current one
|
126
|
+
<<-RUBY
|
127
|
+
def #{level_name}(msg = nil)
|
128
|
+
false
|
129
|
+
end
|
130
|
+
RUBY
|
131
|
+
elsif defined?(IRB)
|
132
|
+
# the call was made from IRB - don't use the formatter
|
133
|
+
<<-RUBY
|
134
|
+
def #{level_name}(msg = nil)
|
135
|
+
@logdev.write((msg || yield) + "\n")
|
136
|
+
end
|
137
|
+
RUBY
|
138
|
+
elsif @formatter.class.name.start_with?("Rage::")
|
139
|
+
# the call was made from within the application and a built-in formatter is used;
|
140
|
+
# in such case we use the `gen_timestamp` method which is much faster than `Time.now.strftime`;
|
141
|
+
# it's not a standard approach however, so it's used with built-in formatters only
|
142
|
+
<<-RUBY
|
143
|
+
def #{level_name}(msg = nil)
|
144
|
+
@logdev.write(
|
145
|
+
@formatter.call("#{level_name}".freeze, Iodine::Rack::Utils.gen_timestamp, nil, msg || yield)
|
146
|
+
)
|
147
|
+
end
|
148
|
+
RUBY
|
149
|
+
else
|
150
|
+
# the call was made from within the application and a custom formatter is used;
|
151
|
+
# stick to the standard approach of using one of the Log Level constants as sevetiry and `Time.now` as time
|
152
|
+
<<-RUBY
|
153
|
+
def #{level_name}(msg = nil)
|
154
|
+
@logdev.write(
|
155
|
+
@formatter.call(#{level_val}, Time.now, nil, msg || yield)
|
156
|
+
)
|
157
|
+
end
|
158
|
+
RUBY
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
self.class.class_eval(methods.join("\n"))
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Rage::TextFormatter
|
2
|
+
def initialize
|
3
|
+
@pid = Process.pid
|
4
|
+
Iodine.on_state(:on_start) do
|
5
|
+
@pid = Process.pid
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(severity, timestamp, _, message)
|
10
|
+
logger = Thread.current[:rage_logger]
|
11
|
+
tags = logger[:tags]
|
12
|
+
|
13
|
+
if final = logger[:final]
|
14
|
+
params, env = final[:params], final[:env]
|
15
|
+
if params
|
16
|
+
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{params[:controller]} action=#{params[:action]} status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
17
|
+
else
|
18
|
+
# no controller/action keys are written if there are no params
|
19
|
+
return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} status=#{final[:response][0]} duration=#{final[:duration]}\n"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if tags.length == 1
|
24
|
+
tags_msg = "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
25
|
+
elsif tags.length == 2
|
26
|
+
tags_msg = "[#{tags[0]}][#{tags[1]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
27
|
+
else
|
28
|
+
tags_msg = "[#{tags[0]}][#{tags[1]}]"
|
29
|
+
i = 2
|
30
|
+
while i < tags.length
|
31
|
+
tags_msg << "[#{tags[i]}]"
|
32
|
+
i += 1
|
33
|
+
end
|
34
|
+
tags_msg << " timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
|
35
|
+
end
|
36
|
+
|
37
|
+
context = logger[:context]
|
38
|
+
|
39
|
+
if !context.empty?
|
40
|
+
context_msg = ""
|
41
|
+
context.each { |k, v| context_msg << "#{k}=#{v} " }
|
42
|
+
end
|
43
|
+
|
44
|
+
"#{tags_msg} #{context_msg}message=#{message}\n"
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::ParamsParser
|
4
|
+
def self.prepare(env, url_params)
|
5
|
+
has_body, query_string, content_type = env["IODINE_HAS_BODY"], env["QUERY_STRING"], env["CONTENT_TYPE"]
|
6
|
+
|
7
|
+
query_params = Iodine::Rack::Utils.parse_nested_query(query_string) if query_string != ""
|
8
|
+
unless has_body
|
9
|
+
if query_params
|
10
|
+
return query_params.merge!(url_params)
|
11
|
+
else
|
12
|
+
return url_params
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
request_params = if content_type.start_with?("application/json")
|
17
|
+
json_parse(env["rack.input"].read)
|
18
|
+
elsif content_type.start_with?("application/x-www-form-urlencoded")
|
19
|
+
Iodine::Rack::Utils.parse_urlencoded_nested_query(env["rack.input"].read)
|
20
|
+
else
|
21
|
+
Iodine::Rack::Utils.parse_multipart(env["rack.input"], content_type)
|
22
|
+
end
|
23
|
+
|
24
|
+
if request_params && !query_params
|
25
|
+
request_params.merge!(url_params)
|
26
|
+
elsif request_params && query_params
|
27
|
+
request_params.merge!(query_params, url_params)
|
28
|
+
else
|
29
|
+
url_params
|
30
|
+
end
|
31
|
+
|
32
|
+
rescue => e
|
33
|
+
raise Rage::Errors::BadRequest
|
34
|
+
end
|
35
|
+
|
36
|
+
if defined?(::FastJsonparser)
|
37
|
+
def self.json_parse(json)
|
38
|
+
FastJsonparser.parse(json, symbolize_keys: true)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
def self.json_parse(json)
|
42
|
+
JSON.parse(json, symbolize_names: true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/rage/request.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Request
|
4
|
+
# @private
|
5
|
+
def initialize(env)
|
6
|
+
@env = env
|
7
|
+
end
|
8
|
+
|
9
|
+
# Get the request headers.
|
10
|
+
# @example
|
11
|
+
# request.headers["Content-Type"] # => "application/json"
|
12
|
+
# request.headers["Connection"] # => "keep-alive"
|
13
|
+
def headers
|
14
|
+
@headers ||= Headers.new(@env)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @private
|
18
|
+
class Headers
|
19
|
+
HTTP = "HTTP_"
|
20
|
+
|
21
|
+
def initialize(env)
|
22
|
+
@env = env
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](requested_header)
|
26
|
+
if requested_header.start_with?(HTTP)
|
27
|
+
@env[requested_header]
|
28
|
+
else
|
29
|
+
(requested_header = requested_header.tr("-", "_")).upcase!
|
30
|
+
|
31
|
+
if "CONTENT_TYPE" == requested_header || "CONTENT_LENGTH" == requested_header
|
32
|
+
@env[requested_header]
|
33
|
+
else
|
34
|
+
@env["#{HTTP}#{requested_header}"]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def inspect
|
40
|
+
headers = @env.select { |k| k == "CONTENT_TYPE" || k == "CONTENT_LENGTH" || k.start_with?(HTTP) }
|
41
|
+
"#<#{self.class.name} @headers=#{headers.inspect}"
|
42
|
+
end
|
43
|
+
end # class Headers
|
44
|
+
end
|
data/lib/rage/router/backend.rb
CHANGED
@@ -14,8 +14,36 @@ class Rage::Router::Backend
|
|
14
14
|
@constrainer = Rage::Router::Constrainer.new({})
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def mount(path, handler, methods)
|
18
|
+
raise "Mount handler should respond to `call`" unless handler.respond_to?(:call)
|
19
|
+
|
18
20
|
raw_handler = handler
|
21
|
+
is_sidekiq = handler.respond_to?(:name) && handler.name == "Sidekiq::Web"
|
22
|
+
|
23
|
+
handler = ->(env, _params) do
|
24
|
+
env["SCRIPT_NAME"] = path
|
25
|
+
sub_path = env["PATH_INFO"].delete_prefix!(path)
|
26
|
+
env["PATH_INFO"] = "/" if sub_path == ""
|
27
|
+
|
28
|
+
if is_sidekiq
|
29
|
+
Rage::SidekiqSession.with_session(env) do
|
30
|
+
raw_handler.call(env)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
raw_handler.call(env)
|
34
|
+
end
|
35
|
+
|
36
|
+
ensure
|
37
|
+
env["PATH_INFO"] = "#{env["SCRIPT_NAME"]}#{sub_path}"
|
38
|
+
end
|
39
|
+
|
40
|
+
methods.each do |method|
|
41
|
+
__on(method, path, handler, {}, {}, { raw_handler:, mount: true })
|
42
|
+
__on(method, "#{path}/*", handler, {}, {}, { raw_handler:, mount: true })
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def on(method, path, handler, constraints: {}, defaults: nil)
|
19
47
|
raise "Path could not be empty" if path&.empty?
|
20
48
|
|
21
49
|
if match_index = (path =~ OPTIONAL_PARAM_REGEXP)
|
@@ -29,12 +57,17 @@ class Rage::Router::Backend
|
|
29
57
|
return
|
30
58
|
end
|
31
59
|
|
60
|
+
meta = { raw_handler: handler }
|
61
|
+
|
32
62
|
if handler.is_a?(String)
|
33
63
|
raise "Invalid route handler format, expected to match the 'controller#action' pattern" unless handler =~ STRING_HANDLER_REGEXP
|
34
64
|
|
35
65
|
controller, action = to_controller_class($1), $2
|
36
66
|
run_action_method_name = controller.__register_action(action.to_sym)
|
37
67
|
|
68
|
+
meta[:controller] = $1
|
69
|
+
meta[:action] = $2
|
70
|
+
|
38
71
|
handler = eval("->(env, params) { #{controller}.new(env, params).#{run_action_method_name} }")
|
39
72
|
else
|
40
73
|
raise "Non-string route handler should respond to `call`" unless handler.respond_to?(:call)
|
@@ -45,7 +78,7 @@ class Rage::Router::Backend
|
|
45
78
|
handler = ->(env, _params) { orig_handler.call(env) }
|
46
79
|
end
|
47
80
|
|
48
|
-
__on(method, path, handler,
|
81
|
+
__on(method, path, handler, constraints, defaults, meta)
|
49
82
|
end
|
50
83
|
|
51
84
|
def lookup(env)
|
@@ -55,7 +88,7 @@ class Rage::Router::Backend
|
|
55
88
|
|
56
89
|
private
|
57
90
|
|
58
|
-
def __on(method, path, handler,
|
91
|
+
def __on(method, path, handler, constraints, defaults, meta)
|
59
92
|
@constrainer.validate_constraints(constraints)
|
60
93
|
# Let the constrainer know if any constraints are being used now
|
61
94
|
@constrainer.note_usage(constraints)
|
@@ -162,7 +195,7 @@ class Rage::Router::Backend
|
|
162
195
|
end
|
163
196
|
end
|
164
197
|
|
165
|
-
route = { method
|
198
|
+
route = { method:, path:, pattern:, params:, constraints:, handler:, defaults:, meta: }
|
166
199
|
@routes << route
|
167
200
|
current_node.add_route(route, @constrainer)
|
168
201
|
end
|