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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- 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
|