tina4ruby 3.11.13 → 3.11.15

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 (132) 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 +935 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -110
  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 -106
  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 +2025 -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 +696 -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/public/css/tina4.css +2463 -2463
  63. data/lib/tina4/public/css/tina4.min.css +1 -1
  64. data/lib/tina4/public/images/logo.svg +5 -5
  65. data/lib/tina4/public/js/frond.min.js +2 -2
  66. data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
  67. data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
  68. data/lib/tina4/public/js/tina4.min.js +92 -92
  69. data/lib/tina4/public/js/tina4js.min.js +48 -48
  70. data/lib/tina4/public/swagger/index.html +90 -90
  71. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  72. data/lib/tina4/query_builder.rb +380 -380
  73. data/lib/tina4/queue.rb +366 -366
  74. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  75. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  76. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  77. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  78. data/lib/tina4/rack_app.rb +817 -817
  79. data/lib/tina4/rate_limiter.rb +130 -130
  80. data/lib/tina4/request.rb +268 -255
  81. data/lib/tina4/response.rb +346 -346
  82. data/lib/tina4/response_cache.rb +551 -551
  83. data/lib/tina4/router.rb +406 -406
  84. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  85. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  86. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  87. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  88. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  89. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  90. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  91. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  92. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  93. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  94. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  95. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  96. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  97. data/lib/tina4/scss/tina4css/base.scss +1 -1
  98. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  99. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  100. data/lib/tina4/scss_compiler.rb +178 -178
  101. data/lib/tina4/seeder.rb +567 -567
  102. data/lib/tina4/service_runner.rb +303 -303
  103. data/lib/tina4/session.rb +297 -297
  104. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  105. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  106. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  107. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  108. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  109. data/lib/tina4/shutdown.rb +84 -84
  110. data/lib/tina4/sql_translation.rb +158 -158
  111. data/lib/tina4/swagger.rb +124 -124
  112. data/lib/tina4/template.rb +894 -894
  113. data/lib/tina4/templates/base.twig +26 -26
  114. data/lib/tina4/templates/errors/302.twig +14 -14
  115. data/lib/tina4/templates/errors/401.twig +9 -9
  116. data/lib/tina4/templates/errors/403.twig +29 -29
  117. data/lib/tina4/templates/errors/404.twig +29 -29
  118. data/lib/tina4/templates/errors/500.twig +38 -38
  119. data/lib/tina4/templates/errors/502.twig +9 -9
  120. data/lib/tina4/templates/errors/503.twig +12 -12
  121. data/lib/tina4/templates/errors/base.twig +37 -37
  122. data/lib/tina4/test_client.rb +159 -159
  123. data/lib/tina4/testing.rb +340 -340
  124. data/lib/tina4/validator.rb +174 -174
  125. data/lib/tina4/version.rb +1 -1
  126. data/lib/tina4/webserver.rb +312 -312
  127. data/lib/tina4/websocket.rb +343 -343
  128. data/lib/tina4/websocket_backplane.rb +190 -190
  129. data/lib/tina4/wsdl.rb +564 -564
  130. data/lib/tina4.rb +458 -458
  131. data/lib/tina4ruby.rb +4 -4
  132. metadata +3 -3
data/lib/tina4.rb CHANGED
@@ -1,458 +1,458 @@
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/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