feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
// 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"))
|
||||
Reference in New Issue
Block a user