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 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 }