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
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Drivers
|
|
5
|
-
class SqliteDriver
|
|
6
|
-
attr_reader :connection
|
|
7
|
-
|
|
8
|
-
def connect(connection_string, username: nil, password: nil)
|
|
9
|
-
require "sqlite3"
|
|
10
|
-
db_path = self.class.resolve_path(connection_string)
|
|
11
|
-
|
|
12
|
-
@connection = SQLite3::Database.new(db_path)
|
|
13
|
-
@connection.results_as_hash = true
|
|
14
|
-
@connection.execute("PRAGMA journal_mode=WAL")
|
|
15
|
-
@connection.execute("PRAGMA foreign_keys=ON")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Resolve a SQLite URL / path against the project root (cwd).
|
|
19
|
-
#
|
|
20
|
-
# Convention (matches tina4-python, tina4-php, tina4-nodejs):
|
|
21
|
-
# sqlite::memory: → :memory:
|
|
22
|
-
# sqlite:///:memory: → :memory:
|
|
23
|
-
# sqlite:///app.db → {cwd}/app.db (relative)
|
|
24
|
-
# sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
|
|
25
|
-
# sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
|
|
26
|
-
# sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
|
|
27
|
-
#
|
|
28
|
-
# Never mkdir outside cwd — that was the root cause of the
|
|
29
|
-
# "Read-only file system: '/data'" crash on macOS.
|
|
30
|
-
def self.resolve_path(connection_string)
|
|
31
|
-
return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
|
|
32
|
-
|
|
33
|
-
# Strip the scheme + up to three slashes, preserving a potential fourth
|
|
34
|
-
# slash (absolute) or drive letter.
|
|
35
|
-
raw = connection_string.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
|
|
36
|
-
return ":memory:" if raw == ":memory:"
|
|
37
|
-
|
|
38
|
-
is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
|
|
39
|
-
is_unix_abs = raw.start_with?("/")
|
|
40
|
-
|
|
41
|
-
if is_windows_abs || is_unix_abs
|
|
42
|
-
# Absolute — trust the user; don't auto-mkdir outside cwd.
|
|
43
|
-
raw
|
|
44
|
-
else
|
|
45
|
-
# Relative — resolve under cwd; auto-mkdir parent dir.
|
|
46
|
-
resolved = File.join(Dir.pwd, raw)
|
|
47
|
-
parent = File.dirname(resolved)
|
|
48
|
-
require "fileutils"
|
|
49
|
-
FileUtils.mkdir_p(parent) unless File.directory?(parent)
|
|
50
|
-
resolved
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def close
|
|
55
|
-
@connection&.close
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def execute_query(sql, params = [])
|
|
59
|
-
results = @connection.execute(sql, params)
|
|
60
|
-
results.map { |row| symbolize_keys(row) }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def execute(sql, params = [])
|
|
64
|
-
@connection.execute(sql, params)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def last_insert_id
|
|
68
|
-
@connection.last_insert_row_id
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def placeholder
|
|
72
|
-
"?"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def placeholders(count)
|
|
76
|
-
(["?"] * count).join(", ")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def apply_limit(sql, limit, offset = 0)
|
|
80
|
-
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def begin_transaction
|
|
84
|
-
@connection.execute("BEGIN TRANSACTION")
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def commit
|
|
88
|
-
@connection.execute("COMMIT")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def rollback
|
|
92
|
-
@connection.execute("ROLLBACK")
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def tables
|
|
96
|
-
rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
97
|
-
rows.map { |r| r[:name] }
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def columns(table_name)
|
|
101
|
-
rows = execute_query("PRAGMA table_info(#{table_name})")
|
|
102
|
-
rows.map do |r|
|
|
103
|
-
{
|
|
104
|
-
name: r[:name],
|
|
105
|
-
type: r[:type],
|
|
106
|
-
nullable: r[:notnull] == 0,
|
|
107
|
-
default: r[:dflt_value],
|
|
108
|
-
primary_key: r[:pk] == 1
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
private
|
|
114
|
-
|
|
115
|
-
def symbolize_keys(hash)
|
|
116
|
-
hash.each_with_object({}) do |(k, v), h|
|
|
117
|
-
h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Drivers
|
|
5
|
+
class SqliteDriver
|
|
6
|
+
attr_reader :connection
|
|
7
|
+
|
|
8
|
+
def connect(connection_string, username: nil, password: nil)
|
|
9
|
+
require "sqlite3"
|
|
10
|
+
db_path = self.class.resolve_path(connection_string)
|
|
11
|
+
|
|
12
|
+
@connection = SQLite3::Database.new(db_path)
|
|
13
|
+
@connection.results_as_hash = true
|
|
14
|
+
@connection.execute("PRAGMA journal_mode=WAL")
|
|
15
|
+
@connection.execute("PRAGMA foreign_keys=ON")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Resolve a SQLite URL / path against the project root (cwd).
|
|
19
|
+
#
|
|
20
|
+
# Convention (matches tina4-python, tina4-php, tina4-nodejs):
|
|
21
|
+
# sqlite::memory: → :memory:
|
|
22
|
+
# sqlite:///:memory: → :memory:
|
|
23
|
+
# sqlite:///app.db → {cwd}/app.db (relative)
|
|
24
|
+
# sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
|
|
25
|
+
# sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
|
|
26
|
+
# sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
|
|
27
|
+
#
|
|
28
|
+
# Never mkdir outside cwd — that was the root cause of the
|
|
29
|
+
# "Read-only file system: '/data'" crash on macOS.
|
|
30
|
+
def self.resolve_path(connection_string)
|
|
31
|
+
return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"
|
|
32
|
+
|
|
33
|
+
# Strip the scheme + up to three slashes, preserving a potential fourth
|
|
34
|
+
# slash (absolute) or drive letter.
|
|
35
|
+
raw = connection_string.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
|
|
36
|
+
return ":memory:" if raw == ":memory:"
|
|
37
|
+
|
|
38
|
+
is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
|
|
39
|
+
is_unix_abs = raw.start_with?("/")
|
|
40
|
+
|
|
41
|
+
if is_windows_abs || is_unix_abs
|
|
42
|
+
# Absolute — trust the user; don't auto-mkdir outside cwd.
|
|
43
|
+
raw
|
|
44
|
+
else
|
|
45
|
+
# Relative — resolve under cwd; auto-mkdir parent dir.
|
|
46
|
+
resolved = File.join(Dir.pwd, raw)
|
|
47
|
+
parent = File.dirname(resolved)
|
|
48
|
+
require "fileutils"
|
|
49
|
+
FileUtils.mkdir_p(parent) unless File.directory?(parent)
|
|
50
|
+
resolved
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
@connection&.close
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def execute_query(sql, params = [])
|
|
59
|
+
results = @connection.execute(sql, params)
|
|
60
|
+
results.map { |row| symbolize_keys(row) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def execute(sql, params = [])
|
|
64
|
+
@connection.execute(sql, params)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def last_insert_id
|
|
68
|
+
@connection.last_insert_row_id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def placeholder
|
|
72
|
+
"?"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def placeholders(count)
|
|
76
|
+
(["?"] * count).join(", ")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply_limit(sql, limit, offset = 0)
|
|
80
|
+
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def begin_transaction
|
|
84
|
+
@connection.execute("BEGIN TRANSACTION")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def commit
|
|
88
|
+
@connection.execute("COMMIT")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def rollback
|
|
92
|
+
@connection.execute("ROLLBACK")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def tables
|
|
96
|
+
rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
97
|
+
rows.map { |r| r[:name] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def columns(table_name)
|
|
101
|
+
rows = execute_query("PRAGMA table_info(#{table_name})")
|
|
102
|
+
rows.map do |r|
|
|
103
|
+
{
|
|
104
|
+
name: r[:name],
|
|
105
|
+
type: r[:type],
|
|
106
|
+
nullable: r[:notnull] == 0,
|
|
107
|
+
default: r[:dflt_value],
|
|
108
|
+
primary_key: r[:pk] == 1
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def symbolize_keys(hash)
|
|
116
|
+
hash.each_with_object({}) do |(k, v), h|
|
|
117
|
+
h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/tina4/env.rb
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "digest"
|
|
3
|
-
|
|
4
|
-
module Tina4
|
|
5
|
-
module Env
|
|
6
|
-
DEFAULT_ENV = {
|
|
7
|
-
"PROJECT_NAME" => "Tina4 Ruby Project",
|
|
8
|
-
"VERSION" => "1.0.0",
|
|
9
|
-
"TINA4_LOCALE" => "en",
|
|
10
|
-
"TINA4_DEBUG" => "true",
|
|
11
|
-
"TINA4_LOG_LEVEL" => "[TINA4_LOG_ALL]",
|
|
12
|
-
"SECRET" => "tina4-secret-change-me"
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
# Check if a value is truthy for env boolean checks.
|
|
16
|
-
#
|
|
17
|
-
# Accepts: "true", "True", "TRUE", "1", "yes", "Yes", "YES", "on", "On", "ON".
|
|
18
|
-
# Everything else is falsy (including empty string, nil, not set).
|
|
19
|
-
def self.is_truthy(val)
|
|
20
|
-
%w[true 1 yes on].include?(val.to_s.strip.downcase)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
class << self
|
|
24
|
-
def load_env(root_dir = Dir.pwd)
|
|
25
|
-
env_file = resolve_env_file(root_dir)
|
|
26
|
-
unless File.exist?(env_file)
|
|
27
|
-
create_default_env(env_file)
|
|
28
|
-
end
|
|
29
|
-
parse_env_file(env_file)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Get an env var value, with optional default
|
|
33
|
-
def get_env(key, default = nil)
|
|
34
|
-
ENV[key.to_s] || default
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Check if an env var exists
|
|
38
|
-
def has_env?(key)
|
|
39
|
-
ENV.key?(key.to_s)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Return all current ENV vars as a hash
|
|
43
|
-
def all_env
|
|
44
|
-
ENV.to_h
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Raise if any of the given keys are missing from ENV
|
|
48
|
-
def require_env!(*keys)
|
|
49
|
-
missing = keys.map(&:to_s).reject { |k| ENV.key?(k) }
|
|
50
|
-
unless missing.empty?
|
|
51
|
-
raise KeyError, "Missing required env vars: #{missing.join(', ')}"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Reset: clear all env vars that were loaded (restore to process defaults)
|
|
56
|
-
def reset_env
|
|
57
|
-
@loaded_keys&.each { |k| ENV.delete(k) }
|
|
58
|
-
@loaded_keys = []
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def resolve_env_file(root_dir)
|
|
64
|
-
environment = ENV["ENVIRONMENT"]
|
|
65
|
-
if environment && !environment.empty?
|
|
66
|
-
candidate = File.join(root_dir, ".env.#{environment}")
|
|
67
|
-
return candidate if File.exist?(candidate)
|
|
68
|
-
end
|
|
69
|
-
File.join(root_dir, ".env")
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def create_default_env(path)
|
|
73
|
-
api_key = Digest::MD5.hexdigest(Time.now.to_s)
|
|
74
|
-
content = DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n")
|
|
75
|
-
content += "\nAPI_KEY=\"#{api_key}\"\n"
|
|
76
|
-
File.write(path, content)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def parse_env_file(path)
|
|
80
|
-
return unless File.exist?(path)
|
|
81
|
-
File.readlines(path).each do |line|
|
|
82
|
-
line = line.strip
|
|
83
|
-
next if line.empty? || line.start_with?("#")
|
|
84
|
-
if (match = line.match(/\A([A-Za-z_][A-Za-z0-9_]*)=["']?(.*)["']?\z/))
|
|
85
|
-
key = match[1]
|
|
86
|
-
value = match[2].gsub(/["']\z/, "")
|
|
87
|
-
ENV[key] ||= value
|
|
88
|
-
@loaded_keys ||= []
|
|
89
|
-
@loaded_keys << key
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
module Tina4
|
|
5
|
+
module Env
|
|
6
|
+
DEFAULT_ENV = {
|
|
7
|
+
"PROJECT_NAME" => "Tina4 Ruby Project",
|
|
8
|
+
"VERSION" => "1.0.0",
|
|
9
|
+
"TINA4_LOCALE" => "en",
|
|
10
|
+
"TINA4_DEBUG" => "true",
|
|
11
|
+
"TINA4_LOG_LEVEL" => "[TINA4_LOG_ALL]",
|
|
12
|
+
"SECRET" => "tina4-secret-change-me"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# Check if a value is truthy for env boolean checks.
|
|
16
|
+
#
|
|
17
|
+
# Accepts: "true", "True", "TRUE", "1", "yes", "Yes", "YES", "on", "On", "ON".
|
|
18
|
+
# Everything else is falsy (including empty string, nil, not set).
|
|
19
|
+
def self.is_truthy(val)
|
|
20
|
+
%w[true 1 yes on].include?(val.to_s.strip.downcase)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def load_env(root_dir = Dir.pwd)
|
|
25
|
+
env_file = resolve_env_file(root_dir)
|
|
26
|
+
unless File.exist?(env_file)
|
|
27
|
+
create_default_env(env_file)
|
|
28
|
+
end
|
|
29
|
+
parse_env_file(env_file)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get an env var value, with optional default
|
|
33
|
+
def get_env(key, default = nil)
|
|
34
|
+
ENV[key.to_s] || default
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if an env var exists
|
|
38
|
+
def has_env?(key)
|
|
39
|
+
ENV.key?(key.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Return all current ENV vars as a hash
|
|
43
|
+
def all_env
|
|
44
|
+
ENV.to_h
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raise if any of the given keys are missing from ENV
|
|
48
|
+
def require_env!(*keys)
|
|
49
|
+
missing = keys.map(&:to_s).reject { |k| ENV.key?(k) }
|
|
50
|
+
unless missing.empty?
|
|
51
|
+
raise KeyError, "Missing required env vars: #{missing.join(', ')}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Reset: clear all env vars that were loaded (restore to process defaults)
|
|
56
|
+
def reset_env
|
|
57
|
+
@loaded_keys&.each { |k| ENV.delete(k) }
|
|
58
|
+
@loaded_keys = []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def resolve_env_file(root_dir)
|
|
64
|
+
environment = ENV["ENVIRONMENT"]
|
|
65
|
+
if environment && !environment.empty?
|
|
66
|
+
candidate = File.join(root_dir, ".env.#{environment}")
|
|
67
|
+
return candidate if File.exist?(candidate)
|
|
68
|
+
end
|
|
69
|
+
File.join(root_dir, ".env")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_default_env(path)
|
|
73
|
+
api_key = Digest::MD5.hexdigest(Time.now.to_s)
|
|
74
|
+
content = DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n")
|
|
75
|
+
content += "\nAPI_KEY=\"#{api_key}\"\n"
|
|
76
|
+
File.write(path, content)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_env_file(path)
|
|
80
|
+
return unless File.exist?(path)
|
|
81
|
+
File.readlines(path).each do |line|
|
|
82
|
+
line = line.strip
|
|
83
|
+
next if line.empty? || line.start_with?("#")
|
|
84
|
+
if (match = line.match(/\A([A-Za-z_][A-Za-z0-9_]*)=["']?(.*)["']?\z/))
|
|
85
|
+
key = match[1]
|
|
86
|
+
value = match[2].gsub(/["']\z/, "")
|
|
87
|
+
ENV[key] ||= value
|
|
88
|
+
@loaded_keys ||= []
|
|
89
|
+
@loaded_keys << key
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|