tina4ruby 3.11.15 → 3.11.17

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 +1291 -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
data/lib/tina4.rb CHANGED
@@ -1,458 +1,460 @@
1
- # frozen_string_literal: true
2
-
3
- # ── Fast JSON: use oj if available, fall back to stdlib json ──────────
4
- begin
5
- require "oj"
6
- Oj.default_options = { mode: :compat, symbol_keys: false }
7
- rescue LoadError
8
- # oj not installed — stdlib json is fine
9
- end
10
-
11
- # ── Core (always loaded) ──────────────────────────────────────────────
12
- require_relative "tina4/version"
13
- require_relative "tina4/constants"
14
- require_relative "tina4/log"
15
- require_relative "tina4/debug" # backward compat alias
16
- require_relative "tina4/env"
17
- require_relative "tina4/router"
18
- require_relative "tina4/request"
19
- require_relative "tina4/response"
20
- require_relative "tina4/rack_app"
21
- require_relative "tina4/database"
22
- require_relative "tina4/database_result"
23
- require_relative "tina4/field_types"
24
- require_relative "tina4/orm"
25
- require_relative "tina4/query_builder"
26
- require_relative "tina4/migration"
27
- require_relative "tina4/auto_crud"
28
- require_relative "tina4/database/sqlite3_adapter"
29
- require_relative "tina4/template"
30
- require_relative "tina4/frond"
31
- require_relative "tina4/auth"
32
- require_relative "tina4/session"
33
- require_relative "tina4/middleware"
34
- require_relative "tina4/cors"
35
- require_relative "tina4/rate_limiter"
36
- require_relative "tina4/health"
37
- require_relative "tina4/shutdown"
38
- require_relative "tina4/localization"
39
- require_relative "tina4/container"
40
- require_relative "tina4/queue"
41
- require_relative "tina4/service_runner"
42
- require_relative "tina4/events"
43
- require_relative "tina4/dev_admin"
44
- require_relative "tina4/messenger"
45
- require_relative "tina4/dev_mailbox"
46
- require_relative "tina4/ai"
47
- require_relative "tina4/cache"
48
- require_relative "tina4/sql_translation"
49
- require_relative "tina4/response_cache"
50
- require_relative "tina4/html_element"
51
- require_relative "tina4/error_overlay"
52
- require_relative "tina4/test_client"
53
- require_relative "tina4/mcp"
54
-
55
- module Tina4
56
- # ── Lazy-loaded: database drivers ─────────────────────────────────────
57
- module Drivers
58
- autoload :SqliteDriver, File.expand_path("tina4/drivers/sqlite_driver", __dir__)
59
- autoload :PostgresDriver, File.expand_path("tina4/drivers/postgres_driver", __dir__)
60
- autoload :MysqlDriver, File.expand_path("tina4/drivers/mysql_driver", __dir__)
61
- autoload :MssqlDriver, File.expand_path("tina4/drivers/mssql_driver", __dir__)
62
- autoload :FirebirdDriver, File.expand_path("tina4/drivers/firebird_driver", __dir__)
63
- autoload :MongodbDriver, File.expand_path("tina4/drivers/mongodb_driver", __dir__)
64
- autoload :OdbcDriver, File.expand_path("tina4/drivers/odbc_driver", __dir__)
65
- end
66
-
67
- # ── Lazy-loaded: session handlers ─────────────────────────────────────
68
- module SessionHandlers
69
- autoload :FileHandler, File.expand_path("tina4/session_handlers/file_handler", __dir__)
70
- autoload :RedisHandler, File.expand_path("tina4/session_handlers/redis_handler", __dir__)
71
- autoload :MongoHandler, File.expand_path("tina4/session_handlers/mongo_handler", __dir__)
72
- autoload :ValkeyHandler, File.expand_path("tina4/session_handlers/valkey_handler", __dir__)
73
- autoload :DatabaseHandler, File.expand_path("tina4/session_handlers/database_handler", __dir__)
74
- end
75
-
76
- # ── Lazy-loaded: queue backends ───────────────────────────────────────
77
- module QueueBackends
78
- autoload :LiteBackend, File.expand_path("tina4/queue_backends/lite_backend", __dir__)
79
- autoload :RabbitmqBackend, File.expand_path("tina4/queue_backends/rabbitmq_backend", __dir__)
80
- autoload :KafkaBackend, File.expand_path("tina4/queue_backends/kafka_backend", __dir__)
81
- autoload :MongoBackend, File.expand_path("tina4/queue_backends/mongo_backend", __dir__)
82
- end
83
-
84
- # ── Lazy-loaded: web server ───────────────────────────────────────────
85
- autoload :WebServer, File.expand_path("tina4/webserver", __dir__)
86
-
87
- # ── Lazy-loaded: optional modules ─────────────────────────────────────
88
- autoload :Swagger, File.expand_path("tina4/swagger", __dir__)
89
- autoload :Crud, File.expand_path("tina4/crud", __dir__)
90
- autoload :CRUD, File.expand_path("tina4/crud", __dir__)
91
- autoload :API, File.expand_path("tina4/api", __dir__)
92
- autoload :APIResponse, File.expand_path("tina4/api", __dir__)
93
- autoload :GraphQLType, File.expand_path("tina4/graphql", __dir__)
94
- autoload :GraphQLSchema, File.expand_path("tina4/graphql", __dir__)
95
- autoload :GraphQLParser, File.expand_path("tina4/graphql", __dir__)
96
- autoload :GraphQLExecutor, File.expand_path("tina4/graphql", __dir__)
97
- autoload :GraphQLError, File.expand_path("tina4/graphql", __dir__)
98
- autoload :GraphQL, File.expand_path("tina4/graphql", __dir__)
99
- autoload :WebSocket, File.expand_path("tina4/websocket", __dir__)
100
- autoload :WebSocketConnection, File.expand_path("tina4/websocket", __dir__)
101
- autoload :Testing, File.expand_path("tina4/testing", __dir__)
102
- autoload :ScssCompiler, File.expand_path("tina4/scss_compiler", __dir__)
103
- autoload :FakeData, File.expand_path("tina4/seeder", __dir__)
104
- autoload :WSDL, File.expand_path("tina4/wsdl", __dir__)
105
- BANNER = <<~'BANNER'
106
-
107
- ______ _ __ __
108
- /_ __/(_)___ ____ _/ // /
109
- / / / / __ \/ __ `/ // /_
110
- / / / / / / / /_/ /__ __/
111
- /_/ /_/_/ /_/\__,_/ /_/
112
- BANNER
113
-
114
- class << self
115
- attr_accessor :root_dir, :database
116
-
117
- def print_banner(host: "0.0.0.0", port: 7147, server_name: nil)
118
- is_tty = $stdout.respond_to?(:isatty) && $stdout.isatty
119
- color = is_tty ? "\e[31m" : ""
120
- reset = is_tty ? "\e[0m" : ""
121
-
122
- is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
123
- log_level = (ENV["TINA4_LOG_LEVEL"] || "[TINA4_LOG_ALL]").upcase
124
- display = (host == "0.0.0.0" || host == "::") ? "localhost" : host
125
-
126
- # Auto-detect server name if not provided
127
- if server_name.nil?
128
- if is_debug
129
- server_name = "WEBrick"
130
- else
131
- begin
132
- require "puma"
133
- server_name = "puma"
134
- rescue LoadError
135
- server_name = "WEBrick"
136
- end
137
- end
138
- end
139
-
140
- puts "#{color}#{BANNER}#{reset}"
141
- puts " TINA4 — The Intelligent Native Application 4ramework"
142
- puts " Simple. Fast. Human. | Built for AI. Built for you."
143
- puts ""
144
- puts " Server: http://#{display}:#{port} (#{server_name})"
145
- puts " Swagger: http://localhost:#{port}/swagger"
146
- puts " Dashboard: http://localhost:#{port}/__dev"
147
- puts " Debug: #{is_debug ? 'ON' : 'OFF'} (Log level: #{log_level})"
148
- puts ""
149
- rescue
150
- puts "#{color}TINA4 Ruby v#{VERSION}#{reset}"
151
- end
152
-
153
- def initialize!(root_dir = Dir.pwd)
154
- @root_dir = root_dir
155
-
156
- # Print banner
157
- print_banner
158
-
159
- # Load environment
160
- Tina4::Env.load_env(root_dir)
161
-
162
- # Setup debug logging
163
- Tina4::Log.configure(root_dir)
164
- Tina4::Log.info("Tina4 Ruby v#{VERSION} initializing...")
165
-
166
- # Setup auth keys
167
- Tina4::Auth.setup(root_dir)
168
-
169
- # Load translations
170
- Tina4::Localization.load(root_dir)
171
-
172
- # Auto-wire t() into template globals if locales were loaded
173
- autowire_i18n_template_global
174
-
175
- # Connect database if configured
176
- setup_database
177
-
178
- # Auto-discover routes
179
- auto_discover(root_dir)
180
-
181
- Tina4::Log.info("Tina4 initialized successfully")
182
- end
183
-
184
- # Initialize and start the web server.
185
- # This is the primary entry point for app.rb files:
186
- # Tina4.initialize!(__dir__)
187
- # Tina4.run!
188
- # Or combined: Tina4.run!(__dir__)
189
- def find_available_port(start, max_tries = 10)
190
- require "socket"
191
- max_tries.times do |offset|
192
- port = start + offset
193
- begin
194
- server = TCPServer.new("127.0.0.1", port)
195
- server.close
196
- return port
197
- rescue Errno::EADDRINUSE, Errno::EACCES
198
- next
199
- end
200
- end
201
- start
202
- end
203
-
204
- def open_browser(url)
205
- require "rbconfig"
206
- Thread.new do
207
- sleep 2
208
- case RbConfig::CONFIG["host_os"]
209
- when /darwin/i then system("open", url)
210
- when /mswin|mingw/i then system("start", url)
211
- else system("xdg-open", url)
212
- end
213
- end
214
- end
215
-
216
- def run!(root_dir = nil, port: nil, host: nil, debug: nil)
217
- # Handle legacy call: run!(port: 7147) where root_dir receives the hash
218
- if root_dir.is_a?(Hash)
219
- port ||= root_dir[:port]
220
- host ||= root_dir[:host]
221
- debug = root_dir[:debug] if debug.nil? && root_dir.key?(:debug)
222
- root_dir = nil
223
- end
224
- root_dir ||= Dir.pwd
225
-
226
- ENV["PORT"] = port.to_s if port
227
- ENV["HOST"] = host.to_s if host
228
- ENV["TINA4_DEBUG"] = debug.to_s unless debug.nil?
229
-
230
- initialize!(root_dir) unless @root_dir
231
-
232
- host = ENV.fetch("HOST", ENV.fetch("TINA4_HOST", "0.0.0.0"))
233
- port = ENV.fetch("PORT", ENV.fetch("TINA4_PORT", "7147")).to_i
234
-
235
- actual_port = find_available_port(port)
236
- if actual_port != port
237
- Tina4::Log.info("Port #{port} in use, using #{actual_port}")
238
- port = actual_port
239
- end
240
-
241
- display_host = (host == "0.0.0.0" || host == "::") ? "localhost" : host
242
- url = "http://#{display_host}:#{port}"
243
-
244
- app = Tina4::RackApp.new(root_dir: root_dir)
245
- is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
246
-
247
- # Try Puma first (production-grade), fall back to WEBrick
248
- if !is_debug
249
- begin
250
- require "puma"
251
- require "puma/configuration"
252
- require "puma/launcher"
253
-
254
- config = Puma::Configuration.new do |user_config|
255
- user_config.bind "tcp://#{host}:#{port}"
256
- user_config.app app
257
- user_config.threads 0, 16
258
- user_config.workers 0
259
- user_config.environment "production"
260
- user_config.log_requests false
261
- user_config.quiet
262
- end
263
-
264
- Tina4::Log.info("Production server: puma")
265
- Tina4::Shutdown.setup
266
-
267
- open_browser(url)
268
- launcher = Puma::Launcher.new(config)
269
- launcher.run
270
- return
271
- rescue LoadError
272
- # Puma not installed, fall through to WEBrick
273
- end
274
- end
275
-
276
- Tina4::Log.info("Development server: WEBrick")
277
- open_browser(url)
278
- server = Tina4::WebServer.new(app, host: host, port: port)
279
- server.start
280
- end
281
-
282
- # DSL methods for route registration
283
- # GET is public by default (matching tina4_python behavior)
284
- # POST/PUT/PATCH/DELETE are secured by default — use auth: false to make public
285
- def get(path, auth: nil, swagger_meta: {}, &block)
286
- auth_handler = auth == false ? nil : auth
287
- Tina4::Router.add("GET", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
288
- end
289
-
290
- def post(path, auth: :default, swagger_meta: {}, &block)
291
- auth_handler = resolve_auth(auth)
292
- Tina4::Router.add("POST", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
293
- end
294
-
295
- def put(path, auth: :default, swagger_meta: {}, &block)
296
- auth_handler = resolve_auth(auth)
297
- Tina4::Router.add("PUT", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
298
- end
299
-
300
- def patch(path, auth: :default, swagger_meta: {}, &block)
301
- auth_handler = resolve_auth(auth)
302
- Tina4::Router.add("PATCH", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
303
- end
304
-
305
- def delete(path, auth: :default, swagger_meta: {}, &block)
306
- auth_handler = resolve_auth(auth)
307
- Tina4::Router.add("DELETE", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
308
- end
309
-
310
- def any(path, auth: false, swagger_meta: {}, &block)
311
- auth_handler = resolve_auth(auth)
312
- %w[GET POST PUT PATCH DELETE].each do |method|
313
- Tina4::Router.add(method, path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
314
- end
315
- end
316
-
317
- def options(path, &block)
318
- Tina4::Router.add("OPTIONS", path, block)
319
- end
320
-
321
- # Explicit secure variants (always secured, regardless of HTTP method)
322
- def secure_get(path, auth: nil, swagger_meta: {}, &block)
323
- auth_handler = auth || Tina4::Auth.default_secure_auth
324
- Tina4::Router.add("GET", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
325
- end
326
-
327
- def secure_post(path, auth: nil, swagger_meta: {}, &block)
328
- auth_handler = auth || Tina4::Auth.default_secure_auth
329
- Tina4::Router.add("POST", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
330
- end
331
-
332
- def secure_put(path, auth: nil, swagger_meta: {}, &block)
333
- auth_handler = auth || Tina4::Auth.default_secure_auth
334
- Tina4::Router.add("PUT", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
335
- end
336
-
337
- def secure_patch(path, auth: nil, swagger_meta: {}, &block)
338
- auth_handler = auth || Tina4::Auth.default_secure_auth
339
- Tina4::Router.add("PATCH", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
340
- end
341
-
342
- def secure_delete(path, auth: nil, swagger_meta: {}, &block)
343
- auth_handler = auth || Tina4::Auth.default_secure_auth
344
- Tina4::Router.add("DELETE", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
345
- end
346
-
347
- # Route groups
348
- def group(prefix, auth: nil, &block)
349
- Tina4::Router.group(prefix, auth_handler: auth, &block)
350
- end
351
-
352
- # WebSocket route registration
353
- def websocket(path, &block)
354
- Tina4::Router.websocket(path, &block)
355
- end
356
-
357
- # Middleware hooks
358
- def before(pattern = nil, &block)
359
- Tina4::Middleware.before(pattern, &block)
360
- end
361
-
362
- def after(pattern = nil, &block)
363
- Tina4::Middleware.after(pattern, &block)
364
- end
365
-
366
- # Template globals
367
- def template_global(key, value)
368
- Tina4::Template.add_global(key, value)
369
- end
370
-
371
- # Inline test DSL
372
- def describe(name, &block)
373
- Tina4::Testing.describe(name, &block)
374
- end
375
-
376
- # Translation shortcut
377
- def t(key, **options)
378
- Tina4::Localization.t(key, **options)
379
- end
380
-
381
- # Service runner DSL
382
- def service(name, options = {}, &block)
383
- Tina4::ServiceRunner.register(name, nil, options, &block)
384
- end
385
-
386
- # DI container shortcuts
387
- def register(name, instance = nil, &block)
388
- Tina4::Container.register(name, instance, &block)
389
- end
390
-
391
- def singleton(name, &block)
392
- Tina4::Container.singleton(name, &block)
393
- end
394
-
395
- def resolve(name)
396
- Tina4::Container.get(name)
397
- end
398
-
399
- private
400
-
401
- # Resolve auth option for route registration
402
- # :default => use bearer auth (default for POST/PUT/PATCH/DELETE)
403
- # false => no auth (public route)
404
- # nil => no auth
405
- # Proc/Lambda => custom auth handler
406
- def resolve_auth(auth)
407
- case auth
408
- when :default
409
- Tina4::Auth.default_secure_auth
410
- when false, nil
411
- nil
412
- else
413
- auth # Custom auth handler (proc/lambda)
414
- end
415
- end
416
-
417
- def autowire_i18n_template_global
418
- # Only register if translations were actually loaded
419
- return if Tina4::Localization.translations.empty?
420
-
421
- # Don't overwrite a user-registered t() global
422
- return if Tina4::Template.globals.key?("t")
423
-
424
- Tina4::Template.add_global("t", ->(key, **opts) { Tina4::Localization.t(key, **opts) })
425
- Tina4::Log.debug("Auto-wired i18n t() as template global")
426
- end
427
-
428
- def setup_database
429
- db_url = ENV["DATABASE_URL"] || ENV["DB_URL"]
430
- if db_url && !db_url.empty?
431
- begin
432
- @database = Tina4::Database.new(db_url)
433
- Tina4::Log.info("Database connected: #{db_url.sub(/:[^:@]+@/, ':***@')}")
434
- rescue => e
435
- Tina4::Log.error("Database connection failed: #{e.message}")
436
- end
437
- end
438
- end
439
-
440
- def auto_discover(root_dir)
441
- # src/ prefixed directories take priority over root-level ones
442
- discover_dirs = %w[src/routes routes src/api api src/orm orm]
443
- discover_dirs.each do |dir|
444
- full_dir = File.join(root_dir, dir)
445
- next unless Dir.exist?(full_dir)
446
-
447
- Dir.glob(File.join(full_dir, "**/*.rb")).sort.each do |file|
448
- begin
449
- load file
450
- Tina4::Log.debug("Auto-loaded: #{file}")
451
- rescue => e
452
- Tina4::Log.error("Failed to load #{file}: #{e.message}")
453
- end
454
- end
455
- end
456
- end
457
- end
458
- end
1
+ # frozen_string_literal: true
2
+
3
+ # ── Fast JSON: use oj if available, fall back to stdlib json ──────────
4
+ begin
5
+ require "oj"
6
+ Oj.default_options = { mode: :compat, symbol_keys: false }
7
+ rescue LoadError
8
+ # oj not installed — stdlib json is fine
9
+ end
10
+
11
+ # ── Core (always loaded) ──────────────────────────────────────────────
12
+ require_relative "tina4/version"
13
+ require_relative "tina4/constants"
14
+ require_relative "tina4/log"
15
+ require_relative "tina4/debug" # backward compat alias
16
+ require_relative "tina4/env"
17
+ require_relative "tina4/router"
18
+ require_relative "tina4/request"
19
+ require_relative "tina4/response"
20
+ require_relative "tina4/rack_app"
21
+ require_relative "tina4/database"
22
+ require_relative "tina4/database_result"
23
+ require_relative "tina4/field_types"
24
+ require_relative "tina4/orm"
25
+ require_relative "tina4/query_builder"
26
+ require_relative "tina4/migration"
27
+ require_relative "tina4/auto_crud"
28
+ require_relative "tina4/database/sqlite3_adapter"
29
+ require_relative "tina4/template"
30
+ require_relative "tina4/frond"
31
+ require_relative "tina4/auth"
32
+ require_relative "tina4/session"
33
+ require_relative "tina4/middleware"
34
+ require_relative "tina4/cors"
35
+ require_relative "tina4/rate_limiter"
36
+ require_relative "tina4/health"
37
+ require_relative "tina4/shutdown"
38
+ require_relative "tina4/localization"
39
+ require_relative "tina4/container"
40
+ require_relative "tina4/queue"
41
+ require_relative "tina4/service_runner"
42
+ require_relative "tina4/events"
43
+ require_relative "tina4/plan"
44
+ require_relative "tina4/project_index"
45
+ require_relative "tina4/dev_admin"
46
+ require_relative "tina4/messenger"
47
+ require_relative "tina4/dev_mailbox"
48
+ require_relative "tina4/ai"
49
+ require_relative "tina4/cache"
50
+ require_relative "tina4/sql_translation"
51
+ require_relative "tina4/response_cache"
52
+ require_relative "tina4/html_element"
53
+ require_relative "tina4/error_overlay"
54
+ require_relative "tina4/test_client"
55
+ require_relative "tina4/mcp"
56
+
57
+ module Tina4
58
+ # ── Lazy-loaded: database drivers ─────────────────────────────────────
59
+ module Drivers
60
+ autoload :SqliteDriver, File.expand_path("tina4/drivers/sqlite_driver", __dir__)
61
+ autoload :PostgresDriver, File.expand_path("tina4/drivers/postgres_driver", __dir__)
62
+ autoload :MysqlDriver, File.expand_path("tina4/drivers/mysql_driver", __dir__)
63
+ autoload :MssqlDriver, File.expand_path("tina4/drivers/mssql_driver", __dir__)
64
+ autoload :FirebirdDriver, File.expand_path("tina4/drivers/firebird_driver", __dir__)
65
+ autoload :MongodbDriver, File.expand_path("tina4/drivers/mongodb_driver", __dir__)
66
+ autoload :OdbcDriver, File.expand_path("tina4/drivers/odbc_driver", __dir__)
67
+ end
68
+
69
+ # ── Lazy-loaded: session handlers ─────────────────────────────────────
70
+ module SessionHandlers
71
+ autoload :FileHandler, File.expand_path("tina4/session_handlers/file_handler", __dir__)
72
+ autoload :RedisHandler, File.expand_path("tina4/session_handlers/redis_handler", __dir__)
73
+ autoload :MongoHandler, File.expand_path("tina4/session_handlers/mongo_handler", __dir__)
74
+ autoload :ValkeyHandler, File.expand_path("tina4/session_handlers/valkey_handler", __dir__)
75
+ autoload :DatabaseHandler, File.expand_path("tina4/session_handlers/database_handler", __dir__)
76
+ end
77
+
78
+ # ── Lazy-loaded: queue backends ───────────────────────────────────────
79
+ module QueueBackends
80
+ autoload :LiteBackend, File.expand_path("tina4/queue_backends/lite_backend", __dir__)
81
+ autoload :RabbitmqBackend, File.expand_path("tina4/queue_backends/rabbitmq_backend", __dir__)
82
+ autoload :KafkaBackend, File.expand_path("tina4/queue_backends/kafka_backend", __dir__)
83
+ autoload :MongoBackend, File.expand_path("tina4/queue_backends/mongo_backend", __dir__)
84
+ end
85
+
86
+ # ── Lazy-loaded: web server ───────────────────────────────────────────
87
+ autoload :WebServer, File.expand_path("tina4/webserver", __dir__)
88
+
89
+ # ── Lazy-loaded: optional modules ─────────────────────────────────────
90
+ autoload :Swagger, File.expand_path("tina4/swagger", __dir__)
91
+ autoload :Crud, File.expand_path("tina4/crud", __dir__)
92
+ autoload :CRUD, File.expand_path("tina4/crud", __dir__)
93
+ autoload :API, File.expand_path("tina4/api", __dir__)
94
+ autoload :APIResponse, File.expand_path("tina4/api", __dir__)
95
+ autoload :GraphQLType, File.expand_path("tina4/graphql", __dir__)
96
+ autoload :GraphQLSchema, File.expand_path("tina4/graphql", __dir__)
97
+ autoload :GraphQLParser, File.expand_path("tina4/graphql", __dir__)
98
+ autoload :GraphQLExecutor, File.expand_path("tina4/graphql", __dir__)
99
+ autoload :GraphQLError, File.expand_path("tina4/graphql", __dir__)
100
+ autoload :GraphQL, File.expand_path("tina4/graphql", __dir__)
101
+ autoload :WebSocket, File.expand_path("tina4/websocket", __dir__)
102
+ autoload :WebSocketConnection, File.expand_path("tina4/websocket", __dir__)
103
+ autoload :Testing, File.expand_path("tina4/testing", __dir__)
104
+ autoload :ScssCompiler, File.expand_path("tina4/scss_compiler", __dir__)
105
+ autoload :FakeData, File.expand_path("tina4/seeder", __dir__)
106
+ autoload :WSDL, File.expand_path("tina4/wsdl", __dir__)
107
+ BANNER = <<~'BANNER'
108
+
109
+ ______ _ __ __
110
+ /_ __/(_)___ ____ _/ // /
111
+ / / / / __ \/ __ `/ // /_
112
+ / / / / / / / /_/ /__ __/
113
+ /_/ /_/_/ /_/\__,_/ /_/
114
+ BANNER
115
+
116
+ class << self
117
+ attr_accessor :root_dir, :database
118
+
119
+ def print_banner(host: "0.0.0.0", port: 7147, server_name: nil)
120
+ is_tty = $stdout.respond_to?(:isatty) && $stdout.isatty
121
+ color = is_tty ? "\e[31m" : ""
122
+ reset = is_tty ? "\e[0m" : ""
123
+
124
+ is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
125
+ log_level = (ENV["TINA4_LOG_LEVEL"] || "[TINA4_LOG_ALL]").upcase
126
+ display = (host == "0.0.0.0" || host == "::") ? "localhost" : host
127
+
128
+ # Auto-detect server name if not provided
129
+ if server_name.nil?
130
+ if is_debug
131
+ server_name = "WEBrick"
132
+ else
133
+ begin
134
+ require "puma"
135
+ server_name = "puma"
136
+ rescue LoadError
137
+ server_name = "WEBrick"
138
+ end
139
+ end
140
+ end
141
+
142
+ puts "#{color}#{BANNER}#{reset}"
143
+ puts " TINA4 — The Intelligent Native Application 4ramework"
144
+ puts " Simple. Fast. Human. | Built for AI. Built for you."
145
+ puts ""
146
+ puts " Server: http://#{display}:#{port} (#{server_name})"
147
+ puts " Swagger: http://localhost:#{port}/swagger"
148
+ puts " Dashboard: http://localhost:#{port}/__dev"
149
+ puts " Debug: #{is_debug ? 'ON' : 'OFF'} (Log level: #{log_level})"
150
+ puts ""
151
+ rescue
152
+ puts "#{color}TINA4 Ruby v#{VERSION}#{reset}"
153
+ end
154
+
155
+ def initialize!(root_dir = Dir.pwd)
156
+ @root_dir = root_dir
157
+
158
+ # Print banner
159
+ print_banner
160
+
161
+ # Load environment
162
+ Tina4::Env.load_env(root_dir)
163
+
164
+ # Setup debug logging
165
+ Tina4::Log.configure(root_dir)
166
+ Tina4::Log.info("Tina4 Ruby v#{VERSION} initializing...")
167
+
168
+ # Setup auth keys
169
+ Tina4::Auth.setup(root_dir)
170
+
171
+ # Load translations
172
+ Tina4::Localization.load(root_dir)
173
+
174
+ # Auto-wire t() into template globals if locales were loaded
175
+ autowire_i18n_template_global
176
+
177
+ # Connect database if configured
178
+ setup_database
179
+
180
+ # Auto-discover routes
181
+ auto_discover(root_dir)
182
+
183
+ Tina4::Log.info("Tina4 initialized successfully")
184
+ end
185
+
186
+ # Initialize and start the web server.
187
+ # This is the primary entry point for app.rb files:
188
+ # Tina4.initialize!(__dir__)
189
+ # Tina4.run!
190
+ # Or combined: Tina4.run!(__dir__)
191
+ def find_available_port(start, max_tries = 10)
192
+ require "socket"
193
+ max_tries.times do |offset|
194
+ port = start + offset
195
+ begin
196
+ server = TCPServer.new("127.0.0.1", port)
197
+ server.close
198
+ return port
199
+ rescue Errno::EADDRINUSE, Errno::EACCES
200
+ next
201
+ end
202
+ end
203
+ start
204
+ end
205
+
206
+ def open_browser(url)
207
+ require "rbconfig"
208
+ Thread.new do
209
+ sleep 2
210
+ case RbConfig::CONFIG["host_os"]
211
+ when /darwin/i then system("open", url)
212
+ when /mswin|mingw/i then system("start", url)
213
+ else system("xdg-open", url)
214
+ end
215
+ end
216
+ end
217
+
218
+ def run!(root_dir = nil, port: nil, host: nil, debug: nil)
219
+ # Handle legacy call: run!(port: 7147) where root_dir receives the hash
220
+ if root_dir.is_a?(Hash)
221
+ port ||= root_dir[:port]
222
+ host ||= root_dir[:host]
223
+ debug = root_dir[:debug] if debug.nil? && root_dir.key?(:debug)
224
+ root_dir = nil
225
+ end
226
+ root_dir ||= Dir.pwd
227
+
228
+ ENV["PORT"] = port.to_s if port
229
+ ENV["HOST"] = host.to_s if host
230
+ ENV["TINA4_DEBUG"] = debug.to_s unless debug.nil?
231
+
232
+ initialize!(root_dir) unless @root_dir
233
+
234
+ host = ENV.fetch("HOST", ENV.fetch("TINA4_HOST", "0.0.0.0"))
235
+ port = ENV.fetch("PORT", ENV.fetch("TINA4_PORT", "7147")).to_i
236
+
237
+ actual_port = find_available_port(port)
238
+ if actual_port != port
239
+ Tina4::Log.info("Port #{port} in use, using #{actual_port}")
240
+ port = actual_port
241
+ end
242
+
243
+ display_host = (host == "0.0.0.0" || host == "::") ? "localhost" : host
244
+ url = "http://#{display_host}:#{port}"
245
+
246
+ app = Tina4::RackApp.new(root_dir: root_dir)
247
+ is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
248
+
249
+ # Try Puma first (production-grade), fall back to WEBrick
250
+ if !is_debug
251
+ begin
252
+ require "puma"
253
+ require "puma/configuration"
254
+ require "puma/launcher"
255
+
256
+ config = Puma::Configuration.new do |user_config|
257
+ user_config.bind "tcp://#{host}:#{port}"
258
+ user_config.app app
259
+ user_config.threads 0, 16
260
+ user_config.workers 0
261
+ user_config.environment "production"
262
+ user_config.log_requests false
263
+ user_config.quiet
264
+ end
265
+
266
+ Tina4::Log.info("Production server: puma")
267
+ Tina4::Shutdown.setup
268
+
269
+ open_browser(url)
270
+ launcher = Puma::Launcher.new(config)
271
+ launcher.run
272
+ return
273
+ rescue LoadError
274
+ # Puma not installed, fall through to WEBrick
275
+ end
276
+ end
277
+
278
+ Tina4::Log.info("Development server: WEBrick")
279
+ open_browser(url)
280
+ server = Tina4::WebServer.new(app, host: host, port: port)
281
+ server.start
282
+ end
283
+
284
+ # DSL methods for route registration
285
+ # GET is public by default (matching tina4_python behavior)
286
+ # POST/PUT/PATCH/DELETE are secured by default use auth: false to make public
287
+ def get(path, auth: nil, swagger_meta: {}, &block)
288
+ auth_handler = auth == false ? nil : auth
289
+ Tina4::Router.add("GET", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
290
+ end
291
+
292
+ def post(path, auth: :default, swagger_meta: {}, &block)
293
+ auth_handler = resolve_auth(auth)
294
+ Tina4::Router.add("POST", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
295
+ end
296
+
297
+ def put(path, auth: :default, swagger_meta: {}, &block)
298
+ auth_handler = resolve_auth(auth)
299
+ Tina4::Router.add("PUT", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
300
+ end
301
+
302
+ def patch(path, auth: :default, swagger_meta: {}, &block)
303
+ auth_handler = resolve_auth(auth)
304
+ Tina4::Router.add("PATCH", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
305
+ end
306
+
307
+ def delete(path, auth: :default, swagger_meta: {}, &block)
308
+ auth_handler = resolve_auth(auth)
309
+ Tina4::Router.add("DELETE", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
310
+ end
311
+
312
+ def any(path, auth: false, swagger_meta: {}, &block)
313
+ auth_handler = resolve_auth(auth)
314
+ %w[GET POST PUT PATCH DELETE].each do |method|
315
+ Tina4::Router.add(method, path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
316
+ end
317
+ end
318
+
319
+ def options(path, &block)
320
+ Tina4::Router.add("OPTIONS", path, block)
321
+ end
322
+
323
+ # Explicit secure variants (always secured, regardless of HTTP method)
324
+ def secure_get(path, auth: nil, swagger_meta: {}, &block)
325
+ auth_handler = auth || Tina4::Auth.default_secure_auth
326
+ Tina4::Router.add("GET", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
327
+ end
328
+
329
+ def secure_post(path, auth: nil, swagger_meta: {}, &block)
330
+ auth_handler = auth || Tina4::Auth.default_secure_auth
331
+ Tina4::Router.add("POST", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
332
+ end
333
+
334
+ def secure_put(path, auth: nil, swagger_meta: {}, &block)
335
+ auth_handler = auth || Tina4::Auth.default_secure_auth
336
+ Tina4::Router.add("PUT", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
337
+ end
338
+
339
+ def secure_patch(path, auth: nil, swagger_meta: {}, &block)
340
+ auth_handler = auth || Tina4::Auth.default_secure_auth
341
+ Tina4::Router.add("PATCH", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
342
+ end
343
+
344
+ def secure_delete(path, auth: nil, swagger_meta: {}, &block)
345
+ auth_handler = auth || Tina4::Auth.default_secure_auth
346
+ Tina4::Router.add("DELETE", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
347
+ end
348
+
349
+ # Route groups
350
+ def group(prefix, auth: nil, &block)
351
+ Tina4::Router.group(prefix, auth_handler: auth, &block)
352
+ end
353
+
354
+ # WebSocket route registration
355
+ def websocket(path, &block)
356
+ Tina4::Router.websocket(path, &block)
357
+ end
358
+
359
+ # Middleware hooks
360
+ def before(pattern = nil, &block)
361
+ Tina4::Middleware.before(pattern, &block)
362
+ end
363
+
364
+ def after(pattern = nil, &block)
365
+ Tina4::Middleware.after(pattern, &block)
366
+ end
367
+
368
+ # Template globals
369
+ def template_global(key, value)
370
+ Tina4::Template.add_global(key, value)
371
+ end
372
+
373
+ # Inline test DSL
374
+ def describe(name, &block)
375
+ Tina4::Testing.describe(name, &block)
376
+ end
377
+
378
+ # Translation shortcut
379
+ def t(key, **options)
380
+ Tina4::Localization.t(key, **options)
381
+ end
382
+
383
+ # Service runner DSL
384
+ def service(name, options = {}, &block)
385
+ Tina4::ServiceRunner.register(name, nil, options, &block)
386
+ end
387
+
388
+ # DI container shortcuts
389
+ def register(name, instance = nil, &block)
390
+ Tina4::Container.register(name, instance, &block)
391
+ end
392
+
393
+ def singleton(name, &block)
394
+ Tina4::Container.singleton(name, &block)
395
+ end
396
+
397
+ def resolve(name)
398
+ Tina4::Container.get(name)
399
+ end
400
+
401
+ private
402
+
403
+ # Resolve auth option for route registration
404
+ # :default => use bearer auth (default for POST/PUT/PATCH/DELETE)
405
+ # false => no auth (public route)
406
+ # nil => no auth
407
+ # Proc/Lambda => custom auth handler
408
+ def resolve_auth(auth)
409
+ case auth
410
+ when :default
411
+ Tina4::Auth.default_secure_auth
412
+ when false, nil
413
+ nil
414
+ else
415
+ auth # Custom auth handler (proc/lambda)
416
+ end
417
+ end
418
+
419
+ def autowire_i18n_template_global
420
+ # Only register if translations were actually loaded
421
+ return if Tina4::Localization.translations.empty?
422
+
423
+ # Don't overwrite a user-registered t() global
424
+ return if Tina4::Template.globals.key?("t")
425
+
426
+ Tina4::Template.add_global("t", ->(key, **opts) { Tina4::Localization.t(key, **opts) })
427
+ Tina4::Log.debug("Auto-wired i18n t() as template global")
428
+ end
429
+
430
+ def setup_database
431
+ db_url = ENV["DATABASE_URL"] || ENV["DB_URL"]
432
+ if db_url && !db_url.empty?
433
+ begin
434
+ @database = Tina4::Database.new(db_url)
435
+ Tina4::Log.info("Database connected: #{db_url.sub(/:[^:@]+@/, ':***@')}")
436
+ rescue => e
437
+ Tina4::Log.error("Database connection failed: #{e.message}")
438
+ end
439
+ end
440
+ end
441
+
442
+ def auto_discover(root_dir)
443
+ # src/ prefixed directories take priority over root-level ones
444
+ discover_dirs = %w[src/routes routes src/api api src/orm orm]
445
+ discover_dirs.each do |dir|
446
+ full_dir = File.join(root_dir, dir)
447
+ next unless Dir.exist?(full_dir)
448
+
449
+ Dir.glob(File.join(full_dir, "**/*.rb")).sort.each do |file|
450
+ begin
451
+ load file
452
+ Tina4::Log.debug("Auto-loaded: #{file}")
453
+ rescue => e
454
+ Tina4::Log.error("Failed to load #{file}: #{e.message}")
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end