conf/conf.v

146 lines
4.6 KiB
V

module conf
import os
import toml
import datatypes { Set }
[params]
pub struct LoadConfig {
prefix string
file_suffix string = '_FILE'
default_path string
// Allows overwriting
env map[string]string = os.environ()
}
// get_env_var tries to read the contents of the given environment variable. It
// looks for either `${env.prefix}${field_name.to_upper()}` or
// `${env.prefix}${field_name.to_upper()}${env.file_suffix}`, returning the
// contents of the file instead if the latter. If both or neither exist, the
// function returns an error. It returns two values, with the first indicating
// whether the env vars were actually present.
fn (ld LoadConfig) get_env_var(field_name string) !(bool, string) {
env_var_name := '${ld.prefix}${field_name.to_upper()}'
env_file_name := '${ld.prefix}${field_name.to_upper()}${ld.file_suffix}'
if env_var_name !in ld.env && env_file_name !in ld.env {
return false, ''
}
// If they're both set, we report a conflict
if env_var_name in ld.env && env_file_name in ld.env {
return error('Only one of ${env_var_name} or ${env_file_name} can be defined.')
}
// If it's the env var itself, we return it.
// I'm pretty sure this also prevents variable ending in _FILE (e.g.
// VIETER_LOG_FILE) from being mistakingely read as an _FILE suffixed env
// var.
if env_var_name in ld.env {
return true, ld.env[env_var_name]
}
// Otherwise, we process the file
return true, os.read_file(ld.env[env_file_name]) or {
error('Failed to read file defined in ${env_file_name}: ${err.msg()}.')
}
}
// load<T> attempts to create an object of type T from the given path to a toml
// file & environment variables. For each field, it will select either a value
// given from an environment variable, a value defined in the config file or a
// configured default if present, in that order.
pub fn load[T](ld LoadConfig) !T {
// Ensure all struct fields consist of supported types
$for field in T.fields {
$if field.typ is string || field.typ is int || field.typ is bool {
} $else {
// I'd prefer changing this to $compile_error, but as of V 0.3.2,
// this seems to be bugged. If I replace this call with a
// $compile_error call, the error *always* happens, even if all
// fields are correct.
return error('Field ${field.name} is of an unsupported type.')
}
}
mut res := T{}
// This array allows us to determine later whether the variable is actually
// zero or just a null'ed struct field
mut has_value := Set[string]{}
// Later, this could be read from an env var as well.
path := ld.default_path
if os.exists(path) {
// We don't use reflect here because reflect also sets any fields not
// in the toml back to their zero value, which we don't want
doc := toml.parse_file(path)!
$for field in T.fields {
s := doc.value(field.name)
if s !is toml.Null {
$if field.typ is string {
res.$(field.name) = s.string()
} $else $if field.typ is int {
res.$(field.name) = s.int()
} $else $if field.typ is bool {
res.$(field.name) = s.bool()
}
has_value.add(field.name)
}
}
}
$for field in T.fields {
env_present, env_value := ld.get_env_var(field.name)!
// The value of an env var will always take precedence over the toml
// file.
if env_present {
$if field.typ is string {
res.$(field.name) = env_value
} $else $if field.typ is int {
res.$(field.name) = env_value.int()
} $else $if field.typ is bool {
// Env var accepts '1' and 'true' as truthy, everything else
// evaluates to false
res.$(field.name) = env_value in ['1', 'true']
}
has_value.add(field.name)
}
// Finally, if there's no env var present either, we check whether the
// variable has a default value. Variables defined with an "empty
// default" will always be marked as containing a value.
if 'empty_default' in field.attrs {
has_value.add(field.name)
} else if !has_value.exists(field.name) {
mut has_default := false
$if field.typ is string {
has_default = res.$(field.name) != ''
} $else $if field.typ is int {
has_default = res.$(field.name) != 0
} $else $if field.typ is bool {
// This explicit comparison is required as the type system gets
// a bit confused otherwise
has_default = res.$(field.name) == true
}
if has_default {
has_value.add(field.name)
}
}
// If there's no value provided in any way, we notify the user with an
// error.
if !has_value.exists(field.name) {
return error("Missing config variable '${field.name}' with no provided default. Either add it to the config file or provide it using an environment variable.")
}
}
return res
}