Module: Tina4::Env
- Defined in:
- lib/tina4/env.rb
Constant Summary collapse
- DEFAULT_ENV =
NOTE: TINA4_SECRET is deliberately ABSENT here. The default signing secret must never become a guessable built-in. A blank secret is the signal for Auth.ensure_dev_secret to mint a per-machine random dev secret (saved to gitignored .env.local) in dev, or to emit the actionable "set TINA4_SECRET" warning in CI/prod. Parity with the Python master.
{ "PROJECT_NAME" => "Tina4 Ruby Project", "TINA4_SWAGGER_VERSION" => "1.0.0", "TINA4_LOCALE" => "en", "TINA4_DEBUG" => "true", "TINA4_LOG_LEVEL" => "[TINA4_LOG_ALL]" }.freeze
- TRUTHY =
The ONE env truthiness table. Every env boolean in every Tina4 framework answers from this set (case-insensitive after strip):
truthy: "true", "1", "yes", "on" falsy: everything elseBREAKING (parity): "y", "t", "n" and "f" were accepted here and NOWHERE ELSE — not by Ruby's own Log/Mcp checks, and not by Python, PHP or Node. So TINA4_LOG_FUNC=y switched function logging ON while TINA4_DEBUG=y left debug OFF, in the same process, from the same .env. Single letters are dropped rather than spread: systemd's boolean set is 1/yes/true/on with no letters, and YAML 1.2 removed y/n precisely because a bare letter reads as a value (the Norway problem). Use "true"/"false". There is deliberately NO falsy table. Falsy is "not in TRUTHY", so there is exactly one list to keep correct. A second table is a second thing that can drift, and it is what let
boolandis_truthydisagree. %w[true 1 yes on].freeze
Class Method Summary collapse
-
.all_env ⇒ Object
Return all current ENV vars as a hash.
-
.bool(name, default: false) ⇒ Object
Read an env var and coerce to Boolean.
-
.float(name, default: 0.0) ⇒ Object
Read an env var and coerce to Float.
-
.get_env(key, default = nil) ⇒ Object
Get an env var value, with optional default.
-
.has_env?(key) ⇒ Boolean
Check if an env var exists.
-
.int(name, default: 0) ⇒ Object
Read an env var and coerce to Integer.
-
.is_truthy(val) ⇒ Object
Check if a value is truthy for env boolean checks.
- .load_env(root_dir = Dir.pwd) ⇒ Object
-
.load_local_env(root_dir = Dir.pwd) ⇒ Object
Load .env.local with first-wins semantics (override=false).
-
.require_env(*keys) ⇒ Object
Raise if any of the given keys are missing from ENV Validate that required env vars exist, and return them.
-
.reset_env ⇒ Object
Reset: clear all env vars that were loaded (restore to process defaults).
-
.str(name, default: "") ⇒ Object
Read an env var as a String.
Class Method Details
.all_env ⇒ Object
Return all current ENV vars as a hash
211 212 213 |
# File 'lib/tina4/env.rb', line 211 def all_env ENV.to_h end |
.bool(name, default: false) ⇒ Object
Read an env var and coerce to Boolean. Returns default only when the
var is UNSET — a value that IS set is answered by the one truthiness
table, never quietly replaced by the default. Never raises.
123 124 125 126 127 |
# File 'lib/tina4/env.rb', line 123 def self.bool(name, default: false) raw = ENV[name.to_s] return default if raw.nil? is_truthy(raw) end |
.float(name, default: 0.0) ⇒ Object
Read an env var and coerce to Float. Logs a warning via Tina4::Log
(if loaded) and returns default on parse failure. Never raises.
142 143 144 145 146 147 148 149 |
# File 'lib/tina4/env.rb', line 142 def self.float(name, default: 0.0) raw = ENV[name.to_s] return default if raw.nil? Float(raw.strip) rescue ArgumentError, TypeError log_warning("Env.float(#{name.inspect}): could not parse #{raw.inspect} as Float — using default #{default.inspect}") default end |
.get_env(key, default = nil) ⇒ Object
Get an env var value, with optional default
201 202 203 |
# File 'lib/tina4/env.rb', line 201 def get_env(key, default = nil) ENV[key.to_s] || default end |
.has_env?(key) ⇒ Boolean
Check if an env var exists
206 207 208 |
# File 'lib/tina4/env.rb', line 206 def has_env?(key) ENV.key?(key.to_s) end |
.int(name, default: 0) ⇒ Object
Read an env var and coerce to Integer. Logs a warning via Tina4::Log
(if loaded) and returns default on parse failure. Never raises.
131 132 133 134 135 136 137 138 |
# File 'lib/tina4/env.rb', line 131 def self.int(name, default: 0) raw = ENV[name.to_s] return default if raw.nil? Integer(raw.strip) rescue ArgumentError, TypeError log_warning("Env.int(#{name.inspect}): could not parse #{raw.inspect} as Integer — using default #{default.inspect}") default end |
.is_truthy(val) ⇒ Object
Check if a value is truthy for env boolean checks.
Accepts: "true", "True", "TRUE", "1", "yes", "Yes", "YES", "on", "On", "ON". Everything else is falsy (including empty string, nil, not set).
116 117 118 |
# File 'lib/tina4/env.rb', line 116 def self.is_truthy(val) TRUTHY.include?(val.to_s.strip.downcase) end |
.load_env(root_dir = Dir.pwd) ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/tina4/env.rb', line 170 def load_env(root_dir = Dir.pwd) env_file = resolve_env_file(root_dir) unless File.exist?(env_file) create_default_env(env_file) end # Precedence: real-env > .env.local > .env. Both loads are first-wins # (override=false / `ENV[key] ||= value`), so a key already present in # the real process environment is NEVER clobbered. .env.local loads # FIRST so its values beat .env, but a real env var set before boot # still wins over both. This is the security-correct ordering: a stray # gitignored .env.local (e.g. a stale auto-generated dev secret) must # not override an explicitly-set real TINA4_SECRET. The ensure-dev-secret # bootstrap runs AFTER this (only mints a secret if still unset in dev). local = load_local_env(root_dir) # .env.local WINS on a duplicate key, so it merges last. Both hashes # already report the value in effect, so a real env var that beat both # is what a caller reads back either way. parse_env_file(env_file).merge(local) end |
.load_local_env(root_dir = Dir.pwd) ⇒ Object
Load .env.local with first-wins semantics (override=false). A real process env var already present wins; this only fills keys not already set. Loaded BEFORE .env so .env.local beats .env (real-env > .env.local
.env). No-op when the file is absent (common for fresh checkouts).
194 195 196 197 198 |
# File 'lib/tina4/env.rb', line 194 def load_local_env(root_dir = Dir.pwd) local_file = File.join(root_dir, ".env.local") return {} unless File.exist?(local_file) parse_env_file(local_file) end |
.require_env(*keys) ⇒ Object
Raise if any of the given keys are missing from ENV Validate that required env vars exist, and return them.
RENAMED from require_env! on 2026-07-31. The bang was Ruby-idiomatic for "raises", but the concept is named require_env in the other three, and the surface-table rule is one name per concept with idiomatic CASING only. An alias would paper over the mismatch instead of fixing it, so the primary is renamed. Breaking for anyone calling require_env!.
Returns a hash of every requested key to its value, matching Python, PHP and Node - it used to return nothing, so a caller could validate but not read in one step.
Reports every missing name in one raise rather than the first: an operator fixing a deployment wants the whole list, not one name per restart.
231 232 233 234 235 236 237 238 |
# File 'lib/tina4/env.rb', line 231 def require_env(*keys) names = keys.flatten.map(&:to_s) missing = names.reject { |k| ENV.key?(k) } unless missing.empty? raise KeyError, "Missing required environment variables: #{missing.join(', ')}" end names.to_h { |k| [k, ENV[k]] } end |
.reset_env ⇒ Object
Reset: clear all env vars that were loaded (restore to process defaults)
241 242 243 244 |
# File 'lib/tina4/env.rb', line 241 def reset_env @loaded_keys&.each { |k| ENV.delete(k) } @loaded_keys = [] end |
.str(name, default: "") ⇒ Object
Read an env var as a String. Returns default when unset.
Whitespace is preserved — this is a pass-through for the raw env value,
matching Python's Env.str semantics.
154 155 156 157 158 |
# File 'lib/tina4/env.rb', line 154 def self.str(name, default: "") raw = ENV[name.to_s] return default if raw.nil? raw end |