178 lines
6.8 KiB
EmacsLisp
178 lines
6.8 KiB
EmacsLisp
// el-config — Layered, typed configuration for el-ui.
|
|
//
|
|
// Each layer is a key->value map. `Config::get(key)` walks layers from highest
|
|
// to lowest priority and returns the first hit. Values are parsed into the
|
|
// caller-requested type via `parse_<type>`.
|
|
|
|
// ── Environment ──────────────────────────────────────────────────────────────
|
|
|
|
let ENV_DEVELOPMENT: String = "development"
|
|
let ENV_STAGING: String = "staging"
|
|
let ENV_PRODUCTION: String = "production"
|
|
let ENV_TEST: String = "test"
|
|
|
|
fn env_current() -> String {
|
|
let raw: String = env("EL_ENV")
|
|
if str_eq(raw, "") { return ENV_DEVELOPMENT }
|
|
raw
|
|
}
|
|
|
|
fn env_is_production(name: String) -> Bool {
|
|
str_eq(name, "production")
|
|
}
|
|
|
|
// ── Errors ───────────────────────────────────────────────────────────────────
|
|
|
|
let CFG_ERR_MISSING: String = "config.missing"
|
|
let CFG_ERR_PARSE: String = "config.parse_error"
|
|
let CFG_ERR_TYPE: String = "config.type_error"
|
|
|
|
// ── Sources ──────────────────────────────────────────────────────────────────
|
|
|
|
let SRC_ENV: String = "env"
|
|
let SRC_DOTENV: String = "dotenv"
|
|
let SRC_MANIFEST: String = "manifest"
|
|
let SRC_DEFAULTS: String = "defaults"
|
|
let SRC_MAP: String = "map"
|
|
|
|
type ConfigSource {
|
|
kind: String
|
|
priority: Int // higher = wins
|
|
map_json: String // JSON: key -> string value
|
|
}
|
|
|
|
fn source_env_vars(prefix: String) -> ConfigSource {
|
|
// Snapshot env at construction (the runtime exposes env_keys()).
|
|
let map: String = "{}"
|
|
let keys: String = env_keys_with_prefix(prefix)
|
|
let n: Int = json_array_len(keys)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let raw_key: String = json_array_get(keys, i)
|
|
let value: String = env(raw_key)
|
|
let logical_key: String = str_to_lower(str_replace(str_slice(raw_key, str_len(prefix), str_len(raw_key)), "_", "."))
|
|
let map = json_set(map, logical_key, "\"" + value + "\"")
|
|
let i = i + 1
|
|
}
|
|
{ "kind": "env", "priority": 100, "map_json": map }
|
|
}
|
|
|
|
fn source_dotenv(path: String) -> ConfigSource {
|
|
let raw: String = ""
|
|
if fs_exists(path) { let raw = fs_read(path) }
|
|
let map: String = dotenv_parse(raw)
|
|
{ "kind": "dotenv", "priority": 80, "map_json": map }
|
|
}
|
|
|
|
fn source_manifest(manifest_path: String, current_env: String) -> ConfigSource {
|
|
let raw: String = fs_read(manifest_path)
|
|
let base: String = manifest_section(raw, "config")
|
|
let env_key: String = "env." + current_env
|
|
let env_overrides: String = manifest_section(raw, env_key)
|
|
let map: String = json_merge(base, env_overrides)
|
|
{ "kind": "manifest", "priority": 60, "map_json": map }
|
|
}
|
|
|
|
fn source_defaults(map_json: String) -> ConfigSource {
|
|
{ "kind": "defaults", "priority": 0, "map_json": map_json }
|
|
}
|
|
|
|
// ── Config ───────────────────────────────────────────────────────────────────
|
|
|
|
type Config {
|
|
environment: String // development | staging | production | test
|
|
sources_json: String // JSON array of ConfigSource
|
|
}
|
|
|
|
fn config_new(env_name: String) -> Config {
|
|
{ "environment": env_name, "sources_json": "[]" }
|
|
}
|
|
|
|
fn config_add_source(c: Config, src: ConfigSource) -> Config {
|
|
let updated: String = json_array_push_sorted(c.sources_json, json_encode(src), "priority", true)
|
|
{ "environment": c.environment, "sources_json": updated }
|
|
}
|
|
|
|
fn config_set_defaults(c: Config, defaults_map: String) -> Config {
|
|
config_add_source(c, source_defaults(defaults_map))
|
|
}
|
|
|
|
fn config_get_string(c: Config, key: String) -> String {
|
|
let n: Int = json_array_len(c.sources_json)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let src_json: String = json_array_get(c.sources_json, i)
|
|
let map: String = json_get(src_json, "map_json")
|
|
let v: String = json_get(map, key)
|
|
if !str_eq(v, "") { return v }
|
|
let i = i + 1
|
|
}
|
|
""
|
|
}
|
|
|
|
fn config_get_int(c: Config, key: String) -> Int {
|
|
let raw: String = config_get_string(c, key)
|
|
if str_eq(raw, "") { return 0 }
|
|
str_to_int(raw)
|
|
}
|
|
|
|
fn config_get_bool(c: Config, key: String) -> Bool {
|
|
let raw: String = str_to_lower(config_get_string(c, key))
|
|
if str_eq(raw, "true") { return true }
|
|
if str_eq(raw, "1") { return true }
|
|
if str_eq(raw, "yes") { return true }
|
|
false
|
|
}
|
|
|
|
// Strict variants — non-empty required.
|
|
fn config_require_string(c: Config, key: String) -> String {
|
|
let v: String = config_get_string(c, key)
|
|
if str_eq(v, "") { panic(CFG_ERR_MISSING + ":" + key) }
|
|
v
|
|
}
|
|
|
|
// ── load_from_toml — convenience for TOML config files ──────────────────────
|
|
|
|
fn config_load_from_toml(path: String, env_name: String) -> Config {
|
|
let cfg: Config = config_new(env_name)
|
|
let raw: String = fs_read(path)
|
|
let map: String = toml_to_json_flat(raw) // dotted keys
|
|
let cfg = config_add_source(cfg, source_defaults(map))
|
|
cfg
|
|
}
|
|
|
|
// ── .env parser (minimal) ───────────────────────────────────────────────────
|
|
|
|
fn dotenv_parse(raw: String) -> String {
|
|
let map: String = "{}"
|
|
let lines: String = str_split(raw, "\n")
|
|
let n: Int = json_array_len(lines)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let line: String = str_trim(json_array_get(lines, i))
|
|
if str_eq(line, "") {
|
|
let i = i + 1
|
|
}
|
|
if !str_eq(line, "") {
|
|
if !str_starts_with(line, "#") {
|
|
let eq: Int = str_index_of(line, "=")
|
|
if eq > 0 {
|
|
let k: String = str_trim(str_slice(line, 0, eq))
|
|
let v: String = str_trim(str_slice(line, eq + 1, str_len(line)))
|
|
let v = str_strip_quotes(v)
|
|
let map = json_set(map, str_to_lower(str_replace(k, "_", ".")), "\"" + v + "\"")
|
|
}
|
|
let i = i + 1
|
|
}
|
|
}
|
|
}
|
|
map
|
|
}
|
|
|
|
// ── Entry — smoke test ──────────────────────────────────────────────────────
|
|
|
|
let cfg: Config = config_new(env_current())
|
|
let defaults: String = "{\"app.name\":\"MyApp\",\"server.port\":\"8080\"}"
|
|
let cfg = config_set_defaults(cfg, defaults)
|
|
println("[el-config] env=" + cfg.environment + " app=" + config_get_string(cfg, "app.name"))
|