|
|
|
@@ -1,19 +1,56 @@
|
|
|
|
import std/[json, nre, os, sequtils, strtabs, strutils]
|
|
|
|
import std/[json, options, os, sequtils, strtabs, strutils]
|
|
|
|
import docopt
|
|
|
|
import docopt, identcasing, regex
|
|
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
type
|
|
|
|
CombinedConfig* = object
|
|
|
|
CombinedConfig* = object
|
|
|
|
docopt*: Table[string, Value]
|
|
|
|
docopt*: Table[string, Value]
|
|
|
|
json*: JsonNode
|
|
|
|
json*: JsonNode
|
|
|
|
|
|
|
|
|
|
|
|
proc keyNames(key: string): tuple[arg, env, json: string] {.raises: [KeyError].} =
|
|
|
|
ValueProvenance* {.pure.} = enum arguments, environment, json, default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const argStyleRegex = re2"(--)?[[:lower:][:digit:]][[:lower:][:digit:]\-]+"
|
|
|
|
|
|
|
|
const envStyleRegex = re2"[[:upper:][:digit:]_]+"
|
|
|
|
|
|
|
|
const jsonStyleRegex = re2"[[:lower:]][[:lower:][:upper:][:digit:]]+"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc keyNames*(key: string): tuple[arg, env, json: string]
|
|
|
|
|
|
|
|
{.raises: [KeyError].} =
|
|
|
|
|
|
|
|
## CombinedConfig tries to be idiomatic in its usage of key names. For
|
|
|
|
|
|
|
|
## environment variables it uses `UPPER_SNAKE_CASE`-style naming. For
|
|
|
|
|
|
|
|
## arguments it uses `lower-kebab-case`, and for JSON it uses
|
|
|
|
|
|
|
|
## `lowerCamelCase`.
|
|
|
|
|
|
|
|
##
|
|
|
|
|
|
|
|
## *keyNames* takes a key in any one of the three styles and returns all
|
|
|
|
|
|
|
|
## three. This is primarily a helper for other parsing functions, but is
|
|
|
|
|
|
|
|
## exposed as it is sometimes useful.
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
result = (
|
|
|
|
if match(key, argStyleRegex):
|
|
|
|
"--" & key,
|
|
|
|
let unprefixed =
|
|
|
|
key.replace('-', '_').toUpper,
|
|
|
|
if key.startsWith("--"): key[2..^1]
|
|
|
|
key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper))
|
|
|
|
else: key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
"--" & unprefixed,
|
|
|
|
|
|
|
|
convertCase(unprefixed, lowerKebabCase, upperSnakeCase),
|
|
|
|
|
|
|
|
convertCase(unprefixed, lowerKebabCase, lowerCamelCase))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif match(key, envStyleRegex):
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
"--" & convertCase(key, upperSnakeCase, lowerKebabCase),
|
|
|
|
|
|
|
|
key,
|
|
|
|
|
|
|
|
convertCase(key, upperSnakeCase, lowerCamelCase))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif match(key, jsonStyleRegex):
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
"--" & convertCase(key, lowerCamelCase, lowerKebabCase),
|
|
|
|
|
|
|
|
convertCase(key, lowerCamelCase, upperSnakeCase),
|
|
|
|
|
|
|
|
key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else: raise newException(KeyError,
|
|
|
|
|
|
|
|
"key style must be --argument-style, ENV_STYLE, or jsonStyle")
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
raise newException(KeyError, "invalid config key: '" & key & "'")
|
|
|
|
raise newException(KeyError, "invalid config key '" & key & "': " &
|
|
|
|
|
|
|
|
getCurrentExceptionMsg())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
template walkFieldDefs*(t: NimNode, body: untyped) =
|
|
|
|
template walkFieldDefs*(t: NimNode, body: untyped) =
|
|
|
|
let tTypeImpl = t.getTypeImpl
|
|
|
|
let tTypeImpl = t.getTypeImpl
|
|
|
|
@@ -51,6 +88,7 @@ proc initCombinedConfig*(
|
|
|
|
|
|
|
|
|
|
|
|
result = CombinedConfig(docopt: docopt, json: parseFile(filename))
|
|
|
|
result = CombinedConfig(docopt: docopt, json: parseFile(filename))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc findConfigFile*(name: string, otherLocations: seq[string] = @[]): string =
|
|
|
|
proc findConfigFile*(name: string, otherLocations: seq[string] = @[]): string =
|
|
|
|
let cfgLocations = @[
|
|
|
|
let cfgLocations = @[
|
|
|
|
$getEnv(name.strip(chars = {'.'}).toUpper()),
|
|
|
|
$getEnv(name.strip(chars = {'.'}).toUpper()),
|
|
|
|
@@ -61,42 +99,105 @@ proc findConfigFile*(name: string, otherLocations: seq[string] = @[]): string =
|
|
|
|
if result == "" or not fileExists(result):
|
|
|
|
if result == "" or not fileExists(result):
|
|
|
|
raise newException(ValueError, "could not find configuration file")
|
|
|
|
raise newException(ValueError, "could not find configuration file")
|
|
|
|
|
|
|
|
|
|
|
|
proc getVal*(cfg: CombinedConfig, key: string): string =
|
|
|
|
|
|
|
|
|
|
|
|
proc jsonConfigNodeToString(node: JsonNode): string =
|
|
|
|
|
|
|
|
case node.kind
|
|
|
|
|
|
|
|
of JString: node.getStr
|
|
|
|
|
|
|
|
of JInt: $node.getInt
|
|
|
|
|
|
|
|
of JFloat: $node.getFloat
|
|
|
|
|
|
|
|
of JBool: $node.getBool
|
|
|
|
|
|
|
|
of JNull: ""
|
|
|
|
|
|
|
|
of JObject: $node
|
|
|
|
|
|
|
|
of JArray: $node
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getValueAndProvenance*(
|
|
|
|
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
|
|
|
|
key: string): tuple[value: string, provenance: ValueProvenance]
|
|
|
|
|
|
|
|
{. raises: [ValueError] .} =
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
|
|
|
|
|
|
|
|
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return $cfg.docopt[argKey]
|
|
|
|
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]:
|
|
|
|
elif existsEnv(envKey): return getEnv(envKey)
|
|
|
|
return ($cfg.docopt[argKey], ValueProvenance.arguments)
|
|
|
|
|
|
|
|
elif existsEnv(envKey):
|
|
|
|
|
|
|
|
return (getEnv(envKey), ValueProvenance.environment)
|
|
|
|
elif cfg.json.hasKey(jsonKey):
|
|
|
|
elif cfg.json.hasKey(jsonKey):
|
|
|
|
let node = cfg.json[jsonKey]
|
|
|
|
return (jsonConfigNodeToString(cfg.json[jsonKey]), ValueProvenance.json)
|
|
|
|
case node.kind
|
|
|
|
|
|
|
|
of JString: return node.getStr
|
|
|
|
|
|
|
|
of JInt: return $node.getInt
|
|
|
|
|
|
|
|
of JFloat: return $node.getFloat
|
|
|
|
|
|
|
|
of JBool: return $node.getBool
|
|
|
|
|
|
|
|
of JNull: return ""
|
|
|
|
|
|
|
|
of JObject: return $node
|
|
|
|
|
|
|
|
of JArray: return $node
|
|
|
|
|
|
|
|
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
|
|
|
|
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getValueAndProvenance*(
|
|
|
|
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
|
|
|
|
key: string,
|
|
|
|
|
|
|
|
default: string): tuple[value: string, provenance: ValueProvenance]
|
|
|
|
|
|
|
|
{.raises: [].} =
|
|
|
|
|
|
|
|
try: return getValueAndProvenance(cfg, key)
|
|
|
|
|
|
|
|
except CatchableError: return (default, ValueProvenance.default)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getVal*(cfg: CombinedConfig, key: string): string =
|
|
|
|
|
|
|
|
getValueAndProvenance(cfg, key).value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getVal*(cfg: CombinedConfig, key, default: string): string {.raises: [].} =
|
|
|
|
proc getVal*(cfg: CombinedConfig, key, default: string): string {.raises: [].} =
|
|
|
|
try: return getVal(cfg, key)
|
|
|
|
try: return getVal(cfg, key)
|
|
|
|
except CatchableError: return default
|
|
|
|
except CatchableError: return default
|
|
|
|
|
|
|
|
|
|
|
|
proc getJson*(
|
|
|
|
|
|
|
|
|
|
|
|
proc `[]`*(cfg: CombinedConfig, key: string): string = getVal(cfg, key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getValueFromSource*(
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
key: string
|
|
|
|
source: ValueProvenance,
|
|
|
|
): JsonNode {.raises: [KeyError, ValueError, IOError, OSError]} =
|
|
|
|
key: string): Option[string] =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = none[string]()
|
|
|
|
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
|
|
|
|
if source == ValueProvenance.arguments and
|
|
|
|
|
|
|
|
cfg.docopt.contains(argKey) and cfg.docopt[argKey]:
|
|
|
|
|
|
|
|
result = some($cfg.docopt[argKey])
|
|
|
|
|
|
|
|
elif source == ValueProvenance.environment and existsEnv(envKey):
|
|
|
|
|
|
|
|
result = some(getEnv(envKey))
|
|
|
|
|
|
|
|
elif source == ValueProvenance.json and cfg.json.hasKey(jsonKey):
|
|
|
|
|
|
|
|
result = some(jsonConfigNodeToString(cfg.json[jsonKey]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getJsonValueAndProvenance*(
|
|
|
|
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
|
|
|
|
key: string): tuple[value: JsonNode, provenance: ValueProvenance]
|
|
|
|
|
|
|
|
{.raises: [KeyError, ValueError, IOError, OSError]} =
|
|
|
|
|
|
|
|
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return parseJson($cfg.docopt[argKey])
|
|
|
|
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]:
|
|
|
|
elif existsEnv(envKey): return parseJson(getEnv(envKey))
|
|
|
|
return (parseJson($cfg.docopt[argKey]), ValueProvenance.arguments)
|
|
|
|
elif cfg.json.hasKey(jsonKey): return cfg.json[jsonKey]
|
|
|
|
elif existsEnv(envKey):
|
|
|
|
|
|
|
|
return (parseJson(getEnv(envKey)), ValueProvenance.environment)
|
|
|
|
|
|
|
|
elif cfg.json.hasKey(jsonKey):
|
|
|
|
|
|
|
|
return (cfg.json[jsonKey], ValueProvenance.json)
|
|
|
|
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
|
|
|
|
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
raise newException(ValueError, "cannot parse [" & getVal(cfg, key) & "] as JSON:" & getCurrentExceptionMsg())
|
|
|
|
raise newException(ValueError, "cannot parse [" & getVal(cfg, key) & "] as JSON:" & getCurrentExceptionMsg())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getJsonValueAndProvenance*(
|
|
|
|
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
|
|
|
|
key: string,
|
|
|
|
|
|
|
|
default: JsonNode): tuple[value: JsonNode, provenance: ValueProvenance]
|
|
|
|
|
|
|
|
{.raises: [].} =
|
|
|
|
|
|
|
|
try: return getJsonValueAndProvenance(cfg, key)
|
|
|
|
|
|
|
|
except CatchableError: return (default, ValueProvenance.default)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getJson*(
|
|
|
|
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
|
|
|
|
key: string
|
|
|
|
|
|
|
|
): JsonNode {.raises: [KeyError, ValueError, IOError, OSError]} =
|
|
|
|
|
|
|
|
getJsonValueAndProvenance(cfg, key).value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getJson*(
|
|
|
|
proc getJson*(
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
key: string,
|
|
|
|
key: string,
|
|
|
|
@@ -106,6 +207,23 @@ proc getJson*(
|
|
|
|
try: return getJson(cfg, key)
|
|
|
|
try: return getJson(cfg, key)
|
|
|
|
except CatchableError: return default
|
|
|
|
except CatchableError: return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc getJsonFromSource*(
|
|
|
|
|
|
|
|
cfg: CombinedConfig,
|
|
|
|
|
|
|
|
source: ValueProvenance,
|
|
|
|
|
|
|
|
key: string): Option[JsonNode] =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = none[JsonNode]()
|
|
|
|
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
|
|
|
|
if source == ValueProvenance.arguments and
|
|
|
|
|
|
|
|
cfg.docopt.contains(argKey) and cfg.docopt[argKey]:
|
|
|
|
|
|
|
|
result = some(parseJson($cfg.docopt[argKey]))
|
|
|
|
|
|
|
|
elif source == ValueProvenance.environment and existsEnv(envKey):
|
|
|
|
|
|
|
|
result = some(parseJson(getEnv(envKey)))
|
|
|
|
|
|
|
|
elif source == ValueProvenance.json and cfg.json.hasKey(jsonKey):
|
|
|
|
|
|
|
|
result = some(cfg.json[jsonKey])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc hasKey*(cfg: CombinedConfig, key: string): bool =
|
|
|
|
proc hasKey*(cfg: CombinedConfig, key: string): bool =
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
let (argKey, envKey, jsonKey) = keyNames(key)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -114,6 +232,10 @@ proc hasKey*(cfg: CombinedConfig, key: string): bool =
|
|
|
|
existsEnv(envKey) or
|
|
|
|
existsEnv(envKey) or
|
|
|
|
cfg.json.hasKey(jsonKey)
|
|
|
|
cfg.json.hasKey(jsonKey)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc contains*(cfg: CombinedConfig, key: string): bool = hasKey(cfg, key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc loadEnv*(): StringTableRef =
|
|
|
|
proc loadEnv*(): StringTableRef =
|
|
|
|
result = newStringTable()
|
|
|
|
result = newStringTable()
|
|
|
|
|
|
|
|
|
|
|
|
|