tina4ruby 3.13.3 → 3.13.5
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/lib/tina4/frond.rb +79 -0
- data/lib/tina4/mcp.rb +6 -2
- data/lib/tina4/rack_app.rb +53 -22
- data/lib/tina4/request.rb +73 -3
- data/lib/tina4/router.rb +49 -7
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5a305c0c2ee88795cac09fabe5c275165fb83d6da1d83bc301a8097660d711c
|
|
4
|
+
data.tar.gz: 68ee188aef588fd0e09d7779771de3eb47ed7ab5d9cfe426d3840b3015000064
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af2cb8dd1b51f7569d864aa41c4f0c52105696bea48e6c6fb50dd76b1d9bd11cf5e724cfdccb717bf5d42e2e84f032e10af1ada89ce9a3b74e660bc0a6f7c4f0
|
|
7
|
+
data.tar.gz: 75b8d8f25a7efcfb38606414e1457b3bdbf0bedbe3d1ee0ec3b5dea2ee0a58ddb5292a9f1078dc212a9cc43df0625122e3a288d2fb78437479cd15bc26efeecd
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -21,6 +21,33 @@ module Tina4
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class Frond
|
|
24
|
+
# -- Class-level registries ------------------------------------------------
|
|
25
|
+
# Persist globals, filters, and tests across hot-reloads and across module
|
|
26
|
+
# boundaries. When app.rb does ``Tina4::Frond.add_filter("money") { ... }``
|
|
27
|
+
# at startup before any instance exists, the registration sits here. Every
|
|
28
|
+
# subsequent ``Tina4::Frond.new`` drains these into its instance-local
|
|
29
|
+
# registries — so hot-reloads (which re-execute ``frond = Frond.new``) and
|
|
30
|
+
# late-constructed engines automatically inherit prior registrations.
|
|
31
|
+
#
|
|
32
|
+
# The same-name dual-callable (class + instance) methods below let callers
|
|
33
|
+
# write either ``Tina4::Frond.add_filter(...)`` (class-level only) or
|
|
34
|
+
# ``frond.add_filter(...)`` (updates both the class registry and the
|
|
35
|
+
# instance's live filter map). Parity with tina4-python's
|
|
36
|
+
# ``_ClassOrInstanceMethod`` descriptor.
|
|
37
|
+
@@class_filters = {}
|
|
38
|
+
@@class_globals = {}
|
|
39
|
+
@@class_tests = {}
|
|
40
|
+
|
|
41
|
+
# Clear the class-level globals/filters/tests registries.
|
|
42
|
+
#
|
|
43
|
+
# Useful in test fixtures to prevent leaking state between tests. Does
|
|
44
|
+
# NOT affect built-in filters or globals — only user-registered ones.
|
|
45
|
+
def self.clear_registry
|
|
46
|
+
@@class_filters = {}
|
|
47
|
+
@@class_globals = {}
|
|
48
|
+
@@class_tests = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
24
51
|
# -- Token types ----------------------------------------------------------
|
|
25
52
|
TEXT = :text
|
|
26
53
|
VAR = :var # {{ ... }}
|
|
@@ -187,6 +214,16 @@ module Tina4
|
|
|
187
214
|
|
|
188
215
|
# Built-in global functions
|
|
189
216
|
register_builtin_globals
|
|
217
|
+
|
|
218
|
+
# Drain class-level registries into this instance. Filters and tests
|
|
219
|
+
# registered via ``Tina4::Frond.add_filter`` BEFORE this instance was
|
|
220
|
+
# constructed flow in here. Globals likewise. This is the key to the
|
|
221
|
+
# static-facade: ``app.rb`` registers once at startup, and every
|
|
222
|
+
# Frond instance created later (including those born from hot-reloads)
|
|
223
|
+
# automatically inherits the registration. Parity with tina4-python.
|
|
224
|
+
@filters.merge!(@@class_filters)
|
|
225
|
+
@globals.merge!(@@class_globals)
|
|
226
|
+
@tests.merge!(@@class_tests)
|
|
190
227
|
end
|
|
191
228
|
|
|
192
229
|
# Render a template file with data. Uses token caching for performance.
|
|
@@ -249,19 +286,61 @@ module Tina4
|
|
|
249
286
|
@dotted_split_cache.clear
|
|
250
287
|
end
|
|
251
288
|
|
|
289
|
+
# Register a custom filter on the class registry only.
|
|
290
|
+
#
|
|
291
|
+
# Callable as ``Tina4::Frond.add_filter("money") { |v| ... }`` at app
|
|
292
|
+
# startup BEFORE any instance exists. The registration is remembered at
|
|
293
|
+
# class level so every later ``Tina4::Frond.new`` inherits it. To also
|
|
294
|
+
# update a live instance's filter map, use the instance method form.
|
|
295
|
+
def self.add_filter(name, &blk)
|
|
296
|
+
@@class_filters[name.to_s] = blk
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Register a custom test on the class registry only.
|
|
300
|
+
#
|
|
301
|
+
# Same dual-callable semantics as ``add_filter`` — see that method for
|
|
302
|
+
# the static-facade pattern.
|
|
303
|
+
def self.add_test(name, &blk)
|
|
304
|
+
@@class_tests[name.to_s] = blk
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Register a global variable on the class registry only.
|
|
308
|
+
#
|
|
309
|
+
# Same dual-callable semantics as ``add_filter`` — see that method for
|
|
310
|
+
# the static-facade pattern.
|
|
311
|
+
def self.add_global(name, value)
|
|
312
|
+
@@class_globals[name.to_s] = value
|
|
313
|
+
end
|
|
314
|
+
|
|
252
315
|
# Register a custom filter.
|
|
316
|
+
#
|
|
317
|
+
# Updates BOTH the class registry (so future ``Tina4::Frond.new`` picks
|
|
318
|
+
# the filter up) AND this instance's live filter map (so the change is
|
|
319
|
+
# visible to subsequent renders on the current engine).
|
|
253
320
|
def add_filter(name, &blk)
|
|
321
|
+
self.class.add_filter(name, &blk)
|
|
254
322
|
@filters[name.to_s] = blk
|
|
323
|
+
self
|
|
255
324
|
end
|
|
256
325
|
|
|
257
326
|
# Register a custom test.
|
|
327
|
+
#
|
|
328
|
+
# Updates BOTH the class registry and this instance's live tests map.
|
|
329
|
+
# See ``add_filter`` for the dual-write semantics.
|
|
258
330
|
def add_test(name, &blk)
|
|
331
|
+
self.class.add_test(name, &blk)
|
|
259
332
|
@tests[name.to_s] = blk
|
|
333
|
+
self
|
|
260
334
|
end
|
|
261
335
|
|
|
262
336
|
# Register a global variable available in all templates.
|
|
337
|
+
#
|
|
338
|
+
# Updates BOTH the class registry and this instance's live globals map.
|
|
339
|
+
# See ``add_filter`` for the dual-write semantics.
|
|
263
340
|
def add_global(name, value)
|
|
341
|
+
self.class.add_global(name, value)
|
|
264
342
|
@globals[name.to_s] = value
|
|
343
|
+
self
|
|
265
344
|
end
|
|
266
345
|
|
|
267
346
|
# Enable sandbox mode.
|
data/lib/tina4/mcp.rb
CHANGED
|
@@ -612,8 +612,12 @@ module Tina4
|
|
|
612
612
|
line = err_lines.first.strip
|
|
613
613
|
# Strip the absolute project_root prefix so the error reads
|
|
614
614
|
# as "src/routes/foo.rb:3: syntax error, ..." instead of the
|
|
615
|
-
# full /Users/... path.
|
|
616
|
-
|
|
615
|
+
# full /Users/... path. Use gsub because Ruby 3.2's MRI parser
|
|
616
|
+
# double-prints the path — once as the file label prefix and
|
|
617
|
+
# once inside the (SyntaxError) reference — and the test asserts
|
|
618
|
+
# the absolute path appears nowhere in import_error.
|
|
619
|
+
# See: spec/mcp_spec.rb defensive file_write test (CI Ruby 3.2).
|
|
620
|
+
line.gsub("#{project_root}/", "")
|
|
617
621
|
end
|
|
618
622
|
|
|
619
623
|
redact_env = lambda do |key, value|
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -108,22 +108,29 @@ module Tina4
|
|
|
108
108
|
else
|
|
109
109
|
# RFC 9110 conformance — before falling through to 404, check whether
|
|
110
110
|
# the PATH is known to the router under any OTHER method.
|
|
111
|
-
# - OPTIONS request → 204 with Allow header (§9.3.7)
|
|
111
|
+
# - OPTIONS request → 204 with Allow header (§9.3.7). Bare OPTIONS
|
|
112
|
+
# on an unknown path also returns 204 (empty Allow header) —
|
|
113
|
+
# OPTIONS is a discovery method; rejecting unknown probes with
|
|
114
|
+
# 404 confuses link checkers and monitoring tools and breaks
|
|
115
|
+
# CORS preflight that lacks the Origin/ACRM headers our earlier
|
|
116
|
+
# fast-path requires. Matches PHP/Node behaviour. Fixes
|
|
117
|
+
# spec/rack_app_spec.rb OPTIONS preflight.
|
|
112
118
|
# - Any other method (PUT on GET-only, TRACE, CONNECT, etc.)
|
|
113
|
-
# → 405 with Allow header (§15.5.6 + §10.2.1)
|
|
119
|
+
# → 405 with Allow header (§15.5.6 + §10.2.1) when the path
|
|
120
|
+
# exists; → 404 when nothing about the path is known.
|
|
114
121
|
allowed = Tina4::Router.methods_allowed_for_path(path)
|
|
115
|
-
if
|
|
122
|
+
if method.to_s.upcase == "OPTIONS"
|
|
123
|
+
allow_header = allowed.empty? ? "" : allowed.join(", ")
|
|
124
|
+
rack_response = [204, { "allow" => allow_header, "content-length" => "0" }, [""]]
|
|
125
|
+
matched_pattern = nil
|
|
126
|
+
elsif !allowed.empty?
|
|
116
127
|
allow_header = allowed.join(", ")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"content-type" => "application/json",
|
|
124
|
-
"content-length" => body.bytesize.to_s
|
|
125
|
-
}, [body]]
|
|
126
|
-
end
|
|
128
|
+
body = %({"error":"Method Not Allowed","path":"#{path}","method":"#{method}","allow":[#{allowed.map { |m| %("#{m}") }.join(",")}],"status":405})
|
|
129
|
+
rack_response = [405, {
|
|
130
|
+
"allow" => allow_header,
|
|
131
|
+
"content-type" => "application/json",
|
|
132
|
+
"content-length" => body.bytesize.to_s
|
|
133
|
+
}, [body]]
|
|
127
134
|
matched_pattern = nil
|
|
128
135
|
else
|
|
129
136
|
rack_response = handle_404(path)
|
|
@@ -317,19 +324,43 @@ module Tina4
|
|
|
317
324
|
end
|
|
318
325
|
end
|
|
319
326
|
|
|
320
|
-
#
|
|
327
|
+
# Build the route-handler call as a lambda so function-style
|
|
328
|
+
# middleware can wrap it. Path params are still bound by name —
|
|
329
|
+
# the continuation just forwards the (possibly-mutated)
|
|
330
|
+
# request/response pair the outer middleware chose to pass in.
|
|
321
331
|
handler_params = route.handler.parameters.map(&:last)
|
|
322
332
|
route_params = path_params || {}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
route_params
|
|
326
|
-
|
|
327
|
-
request
|
|
328
|
-
|
|
329
|
-
|
|
333
|
+
invoke_handler = lambda do |req, resp|
|
|
334
|
+
args = handler_params.map do |name|
|
|
335
|
+
if route_params.key?(name)
|
|
336
|
+
route_params[name]
|
|
337
|
+
elsif name == :request || name == :req
|
|
338
|
+
req
|
|
339
|
+
else
|
|
340
|
+
resp
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
args.empty? ? route.handler.call : route.handler.call(*args)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Fold any function-style middleware on this route into a
|
|
347
|
+
# Russian-doll chain wrapping the handler. First declared is the
|
|
348
|
+
# outermost layer — it receives the request first, calls
|
|
349
|
+
# next_handler to descend, and runs its "after" code on the way
|
|
350
|
+
# out. Class-based middleware (before_*/after_*) is handled
|
|
351
|
+
# separately by run_middleware above and never goes through here.
|
|
352
|
+
# tina4-book#141 PY-10-01 (cross-framework parity).
|
|
353
|
+
fn_mws = route.respond_to?(:function_middleware) ? route.function_middleware : []
|
|
354
|
+
if fn_mws.empty?
|
|
355
|
+
result = invoke_handler.call(request, response)
|
|
356
|
+
else
|
|
357
|
+
chain = invoke_handler
|
|
358
|
+
fn_mws.reverse_each do |mw|
|
|
359
|
+
inner = chain
|
|
360
|
+
chain = lambda { |req, resp| mw.call(req, resp, inner) }
|
|
330
361
|
end
|
|
362
|
+
result = chain.call(request, response)
|
|
331
363
|
end
|
|
332
|
-
result = args.empty? ? route.handler.call : route.handler.call(*args)
|
|
333
364
|
|
|
334
365
|
# Template rendering: when a template is set and the handler returned a Hash,
|
|
335
366
|
# render the template with the hash as data and return the HTML response.
|
data/lib/tina4/request.rb
CHANGED
|
@@ -35,6 +35,63 @@ module Tina4
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Hash subclass for HTTP headers — string keys are case-insensitive.
|
|
39
|
+
#
|
|
40
|
+
# HTTP header field-names are case-insensitive per RFC 7230 §3.2. With
|
|
41
|
+
# this class, request.headers["Content-Type"], .headers["content-type"],
|
|
42
|
+
# and .headers["CONTENT-TYPE"] all return the same value. Keys are
|
|
43
|
+
# stored lowercase internally.
|
|
44
|
+
#
|
|
45
|
+
# Cross-framework parity: same behaviour ships in tina4-python
|
|
46
|
+
# (CaseInsensitiveDict), tina4-php (Tina4\Request), tina4-nodejs
|
|
47
|
+
# (Tina4Request). tina4-book#141 PY-10-03 — chapter 10 examples
|
|
48
|
+
# documented headers["Content-Type"] for years; this makes them work.
|
|
49
|
+
class CaseInsensitiveHash < Hash
|
|
50
|
+
def [](key)
|
|
51
|
+
super(normalize_key(key))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def []=(key, value)
|
|
55
|
+
super(normalize_key(key), value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fetch(key, *args, &block)
|
|
59
|
+
super(normalize_key(key), *args, &block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def key?(key)
|
|
63
|
+
super(normalize_key(key))
|
|
64
|
+
end
|
|
65
|
+
alias has_key? key?
|
|
66
|
+
alias include? key?
|
|
67
|
+
alias member? key?
|
|
68
|
+
|
|
69
|
+
def delete(key, &block)
|
|
70
|
+
super(normalize_key(key), &block)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def store(key, value)
|
|
74
|
+
super(normalize_key(key), value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def merge(other)
|
|
78
|
+
result = dup
|
|
79
|
+
other.each { |k, v| result[k] = v }
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def merge!(other)
|
|
84
|
+
other.each { |k, v| self[k] = v }
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def normalize_key(key)
|
|
91
|
+
key.is_a?(String) ? key.downcase : key
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
38
95
|
class Request
|
|
39
96
|
attr_reader :env, :method, :path, :query_string, :content_type,
|
|
40
97
|
:path_params, :ip
|
|
@@ -139,7 +196,10 @@ module Tina4
|
|
|
139
196
|
end
|
|
140
197
|
|
|
141
198
|
def header(name)
|
|
142
|
-
|
|
199
|
+
# Headers are stored in a CaseInsensitiveHash keyed by lowercase-
|
|
200
|
+
# dashed names ("content-type", "x-api-key"). The hash normalises
|
|
201
|
+
# the lookup case automatically, so pass the dashed form through.
|
|
202
|
+
headers[name.to_s.tr("_", "-")]
|
|
143
203
|
end
|
|
144
204
|
|
|
145
205
|
def json_body
|
|
@@ -169,12 +229,22 @@ module Tina4
|
|
|
169
229
|
end
|
|
170
230
|
|
|
171
231
|
def extract_headers
|
|
172
|
-
h =
|
|
232
|
+
h = CaseInsensitiveHash.new
|
|
173
233
|
@env.each do |key, value|
|
|
174
234
|
if key.start_with?("HTTP_")
|
|
175
|
-
|
|
235
|
+
# Rack normalises "Content-Type" to "HTTP_CONTENT_TYPE" — we
|
|
236
|
+
# store as "content-type" (lowercase, dashed) so both
|
|
237
|
+
# headers["Content-Type"] and the legacy headers["content_type"]
|
|
238
|
+
# via the `header()` helper resolve to the same value. The
|
|
239
|
+
# CaseInsensitiveHash handles the case-insensitive part.
|
|
240
|
+
h[key[5..-1].tr("_", "-").downcase] = value
|
|
176
241
|
end
|
|
177
242
|
end
|
|
243
|
+
# CONTENT_TYPE / CONTENT_LENGTH live at the top level of the Rack
|
|
244
|
+
# env (no HTTP_ prefix per the Rack spec) — surface them too so
|
|
245
|
+
# request.headers["Content-Type"] works for POST bodies.
|
|
246
|
+
h["content-type"] = @env["CONTENT_TYPE"] if @env["CONTENT_TYPE"] && !@env["CONTENT_TYPE"].empty?
|
|
247
|
+
h["content-length"] = @env["CONTENT_LENGTH"] if @env["CONTENT_LENGTH"] && !@env["CONTENT_LENGTH"].to_s.empty?
|
|
178
248
|
h
|
|
179
249
|
end
|
|
180
250
|
|
data/lib/tina4/router.rb
CHANGED
|
@@ -17,9 +17,11 @@ module Tina4
|
|
|
17
17
|
@swagger_meta = swagger_meta
|
|
18
18
|
@middleware = middleware.freeze
|
|
19
19
|
@template = template&.freeze
|
|
20
|
-
# Write routes are secure by default
|
|
21
|
-
#
|
|
22
|
-
|
|
20
|
+
# Write routes are secure by default — bearer-token auth is enforced
|
|
21
|
+
# on POST/PUT/PATCH/DELETE regardless of attached middleware.
|
|
22
|
+
# Middleware is additive, never an auth bypass. tina4-book#141
|
|
23
|
+
# PY-10-02. Call .no_auth on the route to opt out explicitly.
|
|
24
|
+
@auth_required = %w[POST PUT PATCH DELETE].include?(@method)
|
|
23
25
|
@cached = false
|
|
24
26
|
@param_names = []
|
|
25
27
|
@path_regex = compile_pattern(@path)
|
|
@@ -50,13 +52,18 @@ module Tina4
|
|
|
50
52
|
# Dual-mode: getter (no args) returns the middleware array;
|
|
51
53
|
# setter (with args) appends middleware and returns self for chaining.
|
|
52
54
|
# Router.post("/api") { ... }.middleware(AuthMiddleware)
|
|
55
|
+
#
|
|
56
|
+
# Middleware is purely additive — registering middleware NEVER flips
|
|
57
|
+
# @auth_required off. The secure-by-default gate for write methods
|
|
58
|
+
# (POST/PUT/PATCH/DELETE) stays in effect; if a route truly wants to
|
|
59
|
+
# opt out of the built-in bearer check, call .no_auth explicitly.
|
|
60
|
+
# tina4-book#141 PY-10-02 — previously, attaching ANY middleware
|
|
61
|
+
# silently turned off auth_required, which let attackers bypass auth
|
|
62
|
+
# by routing through a logging middleware. Cross-framework parity.
|
|
53
63
|
def middleware(*middleware_classes)
|
|
54
64
|
return @middleware if middleware_classes.empty?
|
|
55
65
|
|
|
56
66
|
@middleware = @middleware.dup + middleware_classes
|
|
57
|
-
# Custom middleware means developer handles auth — disable built-in gate
|
|
58
|
-
# unless .secure was explicitly called.
|
|
59
|
-
@auth_required = false unless @auth_required
|
|
60
67
|
self
|
|
61
68
|
end
|
|
62
69
|
|
|
@@ -83,15 +90,50 @@ module Tina4
|
|
|
83
90
|
end
|
|
84
91
|
end
|
|
85
92
|
|
|
86
|
-
# Run per-route middleware
|
|
93
|
+
# Run per-route CLASS-based middleware. Function-style middleware
|
|
94
|
+
# (Proc/Lambda taking 3+ args: req, resp, next_handler) is skipped
|
|
95
|
+
# here — it's dispatched separately via #function_middleware which
|
|
96
|
+
# wraps the route handler in a continuation chain (see rack_app.rb).
|
|
97
|
+
#
|
|
98
|
+
# Returns true if all class-based middleware passed, false if any
|
|
99
|
+
# returned literal false (halt request).
|
|
87
100
|
def run_middleware(request, response)
|
|
88
101
|
@middleware.each do |mw|
|
|
102
|
+
next if Route.function_middleware?(mw)
|
|
89
103
|
result = mw.call(request, response)
|
|
90
104
|
return false if result == false
|
|
91
105
|
end
|
|
92
106
|
true
|
|
93
107
|
end
|
|
94
108
|
|
|
109
|
+
# Function-style middleware attached to this route, in declaration
|
|
110
|
+
# order. The route dispatcher folds them into a Russian-doll
|
|
111
|
+
# continuation chain — first declared is the OUTERMOST layer (runs
|
|
112
|
+
# first on the way in, last on the way out). tina4-book#141
|
|
113
|
+
# PY-10-01 — chapter 10 documented 8+ examples of function middleware
|
|
114
|
+
# for years; before this fix the framework silently ignored them.
|
|
115
|
+
def function_middleware
|
|
116
|
+
@middleware.select { |mw| Route.function_middleware?(mw) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Detect Express/FastAPI-style function middleware.
|
|
120
|
+
#
|
|
121
|
+
# A Proc/Lambda/Method whose arity indicates 3+ positional params
|
|
122
|
+
# (req, resp, next_handler). Ruby arity quirk: required-args-only
|
|
123
|
+
# arity is non-negative; if the callable accepts a splat or
|
|
124
|
+
# optionals, arity is negated (-required-1). We treat arity >= 3
|
|
125
|
+
# OR arity <= -4 as function-style. Anything else (a class with
|
|
126
|
+
# before_*/after_* methods, or a 2-arg callable) is treated as
|
|
127
|
+
# class-style and goes through #run_middleware.
|
|
128
|
+
def self.function_middleware?(mw)
|
|
129
|
+
return false if mw.is_a?(Class) || mw.is_a?(Module)
|
|
130
|
+
return false unless mw.respond_to?(:arity)
|
|
131
|
+
ar = mw.arity
|
|
132
|
+
ar >= 3 || ar <= -4
|
|
133
|
+
rescue StandardError
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
95
137
|
private
|
|
96
138
|
|
|
97
139
|
def normalize_path(path)
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|