Module: Tina4

Defined in:
lib/tina4/events.rb,
lib/tina4.rb,
lib/tina4/ai.rb,
lib/tina4/api.rb,
lib/tina4/cli.rb,
lib/tina4/env.rb,
lib/tina4/job.rb,
lib/tina4/log.rb,
lib/tina4/mcp.rb,
lib/tina4/orm.rb,
lib/tina4/auth.rb,
lib/tina4/cors.rb,
lib/tina4/crud.rb,
lib/tina4/wsdl.rb,
lib/tina4/debug.rb,
lib/tina4/frond.rb,
lib/tina4/queue.rb,
lib/tina4/health.rb,
lib/tina4/router.rb,
lib/tina4/seeder.rb,
lib/tina4/graphql.rb,
lib/tina4/metrics.rb,
lib/tina4/request.rb,
lib/tina4/session.rb,
lib/tina4/swagger.rb,
lib/tina4/testing.rb,
lib/tina4/version.rb,
lib/tina4/database.rb,
lib/tina4/rack_app.rb,
lib/tina4/response.rb,
lib/tina4/shutdown.rb,
lib/tina4/template.rb,
lib/tina4/auto_crud.rb,
lib/tina4/constants.rb,
lib/tina4/container.rb,
lib/tina4/dev_admin.rb,
lib/tina4/messenger.rb,
lib/tina4/migration.rb,
lib/tina4/validator.rb,
lib/tina4/webserver.rb,
lib/tina4/websocket.rb,
lib/tina4/dev_reload.rb,
lib/tina4/middleware.rb,
lib/tina4/dev_mailbox.rb,
lib/tina4/field_types.rb,
lib/tina4/test_client.rb,
lib/tina4/html_element.rb,
lib/tina4/localization.rb,
lib/tina4/rate_limiter.rb,
lib/tina4/error_overlay.rb,
lib/tina4/query_builder.rb,
lib/tina4/scss_compiler.rb,
lib/tina4/response_cache.rb,
lib/tina4/service_runner.rb,
lib/tina4/database_result.rb,
lib/tina4/sql_translation.rb,
lib/tina4/drivers/odbc_driver.rb,
lib/tina4/websocket_backplane.rb,
lib/tina4/drivers/mssql_driver.rb,
lib/tina4/drivers/mysql_driver.rb,
lib/tina4/drivers/sqlite_driver.rb,
lib/tina4/drivers/mongodb_driver.rb,
lib/tina4/drivers/firebird_driver.rb,
lib/tina4/drivers/postgres_driver.rb,
lib/tina4/database/sqlite3_adapter.rb,
lib/tina4/queue_backends/lite_backend.rb,
lib/tina4/queue_backends/kafka_backend.rb,
lib/tina4/queue_backends/mongo_backend.rb,
lib/tina4/session_handlers/file_handler.rb,
lib/tina4/session_handlers/mongo_handler.rb,
lib/tina4/session_handlers/redis_handler.rb,
lib/tina4/queue_backends/rabbitmq_backend.rb,
lib/tina4/session_handlers/valkey_handler.rb,
lib/tina4/session_handlers/database_handler.rb

Overview

WebSocket Backplane Abstraction for Tina4 Ruby.

Enables broadcasting WebSocket messages across multiple server instances using a shared pub/sub channel (e.g. Redis). Without a backplane configured, broadcast() only reaches connections on the local process.

Configuration via environment variables:

