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.
-
#perms ⇒ Object
readonly
RBAC guard groups (Feature 138 / ADR-0058).
-
#roles ⇒ Object
readonly
RBAC guard groups (Feature 138 / ADR-0058).
-
#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.
-
#can(*permissions) ⇒ Object
RBAC: require ONE of the named permissions (OR).
-
#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.
-
#role(*names) ⇒ Object
RBAC: require ONE of the named roles (OR).
-
#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.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/tina4/router.rb', line 15 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 @roles = [] @perms = [] @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 |
#perms ⇒ Object (readonly)
RBAC guard groups (Feature 138 / ADR-0058). Each is an array of arrays: OR within a group, AND across groups (stacking).
13 14 15 |
# File 'lib/tina4/router.rb', line 13 def perms @perms end |
#roles ⇒ Object (readonly)
RBAC guard groups (Feature 138 / ADR-0058). Each is an array of arrays: OR within a group, AND across groups (stacking).
13 14 15 |
# File 'lib/tina4/router.rb', line 13 def roles @roles 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.
228 229 230 231 232 233 234 |
# File 'lib/tina4/router.rb', line 228 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.
211 212 213 214 215 216 217 218 |
# File 'lib/tina4/router.rb', line 211 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
77 78 79 80 |
# File 'lib/tina4/router.rb', line 77 def cache @cached = true self end |
#can(*permissions) ⇒ Object
RBAC: require ONE of the named permissions (OR). Reads the verified JWT
permissions claim; granted-side wildcards (posts.*, *) satisfy a
concrete requirement. Stack for AND. Implies auth. Feature 138.
Returns self for chaining: Router.delete("/p") { ... }.can("posts.delete")
66 67 68 69 70 71 72 73 |
# File 'lib/tina4/router.rb', line 66 def can(*) = .flatten.map(&:to_s).reject(&:empty?) unless .empty? @perms << @auth_required = true end 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.
198 199 200 |
# File 'lib/tina4/router.rb', line 198 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.
187 188 189 190 |
# File 'lib/tina4/router.rb', line 187 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
101 102 103 104 |
# File 'lib/tina4/router.rb', line 101 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
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/tina4/router.rb', line 107 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.
93 94 95 96 97 98 |
# File 'lib/tina4/router.rb', line 93 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
45 46 47 48 |
# File 'lib/tina4/router.rb', line 45 def no_auth @auth_required = false self end |
#role(*names) ⇒ Object
RBAC: require ONE of the named roles (OR). Reads the verified JWT roles
claim. Stack .role/.can for AND. Implies auth. Feature 138 / ADR-0058.
Returns self for chaining: Router.get("/admin") { ... }.role("admin")
53 54 55 56 57 58 59 60 |
# File 'lib/tina4/router.rb', line 53 def role(*names) names = names.flatten.map(&:to_s).reject(&:empty?) unless names.empty? @roles << names @auth_required = true end 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.
175 176 177 178 179 180 181 182 |
# File 'lib/tina4/router.rb', line 175 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).
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/tina4/router.rb', line 156 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(request, 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
38 39 40 41 |
# File 'lib/tina4/router.rb', line 38 def secure @auth_required = true self end |