Class: Tina4::Route
- Inherits:
-
Object
- Object
- Tina4::Route
- Defined in:
- lib/tina4/router.rb
Instance Attribute Summary collapse
-
#auth_handler ⇒ Object
readonly
Returns the value of attribute auth_handler.
-
#auth_required ⇒ Object
Returns the value of attribute auth_required.
-
#cached ⇒ Object
Returns the value of attribute cached.
-
#handler ⇒ Object
readonly
Returns the value of attribute handler.
-
#method ⇒ Object
readonly
Returns the value of attribute method.
-
#param_names ⇒ Object
readonly
Returns the value of attribute param_names.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
-
#path_regex ⇒ Object
readonly
Returns the value of attribute path_regex.
-
#swagger_meta ⇒ Object
readonly
Returns the value of attribute swagger_meta.
-
#template ⇒ Object
readonly
Returns the value of attribute template.
Class Method Summary collapse
-
.filter_middleware?(mw) ⇒ Boolean
Detect "filter" middleware: a plain 2-arg callable (Proc/Lambda/Method) that receives (request, response) and halts by returning false.
-
.function_middleware?(mw) ⇒ Boolean
Detect Express/FastAPI-style function middleware.
Instance Method Summary collapse
-
#cache ⇒ Object
Mark this route as cacheable.
-
#function_middleware ⇒ Object
Function-style middleware attached to this route, in declaration order.
-
#hook_middleware ⇒ Object
Per-route middleware that takes part in the before/after passes: everything except function-style middleware, with String specs resolved to the middleware they name.
-
#initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil) ⇒ Route
constructor
A new instance of Route.
-
#match?(request_path, request_method = nil) ⇒ Boolean
Returns params hash if matched, false otherwise.
-
#match_path(request_path) ⇒ Object
Returns params hash if matched, false otherwise.
-
#middleware(*middleware_classes) ⇒ Object
Dual-mode: getter (no args) returns the middleware array; setter (with args) appends middleware and returns self for chaining.
-
#no_auth ⇒ Object
Opt out of the secure-by-default auth on write routes.
-
#run_after_middleware(request, response) ⇒ Object
Run per-route class middleware's after_* hooks, once the handler has run (or once a before_* halted).
-
#run_middleware(request, response) ⇒ Object
Run per-route middleware BEFORE the handler.
-
#secure ⇒ Object
Mark this route as requiring bearer-token authentication.
Constructor Details
#initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil) ⇒ Route
Returns a new instance of Route.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/tina4/router.rb', line 12 def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil) @method = method.to_s.upcase.freeze @path = normalize_path(path).freeze @handler = handler @auth_handler = auth_handler @swagger_meta = @middleware = middleware.freeze @template = template&.freeze # Write routes are secure by default — bearer-token auth is enforced # on POST/PUT/PATCH/DELETE regardless of attached middleware. # Middleware is additive, never an auth bypass. tina4-book#141 # PY-10-02. Call .no_auth on the route to opt out explicitly. @auth_required = %w[POST PUT PATCH DELETE].include?(@method) @cached = false @param_names = [] @path_regex = compile_pattern(@path) @param_names.freeze end |
Instance Attribute Details
#auth_handler ⇒ Object (readonly)
Returns the value of attribute auth_handler.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def auth_handler @auth_handler end |
#auth_required ⇒ Object
Returns the value of attribute auth_required.
10 11 12 |
# File 'lib/tina4/router.rb', line 10 def auth_required @auth_required end |
#cached ⇒ Object
Returns the value of attribute cached.
10 11 12 |
# File 'lib/tina4/router.rb', line 10 def cached @cached end |
#handler ⇒ Object (readonly)
Returns the value of attribute handler.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def handler @handler end |
#method ⇒ Object (readonly)
Returns the value of attribute method.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def method @method end |
#param_names ⇒ Object (readonly)
Returns the value of attribute param_names.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def param_names @param_names end |
#path ⇒ Object (readonly)
Returns the value of attribute path.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def path @path end |
#path_regex ⇒ Object (readonly)
Returns the value of attribute path_regex.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def path_regex @path_regex end |
#swagger_meta ⇒ Object (readonly)
Returns the value of attribute swagger_meta.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def @swagger_meta end |
#template ⇒ Object (readonly)
Returns the value of attribute template.
8 9 10 |
# File 'lib/tina4/router.rb', line 8 def template @template end |
Class Method Details
.filter_middleware?(mw) ⇒ Boolean
Detect "filter" middleware: a plain 2-arg callable (Proc/Lambda/Method) that receives (request, response) and halts by returning false.
This is the shape #run_middleware has always supported, kept working unchanged. It is NOT function middleware (3+ args, wraps the handler) and NOT class middleware (declares before_/after_ hooks) — a Class or Module is never a filter even when it defines .call, because its hooks are the documented mechanism.
198 199 200 201 202 203 204 |
# File 'lib/tina4/router.rb', line 198 def self.filter_middleware?(mw) return false if mw.is_a?(Class) || mw.is_a?(Module) mw.respond_to?(:arity) && mw.respond_to?(:call) rescue StandardError false end |
.function_middleware?(mw) ⇒ Boolean
Detect Express/FastAPI-style function middleware.
A Proc/Lambda/Method whose arity indicates 3+ positional params (req, resp, next_handler). Ruby arity quirk: required-args-only arity is non-negative; if the callable accepts a splat or optionals, arity is negated (-required-1). We treat arity >= 3 OR arity <= -4 as function-style. Anything else (a class with before_/after_ methods, or a 2-arg callable) is treated as class-style and goes through #run_middleware.
181 182 183 184 185 186 187 188 |
# File 'lib/tina4/router.rb', line 181 def self.function_middleware?(mw) return false if mw.is_a?(Class) || mw.is_a?(Module) return false unless mw.respond_to?(:arity) ar = mw.arity ar >= 3 || ar <= -4 rescue StandardError false end |
Instance Method Details
#cache ⇒ Object
Mark this route as cacheable. Returns self for chaining: Router.get("/path") { ... }.cache
47 48 49 50 |
# File 'lib/tina4/router.rb', line 47 def cache @cached = true self end |
#function_middleware ⇒ Object
Function-style middleware attached to this route, in declaration order. The route dispatcher folds them into a Russian-doll continuation chain — first declared is the OUTERMOST layer (runs first on the way in, last on the way out). tina4-book#141 PY-10-01 — chapter 10 documented 8+ examples of function middleware for years; before this fix the framework silently ignored them.
168 169 170 |
# File 'lib/tina4/router.rb', line 168 def function_middleware @middleware.select { |mw| Route.function_middleware?(mw) } end |
#hook_middleware ⇒ Object
Per-route middleware that takes part in the before/after passes: everything except function-style middleware, with String specs resolved to the middleware they name.
157 158 159 160 |
# File 'lib/tina4/router.rb', line 157 def hook_middleware @middleware.reject { |mw| Route.function_middleware?(mw) } .map { |mw| mw.is_a?(::String) ? Router.resolve_string_middleware(mw) : mw } end |
#match?(request_path, request_method = nil) ⇒ Boolean
Returns params hash if matched, false otherwise
71 72 73 74 |
# File 'lib/tina4/router.rb', line 71 def match?(request_path, request_method = nil) return false if request_method && @method != "ANY" && @method != request_method.to_s.upcase match_path(request_path) end |
#match_path(request_path) ⇒ Object
Returns params hash if matched, false otherwise
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/tina4/router.rb', line 77 def match_path(request_path) match = @path_regex.match(request_path) return false unless match if @param_names.empty? {} else params = {} @param_names.each_with_index do |param_def, i| raw_value = match[i + 1] # Rack delivers PATH_INFO (and therefore these captures) as # ASCII-8BIT. Relabel as UTF-8 so an untyped param binds to SQL as # TEXT, not a BLOB: SQLite gives a BLOB no numeric affinity, so a # `{id}` bound as ASCII-8BIT never matches an INTEGER column # (GET /users/{id} 404s a real row). No transcode — URL path bytes # are already UTF-8. Parity: Python/PHP/Node path params are text. raw_value = raw_value.dup.force_encoding(Encoding::UTF_8) if raw_value.is_a?(::String) params[param_def[:name]] = cast_param(raw_value, param_def[:type]) end params end end |
#middleware(*middleware_classes) ⇒ Object
Dual-mode: getter (no args) returns the middleware array; setter (with args) appends middleware and returns self for chaining. Router.post("/api") { ... }.middleware(AuthMiddleware)
Middleware is purely additive — registering middleware NEVER flips (POST/PUT/PATCH/DELETE) stays in effect; if a route truly wants to opt out of the built-in bearer check, call .no_auth explicitly. tina4-book#141 PY-10-02 — previously, attaching ANY middleware silently turned off auth_required, which let attackers bypass auth by routing through a logging middleware. Cross-framework parity.
63 64 65 66 67 68 |
# File 'lib/tina4/router.rb', line 63 def middleware(*middleware_classes) return @middleware if middleware_classes.empty? @middleware = @middleware.dup + middleware_classes self end |
#no_auth ⇒ Object
Opt out of the secure-by-default auth on write routes. Returns self for chaining: Router.post("/login") { ... }.no_auth
40 41 42 43 |
# File 'lib/tina4/router.rb', line 40 def no_auth @auth_required = false self end |
#run_after_middleware(request, response) ⇒ Object
Run per-route class middleware's after_* hooks, once the handler has run (or once a before_* halted). Same orchestrator, same discovery — see #run_middleware. Filter middleware has no after phase by construction.
145 146 147 148 149 150 151 152 |
# File 'lib/tina4/router.rb', line 145 def run_after_middleware(request, response) hook_middleware.each do |mw| next if Route.filter_middleware?(mw) Tina4::Middleware.run_after([mw], request, response) end response end |
#run_middleware(request, response) ⇒ Object
Run per-route middleware BEFORE the handler.
Dispatch is by SHAPE, in declaration order:
* a CLASS (or instance) declaring before_*/after_* hooks — handed to
Tina4::Middleware.run_before, the SAME orchestrator global middleware
goes through, so it gets the SAME hook discovery (definition order,
base->derived) and the SAME return-value table. There is no second,
divergent runner here.
* a String spec ("ResponseCache", "ResponseCache:300") — resolved to the
configured middleware first (parity with Python/PHP/Node).
* a 2-arg callable ("filter" middleware) — called directly; false halts.
Function-style middleware (3+ args: req, resp, next_handler) is NOT run here — it wraps the handler in a continuation chain (see #function_middleware and DispatchPipeline#invoke_route_handler).
This method used to call mw.call(request, response) on EVERYTHING. A
class declaring def self.before_auth does not respond to .call, so
per-route class middleware raised NoMethodError and the dispatcher turned
every such request into a clean 500 — the documented per-route
before_/after_ mechanism never ran at all. The .call-everything body is
superseded by the shape dispatch above; the 2-arg filter path below is
what remains of it.
Returns true if every middleware passed, false to halt (handler skipped).
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/tina4/router.rb', line 126 def run_middleware(request, response) hook_middleware.each do |mw| if Route.filter_middleware?(mw) next unless mw.call(request, response) == false # Same `false` row of the return-value table a before_* hook gets: # keep the response the filter set, 403 only if it set nothing. Tina4::Middleware.refuse(response) return false else return false unless Tina4::Middleware.run_before([mw], request, response) end end true end |
#secure ⇒ Object
Mark this route as requiring bearer-token authentication. Returns self for chaining: Router.get("/path") { ... }.secure
33 34 35 36 |
# File 'lib/tina4/router.rb', line 33 def secure @auth_required = true self end |