tina4ruby 3.11.15 → 3.11.16

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.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -80
  3. data/LICENSE.txt +21 -21
  4. data/README.md +137 -137
  5. data/exe/tina4ruby +5 -5
  6. data/lib/tina4/ai.rb +696 -696
  7. data/lib/tina4/api.rb +189 -189
  8. data/lib/tina4/auth.rb +305 -305
  9. data/lib/tina4/auto_crud.rb +244 -244
  10. data/lib/tina4/cache.rb +154 -154
  11. data/lib/tina4/cli.rb +1449 -1449
  12. data/lib/tina4/constants.rb +46 -46
  13. data/lib/tina4/container.rb +74 -74
  14. data/lib/tina4/cors.rb +74 -74
  15. data/lib/tina4/crud.rb +692 -692
  16. data/lib/tina4/database/sqlite3_adapter.rb +165 -165
  17. data/lib/tina4/database.rb +625 -625
  18. data/lib/tina4/database_result.rb +208 -208
  19. data/lib/tina4/debug.rb +8 -8
  20. data/lib/tina4/dev.rb +14 -14
  21. data/lib/tina4/dev_admin.rb +1289 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -124
  24. data/lib/tina4/drivers/mongodb_driver.rb +561 -561
  25. data/lib/tina4/drivers/mssql_driver.rb +112 -112
  26. data/lib/tina4/drivers/mysql_driver.rb +90 -90
  27. data/lib/tina4/drivers/odbc_driver.rb +191 -191
  28. data/lib/tina4/drivers/postgres_driver.rb +116 -116
  29. data/lib/tina4/drivers/sqlite_driver.rb +122 -122
  30. data/lib/tina4/env.rb +95 -95
  31. data/lib/tina4/error_overlay.rb +252 -252
  32. data/lib/tina4/events.rb +109 -109
  33. data/lib/tina4/field_types.rb +154 -154
  34. data/lib/tina4/frond.rb +2087 -2025
  35. data/lib/tina4/gallery/auth/meta.json +1 -1
  36. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
  37. data/lib/tina4/gallery/database/meta.json +1 -1
  38. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
  39. data/lib/tina4/gallery/error-overlay/meta.json +1 -1
  40. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
  41. data/lib/tina4/gallery/orm/meta.json +1 -1
  42. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
  43. data/lib/tina4/gallery/queue/meta.json +1 -1
  44. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
  45. data/lib/tina4/gallery/rest-api/meta.json +1 -1
  46. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
  47. data/lib/tina4/gallery/templates/meta.json +1 -1
  48. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
  49. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
  50. data/lib/tina4/graphql.rb +966 -966
  51. data/lib/tina4/health.rb +39 -39
  52. data/lib/tina4/html_element.rb +170 -170
  53. data/lib/tina4/job.rb +80 -80
  54. data/lib/tina4/localization.rb +168 -168
  55. data/lib/tina4/log.rb +203 -203
  56. data/lib/tina4/mcp.rb +871 -696
  57. data/lib/tina4/messenger.rb +587 -587
  58. data/lib/tina4/metrics.rb +793 -793
  59. data/lib/tina4/middleware.rb +445 -445
  60. data/lib/tina4/migration.rb +451 -451
  61. data/lib/tina4/orm.rb +790 -790
  62. data/lib/tina4/plan.rb +471 -0
  63. data/lib/tina4/project_index.rb +366 -0
  64. data/lib/tina4/public/css/tina4.css +2463 -2463
  65. data/lib/tina4/public/css/tina4.min.css +1 -1
  66. data/lib/tina4/public/images/logo.svg +5 -5
  67. data/lib/tina4/public/js/frond.min.js +2 -2
  68. data/lib/tina4/public/js/tina4-dev-admin.js +1264 -565
  69. data/lib/tina4/public/js/tina4-dev-admin.min.js +1264 -480
  70. data/lib/tina4/public/js/tina4.min.js +92 -92
  71. data/lib/tina4/public/js/tina4js.min.js +48 -48
  72. data/lib/tina4/public/swagger/index.html +90 -90
  73. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  74. data/lib/tina4/query_builder.rb +380 -380
  75. data/lib/tina4/queue.rb +366 -366
  76. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  77. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  78. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  79. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  80. data/lib/tina4/rack_app.rb +817 -817
  81. data/lib/tina4/rate_limiter.rb +130 -130
  82. data/lib/tina4/request.rb +268 -268
  83. data/lib/tina4/response.rb +346 -346
  84. data/lib/tina4/response_cache.rb +551 -551
  85. data/lib/tina4/router.rb +406 -406
  86. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  87. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  88. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  89. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  90. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  91. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  92. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  93. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  94. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  95. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  96. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  97. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  98. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  99. data/lib/tina4/scss/tina4css/base.scss +1 -1
  100. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  101. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  102. data/lib/tina4/scss_compiler.rb +178 -178
  103. data/lib/tina4/seeder.rb +567 -567
  104. data/lib/tina4/service_runner.rb +303 -303
  105. data/lib/tina4/session.rb +297 -297
  106. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  107. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  108. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  109. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  110. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  111. data/lib/tina4/shutdown.rb +84 -84
  112. data/lib/tina4/sql_translation.rb +158 -158
  113. data/lib/tina4/swagger.rb +124 -124
  114. data/lib/tina4/template.rb +894 -894
  115. data/lib/tina4/templates/base.twig +26 -26
  116. data/lib/tina4/templates/errors/302.twig +14 -14
  117. data/lib/tina4/templates/errors/401.twig +9 -9
  118. data/lib/tina4/templates/errors/403.twig +29 -29
  119. data/lib/tina4/templates/errors/404.twig +29 -29
  120. data/lib/tina4/templates/errors/500.twig +38 -38
  121. data/lib/tina4/templates/errors/502.twig +9 -9
  122. data/lib/tina4/templates/errors/503.twig +12 -12
  123. data/lib/tina4/templates/errors/base.twig +37 -37
  124. data/lib/tina4/test_client.rb +159 -159
  125. data/lib/tina4/testing.rb +340 -340
  126. data/lib/tina4/validator.rb +174 -174
  127. data/lib/tina4/version.rb +1 -1
  128. data/lib/tina4/webserver.rb +312 -312
  129. data/lib/tina4/websocket.rb +343 -343
  130. data/lib/tina4/websocket_backplane.rb +190 -190
  131. data/lib/tina4/wsdl.rb +564 -564
  132. data/lib/tina4.rb +460 -458
  133. data/lib/tina4ruby.rb +4 -4
  134. metadata +5 -3
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tina4::ProjectIndex — lightweight "where is what" map of a project.
4
+ # Ported from tina4_python/dev_admin/project_index.py.
5
+ #
6
+ # Storage: .tina4/project_index.json at the project root. Incremental,
7
+ # mtime-based refresh on every read. Per-language extractors (Ruby,
8
+ # ERB/Twig, SQL, JS/TS, Markdown) produce symbol/route/import summaries
9
+ # used by index_search and index_overview.
10
+
11
+ require "json"
12
+ require "digest"
13
+ require "fileutils"
14
+
15
+ module Tina4
16
+ module ProjectIndex
17
+ INDEX_DIRNAME = ".tina4"
18
+ INDEX_FILENAME = "project_index.json"
19
+
20
+ SKIP_DIRS = %w[
21
+ .git .hg .svn node_modules __pycache__ .venv venv .mypy_cache
22
+ .ruff_cache .pytest_cache dist build .tina4 logs .idea .vscode
23
+ vendor coverage tmp .bundle
24
+ ].freeze
25
+
26
+ INDEX_EXT = %w[
27
+ .rb .erb .twig .html .sql .scss .css .js .ts .mjs .md .json .yml
28
+ .yaml .toml .env .rake
29
+ ].freeze
30
+
31
+ MAX_FILE_BYTES = 256 * 1024
32
+
33
+ ROUTE_METHODS = %w[get post put patch delete any any_method secure_get secure_post].freeze
34
+
35
+ class << self
36
+ def project_root
37
+ File.expand_path(Dir.pwd)
38
+ end
39
+
40
+ def index_path
41
+ dir = File.join(project_root, INDEX_DIRNAME)
42
+ FileUtils.mkdir_p(dir)
43
+ File.join(dir, INDEX_FILENAME)
44
+ end
45
+
46
+ # ── Extractors ────────────────────────────────────────────
47
+
48
+ def extract_ruby(text)
49
+ out = { "symbols" => [], "imports" => [], "routes" => [], "docstring" => "" }
50
+ # Pick up first non-blank comment block as pseudo-docstring.
51
+ first_comment = nil
52
+ text.each_line do |ln|
53
+ s = ln.strip
54
+ next if s.empty? || s.start_with?("#!") || s == "# frozen_string_literal: true"
55
+ if s.start_with?("#")
56
+ first_comment = s.sub(/\A#\s*/, "")[0, 200]
57
+ break
58
+ else
59
+ break
60
+ end
61
+ end
62
+ out["docstring"] = first_comment if first_comment
63
+
64
+ text.scan(/^\s*(?:class|module)\s+([A-Z][\w:]*)/) { |m| out["symbols"] << m[0] }
65
+ text.scan(/^\s*def\s+(self\.)?([A-Za-z_][\w!?=]*)/) { |m| out["symbols"] << m[1] }
66
+ text.scan(/^\s*require(?:_relative)?\s+['"]([^'"]+)['"]/) { |m| out["imports"] << m[0] }
67
+ # Tina4.get "/path" OR Tina4::Router.get("/path") OR get "/path" do
68
+ route_re = /(?:Tina4(?:::Router)?\.|^\s*)(get|post|put|patch|delete|any|any_method|secure_get|secure_post)\s*\(?\s*['"]([^'"]+)['"]/
69
+ text.scan(route_re) do |meth, path|
70
+ next unless ROUTE_METHODS.include?(meth)
71
+ out["routes"] << { "method" => meth.upcase, "path" => path, "handler" => "" }
72
+ end
73
+ out["symbols"].uniq!
74
+ out["imports"].uniq!
75
+ out
76
+ end
77
+
78
+ TWIG_EXTENDS = /\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/.freeze
79
+ TWIG_BLOCK = /\{%\s*block\s+([A-Za-z_][\w-]*)/.freeze
80
+ TWIG_INCLUDE = /\{%\s*include\s+['"]([^'"]+)['"]/.freeze
81
+
82
+ def extract_twig(text)
83
+ {
84
+ "extends" => text.scan(TWIG_EXTENDS).flatten,
85
+ "blocks" => text.scan(TWIG_BLOCK).flatten.uniq.sort,
86
+ "includes" => text.scan(TWIG_INCLUDE).flatten.uniq.sort
87
+ }
88
+ end
89
+
90
+ def extract_erb(text)
91
+ # ERB acts a lot like Twig here — extract partial renders.
92
+ renders = text.scan(/render\s+['"]([^'"]+)['"]/).flatten.uniq
93
+ { "renders" => renders }
94
+ end
95
+
96
+ SQL_CREATE = /create\s+(?:unique\s+)?(table|index|view|trigger|sequence|procedure|function)\s+(?:if\s+not\s+exists\s+)?([A-Za-z_][\w.]*)/i.freeze
97
+ SQL_ALTER = /alter\s+(table|index|view)\s+([A-Za-z_][\w.]*)/i.freeze
98
+
99
+ def extract_sql(text)
100
+ out = { "creates" => [], "alters" => [] }
101
+ text.scan(SQL_CREATE) { |kind, name| out["creates"] << "#{kind.upcase} #{name}" }
102
+ text.scan(SQL_ALTER) { |kind, name| out["alters"] << "#{kind.upcase} #{name}" }
103
+ out
104
+ end
105
+
106
+ JS_EXPORT = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/.freeze
107
+ JS_IMPORT = /^\s*import\s+[^'"]+?['"]([^'"]+)['"]/.freeze
108
+
109
+ def extract_js_ts(text)
110
+ {
111
+ "exports" => text.scan(JS_EXPORT).flatten.uniq.sort,
112
+ "imports" => text.scan(JS_IMPORT).flatten.uniq.sort
113
+ }
114
+ end
115
+
116
+ MD_H1 = /^#\s+(.+)$/.freeze
117
+ MD_H2 = /^##\s+(.+)$/.freeze
118
+
119
+ def extract_md(text)
120
+ {
121
+ "title" => (text[MD_H1, 1] || "").strip,
122
+ "sections" => text.scan(MD_H2).flatten.first(30)
123
+ }
124
+ end
125
+
126
+ def extract_generic(text)
127
+ text.each_line do |line|
128
+ s = line.strip
129
+ next if s.empty? || s.start_with?("<!--")
130
+ return { "first_line" => s[0, 200] }
131
+ end
132
+ {}
133
+ end
134
+
135
+ EXTRACTORS = {
136
+ ".rb" => :extract_ruby,
137
+ ".rake" => :extract_ruby,
138
+ ".erb" => :extract_erb,
139
+ ".twig" => :extract_twig,
140
+ ".html" => :extract_twig,
141
+ ".sql" => :extract_sql,
142
+ ".js" => :extract_js_ts,
143
+ ".mjs" => :extract_js_ts,
144
+ ".ts" => :extract_js_ts,
145
+ ".md" => :extract_md
146
+ }.freeze
147
+
148
+ LANGUAGES = {
149
+ ".rb" => "ruby", ".rake" => "ruby",
150
+ ".erb" => "erb", ".twig" => "twig", ".html" => "html",
151
+ ".sql" => "sql", ".scss" => "scss", ".css" => "css",
152
+ ".js" => "javascript", ".mjs" => "javascript", ".ts" => "typescript",
153
+ ".md" => "markdown", ".json" => "json", ".yml" => "yaml",
154
+ ".yaml" => "yaml", ".toml" => "toml", ".env" => "env"
155
+ }.freeze
156
+
157
+ def language_for(path)
158
+ LANGUAGES[File.extname(path)] || "text"
159
+ end
160
+
161
+ # ── Index core ───────────────────────────────────────────
162
+
163
+ def extract(path)
164
+ stat = File.stat(path)
165
+ rel = path.sub("#{project_root}/", "")
166
+ entry = {
167
+ "path" => rel,
168
+ "size" => stat.size,
169
+ "mtime" => stat.mtime.to_i,
170
+ "language" => language_for(path)
171
+ }
172
+ return entry.merge("skipped" => "too large (#{stat.size} bytes)") if stat.size > MAX_FILE_BYTES
173
+
174
+ text = begin
175
+ File.read(path, encoding: "utf-8", invalid: :replace, undef: :replace)
176
+ rescue StandardError
177
+ return entry
178
+ end
179
+ entry["sha256"] = Digest::SHA256.hexdigest(text)[0, 16]
180
+ ext = File.extname(path)
181
+ extractor = EXTRACTORS[ext]
182
+ begin
183
+ data = extractor ? send(extractor, text) : extract_generic(text)
184
+ entry.merge!(data) if data.is_a?(Hash)
185
+ rescue StandardError => e
186
+ entry["extraction_error"] = e.message[0, 200]
187
+ end
188
+ entry["summary"] = summarise(entry)
189
+ entry
190
+ rescue Errno::ENOENT, Errno::EACCES
191
+ {}
192
+ end
193
+
194
+ def summarise(entry)
195
+ return entry["skipped"] if entry["skipped"]
196
+ return entry["docstring"] if entry["docstring"] && !entry["docstring"].to_s.empty?
197
+ return entry["title"] if entry["title"] && !entry["title"].to_s.empty?
198
+ if entry["routes"] && !entry["routes"].empty?
199
+ r = entry["routes"][0]
200
+ extra = entry["routes"].size > 1 ? " (+#{entry["routes"].size - 1} more)" : ""
201
+ return "#{r["method"]} #{r["path"]}#{extra}"
202
+ end
203
+ return "defines " + entry["symbols"].first(4).join(", ") if entry["symbols"] && !entry["symbols"].empty?
204
+ return "exports " + entry["exports"].first(4).join(", ") if entry["exports"] && !entry["exports"].empty?
205
+ return "schema: " + entry["creates"].first(3).join(", ") if entry["creates"] && !entry["creates"].empty?
206
+ return "template, extends #{entry["extends"][0]}" if entry["extends"] && !entry["extends"].empty?
207
+ return entry["first_line"] if entry["first_line"]
208
+ ""
209
+ end
210
+
211
+ def walk_project
212
+ root = project_root
213
+ found = []
214
+ prefix_len = root.length + 1
215
+ walker = lambda do |dir|
216
+ Dir.each_child(dir) do |name|
217
+ next if name == "." || name == ".."
218
+ # Skip hidden dirs (allow .env as a file below)
219
+ full = File.join(dir, name)
220
+ if File.directory?(full)
221
+ next if SKIP_DIRS.include?(name)
222
+ next if name.start_with?(".")
223
+ walker.call(full)
224
+ elsif File.file?(full)
225
+ if name.start_with?(".")
226
+ next unless name == ".env"
227
+ end
228
+ ext = File.extname(name)
229
+ next unless INDEX_EXT.include?(ext) || name == ".env"
230
+ found << full
231
+ end
232
+ end
233
+ rescue Errno::EACCES, Errno::ENOENT
234
+ # skip
235
+ end
236
+ walker.call(root)
237
+ found
238
+ end
239
+
240
+ def load_raw
241
+ p = index_path
242
+ return { "version" => 1, "files" => {}, "generated_at" => 0 } unless File.exist?(p)
243
+ JSON.parse(File.read(p, encoding: "utf-8"))
244
+ rescue StandardError
245
+ { "version" => 1, "files" => {}, "generated_at" => 0 }
246
+ end
247
+
248
+ def save_raw(data)
249
+ data["generated_at"] = Time.now.to_i
250
+ File.write(index_path, JSON.pretty_generate(data), encoding: "utf-8")
251
+ end
252
+
253
+ def refresh
254
+ data = load_raw
255
+ files = data["files"] || {}
256
+ added = 0
257
+ updated = 0
258
+ seen = {}
259
+ root = project_root
260
+
261
+ walk_project.each do |p|
262
+ rel = p.sub("#{root}/", "")
263
+ seen[rel] = true
264
+ begin
265
+ mtime = File.mtime(p).to_i
266
+ rescue Errno::ENOENT
267
+ next
268
+ end
269
+ existing = files[rel]
270
+ if existing && existing["mtime"] == mtime
271
+ next
272
+ end
273
+ files[rel] = extract(p)
274
+ if existing
275
+ updated += 1
276
+ else
277
+ added += 1
278
+ end
279
+ end
280
+
281
+ removed_paths = files.keys - seen.keys
282
+ removed_paths.each { |k| files.delete(k) }
283
+ data["files"] = files
284
+ save_raw(data)
285
+ {
286
+ "added" => added,
287
+ "updated" => updated,
288
+ "removed" => removed_paths.size,
289
+ "total" => files.size,
290
+ "path" => index_path.sub("#{root}/", "")
291
+ }
292
+ end
293
+
294
+ def search(query, limit = 20)
295
+ refresh
296
+ data = load_raw
297
+ q = query.to_s.downcase.strip
298
+ return [] if q.empty?
299
+ hits = []
300
+ data["files"].each do |rel, entry|
301
+ score = 0
302
+ score += 10 if rel.downcase.include?(q)
303
+ (entry["symbols"] || []).each do |s|
304
+ sl = s.downcase
305
+ if sl == q
306
+ score += 8
307
+ elsif sl.include?(q)
308
+ score += 4
309
+ end
310
+ end
311
+ (entry["routes"] || []).each do |r|
312
+ combined = "#{r["path"]} #{r["handler"]}".downcase
313
+ score += 5 if combined.include?(q)
314
+ end
315
+ score += 3 if (entry["summary"] || "").downcase.include?(q)
316
+ (entry["imports"] || []).each { |imp| score += 1 if imp.downcase.include?(q) }
317
+ if score.positive?
318
+ hits << [score, {
319
+ "path" => rel,
320
+ "summary" => entry["summary"] || "",
321
+ "score" => score,
322
+ "language" => entry["language"] || ""
323
+ }]
324
+ end
325
+ end
326
+ hits.sort_by! { |h| -h[0] }
327
+ hits.first([1, limit].max).map { |_, info| info }
328
+ end
329
+
330
+ def file_entry(rel_path)
331
+ refresh
332
+ data = load_raw
333
+ entry = data["files"][rel_path]
334
+ entry || { "error" => "Not in index: #{rel_path}" }
335
+ end
336
+
337
+ def overview
338
+ refresh
339
+ data = load_raw
340
+ files = data["files"]
341
+ langs = Hash.new(0)
342
+ route_count = 0
343
+ model_count = 0
344
+ files.each_value do |e|
345
+ langs[e["language"] || "other"] += 1
346
+ route_count += (e["routes"] || []).size
347
+ path = e["path"].to_s
348
+ if (path.start_with?("src/orm/") || path.start_with?("orm/") || path.start_with?("app/models/")) && (e["symbols"] && !e["symbols"].empty?)
349
+ model_count += 1
350
+ end
351
+ end
352
+ recent = files.values.map do |e|
353
+ { "path" => e["path"], "summary" => e["summary"] || "", "mtime" => e["mtime"] || 0 }
354
+ end.sort_by { |e| -(e["mtime"] || 0) }.first(10)
355
+ {
356
+ "total_files" => files.size,
357
+ "by_language" => langs,
358
+ "routes_declared" => route_count,
359
+ "orm_models" => model_count,
360
+ "recently_changed" => recent,
361
+ "index_generated_at" => data["generated_at"] || 0
362
+ }
363
+ end
364
+ end
365
+ end
366
+ end