tina4ruby 3.10.31 → 3.10.38
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/ai.rb +237 -211
- data/lib/tina4/cli.rb +5 -13
- data/lib/tina4/dev_admin.rb +281 -2
- data/lib/tina4/frond.rb +15 -3
- data/lib/tina4/mcp.rb +696 -0
- data/lib/tina4/metrics.rb +673 -0
- data/lib/tina4/rack_app.rb +57 -2
- data/lib/tina4/request.rb +40 -2
- data/lib/tina4/response.rb +7 -2
- data/lib/tina4/router.rb +5 -1
- data/lib/tina4/test_client.rb +159 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +2 -0
- metadata +10 -4
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -616,16 +616,71 @@ module Tina4
|
|
|
616
616
|
|
|
617
617
|
toolbar = <<~HTML.strip
|
|
618
618
|
<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
|
|
619
|
-
<span style="color:#d32f2f;font-weight:bold;">Tina4 v#{version}</span>
|
|
619
|
+
<span id="tina4-ver-btn" style="color:#d32f2f;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v#{version}</span>
|
|
620
|
+
<div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #d32f2f;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
|
|
621
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
622
|
+
<strong style="color:#89b4fa;">Version Info</strong>
|
|
623
|
+
<span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">×</span>
|
|
624
|
+
</div>
|
|
625
|
+
<div id="tina4-ver-body" style="line-height:1.8;">
|
|
626
|
+
<div>Current: <strong style="color:#a6e3a1;">v#{version}</strong></div>
|
|
627
|
+
<div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
620
630
|
<span style="color:#4caf50;">#{method}</span>
|
|
621
631
|
<span>#{path}</span>
|
|
622
632
|
<span style="color:#666;">→ #{matched_pattern}</span>
|
|
623
633
|
<span style="color:#ffeb3b;">req:#{request_id}</span>
|
|
624
634
|
<span style="color:#90caf9;">#{route_count} routes</span>
|
|
625
635
|
<span style="color:#888;">Ruby #{RUBY_VERSION}</span>
|
|
626
|
-
<a href="#" onclick="(function(e){e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){p.style.display=p.style.display==='none'?'block':'none';return;}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;
|
|
636
|
+
<a href="#" onclick="(function(e){e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){p.style.display=p.style.display==='none'?'block':'none';return;}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';var f=document.createElement('iframe');f.src='/__dev';f.style.cssText='width:100%;height:100%;border:1px solid #CC342D;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';c.appendChild(f);document.body.appendChild(c);})(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard ↗</a>
|
|
627
637
|
<span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">✕</span>
|
|
628
638
|
</div>
|
|
639
|
+
<script>
|
|
640
|
+
function tina4VersionModal(){
|
|
641
|
+
var m=document.getElementById('tina4-ver-modal');
|
|
642
|
+
if(m.style.display==='block'){m.style.display='none';return;}
|
|
643
|
+
m.style.display='block';
|
|
644
|
+
var el=document.getElementById('tina4-ver-latest');
|
|
645
|
+
el.innerHTML='Checking for updates...';
|
|
646
|
+
el.style.color='#888';
|
|
647
|
+
fetch('/__dev/api/version-check')
|
|
648
|
+
.then(function(r){return r.json()})
|
|
649
|
+
.then(function(d){
|
|
650
|
+
var latest=d.latest;
|
|
651
|
+
var current=d.current;
|
|
652
|
+
if(latest===current){
|
|
653
|
+
el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> — You are up to date!';
|
|
654
|
+
el.style.color='#a6e3a1';
|
|
655
|
+
}else{
|
|
656
|
+
var cParts=current.split('.').map(Number);
|
|
657
|
+
var lParts=latest.split('.').map(Number);
|
|
658
|
+
var isNewer=false;
|
|
659
|
+
for(var i=0;i<Math.max(cParts.length,lParts.length);i++){
|
|
660
|
+
var c=cParts[i]||0,l=lParts[i]||0;
|
|
661
|
+
if(l>c){isNewer=true;break;}
|
|
662
|
+
if(l<c)break;
|
|
663
|
+
}
|
|
664
|
+
if(isNewer){
|
|
665
|
+
var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
|
|
666
|
+
el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
|
|
667
|
+
if(breaking){
|
|
668
|
+
el.innerHTML+='<div style="color:#f38ba8;margin-top:6px;">⚠ Major/minor version change — check the <a href="https://github.com/tina4stack/tina4-ruby/releases" target="_blank" style="color:#89b4fa;">changelog</a> for breaking changes before upgrading.</div>';
|
|
669
|
+
}else{
|
|
670
|
+
el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">gem install tina4ruby</code></div>';
|
|
671
|
+
}
|
|
672
|
+
}else{
|
|
673
|
+
el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> — You are up to date!';
|
|
674
|
+
el.style.color='#a6e3a1';
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
.catch(function(){
|
|
679
|
+
el.innerHTML='Could not check for updates (offline?)';
|
|
680
|
+
el.style.color='#f38ba8';
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
</script>
|
|
629
684
|
HTML
|
|
630
685
|
|
|
631
686
|
if body.include?("</body>")
|
data/lib/tina4/request.rb
CHANGED
|
@@ -3,6 +3,38 @@ require "uri"
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Tina4
|
|
6
|
+
# A Hash subclass that supports indifferent access (both string and symbol keys).
|
|
7
|
+
# Used by Request#params so that params[:id] and params["id"] both work.
|
|
8
|
+
class IndifferentHash < Hash
|
|
9
|
+
def [](key)
|
|
10
|
+
super(convert_key(key))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def []=(key, value)
|
|
14
|
+
super(convert_key(key), value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(key, *args, &block)
|
|
18
|
+
super(convert_key(key), *args, &block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def key?(key)
|
|
22
|
+
super(convert_key(key))
|
|
23
|
+
end
|
|
24
|
+
alias has_key? key?
|
|
25
|
+
alias include? key?
|
|
26
|
+
|
|
27
|
+
def delete(key, &block)
|
|
28
|
+
super(convert_key(key), &block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def convert_key(key)
|
|
34
|
+
key.is_a?(Symbol) ? key.to_s : key
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
6
38
|
class Request
|
|
7
39
|
attr_reader :env, :method, :path, :query_string, :content_type,
|
|
8
40
|
:path_params, :ip
|
|
@@ -88,10 +120,16 @@ module Tina4
|
|
|
88
120
|
end
|
|
89
121
|
|
|
90
122
|
# Merged params: query + body + path_params (path_params highest priority)
|
|
123
|
+
# Supports both string and symbol key access (indifferent access).
|
|
91
124
|
def params
|
|
92
125
|
@params ||= build_params
|
|
93
126
|
end
|
|
94
127
|
|
|
128
|
+
# Look up a param by symbol or string key (indifferent access shortcut).
|
|
129
|
+
def param(key)
|
|
130
|
+
params[key.to_s] || params[key.to_sym]
|
|
131
|
+
end
|
|
132
|
+
|
|
95
133
|
def [](key)
|
|
96
134
|
params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
|
|
97
135
|
end
|
|
@@ -168,10 +206,10 @@ module Tina4
|
|
|
168
206
|
end
|
|
169
207
|
|
|
170
208
|
def build_params
|
|
171
|
-
p =
|
|
209
|
+
p = IndifferentHash.new
|
|
172
210
|
|
|
173
211
|
# Query string params
|
|
174
|
-
query.each { |k, v| p[k] = v }
|
|
212
|
+
query.each { |k, v| p[k.to_s] = v }
|
|
175
213
|
|
|
176
214
|
# Body params
|
|
177
215
|
body_parsed.each { |k, v| p[k.to_s] = v }
|
data/lib/tina4/response.rb
CHANGED
|
@@ -103,10 +103,15 @@ module Tina4
|
|
|
103
103
|
self
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def render(template_path, data = {}, status: 200)
|
|
106
|
+
def render(template_path, data = {}, status: 200, template_dir: nil)
|
|
107
107
|
@status_code = status
|
|
108
108
|
@headers["content-type"] = HTML_CONTENT_TYPE
|
|
109
|
-
|
|
109
|
+
if template_dir
|
|
110
|
+
frond = Tina4::Frond.new(template_dir: template_dir)
|
|
111
|
+
@body = frond.render(template_path, data)
|
|
112
|
+
else
|
|
113
|
+
@body = Tina4::Template.render(template_path, data)
|
|
114
|
+
end
|
|
110
115
|
self
|
|
111
116
|
end
|
|
112
117
|
|
data/lib/tina4/router.rb
CHANGED
|
@@ -89,10 +89,14 @@ module Tina4
|
|
|
89
89
|
parts = path.split("/").reject(&:empty?)
|
|
90
90
|
regex_parts = parts.map do |part|
|
|
91
91
|
if part =~ /\A\*(\w+)\z/
|
|
92
|
-
#
|
|
92
|
+
# Named catch-all splat parameter: *path captures everything after
|
|
93
93
|
name = Regexp.last_match(1)
|
|
94
94
|
@param_names << { name: name.to_sym, type: "path" }
|
|
95
95
|
'(.+)'
|
|
96
|
+
elsif part == "*"
|
|
97
|
+
# Bare catch-all wildcard: captures everything after (unnamed)
|
|
98
|
+
@param_names << { name: :splat, type: "path" }
|
|
99
|
+
'(.+)'
|
|
96
100
|
elsif part =~ /\A\{(\w+)(?::(\w+))?\}\z/
|
|
97
101
|
# Tina4/Python-style brace params: {id} or {id:int}
|
|
98
102
|
# This is the ONLY supported param syntax, matching Python exactly.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Tina4 Test Client — Test routes without starting a server.
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
#
|
|
5
|
+
# client = Tina4::TestClient.new
|
|
6
|
+
#
|
|
7
|
+
# response = client.get("/api/users")
|
|
8
|
+
# assert_equal 200, response.status
|
|
9
|
+
# assert response.json["users"]
|
|
10
|
+
#
|
|
11
|
+
# response = client.post("/api/users", json: { name: "Alice" })
|
|
12
|
+
# assert_equal 201, response.status
|
|
13
|
+
#
|
|
14
|
+
# response = client.get("/api/users/1", headers: { "Authorization" => "Bearer token123" })
|
|
15
|
+
#
|
|
16
|
+
module Tina4
|
|
17
|
+
class TestResponse
|
|
18
|
+
attr_reader :status, :body, :headers, :content_type
|
|
19
|
+
|
|
20
|
+
# Build from a Rack response tuple [status, headers, body_array]
|
|
21
|
+
def initialize(rack_response)
|
|
22
|
+
@status = rack_response[0]
|
|
23
|
+
@headers = rack_response[1] || {}
|
|
24
|
+
@content_type = @headers["content-type"] || ""
|
|
25
|
+
raw_body = rack_response[2]
|
|
26
|
+
@body = raw_body.is_a?(Array) ? raw_body.join : raw_body.to_s
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Parse body as JSON.
|
|
30
|
+
def json
|
|
31
|
+
return nil if @body.nil? || @body.empty?
|
|
32
|
+
JSON.parse(@body)
|
|
33
|
+
rescue JSON::ParserError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return body as a string.
|
|
38
|
+
def text
|
|
39
|
+
@body.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inspect
|
|
43
|
+
"<TestResponse status=#{@status} content_type=#{@content_type.inspect}>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class TestClient
|
|
48
|
+
# Send a GET request.
|
|
49
|
+
def get(path, headers: nil)
|
|
50
|
+
request("GET", path, headers: headers)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Send a POST request.
|
|
54
|
+
def post(path, json: nil, body: nil, headers: nil)
|
|
55
|
+
request("POST", path, json: json, body: body, headers: headers)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Send a PUT request.
|
|
59
|
+
def put(path, json: nil, body: nil, headers: nil)
|
|
60
|
+
request("PUT", path, json: json, body: body, headers: headers)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Send a PATCH request.
|
|
64
|
+
def patch(path, json: nil, body: nil, headers: nil)
|
|
65
|
+
request("PATCH", path, json: json, body: body, headers: headers)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Send a DELETE request.
|
|
69
|
+
def delete(path, headers: nil)
|
|
70
|
+
request("DELETE", path, headers: headers)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Build a mock Rack env, match the route, execute the handler.
|
|
76
|
+
def request(method, path, json: nil, body: nil, headers: nil)
|
|
77
|
+
# Build raw body
|
|
78
|
+
raw_body = ""
|
|
79
|
+
content_type = ""
|
|
80
|
+
|
|
81
|
+
if json
|
|
82
|
+
raw_body = JSON.generate(json)
|
|
83
|
+
content_type = "application/json"
|
|
84
|
+
elsif body
|
|
85
|
+
raw_body = body.to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Split path and query string
|
|
89
|
+
clean_path, query_string = path.include?("?") ? path.split("?", 2) : [path, ""]
|
|
90
|
+
|
|
91
|
+
# Build Rack env hash
|
|
92
|
+
env = {
|
|
93
|
+
"REQUEST_METHOD" => method.upcase,
|
|
94
|
+
"PATH_INFO" => clean_path,
|
|
95
|
+
"QUERY_STRING" => query_string || "",
|
|
96
|
+
"SERVER_NAME" => "localhost",
|
|
97
|
+
"SERVER_PORT" => "7145",
|
|
98
|
+
"HTTP_HOST" => "localhost:7145",
|
|
99
|
+
"REMOTE_ADDR" => "127.0.0.1",
|
|
100
|
+
"rack.input" => StringIO.new(raw_body),
|
|
101
|
+
"rack.url_scheme" => "http"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Add content type
|
|
105
|
+
env["CONTENT_TYPE"] = content_type unless content_type.empty?
|
|
106
|
+
env["CONTENT_LENGTH"] = raw_body.bytesize.to_s unless raw_body.empty?
|
|
107
|
+
|
|
108
|
+
# Add custom headers (convert to Rack format: X-Custom → HTTP_X_CUSTOM)
|
|
109
|
+
if headers
|
|
110
|
+
headers.each do |key, value|
|
|
111
|
+
rack_key = "HTTP_#{key.upcase.tr('-', '_')}"
|
|
112
|
+
env[rack_key] = value
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Match route
|
|
117
|
+
result = Tina4::Router.find_route(clean_path, method.upcase)
|
|
118
|
+
|
|
119
|
+
unless result
|
|
120
|
+
return TestResponse.new([404, { "content-type" => "application/json" }, ['{"error":"Not found"}']])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
route, path_params = result
|
|
124
|
+
|
|
125
|
+
# Create request and response
|
|
126
|
+
req = Tina4::Request.new(env, path_params || {})
|
|
127
|
+
res = Tina4::Response.new
|
|
128
|
+
|
|
129
|
+
# Build handler args (same logic as RackApp.handle_route)
|
|
130
|
+
handler_params = route.handler.parameters.map(&:last)
|
|
131
|
+
route_params = path_params || {}
|
|
132
|
+
args = handler_params.map do |name|
|
|
133
|
+
if route_params.key?(name)
|
|
134
|
+
route_params[name]
|
|
135
|
+
elsif name == :request || name == :req
|
|
136
|
+
req
|
|
137
|
+
else
|
|
138
|
+
res
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Execute handler
|
|
143
|
+
handler_result = args.empty? ? route.handler.call : route.handler.call(*args)
|
|
144
|
+
|
|
145
|
+
# Auto-detect response type
|
|
146
|
+
if handler_result.is_a?(Tina4::Response)
|
|
147
|
+
final = handler_result
|
|
148
|
+
elsif route.respond_to?(:template) && route.template && handler_result.is_a?(Hash)
|
|
149
|
+
html = Tina4::Template.render(route.template, handler_result)
|
|
150
|
+
res.html(html)
|
|
151
|
+
final = res
|
|
152
|
+
else
|
|
153
|
+
final = Tina4::Response.auto_detect(handler_result, res)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
TestResponse.new(final.to_rack)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -48,6 +48,8 @@ require_relative "tina4/sql_translation"
|
|
|
48
48
|
require_relative "tina4/response_cache"
|
|
49
49
|
require_relative "tina4/html_element"
|
|
50
50
|
require_relative "tina4/error_overlay"
|
|
51
|
+
require_relative "tina4/test_client"
|
|
52
|
+
require_relative "tina4/mcp"
|
|
51
53
|
|
|
52
54
|
module Tina4
|
|
53
55
|
# ── Lazy-loaded: database drivers ─────────────────────────────────────
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.10.
|
|
4
|
+
version: 3.10.38
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-04-01 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rack
|
|
@@ -170,7 +171,7 @@ dependencies:
|
|
|
170
171
|
- - "~>"
|
|
171
172
|
- !ruby/object:Gem::Version
|
|
172
173
|
version: '1.8'
|
|
173
|
-
type: :
|
|
174
|
+
type: :runtime
|
|
174
175
|
prerelease: false
|
|
175
176
|
version_requirements: !ruby/object:Gem::Requirement
|
|
176
177
|
requirements:
|
|
@@ -321,7 +322,9 @@ files:
|
|
|
321
322
|
- lib/tina4/html_element.rb
|
|
322
323
|
- lib/tina4/localization.rb
|
|
323
324
|
- lib/tina4/log.rb
|
|
325
|
+
- lib/tina4/mcp.rb
|
|
324
326
|
- lib/tina4/messenger.rb
|
|
327
|
+
- lib/tina4/metrics.rb
|
|
325
328
|
- lib/tina4/middleware.rb
|
|
326
329
|
- lib/tina4/migration.rb
|
|
327
330
|
- lib/tina4/orm.rb
|
|
@@ -386,6 +389,7 @@ files:
|
|
|
386
389
|
- lib/tina4/templates/errors/502.twig
|
|
387
390
|
- lib/tina4/templates/errors/503.twig
|
|
388
391
|
- lib/tina4/templates/errors/base.twig
|
|
392
|
+
- lib/tina4/test_client.rb
|
|
389
393
|
- lib/tina4/testing.rb
|
|
390
394
|
- lib/tina4/validator.rb
|
|
391
395
|
- lib/tina4/version.rb
|
|
@@ -399,6 +403,7 @@ licenses:
|
|
|
399
403
|
- MIT
|
|
400
404
|
metadata:
|
|
401
405
|
homepage_uri: https://tina4.com
|
|
406
|
+
post_install_message:
|
|
402
407
|
rdoc_options: []
|
|
403
408
|
require_paths:
|
|
404
409
|
- lib
|
|
@@ -413,7 +418,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
413
418
|
- !ruby/object:Gem::Version
|
|
414
419
|
version: '0'
|
|
415
420
|
requirements: []
|
|
416
|
-
rubygems_version: 4.
|
|
421
|
+
rubygems_version: 3.4.19
|
|
422
|
+
signing_key:
|
|
417
423
|
specification_version: 4
|
|
418
424
|
summary: Simple. Fast. Human. This is not a framework.
|
|
419
425
|
test_files: []
|