Files
el/ui/vessels/el-config/src/main.el
T

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"))