tina4ruby 3.11.13 → 3.11.15
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 +4 -4
- data/CHANGELOG.md +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/router.rb
CHANGED
|
@@ -1,406 +1,406 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
class Route
|
|
5
|
-
attr_reader :method, :path, :handler, :auth_handler, :swagger_meta,
|
|
6
|
-
:path_regex, :param_names, :template
|
|
7
|
-
attr_accessor :auth_required, :cached
|
|
8
|
-
|
|
9
|
-
def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
10
|
-
@method = method.to_s.upcase.freeze
|
|
11
|
-
@path = normalize_path(path).freeze
|
|
12
|
-
@handler = handler
|
|
13
|
-
@auth_handler = auth_handler
|
|
14
|
-
@swagger_meta = swagger_meta
|
|
15
|
-
@middleware = middleware.freeze
|
|
16
|
-
@template = template&.freeze
|
|
17
|
-
# Write routes are secure by default, unless custom middleware is registered
|
|
18
|
-
# (developer handles auth themselves via middleware)
|
|
19
|
-
@auth_required = %w[POST PUT PATCH DELETE].include?(@method) && middleware.empty?
|
|
20
|
-
@cached = false
|
|
21
|
-
@param_names = []
|
|
22
|
-
@path_regex = compile_pattern(@path)
|
|
23
|
-
@param_names.freeze
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Mark this route as requiring bearer-token authentication.
|
|
27
|
-
# Returns self for chaining: Router.get("/path") { ... }.secure
|
|
28
|
-
def secure
|
|
29
|
-
@auth_required = true
|
|
30
|
-
self
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Opt out of the secure-by-default auth on write routes.
|
|
34
|
-
# Returns self for chaining: Router.post("/login") { ... }.no_auth
|
|
35
|
-
def no_auth
|
|
36
|
-
@auth_required = false
|
|
37
|
-
self
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Mark this route as cacheable.
|
|
41
|
-
# Returns self for chaining: Router.get("/path") { ... }.cache
|
|
42
|
-
def cache
|
|
43
|
-
@cached = true
|
|
44
|
-
self
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Dual-mode: getter (no args) returns the middleware array;
|
|
48
|
-
# setter (with args) appends middleware and returns self for chaining.
|
|
49
|
-
# Router.post("/api") { ... }.middleware(AuthMiddleware)
|
|
50
|
-
def middleware(*middleware_classes)
|
|
51
|
-
return @middleware if middleware_classes.empty?
|
|
52
|
-
|
|
53
|
-
@middleware = @middleware.dup + middleware_classes
|
|
54
|
-
# Custom middleware means developer handles auth — disable built-in gate
|
|
55
|
-
# unless .secure was explicitly called.
|
|
56
|
-
@auth_required = false unless @auth_required
|
|
57
|
-
self
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Returns params hash if matched, false otherwise
|
|
61
|
-
def match?(request_path, request_method = nil)
|
|
62
|
-
return false if request_method && @method != "ANY" && @method != request_method.to_s.upcase
|
|
63
|
-
match_path(request_path)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Returns params hash if matched, false otherwise
|
|
67
|
-
def match_path(request_path)
|
|
68
|
-
match = @path_regex.match(request_path)
|
|
69
|
-
return false unless match
|
|
70
|
-
|
|
71
|
-
if @param_names.empty?
|
|
72
|
-
{}
|
|
73
|
-
else
|
|
74
|
-
params = {}
|
|
75
|
-
@param_names.each_with_index do |param_def, i|
|
|
76
|
-
raw_value = match[i + 1]
|
|
77
|
-
params[param_def[:name]] = cast_param(raw_value, param_def[:type])
|
|
78
|
-
end
|
|
79
|
-
params
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Run per-route middleware chain; returns true if all pass
|
|
84
|
-
def run_middleware(request, response)
|
|
85
|
-
@middleware.each do |mw|
|
|
86
|
-
result = mw.call(request, response)
|
|
87
|
-
return false if result == false
|
|
88
|
-
end
|
|
89
|
-
true
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
def normalize_path(path)
|
|
95
|
-
p = path.to_s.gsub("\\", "/")
|
|
96
|
-
p = "/#{p}" unless p.start_with?("/")
|
|
97
|
-
p = p.chomp("/") unless p == "/"
|
|
98
|
-
p
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Supported typed-parameter constraints. Mirrored verbatim in
|
|
102
|
-
# tina4-python / tina4-php / tina4-nodejs for cross-framework parity.
|
|
103
|
-
#
|
|
104
|
-
# Any type name not in this table raises ``ArgumentError`` at route
|
|
105
|
-
# registration — we never silently fall through to the default matcher,
|
|
106
|
-
# because a typo like ``{id:inetger}`` would otherwise match anything
|
|
107
|
-
# and create a security footgun (see tina4-book#125).
|
|
108
|
-
PARAM_TYPE_PATTERNS = {
|
|
109
|
-
"string" => "[^/]+", # default, any non-slash segment
|
|
110
|
-
"int" => '\d+',
|
|
111
|
-
"integer" => '\d+',
|
|
112
|
-
"float" => '[\d.]+',
|
|
113
|
-
"number" => '[\d.]+',
|
|
114
|
-
"alpha" => "[A-Za-z]+", # letters only
|
|
115
|
-
"alnum" => "[A-Za-z0-9]+", # letters + digits
|
|
116
|
-
"slug" => "[a-z0-9-]+", # URL slug
|
|
117
|
-
"uuid" => "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
118
|
-
"path" => ".+", # greedy
|
|
119
|
-
".*" => ".+",
|
|
120
|
-
}.freeze
|
|
121
|
-
|
|
122
|
-
def compile_pattern(path)
|
|
123
|
-
return Regexp.new("\\A/\\z") if path == "/"
|
|
124
|
-
|
|
125
|
-
parts = path.split("/").reject(&:empty?)
|
|
126
|
-
regex_parts = parts.map do |part|
|
|
127
|
-
if part =~ /\A\*(\w+)\z/
|
|
128
|
-
# Named catch-all splat parameter: *path captures everything after
|
|
129
|
-
name = Regexp.last_match(1)
|
|
130
|
-
@param_names << { name: name.to_sym, type: "path" }
|
|
131
|
-
'(.+)'
|
|
132
|
-
elsif part == "*"
|
|
133
|
-
# Bare catch-all wildcard: captures everything after under the "*" key
|
|
134
|
-
# to match Python/PHP/Node parity (docs say `request.params["*"]`).
|
|
135
|
-
@param_names << { name: :"*", type: "path" }
|
|
136
|
-
'(.+)'
|
|
137
|
-
elsif part =~ /\A\{(\w+)(?::([\w.*]+))?\}\z/
|
|
138
|
-
# Tina4/Python-style brace params: {id} or {id:int}
|
|
139
|
-
# This is the ONLY supported param syntax, matching Python exactly.
|
|
140
|
-
# Do NOT add :id (colon) style params.
|
|
141
|
-
name = Regexp.last_match(1)
|
|
142
|
-
type = Regexp.last_match(2) || "string"
|
|
143
|
-
unless PARAM_TYPE_PATTERNS.key?(type)
|
|
144
|
-
valid = PARAM_TYPE_PATTERNS.keys.reject { |k| k == ".*" }.sort.join(", ")
|
|
145
|
-
raise ArgumentError,
|
|
146
|
-
"Unknown param type '#{type}' in route '#{path}'. Valid types: #{valid}."
|
|
147
|
-
end
|
|
148
|
-
@param_names << { name: name.to_sym, type: type }
|
|
149
|
-
"(#{PARAM_TYPE_PATTERNS[type]})"
|
|
150
|
-
else
|
|
151
|
-
Regexp.escape(part)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
Regexp.new("\\A/#{regex_parts.join("/")}\\z")
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def cast_param(value, type)
|
|
158
|
-
case type
|
|
159
|
-
when "int", "integer"
|
|
160
|
-
value.to_i
|
|
161
|
-
when "float", "number"
|
|
162
|
-
value.to_f
|
|
163
|
-
else
|
|
164
|
-
value
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# A registered WebSocket route with path pattern matching (reuses Route's compile logic)
|
|
170
|
-
class WebSocketRoute
|
|
171
|
-
attr_reader :path, :handler, :path_regex, :param_names
|
|
172
|
-
|
|
173
|
-
def initialize(path, handler)
|
|
174
|
-
@path = normalize_path(path).freeze
|
|
175
|
-
@handler = handler
|
|
176
|
-
@param_names = []
|
|
177
|
-
@path_regex = compile_pattern(@path)
|
|
178
|
-
@param_names.freeze
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Returns params hash if matched, false otherwise
|
|
182
|
-
def match?(request_path)
|
|
183
|
-
match = @path_regex.match(request_path)
|
|
184
|
-
return false unless match
|
|
185
|
-
|
|
186
|
-
if @param_names.empty?
|
|
187
|
-
{}
|
|
188
|
-
else
|
|
189
|
-
params = {}
|
|
190
|
-
@param_names.each_with_index do |param_def, i|
|
|
191
|
-
raw_value = match[i + 1]
|
|
192
|
-
params[param_def[:name]] = raw_value
|
|
193
|
-
end
|
|
194
|
-
params
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
private
|
|
199
|
-
|
|
200
|
-
def normalize_path(path)
|
|
201
|
-
p = path.to_s.gsub("\\", "/")
|
|
202
|
-
p = "/#{p}" unless p.start_with?("/")
|
|
203
|
-
p = p.chomp("/") unless p == "/"
|
|
204
|
-
p
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def compile_pattern(path)
|
|
208
|
-
return Regexp.new("\\A/\\z") if path == "/"
|
|
209
|
-
|
|
210
|
-
parts = path.split("/").reject(&:empty?)
|
|
211
|
-
regex_parts = parts.map do |part|
|
|
212
|
-
if part =~ /\A\{(\w+)\}\z/
|
|
213
|
-
name = Regexp.last_match(1)
|
|
214
|
-
@param_names << { name: name.to_sym }
|
|
215
|
-
'([^/]+)'
|
|
216
|
-
else
|
|
217
|
-
Regexp.escape(part)
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
Regexp.new("\\A/#{regex_parts.join("/")}\\z")
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
module Router
|
|
225
|
-
class << self
|
|
226
|
-
def routes
|
|
227
|
-
@routes ||= []
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def get_routes
|
|
231
|
-
routes
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def list_routes
|
|
235
|
-
routes
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Registered WebSocket routes
|
|
239
|
-
def ws_routes
|
|
240
|
-
@ws_routes ||= []
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Parity alias — returns all registered WebSocket routes.
|
|
244
|
-
def get_web_socket_routes
|
|
245
|
-
ws_routes
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Register a WebSocket route.
|
|
249
|
-
# The handler block receives (connection, event, data) where:
|
|
250
|
-
# connection — WebSocketConnection with #send, #broadcast, #close, #params
|
|
251
|
-
# event — :open, :message, or :close
|
|
252
|
-
# data — String payload for :message, nil for :open/:close
|
|
253
|
-
def websocket(path, &block)
|
|
254
|
-
ws_route = WebSocketRoute.new(path, block)
|
|
255
|
-
ws_routes << ws_route
|
|
256
|
-
Tina4::Log.debug("WebSocket route registered: #{path}")
|
|
257
|
-
ws_route
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# Find a matching WebSocket route for a given path.
|
|
261
|
-
# Returns [ws_route, params] or nil.
|
|
262
|
-
def find_ws_route(path)
|
|
263
|
-
normalized = path.gsub("\\", "/")
|
|
264
|
-
normalized = "/#{normalized}" unless normalized.start_with?("/")
|
|
265
|
-
normalized = normalized.chomp("/") unless normalized == "/"
|
|
266
|
-
|
|
267
|
-
ws_routes.each do |ws_route|
|
|
268
|
-
params = ws_route.match?(normalized)
|
|
269
|
-
return [ws_route, params] if params
|
|
270
|
-
end
|
|
271
|
-
nil
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Routes indexed by HTTP method for O(1) method lookup
|
|
275
|
-
def method_index
|
|
276
|
-
@method_index ||= Hash.new { |h, k| h[k] = [] }
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def add(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
280
|
-
route = Route.new(method, path, handler,
|
|
281
|
-
auth_handler: auth_handler,
|
|
282
|
-
swagger_meta: swagger_meta,
|
|
283
|
-
middleware: middleware,
|
|
284
|
-
template: template)
|
|
285
|
-
routes << route
|
|
286
|
-
method_index[route.method] << route
|
|
287
|
-
Tina4::Log.debug("Route registered: #{method.upcase} #{path}")
|
|
288
|
-
route
|
|
289
|
-
end
|
|
290
|
-
# Convenience registration methods
|
|
291
|
-
def get(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
292
|
-
add("GET", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def post(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
296
|
-
add("POST", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
def put(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
300
|
-
add("PUT", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def patch(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
304
|
-
add("PATCH", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def delete(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
308
|
-
add("DELETE", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def any(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
312
|
-
add("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def find_route(method, path)
|
|
316
|
-
normalized_method = method.upcase
|
|
317
|
-
# Normalize path once (not per-route)
|
|
318
|
-
normalized_path = path.gsub("\\", "/")
|
|
319
|
-
normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
|
|
320
|
-
normalized_path = normalized_path.chomp("/") unless normalized_path == "/"
|
|
321
|
-
|
|
322
|
-
# Check ANY routes first, then method-specific routes
|
|
323
|
-
candidates = (method_index["ANY"] || []) + (method_index[normalized_method] || [])
|
|
324
|
-
candidates.each do |route|
|
|
325
|
-
params = route.match_path(normalized_path)
|
|
326
|
-
return [route, params] if params
|
|
327
|
-
end
|
|
328
|
-
nil
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Find a route matching method + path. Returns [route, params] or nil.
|
|
332
|
-
# match(method, path) — consistent with Python, PHP, and Node.
|
|
333
|
-
def match(method, path)
|
|
334
|
-
find_route(method, path)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
# Register a class-based middleware globally.
|
|
338
|
-
# The class should define static before_* and/or after_* methods.
|
|
339
|
-
# Example:
|
|
340
|
-
# class AuthMiddleware
|
|
341
|
-
# def self.before_auth(request, response)
|
|
342
|
-
# unless request.headers["authorization"]
|
|
343
|
-
# return [request, response.json({ error: "Unauthorized" }, 401)]
|
|
344
|
-
# end
|
|
345
|
-
# [request, response]
|
|
346
|
-
# end
|
|
347
|
-
# end
|
|
348
|
-
# Tina4::Router.use(AuthMiddleware)
|
|
349
|
-
def use(klass)
|
|
350
|
-
Tina4::Middleware.use(klass)
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def clear!
|
|
354
|
-
@routes = []
|
|
355
|
-
@method_index = Hash.new { |h, k| h[k] = [] }
|
|
356
|
-
@ws_routes = []
|
|
357
|
-
end
|
|
358
|
-
alias clear clear!
|
|
359
|
-
|
|
360
|
-
def group(prefix, auth_handler: nil, middleware: [], &block)
|
|
361
|
-
GroupContext.new(prefix, auth_handler, middleware).instance_eval(&block)
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# Load route files from a directory (file-based route discovery)
|
|
365
|
-
def load_routes(directory)
|
|
366
|
-
return unless Dir.exist?(directory)
|
|
367
|
-
Dir.glob(File.join(directory, "**/*.rb")).sort.each do |file|
|
|
368
|
-
begin
|
|
369
|
-
load file
|
|
370
|
-
Tina4::Log.debug("Route loaded: #{file}")
|
|
371
|
-
rescue => e
|
|
372
|
-
Tina4::Log.error("Failed to load route #{file}: #{e.message}")
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
class GroupContext
|
|
379
|
-
def initialize(prefix, auth_handler = nil, middleware = [])
|
|
380
|
-
@prefix = prefix.chomp("/")
|
|
381
|
-
@auth_handler = auth_handler
|
|
382
|
-
@middleware = middleware
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
%w[get post put patch delete any].each do |m|
|
|
386
|
-
define_method(m) do |path, middleware: [], swagger_meta: {}, template: nil, &handler|
|
|
387
|
-
full_path = "#{@prefix}#{path}"
|
|
388
|
-
combined_middleware = @middleware + middleware
|
|
389
|
-
Tina4::Router.add(m, full_path, handler,
|
|
390
|
-
auth_handler: @auth_handler,
|
|
391
|
-
swagger_meta: swagger_meta,
|
|
392
|
-
middleware: combined_middleware,
|
|
393
|
-
template: template)
|
|
394
|
-
end
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# Nested groups
|
|
398
|
-
def group(prefix, auth_handler: nil, middleware: [], &block)
|
|
399
|
-
full_prefix = "#{@prefix}#{prefix}"
|
|
400
|
-
combined_middleware = @middleware + middleware
|
|
401
|
-
nested_auth = auth_handler || @auth_handler
|
|
402
|
-
GroupContext.new(full_prefix, nested_auth, combined_middleware).instance_eval(&block)
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
end
|
|
406
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
class Route
|
|
5
|
+
attr_reader :method, :path, :handler, :auth_handler, :swagger_meta,
|
|
6
|
+
:path_regex, :param_names, :template
|
|
7
|
+
attr_accessor :auth_required, :cached
|
|
8
|
+
|
|
9
|
+
def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
10
|
+
@method = method.to_s.upcase.freeze
|
|
11
|
+
@path = normalize_path(path).freeze
|
|
12
|
+
@handler = handler
|
|
13
|
+
@auth_handler = auth_handler
|
|
14
|
+
@swagger_meta = swagger_meta
|
|
15
|
+
@middleware = middleware.freeze
|
|
16
|
+
@template = template&.freeze
|
|
17
|
+
# Write routes are secure by default, unless custom middleware is registered
|
|
18
|
+
# (developer handles auth themselves via middleware)
|
|
19
|
+
@auth_required = %w[POST PUT PATCH DELETE].include?(@method) && middleware.empty?
|
|
20
|
+
@cached = false
|
|
21
|
+
@param_names = []
|
|
22
|
+
@path_regex = compile_pattern(@path)
|
|
23
|
+
@param_names.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Mark this route as requiring bearer-token authentication.
|
|
27
|
+
# Returns self for chaining: Router.get("/path") { ... }.secure
|
|
28
|
+
def secure
|
|
29
|
+
@auth_required = true
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Opt out of the secure-by-default auth on write routes.
|
|
34
|
+
# Returns self for chaining: Router.post("/login") { ... }.no_auth
|
|
35
|
+
def no_auth
|
|
36
|
+
@auth_required = false
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Mark this route as cacheable.
|
|
41
|
+
# Returns self for chaining: Router.get("/path") { ... }.cache
|
|
42
|
+
def cache
|
|
43
|
+
@cached = true
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Dual-mode: getter (no args) returns the middleware array;
|
|
48
|
+
# setter (with args) appends middleware and returns self for chaining.
|
|
49
|
+
# Router.post("/api") { ... }.middleware(AuthMiddleware)
|
|
50
|
+
def middleware(*middleware_classes)
|
|
51
|
+
return @middleware if middleware_classes.empty?
|
|
52
|
+
|
|
53
|
+
@middleware = @middleware.dup + middleware_classes
|
|
54
|
+
# Custom middleware means developer handles auth — disable built-in gate
|
|
55
|
+
# unless .secure was explicitly called.
|
|
56
|
+
@auth_required = false unless @auth_required
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns params hash if matched, false otherwise
|
|
61
|
+
def match?(request_path, request_method = nil)
|
|
62
|
+
return false if request_method && @method != "ANY" && @method != request_method.to_s.upcase
|
|
63
|
+
match_path(request_path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns params hash if matched, false otherwise
|
|
67
|
+
def match_path(request_path)
|
|
68
|
+
match = @path_regex.match(request_path)
|
|
69
|
+
return false unless match
|
|
70
|
+
|
|
71
|
+
if @param_names.empty?
|
|
72
|
+
{}
|
|
73
|
+
else
|
|
74
|
+
params = {}
|
|
75
|
+
@param_names.each_with_index do |param_def, i|
|
|
76
|
+
raw_value = match[i + 1]
|
|
77
|
+
params[param_def[:name]] = cast_param(raw_value, param_def[:type])
|
|
78
|
+
end
|
|
79
|
+
params
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Run per-route middleware chain; returns true if all pass
|
|
84
|
+
def run_middleware(request, response)
|
|
85
|
+
@middleware.each do |mw|
|
|
86
|
+
result = mw.call(request, response)
|
|
87
|
+
return false if result == false
|
|
88
|
+
end
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def normalize_path(path)
|
|
95
|
+
p = path.to_s.gsub("\\", "/")
|
|
96
|
+
p = "/#{p}" unless p.start_with?("/")
|
|
97
|
+
p = p.chomp("/") unless p == "/"
|
|
98
|
+
p
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Supported typed-parameter constraints. Mirrored verbatim in
|
|
102
|
+
# tina4-python / tina4-php / tina4-nodejs for cross-framework parity.
|
|
103
|
+
#
|
|
104
|
+
# Any type name not in this table raises ``ArgumentError`` at route
|
|
105
|
+
# registration — we never silently fall through to the default matcher,
|
|
106
|
+
# because a typo like ``{id:inetger}`` would otherwise match anything
|
|
107
|
+
# and create a security footgun (see tina4-book#125).
|
|
108
|
+
PARAM_TYPE_PATTERNS = {
|
|
109
|
+
"string" => "[^/]+", # default, any non-slash segment
|
|
110
|
+
"int" => '\d+',
|
|
111
|
+
"integer" => '\d+',
|
|
112
|
+
"float" => '[\d.]+',
|
|
113
|
+
"number" => '[\d.]+',
|
|
114
|
+
"alpha" => "[A-Za-z]+", # letters only
|
|
115
|
+
"alnum" => "[A-Za-z0-9]+", # letters + digits
|
|
116
|
+
"slug" => "[a-z0-9-]+", # URL slug
|
|
117
|
+
"uuid" => "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
118
|
+
"path" => ".+", # greedy
|
|
119
|
+
".*" => ".+",
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
def compile_pattern(path)
|
|
123
|
+
return Regexp.new("\\A/\\z") if path == "/"
|
|
124
|
+
|
|
125
|
+
parts = path.split("/").reject(&:empty?)
|
|
126
|
+
regex_parts = parts.map do |part|
|
|
127
|
+
if part =~ /\A\*(\w+)\z/
|
|
128
|
+
# Named catch-all splat parameter: *path captures everything after
|
|
129
|
+
name = Regexp.last_match(1)
|
|
130
|
+
@param_names << { name: name.to_sym, type: "path" }
|
|
131
|
+
'(.+)'
|
|
132
|
+
elsif part == "*"
|
|
133
|
+
# Bare catch-all wildcard: captures everything after under the "*" key
|
|
134
|
+
# to match Python/PHP/Node parity (docs say `request.params["*"]`).
|
|
135
|
+
@param_names << { name: :"*", type: "path" }
|
|
136
|
+
'(.+)'
|
|
137
|
+
elsif part =~ /\A\{(\w+)(?::([\w.*]+))?\}\z/
|
|
138
|
+
# Tina4/Python-style brace params: {id} or {id:int}
|
|
139
|
+
# This is the ONLY supported param syntax, matching Python exactly.
|
|
140
|
+
# Do NOT add :id (colon) style params.
|
|
141
|
+
name = Regexp.last_match(1)
|
|
142
|
+
type = Regexp.last_match(2) || "string"
|
|
143
|
+
unless PARAM_TYPE_PATTERNS.key?(type)
|
|
144
|
+
valid = PARAM_TYPE_PATTERNS.keys.reject { |k| k == ".*" }.sort.join(", ")
|
|
145
|
+
raise ArgumentError,
|
|
146
|
+
"Unknown param type '#{type}' in route '#{path}'. Valid types: #{valid}."
|
|
147
|
+
end
|
|
148
|
+
@param_names << { name: name.to_sym, type: type }
|
|
149
|
+
"(#{PARAM_TYPE_PATTERNS[type]})"
|
|
150
|
+
else
|
|
151
|
+
Regexp.escape(part)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
Regexp.new("\\A/#{regex_parts.join("/")}\\z")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def cast_param(value, type)
|
|
158
|
+
case type
|
|
159
|
+
when "int", "integer"
|
|
160
|
+
value.to_i
|
|
161
|
+
when "float", "number"
|
|
162
|
+
value.to_f
|
|
163
|
+
else
|
|
164
|
+
value
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A registered WebSocket route with path pattern matching (reuses Route's compile logic)
|
|
170
|
+
class WebSocketRoute
|
|
171
|
+
attr_reader :path, :handler, :path_regex, :param_names
|
|
172
|
+
|
|
173
|
+
def initialize(path, handler)
|
|
174
|
+
@path = normalize_path(path).freeze
|
|
175
|
+
@handler = handler
|
|
176
|
+
@param_names = []
|
|
177
|
+
@path_regex = compile_pattern(@path)
|
|
178
|
+
@param_names.freeze
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Returns params hash if matched, false otherwise
|
|
182
|
+
def match?(request_path)
|
|
183
|
+
match = @path_regex.match(request_path)
|
|
184
|
+
return false unless match
|
|
185
|
+
|
|
186
|
+
if @param_names.empty?
|
|
187
|
+
{}
|
|
188
|
+
else
|
|
189
|
+
params = {}
|
|
190
|
+
@param_names.each_with_index do |param_def, i|
|
|
191
|
+
raw_value = match[i + 1]
|
|
192
|
+
params[param_def[:name]] = raw_value
|
|
193
|
+
end
|
|
194
|
+
params
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def normalize_path(path)
|
|
201
|
+
p = path.to_s.gsub("\\", "/")
|
|
202
|
+
p = "/#{p}" unless p.start_with?("/")
|
|
203
|
+
p = p.chomp("/") unless p == "/"
|
|
204
|
+
p
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def compile_pattern(path)
|
|
208
|
+
return Regexp.new("\\A/\\z") if path == "/"
|
|
209
|
+
|
|
210
|
+
parts = path.split("/").reject(&:empty?)
|
|
211
|
+
regex_parts = parts.map do |part|
|
|
212
|
+
if part =~ /\A\{(\w+)\}\z/
|
|
213
|
+
name = Regexp.last_match(1)
|
|
214
|
+
@param_names << { name: name.to_sym }
|
|
215
|
+
'([^/]+)'
|
|
216
|
+
else
|
|
217
|
+
Regexp.escape(part)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
Regexp.new("\\A/#{regex_parts.join("/")}\\z")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
module Router
|
|
225
|
+
class << self
|
|
226
|
+
def routes
|
|
227
|
+
@routes ||= []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def get_routes
|
|
231
|
+
routes
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def list_routes
|
|
235
|
+
routes
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Registered WebSocket routes
|
|
239
|
+
def ws_routes
|
|
240
|
+
@ws_routes ||= []
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Parity alias — returns all registered WebSocket routes.
|
|
244
|
+
def get_web_socket_routes
|
|
245
|
+
ws_routes
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Register a WebSocket route.
|
|
249
|
+
# The handler block receives (connection, event, data) where:
|
|
250
|
+
# connection — WebSocketConnection with #send, #broadcast, #close, #params
|
|
251
|
+
# event — :open, :message, or :close
|
|
252
|
+
# data — String payload for :message, nil for :open/:close
|
|
253
|
+
def websocket(path, &block)
|
|
254
|
+
ws_route = WebSocketRoute.new(path, block)
|
|
255
|
+
ws_routes << ws_route
|
|
256
|
+
Tina4::Log.debug("WebSocket route registered: #{path}")
|
|
257
|
+
ws_route
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Find a matching WebSocket route for a given path.
|
|
261
|
+
# Returns [ws_route, params] or nil.
|
|
262
|
+
def find_ws_route(path)
|
|
263
|
+
normalized = path.gsub("\\", "/")
|
|
264
|
+
normalized = "/#{normalized}" unless normalized.start_with?("/")
|
|
265
|
+
normalized = normalized.chomp("/") unless normalized == "/"
|
|
266
|
+
|
|
267
|
+
ws_routes.each do |ws_route|
|
|
268
|
+
params = ws_route.match?(normalized)
|
|
269
|
+
return [ws_route, params] if params
|
|
270
|
+
end
|
|
271
|
+
nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Routes indexed by HTTP method for O(1) method lookup
|
|
275
|
+
def method_index
|
|
276
|
+
@method_index ||= Hash.new { |h, k| h[k] = [] }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def add(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
280
|
+
route = Route.new(method, path, handler,
|
|
281
|
+
auth_handler: auth_handler,
|
|
282
|
+
swagger_meta: swagger_meta,
|
|
283
|
+
middleware: middleware,
|
|
284
|
+
template: template)
|
|
285
|
+
routes << route
|
|
286
|
+
method_index[route.method] << route
|
|
287
|
+
Tina4::Log.debug("Route registered: #{method.upcase} #{path}")
|
|
288
|
+
route
|
|
289
|
+
end
|
|
290
|
+
# Convenience registration methods
|
|
291
|
+
def get(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
292
|
+
add("GET", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def post(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
296
|
+
add("POST", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def put(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
300
|
+
add("PUT", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def patch(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
304
|
+
add("PATCH", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def delete(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
308
|
+
add("DELETE", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def any(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
312
|
+
add("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def find_route(method, path)
|
|
316
|
+
normalized_method = method.upcase
|
|
317
|
+
# Normalize path once (not per-route)
|
|
318
|
+
normalized_path = path.gsub("\\", "/")
|
|
319
|
+
normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
|
|
320
|
+
normalized_path = normalized_path.chomp("/") unless normalized_path == "/"
|
|
321
|
+
|
|
322
|
+
# Check ANY routes first, then method-specific routes
|
|
323
|
+
candidates = (method_index["ANY"] || []) + (method_index[normalized_method] || [])
|
|
324
|
+
candidates.each do |route|
|
|
325
|
+
params = route.match_path(normalized_path)
|
|
326
|
+
return [route, params] if params
|
|
327
|
+
end
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Find a route matching method + path. Returns [route, params] or nil.
|
|
332
|
+
# match(method, path) — consistent with Python, PHP, and Node.
|
|
333
|
+
def match(method, path)
|
|
334
|
+
find_route(method, path)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Register a class-based middleware globally.
|
|
338
|
+
# The class should define static before_* and/or after_* methods.
|
|
339
|
+
# Example:
|
|
340
|
+
# class AuthMiddleware
|
|
341
|
+
# def self.before_auth(request, response)
|
|
342
|
+
# unless request.headers["authorization"]
|
|
343
|
+
# return [request, response.json({ error: "Unauthorized" }, 401)]
|
|
344
|
+
# end
|
|
345
|
+
# [request, response]
|
|
346
|
+
# end
|
|
347
|
+
# end
|
|
348
|
+
# Tina4::Router.use(AuthMiddleware)
|
|
349
|
+
def use(klass)
|
|
350
|
+
Tina4::Middleware.use(klass)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def clear!
|
|
354
|
+
@routes = []
|
|
355
|
+
@method_index = Hash.new { |h, k| h[k] = [] }
|
|
356
|
+
@ws_routes = []
|
|
357
|
+
end
|
|
358
|
+
alias clear clear!
|
|
359
|
+
|
|
360
|
+
def group(prefix, auth_handler: nil, middleware: [], &block)
|
|
361
|
+
GroupContext.new(prefix, auth_handler, middleware).instance_eval(&block)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Load route files from a directory (file-based route discovery)
|
|
365
|
+
def load_routes(directory)
|
|
366
|
+
return unless Dir.exist?(directory)
|
|
367
|
+
Dir.glob(File.join(directory, "**/*.rb")).sort.each do |file|
|
|
368
|
+
begin
|
|
369
|
+
load file
|
|
370
|
+
Tina4::Log.debug("Route loaded: #{file}")
|
|
371
|
+
rescue => e
|
|
372
|
+
Tina4::Log.error("Failed to load route #{file}: #{e.message}")
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
class GroupContext
|
|
379
|
+
def initialize(prefix, auth_handler = nil, middleware = [])
|
|
380
|
+
@prefix = prefix.chomp("/")
|
|
381
|
+
@auth_handler = auth_handler
|
|
382
|
+
@middleware = middleware
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
%w[get post put patch delete any].each do |m|
|
|
386
|
+
define_method(m) do |path, middleware: [], swagger_meta: {}, template: nil, &handler|
|
|
387
|
+
full_path = "#{@prefix}#{path}"
|
|
388
|
+
combined_middleware = @middleware + middleware
|
|
389
|
+
Tina4::Router.add(m, full_path, handler,
|
|
390
|
+
auth_handler: @auth_handler,
|
|
391
|
+
swagger_meta: swagger_meta,
|
|
392
|
+
middleware: combined_middleware,
|
|
393
|
+
template: template)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Nested groups
|
|
398
|
+
def group(prefix, auth_handler: nil, middleware: [], &block)
|
|
399
|
+
full_prefix = "#{@prefix}#{prefix}"
|
|
400
|
+
combined_middleware = @middleware + middleware
|
|
401
|
+
nested_auth = auth_handler || @auth_handler
|
|
402
|
+
GroupContext.new(full_prefix, nested_auth, combined_middleware).instance_eval(&block)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|