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 else

BREAKING (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 bool and is_truthy disagree.

%w[true 1 yes on].freeze

Class Method Summary collapse

Class Method Details

.all_envObject

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

Returns:

  • (Boolean)


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_envObject

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