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/cache.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_backplane
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.11.8"
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"
WEBSOCKET_GUID =
"258EAFA5-E914-47DA-95CA-5AB5DC11AD37"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.databaseObject

Returns the value of attribute database.



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

def database
  @database
end

.root_dirObject

Returns the value of attribute root_dir.



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

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

.add_html_helpers(namespace) ⇒ Object

Inject _div(), _p(), _a(), etc. helper methods into the given namespace (hash or object).

Usage:

h = {}
Tina4.add_html_helpers(h)
h[:_div].call({ class: "card" }, h[:_p].call("Hello"))


156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/tina4/html_element.rb', line 156

def self.add_html_helpers(namespace)
  helper = Object.new.extend(HtmlHelpers)

  HtmlElement::HTML_TAGS.each do |tag|
    name = "_#{tag}"
    fn = helper.method(name.to_sym)

    if namespace.is_a?(Hash)
      namespace[name.to_sym] = fn
    else
      namespace.define_singleton_method(name.to_sym, &fn)
    end
  end
end

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



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

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

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



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

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(method, path, block, auth_handler: auth_handler, swagger_meta: swagger_meta)
  end
end

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

Middleware hooks



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

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

.build_frame(opcode, data, fin: true) ⇒ Object

Build a WebSocket frame (server→client, never masked).



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/tina4/websocket.rb', line 16

def self.build_frame(opcode, data, fin: true)
  first_byte = (fin ? 0x80 : 0x00) | opcode
  frame = [first_byte].pack("C")
  length = data.bytesize

  if length < 126
    frame += [length].pack("C")
  elsif length < 65536
    frame += [126, length].pack("Cn")
  else
    frame += [127, length].pack("CQ>")
  end

  frame + data
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

.compute_accept_key(key) ⇒ Object

Compute Sec-WebSocket-Accept from Sec-WebSocket-Key per RFC 6455.



11
12
13
# File 'lib/tina4/websocket.rb', line 11

def self.compute_accept_key(key)
  Base64.strict_encode64(Digest::SHA1.digest("#{key}#{WEBSOCKET_GUID}"))
end

.create_messenger(**options) ⇒ Object

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



542
543
544
545
546
547
548
549
550
551
552
553
554
555
# File 'lib/tina4/messenger.rb', line 542

def self.create_messenger(**options)
  dev_mode = Tina4::Env.is_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



306
307
308
309
# File 'lib/tina4.rb', line 306

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

.describe(name, &block) ⇒ Object

Inline test DSL



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

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__)



190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/tina4.rb', line 190

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



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

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

.get_framework_frondObject

Return the singleton Frond engine for built-in framework templates.



18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/tina4/response.rb', line 18

def self.get_framework_frond
  framework_dir = ::File.join(::File.dirname(__FILE__), "templates")
  if @_framework_frond.nil? && ::File.directory?(framework_dir)
    @_framework_frond = Tina4::Frond.new(template_dir: framework_dir)
  end
  # Sync custom filters/globals from the user engine
  if @_framework_frond
    user_engine = get_frond
    @_framework_frond.instance_variable_get(:@filters).merge!(user_engine.instance_variable_get(:@filters))
    @_framework_frond.instance_variable_get(:@globals).merge!(user_engine.instance_variable_get(:@globals))
  end
  @_framework_frond
end

.get_frondObject

Return the global Frond engine, creating a default if needed.



13
14
15
# File 'lib/tina4/response.rb', line 13

def self.get_frond
  @_global_frond ||= Tina4::Frond.new(template_dir: "src/templates")
end

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

Route groups



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

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



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
180
181
182
183
# File 'lib/tina4.rb', line 154

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.configure(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)

  # Auto-wire t() into template globals if locales were loaded
  autowire_i18n_template_global

  # 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



205
206
207
208
209
210
211
212
213
214
215
# File 'lib/tina4.rb', line 205

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



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

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

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



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

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

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



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

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


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
152
# File 'lib/tina4.rb', line 118

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.is_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



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

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

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

DI container shortcuts



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

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

.resolve(name) ⇒ Object



396
397
398
# File 'lib/tina4.rb', line 396

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

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



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
278
279
280
281
# File 'lib/tina4.rb', line 217

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.is_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



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

def secure_delete(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add("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)



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

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

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



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

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

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



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

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

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



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

def secure_put(path, auth: nil, swagger_meta: {}, &block)
  auth_handler = auth || Tina4::Auth.default_secure_auth
  Tina4::Router.add("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”)



530
531
532
# File 'lib/tina4/seeder.rb', line 530

def self.seed(seed_folder: "seeds", clear: false)
  seed_dir(seed_folder: seed_folder, clear: clear)
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, … }



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
524
525
# File 'lib/tina4/seeder.rb', line 499

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_dir(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”)



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/tina4/seeder.rb', line 537

def self.seed_dir(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_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



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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/tina4/seeder.rb', line 364

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



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/tina4/seeder.rb', line 446

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



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

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

.set_frond(engine) ⇒ Object

Register a pre-configured Frond engine for response.render().



33
34
35
# File 'lib/tina4/response.rb', line 33

def self.set_frond(engine)
  @_global_frond = engine
end

.singleton(name, &block) ⇒ Object



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

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.downcase.split("_")
  parts[0] + parts[1..].map(&:capitalize).join
end

.t(key, **options) ⇒ Object

Translation shortcut



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

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

.template_global(key, value) ⇒ Object

Template globals



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

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

.websocket(path, &block) ⇒ Object

WebSocket route registration



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

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