Module: Tina4::Router

Defined in:
lib/tina4/router.rb

Defined Under Namespace

Classes: GroupContext

Class Method Summary collapse

Class Method Details

.add(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil) ⇒ Object



279
280
281
282
283
284
285
286
287
288
289
# File 'lib/tina4/router.rb', line 279

def add(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
  route = Route.new(method, path, handler,
                    auth_handler: auth_handler,
                    swagger_meta: swagger_meta,
                    middleware: middleware,
                    template: template)
  routes << route
  method_index[route.method] << route
  Tina4::Log.debug("Route registered: #{method.upcase} #{path}")
  route
end

.any(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object



311
312
313
# File 'lib/tina4/router.rb', line 311

def any(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.clear!Object Also known as: clear



430
431
432
433
434
# File 'lib/tina4/router.rb', line 430

def clear!
  @routes = []
  @method_index = Hash.new { |h, k| h[k] = [] }
  @ws_routes = []
end

.delete(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object



307
308
309
# File 'lib/tina4/router.rb', line 307

def delete(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("DELETE", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.find_route(method, path) ⇒ Object



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/tina4/router.rb', line 334

def find_route(method, path)
  normalized_method = method.upcase
  # Normalize path once (not per-route)
  normalized_path = path.gsub("\\", "/")
  normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
  normalized_path = normalized_path.chomp("/") unless normalized_path == "/"

  # Check ANY routes first, then method-specific routes
  candidates = (method_index["ANY"] || []) + (method_index[normalized_method] || [])
  candidates.each do |route|
    params = route.match_path(normalized_path)
    return [route, params] if params
  end

  # RFC 9110 §9.3.2: HEAD is identical to GET except for the absence
  # of a response body. If no explicit HEAD route matched, fall back
  # to the GET route — the dispatcher strips the body on the way out
  # so the handler doesn't need to know HEAD even happened.
  if normalized_method == "HEAD"
    (method_index["GET"] || []).each do |route|
      params = route.match_path(normalized_path)
      return [route, params] if params
    end
  end

  nil
end

.find_ws_route(path) ⇒ Object

Find a matching WebSocket route for a given path. Returns [ws_route, params] or nil.



262
263
264
265
266
267
268
269
270
271
272
# File 'lib/tina4/router.rb', line 262

def find_ws_route(path)
  normalized = path.gsub("\\", "/")
  normalized = "/#{normalized}" unless normalized.start_with?("/")
  normalized = normalized.chomp("/") unless normalized == "/"

  ws_routes.each do |ws_route|
    params = ws_route.match?(normalized)
    return [ws_route, params] if params
  end
  nil
end

.get(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object

Convenience registration methods



291
292
293
# File 'lib/tina4/router.rb', line 291

def get(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("GET", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.get_routesObject



230
231
232
# File 'lib/tina4/router.rb', line 230

def get_routes
  routes
end

.get_web_socket_routesObject

Parity alias — returns all registered WebSocket routes.



244
245
246
# File 'lib/tina4/router.rb', line 244

def get_web_socket_routes
  ws_routes
end

.group(prefix, auth_handler: nil, middleware: [], &block) ⇒ Object



437
438
439
# File 'lib/tina4/router.rb', line 437

def group(prefix, auth_handler: nil, middleware: [], &block)
  GroupContext.new(prefix, auth_handler, middleware).instance_eval(&block)
end

.head(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object

Register an explicit HEAD route. By default the framework auto-handles HEAD by falling back to the GET route and stripping the body (RFC 9110 §9.3.2). Use this only when you need a HEAD handler that does something different from GET — e.g. cheaper existence-check logic, custom validator headers without the cost of building the body. The framework still strips the response body for you on the way out.



321
322
323
# File 'lib/tina4/router.rb', line 321

def head(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("HEAD", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.list_routesObject



234
235
236
# File 'lib/tina4/router.rb', line 234

def list_routes
  routes
end

.load_routes(directory) ⇒ Object

Load route files from a directory (file-based route discovery)



442
443
444
445
446
447
448
449
450
451
452
# File 'lib/tina4/router.rb', line 442

def load_routes(directory)
  return unless Dir.exist?(directory)
  Dir.glob(File.join(directory, "**/*.rb")).sort.each do |file|
    begin
      load file
      Tina4::Log.debug("Route loaded: #{file}")
    rescue => e
      Tina4::Log.error("Failed to load route #{file}: #{e.message}")
    end
  end
end

.match(method, path) ⇒ Object

Find a route matching method + path. Returns [route, params] or nil. match(method, path) — consistent with Python, PHP, and Node.



410
411
412
# File 'lib/tina4/router.rb', line 410

def match(method, path)
  find_route(method, path)
end

.method_indexObject

Routes indexed by HTTP method for O(1) method lookup



275
276
277
# File 'lib/tina4/router.rb', line 275

def method_index
  @method_index ||= Hash.new { |h, k| h[k] = [] }
end

.methods_allowed_for_path(path) ⇒ Object

Return the list of HTTP methods registered for “path“, in the order GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS. Used by the dispatcher to build the “Allow:“ header on 405 / OPTIONS responses (RFC 9110 §10.2.1, §9.3.7).

If GET is registered for the path, HEAD is appended implicitly (HEAD auto-fallback). OPTIONS is appended whenever the path has any registered method (the framework auto-handles OPTIONS).



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/tina4/router.rb', line 370

def methods_allowed_for_path(path)
  normalized_path = path.gsub("\\", "/")
  normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
  normalized_path = normalized_path.chomp("/") unless normalized_path == "/"

  method_order = %w[GET POST PUT PATCH DELETE HEAD OPTIONS]
  seen = []
  any_matched = false

  method_index.each do |m, routes_for_method|
    next if routes_for_method.empty?
    matched = routes_for_method.any? { |r| r.match_path(normalized_path) }
    next unless matched
    if m == "ANY"
      any_matched = true
    elsif method_order.include?(m)
      seen << m unless seen.include?(m)
    end
  end

  seen = method_order.dup if any_matched

  if !seen.empty?
    seen << "HEAD" if seen.include?("GET") && !seen.include?("HEAD")
    seen << "OPTIONS" unless seen.include?("OPTIONS")
  end

  method_order.select { |m| seen.include?(m) }
end

.options(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object

Register an explicit OPTIONS route. By default the framework auto- handles OPTIONS by building an Allow header from every method registered for the path and returning 204 (RFC 9110 §9.3.7). Use this to take over that behaviour — e.g. to return a richer OPTIONS payload describing the resource.



330
331
332
# File 'lib/tina4/router.rb', line 330

def options(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("OPTIONS", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.patch(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object



303
304
305
# File 'lib/tina4/router.rb', line 303

def patch(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("PATCH", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.post(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object



295
296
297
# File 'lib/tina4/router.rb', line 295

def post(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("POST", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.put(path, middleware: [], swagger_meta: {}, template: nil, &block) ⇒ Object



299
300
301
# File 'lib/tina4/router.rb', line 299

def put(path, middleware: [], swagger_meta: {}, template: nil, &block)
  add("PUT", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
end

.routesObject



226
227
228
# File 'lib/tina4/router.rb', line 226

def routes
  @routes ||= []
end

.trailing_slash_redirect?Boolean

When TINA4_TRAILING_SLASH_REDIRECT is truthy, the rack app uses this to detect whether the original (un-stripped) path differed from the canonical form so it can issue a 301 redirect. Default false — silent match keeps backward compatibility.

Returns:

  • (Boolean)


404
405
406
# File 'lib/tina4/router.rb', line 404

def trailing_slash_redirect?
  %w[true 1 yes on].include?(ENV.fetch("TINA4_TRAILING_SLASH_REDIRECT", "").to_s.strip.downcase)
end

.use(klass) ⇒ Object

Register a class-based middleware globally. The class should define static before_* and/or after_* methods. Example:

class AuthMiddleware
  def self.before_auth(request, response)
    unless request.headers["authorization"]
      return [request, response.json({ error: "Unauthorized" }, 401)]
    end
    [request, response]
  end
end
Tina4::Router.use(AuthMiddleware)


426
427
428
# File 'lib/tina4/router.rb', line 426

def use(klass)
  Tina4::Middleware.use(klass)
end

.websocket(path, &block) ⇒ Object

Register a WebSocket route. The handler block receives (connection, event, data) where:

connection — WebSocketConnection with #send, #broadcast, #close, #params
event      — :open, :message, or :close
data       — String payload for :message, nil for :open/:close


253
254
255
256
257
258
# File 'lib/tina4/router.rb', line 253

def websocket(path, &block)
  ws_route = WebSocketRoute.new(path, block)
  ws_routes << ws_route
  Tina4::Log.debug("WebSocket route registered: #{path}")
  ws_route
end

.ws_routesObject

Registered WebSocket routes



239
240
241
# File 'lib/tina4/router.rb', line 239

def ws_routes
  @ws_routes ||= []
end