tina4ruby 3.2.1 → 3.10.0
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/README.md +19 -20
- data/lib/tina4/auth.rb +137 -27
- data/lib/tina4/auto_crud.rb +55 -3
- data/lib/tina4/cli.rb +75 -2
- data/lib/tina4/cors.rb +1 -1
- data/lib/tina4/database.rb +131 -28
- data/lib/tina4/database_result.rb +122 -8
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/frond.rb +148 -2
- data/lib/tina4/localization.rb +1 -1
- data/lib/tina4/middleware.rb +349 -1
- data/lib/tina4/migration.rb +132 -11
- data/lib/tina4/orm.rb +17 -8
- data/lib/tina4/public/js/tina4-dev-admin.min.js +1 -1
- data/lib/tina4/public/js/tina4js.min.js +47 -0
- data/lib/tina4/query_builder.rb +374 -0
- data/lib/tina4/queue.rb +128 -90
- data/lib/tina4/queue_backends/lite_backend.rb +42 -7
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -0
- data/lib/tina4/rack_app.rb +194 -18
- data/lib/tina4/request.rb +14 -1
- data/lib/tina4/response.rb +26 -0
- data/lib/tina4/router.rb +127 -0
- data/lib/tina4/service_runner.rb +1 -1
- data/lib/tina4/session.rb +6 -1
- data/lib/tina4/session_handlers/database_handler.rb +66 -0
- data/lib/tina4/swagger.rb +1 -1
- data/lib/tina4/template.rb +10 -1
- data/lib/tina4/validator.rb +174 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +23 -4
- data/lib/tina4/websocket_backplane.rb +118 -0
- data/lib/tina4.rb +64 -4
- metadata +12 -3
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
# Request body validator with chainable rules.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# validator = Tina4::Validator.new(request.body)
|
|
8
|
+
# validator.required("name", "email")
|
|
9
|
+
# .email("email")
|
|
10
|
+
# .min_length("name", 2)
|
|
11
|
+
# .max_length("name", 100)
|
|
12
|
+
# .integer("age")
|
|
13
|
+
# .min("age", 0)
|
|
14
|
+
# .max("age", 150)
|
|
15
|
+
# .in_list("role", ["admin", "user", "guest"])
|
|
16
|
+
# .regex("phone", /^\+?[\d\s\-]+$/)
|
|
17
|
+
#
|
|
18
|
+
# unless validator.is_valid?
|
|
19
|
+
# return response.error("VALIDATION_FAILED", validator.errors.first[:message], 400)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class Validator
|
|
23
|
+
attr_reader :validation_errors
|
|
24
|
+
|
|
25
|
+
EMAIL_REGEX = /\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\z/
|
|
26
|
+
|
|
27
|
+
def initialize(data = {})
|
|
28
|
+
@data = data.is_a?(Hash) ? data : {}
|
|
29
|
+
# Normalise keys to strings for consistent lookup
|
|
30
|
+
@data = @data.transform_keys(&:to_s) unless @data.empty?
|
|
31
|
+
@validation_errors = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check that one or more fields are present and non-empty.
|
|
35
|
+
def required(*fields)
|
|
36
|
+
fields.each do |field|
|
|
37
|
+
key = field.to_s
|
|
38
|
+
value = @data[key]
|
|
39
|
+
if value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
40
|
+
@validation_errors << { field: key, message: "#{key} is required" }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check that a field contains a valid email address.
|
|
47
|
+
def email(field)
|
|
48
|
+
key = field.to_s
|
|
49
|
+
value = @data[key]
|
|
50
|
+
return self if value.nil?
|
|
51
|
+
|
|
52
|
+
unless value.is_a?(String) && value.match?(EMAIL_REGEX)
|
|
53
|
+
@validation_errors << { field: key, message: "#{key} must be a valid email address" }
|
|
54
|
+
end
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check that a string field has at least +length+ characters.
|
|
59
|
+
def min_length(field, length)
|
|
60
|
+
key = field.to_s
|
|
61
|
+
value = @data[key]
|
|
62
|
+
return self if value.nil?
|
|
63
|
+
|
|
64
|
+
unless value.is_a?(String) && value.length >= length
|
|
65
|
+
@validation_errors << { field: key, message: "#{key} must be at least #{length} characters" }
|
|
66
|
+
end
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check that a string field has at most +length+ characters.
|
|
71
|
+
def max_length(field, length)
|
|
72
|
+
key = field.to_s
|
|
73
|
+
value = @data[key]
|
|
74
|
+
return self if value.nil?
|
|
75
|
+
|
|
76
|
+
unless value.is_a?(String) && value.length <= length
|
|
77
|
+
@validation_errors << { field: key, message: "#{key} must be at most #{length} characters" }
|
|
78
|
+
end
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check that a field is an integer (or can be parsed as one).
|
|
83
|
+
def integer(field)
|
|
84
|
+
key = field.to_s
|
|
85
|
+
value = @data[key]
|
|
86
|
+
return self if value.nil?
|
|
87
|
+
|
|
88
|
+
if value.is_a?(Integer)
|
|
89
|
+
return self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
Integer(value)
|
|
94
|
+
rescue ArgumentError, TypeError
|
|
95
|
+
@validation_errors << { field: key, message: "#{key} must be an integer" }
|
|
96
|
+
end
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check that a numeric field is >= +minimum+.
|
|
101
|
+
def min(field, minimum)
|
|
102
|
+
key = field.to_s
|
|
103
|
+
value = @data[key]
|
|
104
|
+
return self if value.nil?
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
num = Float(value)
|
|
108
|
+
rescue ArgumentError, TypeError
|
|
109
|
+
return self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if num < minimum
|
|
113
|
+
@validation_errors << { field: key, message: "#{key} must be at least #{minimum}" }
|
|
114
|
+
end
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check that a numeric field is <= +maximum+.
|
|
119
|
+
def max(field, maximum)
|
|
120
|
+
key = field.to_s
|
|
121
|
+
value = @data[key]
|
|
122
|
+
return self if value.nil?
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
num = Float(value)
|
|
126
|
+
rescue ArgumentError, TypeError
|
|
127
|
+
return self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if num > maximum
|
|
131
|
+
@validation_errors << { field: key, message: "#{key} must be at most #{maximum}" }
|
|
132
|
+
end
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check that a field's value is one of the allowed values.
|
|
137
|
+
def in_list(field, allowed)
|
|
138
|
+
key = field.to_s
|
|
139
|
+
value = @data[key]
|
|
140
|
+
return self if value.nil?
|
|
141
|
+
|
|
142
|
+
unless allowed.include?(value)
|
|
143
|
+
@validation_errors << { field: key, message: "#{key} must be one of #{allowed}" }
|
|
144
|
+
end
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check that a field matches a regular expression.
|
|
149
|
+
def regex(field, pattern)
|
|
150
|
+
key = field.to_s
|
|
151
|
+
value = @data[key]
|
|
152
|
+
return self if value.nil?
|
|
153
|
+
|
|
154
|
+
regexp = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern)
|
|
155
|
+
unless value.is_a?(String) && value.match?(regexp)
|
|
156
|
+
@validation_errors << { field: key, message: "#{key} does not match the required format" }
|
|
157
|
+
end
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Return the list of validation errors (empty if valid).
|
|
162
|
+
def errors
|
|
163
|
+
@validation_errors.dup
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Return true if no validation errors have been recorded.
|
|
167
|
+
def is_valid?
|
|
168
|
+
@validation_errors.empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Alias for is_valid? (Ruby convention)
|
|
172
|
+
alias_method :valid?, :is_valid?
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/websocket.rb
CHANGED
|
@@ -44,7 +44,8 @@ module Tina4
|
|
|
44
44
|
socket.write(response)
|
|
45
45
|
|
|
46
46
|
conn_id = SecureRandom.hex(16)
|
|
47
|
-
|
|
47
|
+
ws_path = env["REQUEST_PATH"] || env["PATH_INFO"] || "/"
|
|
48
|
+
connection = WebSocketConnection.new(conn_id, socket, ws_server: self, path: ws_path)
|
|
48
49
|
@connections[conn_id] = connection
|
|
49
50
|
|
|
50
51
|
emit(:open, connection)
|
|
@@ -74,9 +75,10 @@ module Tina4
|
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
77
|
-
def broadcast(message, exclude: nil)
|
|
78
|
+
def broadcast(message, exclude: nil, path: nil)
|
|
78
79
|
@connections.each do |id, conn|
|
|
79
80
|
next if exclude && id == exclude
|
|
81
|
+
next if path && conn.path != path
|
|
80
82
|
conn.send_text(message)
|
|
81
83
|
end
|
|
82
84
|
end
|
|
@@ -90,13 +92,28 @@ module Tina4
|
|
|
90
92
|
|
|
91
93
|
class WebSocketConnection
|
|
92
94
|
attr_reader :id
|
|
95
|
+
attr_accessor :params, :path
|
|
93
96
|
|
|
94
|
-
def initialize(id, socket)
|
|
97
|
+
def initialize(id, socket, ws_server: nil, path: "/")
|
|
95
98
|
@id = id
|
|
96
99
|
@socket = socket
|
|
100
|
+
@params = {}
|
|
101
|
+
@ws_server = ws_server
|
|
102
|
+
@path = path
|
|
97
103
|
end
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
# Broadcast a message to all other connections on the same path
|
|
106
|
+
def broadcast(message, include_self: false)
|
|
107
|
+
return unless @ws_server
|
|
108
|
+
|
|
109
|
+
@ws_server.connections.each do |cid, conn|
|
|
110
|
+
next if !include_self && cid == @id
|
|
111
|
+
next if conn.path != @path
|
|
112
|
+
conn.send_text(message)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def send(message)
|
|
100
117
|
data = message.encode("UTF-8")
|
|
101
118
|
frame = build_frame(0x1, data)
|
|
102
119
|
@socket.write(frame)
|
|
@@ -104,6 +121,8 @@ module Tina4
|
|
|
104
121
|
# Connection closed
|
|
105
122
|
end
|
|
106
123
|
|
|
124
|
+
alias_method :send_text, :send
|
|
125
|
+
|
|
107
126
|
def send_pong(data)
|
|
108
127
|
frame = build_frame(0xA, data || "")
|
|
109
128
|
@socket.write(frame)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WebSocket Backplane Abstraction for Tina4 Ruby.
|
|
4
|
+
#
|
|
5
|
+
# Enables broadcasting WebSocket messages across multiple server instances
|
|
6
|
+
# using a shared pub/sub channel (e.g. Redis). Without a backplane configured,
|
|
7
|
+
# broadcast() only reaches connections on the local process.
|
|
8
|
+
#
|
|
9
|
+
# Configuration via environment variables:
|
|
10
|
+
# TINA4_WS_BACKPLANE — Backend type: "redis", "nats", or "" (default: none)
|
|
11
|
+
# TINA4_WS_BACKPLANE_URL — Connection string (default: redis://localhost:6379)
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# backplane = Tina4::WebSocketBackplane.create
|
|
15
|
+
# if backplane
|
|
16
|
+
# backplane.subscribe("chat") { |msg| relay_to_local(msg) }
|
|
17
|
+
# backplane.publish("chat", '{"user":"A","text":"hello"}')
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
module Tina4
|
|
21
|
+
# Base backplane interface for scaling WebSocket broadcast across instances.
|
|
22
|
+
#
|
|
23
|
+
# Subclasses implement publish/subscribe over a shared message bus so that
|
|
24
|
+
# every server instance receives every broadcast, not just the originator.
|
|
25
|
+
class WebSocketBackplane
|
|
26
|
+
# Publish a message to all instances listening on +channel+.
|
|
27
|
+
def publish(channel, message)
|
|
28
|
+
raise NotImplementedError, "#{self.class}#publish not implemented"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Subscribe to +channel+. The block is called with each incoming message.
|
|
32
|
+
# Runs in a background thread.
|
|
33
|
+
def subscribe(channel, &block)
|
|
34
|
+
raise NotImplementedError, "#{self.class}#subscribe not implemented"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Stop listening on +channel+.
|
|
38
|
+
def unsubscribe(channel)
|
|
39
|
+
raise NotImplementedError, "#{self.class}#unsubscribe not implemented"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Tear down connections and background threads.
|
|
43
|
+
def close
|
|
44
|
+
raise NotImplementedError, "#{self.class}#close not implemented"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Factory that reads TINA4_WS_BACKPLANE and returns the appropriate
|
|
48
|
+
# backplane instance, or +nil+ if no backplane is configured.
|
|
49
|
+
#
|
|
50
|
+
# This keeps backplane usage entirely optional — callers simply check
|
|
51
|
+
# +if backplane+ before publishing.
|
|
52
|
+
def self.create(url: nil)
|
|
53
|
+
backend = ENV.fetch("TINA4_WS_BACKPLANE", "").strip.downcase
|
|
54
|
+
|
|
55
|
+
case backend
|
|
56
|
+
when "redis"
|
|
57
|
+
RedisBackplane.new(url: url)
|
|
58
|
+
when "nats"
|
|
59
|
+
raise NotImplementedError, "NATS backplane is on the roadmap but not yet implemented."
|
|
60
|
+
when ""
|
|
61
|
+
nil
|
|
62
|
+
else
|
|
63
|
+
raise ArgumentError, "Unknown TINA4_WS_BACKPLANE value: '#{backend}'"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Redis pub/sub backplane.
|
|
69
|
+
#
|
|
70
|
+
# Requires the +redis+ gem (+gem install redis+). The require is deferred
|
|
71
|
+
# so the rest of Tina4 works fine without it installed — an error is raised
|
|
72
|
+
# only when this class is actually instantiated.
|
|
73
|
+
class RedisBackplane < WebSocketBackplane
|
|
74
|
+
def initialize(url: nil)
|
|
75
|
+
begin
|
|
76
|
+
require "redis"
|
|
77
|
+
rescue LoadError
|
|
78
|
+
raise LoadError,
|
|
79
|
+
"The 'redis' gem is required for RedisBackplane. " \
|
|
80
|
+
"Install it with: gem install redis"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@url = url || ENV.fetch("TINA4_WS_BACKPLANE_URL", "redis://localhost:6379")
|
|
84
|
+
@redis = Redis.new(url: @url)
|
|
85
|
+
@subscriber = Redis.new(url: @url)
|
|
86
|
+
@threads = {}
|
|
87
|
+
@running = true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def publish(channel, message)
|
|
91
|
+
@redis.publish(channel, message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def subscribe(channel, &block)
|
|
95
|
+
@threads[channel] = Thread.new do
|
|
96
|
+
@subscriber.subscribe(channel) do |on|
|
|
97
|
+
on.message do |_chan, msg|
|
|
98
|
+
block.call(msg) if @running
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def unsubscribe(channel)
|
|
105
|
+
@subscriber.unsubscribe(channel)
|
|
106
|
+
thread = @threads.delete(channel)
|
|
107
|
+
thread&.join(1)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def close
|
|
111
|
+
@running = false
|
|
112
|
+
@threads.each_value { |t| t.kill }
|
|
113
|
+
@threads.clear
|
|
114
|
+
@subscriber.close
|
|
115
|
+
@redis.close
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/tina4.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative "tina4/database"
|
|
|
22
22
|
require_relative "tina4/database_result"
|
|
23
23
|
require_relative "tina4/field_types"
|
|
24
24
|
require_relative "tina4/orm"
|
|
25
|
+
require_relative "tina4/query_builder"
|
|
25
26
|
require_relative "tina4/migration"
|
|
26
27
|
require_relative "tina4/auto_crud"
|
|
27
28
|
require_relative "tina4/database/sqlite3_adapter"
|
|
@@ -63,7 +64,8 @@ module Tina4
|
|
|
63
64
|
autoload :FileHandler, File.expand_path("tina4/session_handlers/file_handler", __dir__)
|
|
64
65
|
autoload :RedisHandler, File.expand_path("tina4/session_handlers/redis_handler", __dir__)
|
|
65
66
|
autoload :MongoHandler, File.expand_path("tina4/session_handlers/mongo_handler", __dir__)
|
|
66
|
-
autoload :ValkeyHandler,
|
|
67
|
+
autoload :ValkeyHandler, File.expand_path("tina4/session_handlers/valkey_handler", __dir__)
|
|
68
|
+
autoload :DatabaseHandler, File.expand_path("tina4/session_handlers/database_handler", __dir__)
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
# ── Lazy-loaded: queue backends ───────────────────────────────────────
|
|
@@ -71,6 +73,7 @@ module Tina4
|
|
|
71
73
|
autoload :LiteBackend, File.expand_path("tina4/queue_backends/lite_backend", __dir__)
|
|
72
74
|
autoload :RabbitmqBackend, File.expand_path("tina4/queue_backends/rabbitmq_backend", __dir__)
|
|
73
75
|
autoload :KafkaBackend, File.expand_path("tina4/queue_backends/kafka_backend", __dir__)
|
|
76
|
+
autoload :MongoBackend, File.expand_path("tina4/queue_backends/mongo_backend", __dir__)
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
# ── Lazy-loaded: web server ───────────────────────────────────────────
|
|
@@ -174,12 +177,61 @@ module Tina4
|
|
|
174
177
|
# Tina4.initialize!(__dir__)
|
|
175
178
|
# Tina4.run!
|
|
176
179
|
# Or combined: Tina4.run!(__dir__)
|
|
177
|
-
def
|
|
180
|
+
def find_available_port(start, max_tries = 10)
|
|
181
|
+
require "socket"
|
|
182
|
+
max_tries.times do |offset|
|
|
183
|
+
port = start + offset
|
|
184
|
+
begin
|
|
185
|
+
server = TCPServer.new("127.0.0.1", port)
|
|
186
|
+
server.close
|
|
187
|
+
return port
|
|
188
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
|
189
|
+
next
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
start
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def open_browser(url)
|
|
196
|
+
require "rbconfig"
|
|
197
|
+
Thread.new do
|
|
198
|
+
sleep 2
|
|
199
|
+
case RbConfig::CONFIG["host_os"]
|
|
200
|
+
when /darwin/i then system("open", url)
|
|
201
|
+
when /mswin|mingw/i then system("start", url)
|
|
202
|
+
else system("xdg-open", url)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def run!(root_dir = nil, port: nil, host: nil, debug: nil)
|
|
208
|
+
# Handle legacy call: run!(port: 7147) where root_dir receives the hash
|
|
209
|
+
if root_dir.is_a?(Hash)
|
|
210
|
+
port ||= root_dir[:port]
|
|
211
|
+
host ||= root_dir[:host]
|
|
212
|
+
debug = root_dir[:debug] if debug.nil? && root_dir.key?(:debug)
|
|
213
|
+
root_dir = nil
|
|
214
|
+
end
|
|
215
|
+
root_dir ||= Dir.pwd
|
|
216
|
+
|
|
217
|
+
ENV["PORT"] = port.to_s if port
|
|
218
|
+
ENV["HOST"] = host.to_s if host
|
|
219
|
+
ENV["TINA4_DEBUG"] = debug.to_s unless debug.nil?
|
|
220
|
+
|
|
178
221
|
initialize!(root_dir) unless @root_dir
|
|
179
222
|
|
|
180
223
|
host = ENV.fetch("HOST", ENV.fetch("TINA4_HOST", "0.0.0.0"))
|
|
181
224
|
port = ENV.fetch("PORT", ENV.fetch("TINA4_PORT", "7147")).to_i
|
|
182
225
|
|
|
226
|
+
actual_port = find_available_port(port)
|
|
227
|
+
if actual_port != port
|
|
228
|
+
Tina4::Log.info("Port #{port} in use, using #{actual_port}")
|
|
229
|
+
port = actual_port
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
display_host = (host == "0.0.0.0" || host == "::") ? "localhost" : host
|
|
233
|
+
url = "http://#{display_host}:#{port}"
|
|
234
|
+
|
|
183
235
|
app = Tina4::RackApp.new(root_dir: root_dir)
|
|
184
236
|
is_debug = Tina4::Env.truthy?(ENV["TINA4_DEBUG"])
|
|
185
237
|
|
|
@@ -203,6 +255,7 @@ module Tina4
|
|
|
203
255
|
Tina4::Log.info("Production server: puma")
|
|
204
256
|
Tina4::Shutdown.setup
|
|
205
257
|
|
|
258
|
+
open_browser(url)
|
|
206
259
|
launcher = Puma::Launcher.new(config)
|
|
207
260
|
launcher.run
|
|
208
261
|
return
|
|
@@ -212,6 +265,7 @@ module Tina4
|
|
|
212
265
|
end
|
|
213
266
|
|
|
214
267
|
Tina4::Log.info("Development server: WEBrick")
|
|
268
|
+
open_browser(url)
|
|
215
269
|
server = Tina4::WebServer.new(app, host: host, port: port)
|
|
216
270
|
server.start
|
|
217
271
|
end
|
|
@@ -286,6 +340,11 @@ module Tina4
|
|
|
286
340
|
Tina4::Router.group(prefix, auth_handler: auth, &block)
|
|
287
341
|
end
|
|
288
342
|
|
|
343
|
+
# WebSocket route registration
|
|
344
|
+
def websocket(path, &block)
|
|
345
|
+
Tina4::Router.websocket(path, &block)
|
|
346
|
+
end
|
|
347
|
+
|
|
289
348
|
# Middleware hooks
|
|
290
349
|
def before(pattern = nil, &block)
|
|
291
350
|
Tina4::Middleware.before(pattern, &block)
|
|
@@ -355,8 +414,9 @@ module Tina4
|
|
|
355
414
|
end
|
|
356
415
|
|
|
357
416
|
def auto_discover(root_dir)
|
|
358
|
-
|
|
359
|
-
|
|
417
|
+
# src/ prefixed directories take priority over root-level ones
|
|
418
|
+
discover_dirs = %w[src/routes routes src/api api src/orm orm]
|
|
419
|
+
discover_dirs.each do |dir|
|
|
360
420
|
full_dir = File.join(root_dir, dir)
|
|
361
421
|
next unless Dir.exist?(full_dir)
|
|
362
422
|
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-03-28 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rack
|
|
@@ -319,11 +320,14 @@ files:
|
|
|
319
320
|
- lib/tina4/public/js/frond.min.js
|
|
320
321
|
- lib/tina4/public/js/tina4-dev-admin.min.js
|
|
321
322
|
- lib/tina4/public/js/tina4.min.js
|
|
323
|
+
- lib/tina4/public/js/tina4js.min.js
|
|
322
324
|
- lib/tina4/public/swagger/index.html
|
|
323
325
|
- lib/tina4/public/swagger/oauth2-redirect.html
|
|
326
|
+
- lib/tina4/query_builder.rb
|
|
324
327
|
- lib/tina4/queue.rb
|
|
325
328
|
- lib/tina4/queue_backends/kafka_backend.rb
|
|
326
329
|
- lib/tina4/queue_backends/lite_backend.rb
|
|
330
|
+
- lib/tina4/queue_backends/mongo_backend.rb
|
|
327
331
|
- lib/tina4/queue_backends/rabbitmq_backend.rb
|
|
328
332
|
- lib/tina4/rack_app.rb
|
|
329
333
|
- lib/tina4/rate_limiter.rb
|
|
@@ -351,6 +355,7 @@ files:
|
|
|
351
355
|
- lib/tina4/seeder.rb
|
|
352
356
|
- lib/tina4/service_runner.rb
|
|
353
357
|
- lib/tina4/session.rb
|
|
358
|
+
- lib/tina4/session_handlers/database_handler.rb
|
|
354
359
|
- lib/tina4/session_handlers/file_handler.rb
|
|
355
360
|
- lib/tina4/session_handlers/mongo_handler.rb
|
|
356
361
|
- lib/tina4/session_handlers/redis_handler.rb
|
|
@@ -369,9 +374,11 @@ files:
|
|
|
369
374
|
- lib/tina4/templates/errors/503.twig
|
|
370
375
|
- lib/tina4/templates/errors/base.twig
|
|
371
376
|
- lib/tina4/testing.rb
|
|
377
|
+
- lib/tina4/validator.rb
|
|
372
378
|
- lib/tina4/version.rb
|
|
373
379
|
- lib/tina4/webserver.rb
|
|
374
380
|
- lib/tina4/websocket.rb
|
|
381
|
+
- lib/tina4/websocket_backplane.rb
|
|
375
382
|
- lib/tina4/wsdl.rb
|
|
376
383
|
- lib/tina4ruby.rb
|
|
377
384
|
homepage: https://tina4.com
|
|
@@ -379,6 +386,7 @@ licenses:
|
|
|
379
386
|
- MIT
|
|
380
387
|
metadata:
|
|
381
388
|
homepage_uri: https://tina4.com
|
|
389
|
+
post_install_message:
|
|
382
390
|
rdoc_options: []
|
|
383
391
|
require_paths:
|
|
384
392
|
- lib
|
|
@@ -393,7 +401,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
393
401
|
- !ruby/object:Gem::Version
|
|
394
402
|
version: '0'
|
|
395
403
|
requirements: []
|
|
396
|
-
rubygems_version: 4.
|
|
404
|
+
rubygems_version: 3.4.19
|
|
405
|
+
signing_key:
|
|
397
406
|
specification_version: 4
|
|
398
407
|
summary: Simple. Fast. Human. This is not a framework.
|
|
399
408
|
test_files: []
|