TINA4_WS_BACKPLANE     — Backend type: "redis", "nats", or "" (default: none)
TINA4_WS_BACKPLANE_URL — Connection string (default: redis://localhost:6379)

Usage:

backplane = Tina4::WebSocketBackplane.create
if backplane
  backplane.subscribe("chat") { |msg| relay_to_local(msg) }
  backplane.publish("chat", '{"user":"A","text":"hello"}')
end

Defined Under Namespace

Modules: AI, Adapters, Auth, AutoCrud, Container, CorsMiddleware, Crud, DevAdmin, DevReload, Drivers, Env, ErrorOverlay, FieldTypes, Health, HtmlHelpers, Localization, Log, McpDevTools, McpProtocol, Metrics, QueueBackends, Router, ScssCompiler, SessionHandlers, Shutdown, Swagger, Template, Testing Classes: API, APIResponse, AiPortRackApp, CLI, ConnectionPool, CorsClassMiddleware, CsrfMiddleware, Database, DatabaseResult, DevMailbox, DevMessengerProxy, ErrorTracker, Events, FakeData, Frond, GraphQL, GraphQLError, GraphQLExecutor, GraphQLParser, GraphQLSchema, GraphQLType, HtmlElement, IndifferentHash, Job, LazySession, McpServer, MessageLog, Messenger, Middleware, Migration, MigrationBase, NATSBackplane, ORM, QueryBuilder, QueryCache, Queue, RackApp, RateLimiter, RateLimiterMiddleware, RedisBackplane, Request, RequestInspector, RequestLoggerMiddleware, Response, ResponseCache, Route, SQLTranslator, SafeString, SecurityHeadersMiddleware, ServiceContext, ServiceRunner, Session, TestClient, TestResponse, Validator, WSDL, WebServer, WebSocket, WebSocketBackplane, WebSocketConnection, WebSocketRoute

Constant Summary collapse

<<~'BANNER'

______ _             __ __
 /_  __/(_)___  ____ _/ // /
/ /  / / __ \/ __ `/ // /_
 / /  / / / / / /_/ /__  __/
/_/  /_/_/ /_/\__,_/  /_/
BANNER
TYPE_MAP =

── Type mapping ──────────────────────────────────────────────────

{
  "String"    => "string",
  "Integer"   => "integer",
  "Float"     => "number",
  "Numeric"   => "number",
  "TrueClass" => "boolean",
  "FalseClass"=> "boolean",
  "Array"     => "array",
  "Hash"      => "object"
}.freeze
CRUD =

Uppercase alias for convenience: Tina4::CRUD.to_crud(…)

Crud
Debug =
Log
VERSION =
"3.10.84"
HTTP_OK =

── HTTP Status Codes ──

200
HTTP_CREATED =
201
HTTP_ACCEPTED =
202
HTTP_NO_CONTENT =
204
HTTP_MOVED =
301
HTTP_REDIRECT =
302
HTTP_NOT_MODIFIED =
304
HTTP_BAD_REQUEST =
400
HTTP_UNAUTHORIZED =
401
HTTP_FORBIDDEN =
403
HTTP_NOT_FOUND =
404
HTTP_METHOD_NOT_ALLOWED =
405
HTTP_CONFLICT =
409
HTTP_GONE =
410
HTTP_UNPROCESSABLE =
422
HTTP_TOO_MANY =
429
HTTP_SERVER_ERROR =
500
HTTP_BAD_GATEWAY =
502
HTTP_UNAVAILABLE =
503
APPLICATION_JSON =

── Content Types ──

"application/json"
APPLICATION_XML =
"application/xml"
APPLICATION_FORM =
"application/x-www-form-urlencoded"
APPLICATION_OCTET =
"application/octet-stream"
TEXT_HTML =
"text/html; charset=utf-8"
TEXT_PLAIN =
"text/plain; charset=utf-8"
TEXT_CSV =
"text/csv"
TEXT_XML =
"text/xml"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.databaseObject

Returns the value of attribute database.



115
116
117
# File 'lib/tina4.rb', line 115

def database
  @database
end

.root_dirObject

Returns the value of attribute root_dir.



115
116
117
# File 'lib/tina4.rb', line 115

def root_dir
  @root_dir
end

Class Method Details

._default_mcp_serverObject



401
402
403
# File 'lib/tina4/mcp.rb', line 401

def self._default_mcp_server
  @_default_mcp_server ||= McpServer.new("/__dev/mcp", name: "Tina4 Dev Tools")
end

.after(pattern = nil, &block) ⇒ Object



359
360
361
# File 'lib/tina4.rb', line 359

def after(pattern = nil, &block)
  Tina4::Middleware.after(pattern, &block)
end

.any(path, auth: false, swagger_meta: {}, &block) ⇒ Object



307
308
309
310
311
312
# File 'lib/tina4.rb', line 307

def any(path, auth: false, swagger_meta: {}, &block)
  auth_handler = resolve_auth(auth)
  %w[GET POST PUT PATCH DELETE].each do |method|
    Tina4::Router.add_route(method, path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
  end
end

.before(pattern = nil, &block) ⇒ Object

Middleware hooks



355
356
357
# File 'lib/tina4.rb', line 355

def before(pattern = nil, &block)
  Tina4::Middleware.before(pattern, &block)
end

.cache_clearObject



543
544
545
# File 'lib/tina4/response_cache.rb', line 543

def cache_clear
  cache_instance.clear_cache
end

.cache_delete(key) ⇒ Object



539
540
541
# File 'lib/tina4/response_cache.rb', line 539

def cache_delete(key)
  cache_instance.cache_delete(key)
end

.cache_get(key) ⇒ Object



531
532
533
# File 'lib/tina4/response_cache.rb', line 531

def cache_get(key)
  cache_instance.cache_get(key)
end

.cache_instanceObject



527
528
529
# File 'lib/tina4/response_cache.rb', line 527

def cache_instance
  @default_cache ||= ResponseCache.new(ttl: ENV["TINA4_CACHE_TTL"] ? ENV["TINA4_CACHE_TTL"].to_i : 60)
end

.cache_set(key, value, ttl: 0) ⇒ Object



535
536
537
# File 'lib/tina4/response_cache.rb', line 535

def cache_set(key, value, ttl: 0)
  cache_instance.cache_set(key, value, ttl: ttl)
end

.cache_statsObject



547
548
549
# File 'lib/tina4/response_cache.rb', line 547

def cache_stats
  cache_instance.cache_stats
end

.camel_to_snake(name) ⇒ Object

Convert a camelCase name to snake_case.



12
13
14
# File 'lib/tina4/orm.rb', line 12

def self.camel_to_snake(name)
  name.to_s.gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/^_/, "")
end

.create_messenger(**options) ⇒ Object

Factory: returns a DevMailbox-intercepting messenger in dev mode, or a real Messenger in production.



517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/tina4/messenger.rb', line 517

def self.create_messenger(**options)
  dev_mode = Tina4::Env.truthy?(ENV["TINA4_DEBUG"])

  smtp_configured = (ENV["TINA4_MAIL_HOST"] && !ENV["TINA4_MAIL_HOST"].empty?) ||
                    (ENV["SMTP_HOST"] && !ENV["SMTP_HOST"].empty?)

  if dev_mode && !smtp_configured
    mailbox_dir = options.delete(:mailbox_dir) || ENV["TINA4_MAILBOX_DIR"]
    mailbox = DevMailbox.new(mailbox_dir: mailbox_dir)
    DevMessengerProxy.new(mailbox, **options)
  else
    Messenger.new(**options)
  end
end

.delete(path, auth: :default, swagger_meta: {}, &block) ⇒ Object



302
303
304
305
# File 'lib/tina4.rb', line 302

def delete(path, auth: :default, swagger_meta: {}, &block)
  auth_handler = resolve_auth(auth)
  Tina4::Router.add_route("DELETE", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.describe(name, &block) ⇒ Object

Inline test DSL



369
370
371
# File 'lib/tina4.rb', line 369

def describe(name, &block)
  Tina4::Testing.describe(name, &block)
end

.find_available_port(start, max_tries = 10) ⇒ Object

Initialize and start the web server. This is the primary entry point for app.rb files:

Tina4.initialize!(__dir__)
Tina4.run!

Or combined: Tina4.run!(__dir__)



186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/tina4.rb', line 186

def find_available_port(start, max_tries = 10)
  require "socket"
  max_tries.times do |offset|
    port = start + offset
    begin
      server = TCPServer.new("127.0.0.1", port)
      server.close
      return port
    rescue Errno::EADDRINUSE, Errno::EACCES
      next
    end
  end
  start
end

.get(path, auth: nil, swagger_meta: {}, &block) ⇒ Object

DSL methods for route registration GET is public by default (matching tina4_python behavior) POST/PUT/PATCH/DELETE are secured by default — use auth: false to make public



282
283
284
285
# File 'lib/tina4.rb', line 282

def get(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth == false ? nil : auth
  Tina4::Router.add_route("GET", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.group(prefix, auth: nil, &block) ⇒ Object

Route groups



345
346
347
# File 'lib/tina4.rb', line 345

def group(prefix, auth: nil, &block)
  Tina4::Router.group(prefix, auth_handler: auth, &block)
end

.html_helpersObject

Module-level convenience: Tina4.html_helpers returns a module you can include.



145
146
147
# File 'lib/tina4/html_element.rb', line 145

def self.html_helpers
  HtmlHelpers
end

.initialize!(root_dir = Dir.pwd) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/tina4.rb', line 153

def initialize!(root_dir = Dir.pwd)
  @root_dir = root_dir

  # Print banner
  print_banner

  # Load environment
  Tina4::Env.load_env(root_dir)

  # Setup debug logging
  Tina4::Log.setup(root_dir)
  Tina4::Log.info("Tina4 Ruby v#{VERSION} initializing...")

  # Setup auth keys
  Tina4::Auth.setup(root_dir)

  # Load translations
  Tina4::Localization.load(root_dir)

  # Connect database if configured
  setup_database

  # Auto-discover routes
  auto_discover(root_dir)

  Tina4::Log.info("Tina4 initialized successfully")
end

.is_localhost?Boolean

Check if the server is running on localhost.

Returns:

  • (Boolean)


134
135
136
137
# File 'lib/tina4/mcp.rb', line 134

def self.is_localhost?
  host = ENV.fetch("HOST_NAME", "localhost:7145").split(":").first
  ["localhost", "127.0.0.1", "0.0.0.0", "::1", ""].include?(host)
end

.mcp_resource(uri, description: "", mime_type: "application/json", server: nil, &block) ⇒ Object

Register a block as an MCP resource.

Tina4.mcp_resource("app://tables", description: "Database tables") do
  db.tables
end


423
424
425
426
427
# File 'lib/tina4/mcp.rb', line 423

def self.mcp_resource(uri, description: "", mime_type: "application/json", server: nil, &block)
  target = server || _default_mcp_server
  target.register_resource(uri, block, description, mime_type)
  block
end

.mcp_tool(name, description: "", server: nil, &block) ⇒ Object

Register a block as an MCP tool.

Tina4.mcp_tool("lookup_invoice", description: "Find invoice by number") do |invoice_no:|
  db.fetch_one("SELECT * FROM invoices WHERE invoice_no = ?", [invoice_no])
end


410
411
412
413
414
415
416
# File 'lib/tina4/mcp.rb', line 410

def self.mcp_tool(name, description: "", server: nil, &block)
  target = server || _default_mcp_server
  handler = block
  tool_desc = description.empty? ? name : description
  target.register_tool(name, handler, tool_desc)
  handler
end

.open_browser(url) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/tina4.rb', line 201

def open_browser(url)
  require "rbconfig"
  Thread.new do
    sleep 2
    case RbConfig::CONFIG["host_os"]
    when /darwin/i then system("open", url)
    when /mswin|mingw/i then system("start", url)
    else system("xdg-open", url)
    end
  end
end

.options(path, &block) ⇒ Object



314
315
316
# File 'lib/tina4.rb', line 314

def options(path, &block)
  Tina4::Router.add_route("OPTIONS", path, block)
end

.patch(path, auth: :default, swagger_meta: {}, &block) ⇒ Object



297
298
299
300
# File 'lib/tina4.rb', line 297

def patch(path, auth: :default, swagger_meta: {}, &block)
  auth_handler = resolve_auth(auth)
  Tina4::Router.add_route("PATCH", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.post(path, auth: :default, swagger_meta: {}, &block) ⇒ Object



287
288
289
290
# File 'lib/tina4.rb', line 287

def post(path, auth: :default, swagger_meta: {}, &block)
  auth_handler = resolve_auth(auth)
  Tina4::Router.add_route("POST", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end


117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/tina4.rb', line 117

def print_banner(host: "0.0.0.0", port: 7147, server_name: nil)
  is_tty = $stdout.respond_to?(:isatty) && $stdout.isatty
  color = is_tty ? "\e[31m" : ""
  reset = is_tty ? "\e[0m" : ""

  is_debug = Tina4::Env.truthy?(ENV["TINA4_DEBUG"])
  log_level = (ENV["TINA4_LOG_LEVEL"] || "[TINA4_LOG_ALL]").upcase
  display = (host == "0.0.0.0" || host == "::") ? "localhost" : host

  # Auto-detect server name if not provided
  if server_name.nil?
    if is_debug
      server_name = "WEBrick"
    else
      begin
        require "puma"
        server_name = "puma"
      rescue LoadError
        server_name = "WEBrick"
      end
    end
  end

  puts "#{color}#{BANNER}#{reset}"
  puts "  TINA4 — The Intelligent Native Application 4ramework"
  puts "  Simple. Fast. Human. | Built for AI. Built for you."
  puts ""
  puts "  Server:    http://#{display}:#{port} (#{server_name})"
  puts "  Swagger:   http://localhost:#{port}/swagger"
  puts "  Dashboard: http://localhost:#{port}/__dev"
  puts "  Debug:     #{is_debug ? 'ON' : 'OFF'} (Log level: #{log_level})"
  puts ""
rescue
  puts "#{color}TINA4 Ruby v#{VERSION}#{reset}"
end

.put(path, auth: :default, swagger_meta: {}, &block) ⇒ Object



292
293
294
295
# File 'lib/tina4.rb', line 292

def put(path, auth: :default, swagger_meta: {}, &block)
  auth_handler = resolve_auth(auth)
  Tina4::Router.add_route("PUT", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.register(name, instance = nil, &block) ⇒ Object

DI container shortcuts



384
385
386
# File 'lib/tina4.rb', line 384

def register(name, instance = nil, &block)
  Tina4::Container.register(name, instance, &block)
end

.resolve(name) ⇒ Object



392
393
394
# File 'lib/tina4.rb', line 392

def resolve(name)
  Tina4::Container.resolve(name)
end

.run!(root_dir = nil, port: nil, host: nil, debug: nil) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/tina4.rb', line 213

def run!(root_dir = nil, port: nil, host: nil, debug: nil)
  # Handle legacy call: run!(port: 7147) where root_dir receives the hash
  if root_dir.is_a?(Hash)
    port ||= root_dir[:port]
    host ||= root_dir[:host]
    debug = root_dir[:debug] if debug.nil? && root_dir.key?(:debug)
    root_dir = nil
  end
  root_dir ||= Dir.pwd

  ENV["PORT"] = port.to_s if port
  ENV["HOST"] = host.to_s if host
  ENV["TINA4_DEBUG"] = debug.to_s unless debug.nil?

  initialize!(root_dir) unless @root_dir

  host = ENV.fetch("HOST", ENV.fetch("TINA4_HOST", "0.0.0.0"))
  port = ENV.fetch("PORT", ENV.fetch("TINA4_PORT", "7147")).to_i

  actual_port = find_available_port(port)
  if actual_port != port
    Tina4::Log.info("Port #{port} in use, using #{actual_port}")
    port = actual_port
  end

  display_host = (host == "0.0.0.0" || host == "::") ? "localhost" : host
  url = "http://#{display_host}:#{port}"

  app = Tina4::RackApp.new(root_dir: root_dir)
  is_debug = Tina4::Env.truthy?(ENV["TINA4_DEBUG"])

  # Try Puma first (production-grade), fall back to WEBrick
  if !is_debug
    begin
      require "puma"
      require "puma/configuration"
      require "puma/launcher"

      config = Puma::Configuration.new do |user_config|
        user_config.bind "tcp://#{host}:#{port}"
        user_config.app app
        user_config.threads 0, 16
        user_config.workers 0
        user_config.environment "production"
        user_config.log_requests false
        user_config.quiet
      end

      Tina4::Log.info("Production server: puma")
      Tina4::Shutdown.setup

      open_browser(url)
      launcher = Puma::Launcher.new(config)
      launcher.run
      return
    rescue LoadError
      # Puma not installed, fall through to WEBrick
    end
  end

  Tina4::Log.info("Development server: WEBrick")
  open_browser(url)
  server = Tina4::WebServer.new(app, host: host, port: port)
  server.start
end

.schema_from_method(method_obj) ⇒ Object

Extract JSON Schema input schema from a Ruby method’s parameters.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tina4/mcp.rb', line 106

def self.schema_from_method(method_obj)
  properties = {}
  required   = []

  method_obj.parameters.each do |kind, name|
    next if name == :self
    name_s = name.to_s

    # Default type is "string" -- Ruby doesn't have inline type annotations
    prop = { "type" => "string" }

    case kind
    when :req, :keyreq
      required << name_s
    when :opt, :key
      # Has a default -- we cannot inspect the default value easily in Ruby,
      # so we just mark it as optional (no "default" key)
    end

    properties[name_s] = prop
  end

  schema = { "type" => "object", "properties" => properties }
  schema["required"] = required unless required.empty?
  schema
end

.secure_delete(path, auth: nil, swagger_meta: {}, &block) ⇒ Object



339
340
341
342
# File 'lib/tina4.rb', line 339

def secure_delete(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add_route("DELETE", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.secure_get(path, auth: nil, swagger_meta: {}, &block) ⇒ Object

Explicit secure variants (always secured, regardless of HTTP method)



319
320
321
322
# File 'lib/tina4.rb', line 319

def secure_get(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add_route("GET", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.secure_patch(path, auth: nil, swagger_meta: {}, &block) ⇒ Object



334
335
336
337
# File 'lib/tina4.rb', line 334

def secure_patch(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add_route("PATCH", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.secure_post(path, auth: nil, swagger_meta: {}, &block) ⇒ Object



324
325
326
327
# File 'lib/tina4.rb', line 324

def secure_post(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add_route("POST", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.secure_put(path, auth: nil, swagger_meta: {}, &block) ⇒ Object



329
330
331
332
# File 'lib/tina4.rb', line 329

def secure_put(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add_route("PUT", path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
end

.seed(seed_folder: "seeds", clear: false) ⇒ Object

Run all seed files in the given folder.

Parameters:

  • seed_folder (String) (defaults to: "seeds")

    path to seed files (default: “seeds”)



498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/tina4/seeder.rb', line 498

def self.seed(seed_folder: "seeds", clear: false)
  unless Dir.exist?(seed_folder)
    Tina4::Log.info("Seeder: No seeds folder found at #{seed_folder}")
    return
  end

  files = Dir.glob(File.join(seed_folder, "*.rb")).sort
  files.reject! { |f| File.basename(f).start_with?("_") }

  if files.empty?
    Tina4::Log.info("Seeder: No seed files found in #{seed_folder}")
    return
  end

  Tina4::Log.info("Seeder: Found #{files.length} seed file(s) in #{seed_folder}")

  files.each do |filepath|
    begin
      Tina4::Log.info("Seeder: Running #{File.basename(filepath)}...")
      load filepath
      Tina4::Log.info("Seeder: Completed #{File.basename(filepath)}")
    rescue => e
      Tina4::Log.error("Seeder: Failed to run #{File.basename(filepath)}: #{e.message}")
    end
  end
end

.seed_batch(tasks, clear: false) ⇒ Hash

Seed multiple ORM classes in batch with optional dependency-aware clearing.

Examples:

Tina4.seed_batch([
  { orm_class: User, count: 20 },
  { orm_class: Order, count: 100, overrides: { status: "pending" } }
], clear: true)

Parameters:

  • tasks (Array<Hash>)

    each hash has :orm_class, :count, :overrides, :seed

  • clear (Boolean) (defaults to: false)

    delete existing records (in reverse order) before seeding

Returns:

  • (Hash)

    { “ClassName” => inserted_count, … }



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'lib/tina4/seeder.rb', line 467

def self.seed_batch(tasks, clear: false)
  results = {}

  if clear
    tasks.reverse_each do |task|
      begin
        Tina4.database&.execute("DELETE FROM #{task[:orm_class].table_name}")
        Tina4::Log.info("Seeder: Cleared #{task[:orm_class].table_name}")
      rescue => e
        Tina4::Log.warn("Seeder: Could not clear #{task[:orm_class].table_name}: #{e.message}")
      end
    end
  end

  tasks.each do |task|
    n = Tina4.seed_orm(
      task[:orm_class],
      count: task[:count] || 10,
      overrides: task[:overrides] || {},
      clear: false,
      seed: task[:seed]
    )
    results[task[:orm_class].name] = n
  end

  results
end

.seed_orm(orm_class, count: 10, overrides: {}, clear: false, seed: nil) ⇒ Integer

Seed an ORM class with auto-generated fake data.

Examples:

Tina4.seed_orm(User, count: 50)
Tina4.seed_orm(Order, count: 200, overrides: { status: ->(f) { f.choice(%w[pending shipped]) } })

Parameters:

  • orm_class (Class)

    ORM subclass (e.g., User, Product)

  • count (Integer) (defaults to: 10)

    number of records to insert

  • overrides (Hash) (defaults to: {})

    field overrides — static values or lambdas receiving FakeData

  • clear (Boolean) (defaults to: false)

    delete existing records before seeding

  • seed (Integer, nil) (defaults to: nil)

    random seed for reproducible data

Returns:

  • (Integer)

    number of records inserted



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/tina4/seeder.rb', line 332

def self.seed_orm(orm_class, count: 10, overrides: {}, clear: false, seed: nil)
  fake = FakeData.new(seed: seed)
  fields = orm_class.field_definitions
  table = orm_class.table_name

  if fields.empty?
    Tina4::Log.error("Seeder: No fields found on #{orm_class.name}")
    return 0
  end

  db = Tina4.database
  unless db
    Tina4::Log.error("Seeder: No database connection. Set Tina4.database first.")
    return 0
  end

  # Idempotency check
  unless clear
    begin
      result = db.fetch_one("SELECT count(*) as cnt FROM #{table}")
      if result && result[:cnt].to_i >= count
        Tina4::Log.info("Seeder: #{table} already has #{result[:cnt]} records, skipping")
        return 0
      end
    rescue => e
      # Table might not exist
    end
  end

  # Clear if requested
  if clear
    begin
      db.execute("DELETE FROM #{table}")
      Tina4::Log.info("Seeder: Cleared #{table}")
    rescue => e
      Tina4::Log.warn("Seeder: Could not clear #{table}: #{e.message}")
    end
  end

  # Identify fields to populate
  pk_field = orm_class.primary_key_field
  insert_fields = fields.reject { |name, opts| opts[:primary_key] && opts[:auto_increment] }

  inserted = 0
  count.times do |i|
    attrs = {}

    insert_fields.each do |name, field_def|
      if overrides.key?(name)
        val = overrides[name]
        attrs[name] = val.respond_to?(:call) ? val.call(fake) : val
      else
        generated = fake.for_field(field_def, name)
        attrs[name] = generated unless generated.nil?
      end
    end

    begin
      obj = orm_class.new(attrs)
      if obj.save
        inserted += 1
      else
        Tina4::Log.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{obj.errors.join(', ')}")
      end
    rescue => e
      Tina4::Log.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{e.message}")
    end
  end

  Tina4::Log.info("Seeder: Inserted #{inserted}/#{count} records into #{table}")
  inserted
end

.seed_table(table_name, columns, count: 10, overrides: {}, clear: false, seed: nil) ⇒ Integer

Seed a raw database table (no ORM class needed).

Parameters:

  • table_name (String)

    name of the table

  • columns (Hash)

    { column_name: type_string } — supports :integer, :string, :text, etc.

  • count (Integer) (defaults to: 10)

    number of records to insert

  • overrides (Hash) (defaults to: {})

    field overrides

  • clear (Boolean) (defaults to: false)

    delete before seeding

  • seed (Integer, nil) (defaults to: nil)

    random seed

Returns:

  • (Integer)

    records inserted



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/tina4/seeder.rb', line 414

def self.seed_table(table_name, columns, count: 10, overrides: {}, clear: false, seed: nil)
  fake = FakeData.new(seed: seed)
  db = Tina4.database

  unless db
    Tina4::Log.error("Seeder: No database connection.")
    return 0
  end

  if clear
    begin
      db.execute("DELETE FROM #{table_name}")
    rescue => e
      Tina4::Log.warn("Seeder: Could not clear #{table_name}: #{e.message}")
    end
  end

  inserted = 0
  count.times do |i|
    row = {}
    columns.each do |col_name, type_str|
      if overrides.key?(col_name)
        val = overrides[col_name]
        row[col_name] = val.respond_to?(:call) ? val.call(fake) : val
      else
        field_def = { type: type_str.to_sym }
        row[col_name] = fake.for_field(field_def, col_name)
      end
    end

    begin
      db.insert(table_name, row)
      inserted += 1
    rescue => e
      Tina4::Log.warn("Seeder: Insert failed for #{table_name} row #{i + 1}: #{e.message}")
    end
  end

  Tina4::Log.info("Seeder: Inserted #{inserted}/#{count} records into #{table_name}")
  inserted
end

.service(name, options = {}, &block) ⇒ Object

Service runner DSL



379
380
381
# File 'lib/tina4.rb', line 379

def service(name, options = {}, &block)
  Tina4::ServiceRunner.register(name, nil, options, &block)
end

.singleton(name, &block) ⇒ Object



388
389
390
# File 'lib/tina4.rb', line 388

def singleton(name, &block)
  Tina4::Container.singleton(name, &block)
end

.snake_to_camel(name) ⇒ Object

Convert a snake_case name to camelCase.



6
7
8
9
# File 'lib/tina4/orm.rb', line 6

def self.snake_to_camel(name)
  parts = name.to_s.split("_")
  parts[0] + parts[1..].map(&:capitalize).join
end

.t(key, **options) ⇒ Object

Translation shortcut



374
375
376
# File 'lib/tina4.rb', line 374

def t(key, **options)
  Tina4::Localization.t(key, **options)
end

.template_global(key, value) ⇒ Object

Template globals



364
365
366
# File 'lib/tina4.rb', line 364

def template_global(key, value)
  Tina4::Template.add_global(key, value)
end

.websocket(path, &block) ⇒ Object

WebSocket route registration



350
351
352
# File 'lib/tina4.rb', line 350

def websocket(path, &block)
  Tina4::Router.websocket(path, &block)
end