2022-06-22 09:12:59 +02:00
module conf
2022-06-15 12:40:35 +02:00
import os
import toml
2022-12-28 17:39:20 +01:00
import datatypes { Set }
2022-06-15 12:40:35 +02:00
2022-12-28 18:13:20 +01:00
[ params ]
pub struct LoadConfig {
prefix string
file_suffix string = ' _ FILE'
default_path string
// Allows overwriting
env map [ string ] string = os . environ ()
}
2022-06-15 12:40:35 +02:00
// 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
2022-12-28 18:36:58 +01:00
// 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 ) {
2023-02-08 10:40:37 +01:00
env_var_name := ' $ { ld . prefix } $ { field_name . to_upper () } '
env_file_name := ' $ { ld . prefix } $ { field_name . to_upper () } $ { ld . file_suffix } '
2022-06-15 12:40:35 +02:00
2022-12-28 18:36:58 +01:00
if env_var_name ! in ld . env && env_file_name ! in ld . env {
return false , ' '
2022-06-15 12:40:35 +02:00
}
// If they're both set , we report a conflict
2022-12-28 18:36:58 +01:00
if env_var_name in ld . env && env_file_name in ld . env {
2023-02-08 10:40:37 +01:00
return error ( ' Only one of $ { env_var_name } or $ { env_file_name } can be defined . ' )
2022-06-15 12:40:35 +02:00
}
// 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 .
2022-12-28 18:36:58 +01:00
if env_var_name in ld . env {
return true , ld . env [ env_var_name ]
2022-06-15 12:40:35 +02:00
}
// Otherwise , we process the file
2022-12-28 18:36:58 +01:00
return true , os . read_file ( ld . env [ env_file_name ] ) or {
2023-02-08 10:40:37 +01:00
error ( ' Failed to read file defined in $ { env_file_name } : $ { err . msg () } . ' )
2022-06-15 12:40:35 +02:00
}
}
// 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 .
2023-02-08 10:40:37 +01:00
pub fn load [ T ] ( ld LoadConfig ) ! T {
2022-12-28 19:43:10 +01:00
// Ensure all struct fields consist of supported types
$ for field in T . fields {
2022-12-28 20:04:45 +01:00
$ if field . typ is string | | field . typ is int | | field . typ is bool {
2022-12-28 19:43:10 +01:00
} $ 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 .
2023-02-08 10:40:37 +01:00
return error ( ' Field $ { field . name } is of an unsupported type . ' )
2022-12-28 19:43:10 +01:00
}
}
2022-06-15 12:40:35 +02:00
mut res := T { }
2022-12-28 16:42:19 +01:00
// This array allows us to determine later whether the variable is actually
// zero or just a null'ed struct field
2023-02-08 10:40:37 +01:00
mut has_value := Set [ string ] { }
2022-12-28 16:42:19 +01:00
// Later , this could be read from an env var as well .
2022-12-28 18:13:20 +01:00
path := ld . default_path
2022-06-15 12:40:35 +02:00
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
2022-11-01 20:57:04 +01:00
doc := toml . parse_file ( path ) !
2022-06-15 12:40:35 +02:00
$ 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 ()
2022-12-28 20:04:45 +01:00
} $ else $ if field . typ is bool {
res . $ ( field . name ) = s . bool ()
2022-06-15 12:40:35 +02:00
}
2022-12-28 16:42:19 +01:00
2022-12-28 17:39:20 +01:00
has_value . add ( field . name )
2022-06-15 12:40:35 +02:00
}
}
}
$ for field in T . fields {
2022-12-28 18:36:58 +01:00
env_present , env_value := ld . get_env_var ( field . name ) !
2022-06-15 12:40:35 +02:00
// The value of an env var will always take precedence over the toml
// file .
2022-12-28 18:36:58 +01:00
if env_present {
2022-06-15 12:40:35 +02:00
$ if field . typ is string {
res . $ ( field . name ) = env_value
} $ else $ if field . typ is int {
res . $ ( field . name ) = env_value . int ()
2022-12-28 20:04:45 +01:00
} $ 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 ' ]
2022-06-15 12:40:35 +02:00
}
2022-12-28 17:39:20 +01:00
has_value . add ( field . name )
2022-06-15 12:40:35 +02:00
}
2022-12-28 17:39:20 +01:00
// Finally , if there's no env var present either , we check whether the
2022-12-28 18:50:12 +01:00
// 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 ) {
2022-12-28 17:39:20 +01:00
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
2022-12-28 20:04:45 +01:00
} $ 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
2022-12-28 17:39:20 +01:00
}
if has_default {
has_value . add ( field . name )
}
}
2022-12-28 18:13:20 +01:00
// If there's no value provided in any way , we notify the user with an
// error .
2022-12-28 17:39:20 +01:00
if ! has_value . exists ( field . name ) {
2023-02-08 10:40:37 +01:00
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. " )
2022-06-15 12:40:35 +02:00
}
}
return res
}