nim-cli-utils/cliutils/config.nim

122 lines
3.9 KiB
Nim

import std/[json, nre, os, sequtils, strtabs, strutils]
import docopt
type
CombinedConfig* = object
docopt*: Table[string, Value]
json*: JsonNode
proc keyNames(key: string): tuple[arg, env, json: string] {.raises: [KeyError].} =
try:
result = (
"--" & key,
key.replace('-', '_').toUpper,
key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper))
except Exception:
raise newException(KeyError, "invalid config key: '" & key & "'")
template walkFieldDefs*(t: NimNode, body: untyped) =
let tTypeImpl = t.getTypeImpl
var nodeToItr: NimNode
if tTypeImpl.typeKind == ntyObject: nodeToItr = tTypeImpl[2]
elif tTypeImpl.typeKind == ntyTypeDesc: nodeToItr = tTypeImpl.getType[1].getType[2]
else: error $t & " is not an object or type desc (it's a " & $tTypeImpl.typeKind & ")."
for fieldDef {.inject.} in nodeToItr.children:
# ignore AST nodes that are not field definitions
if fieldDef.kind == nnkIdentDefs:
let fieldIdent {.inject.} = fieldDef[0]
let fieldType {.inject.} = fieldDef[1]
body
elif fieldDef.kind == nnkSym:
let fieldIdent {.inject.} = fieldDef
let fieldType {.inject.} = fieldDef.getType
body
# TODO: Generic config loader
# macro loadConfig*(filePath: string, cfgType: typed, args: Table[string, docopt.Value): untyped =
# result = newNimNode(nnkObjConstr).(cfgType)
#
# var idx = 0
# cfgType.walkFieldDefs:
# let valLookup = quote do: `getVal`(
proc initCombinedConfig*(
filename: string,
docopt: Table[string, Value] = initTable[string, Value]()
): CombinedConfig =
result = CombinedConfig(docopt: docopt, json: parseFile(filename))
proc findConfigFile*(name: string, otherLocations: seq[string] = @[]): string =
let cfgLocations = @[
$getEnv(name.strip(chars = {'.'}).toUpper()),
$getEnv("HOME") / name,
"." / name ] & otherLocations
result = cfgLocations.foldl(if fileExists(b): b else: a, "")
if result == "" or not fileExists(result):
raise newException(ValueError, "could not find configuration file")
proc getVal*(cfg: CombinedConfig, key: string): string =
let (argKey, envKey, jsonKey) = keyNames(key)
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return $cfg.docopt[argKey]
elif existsEnv(envKey): return getEnv(envKey)
elif cfg.json.hasKey(jsonKey):
let node = cfg.json[jsonKey]
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 & "\"")
proc getVal*(cfg: CombinedConfig, key, default: string): string {.raises: [].} =
try: return getVal(cfg, key)
except CatchableError: return default
proc getJson*(
cfg: CombinedConfig,
key: string
): JsonNode {.raises: [KeyError, ValueError, IOError, OSError]} =
let (argKey, envKey, jsonKey) = keyNames(key)
try:
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return parseJson($cfg.docopt[argKey])
elif existsEnv(envKey): return parseJson(getEnv(envKey))
elif cfg.json.hasKey(jsonKey): return cfg.json[jsonKey]
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
except Exception:
raise newException(ValueError, "cannot parse value as JSON:" & getCurrentExceptionMsg())
proc getJson*(
cfg: CombinedConfig,
key: string,
default: JsonNode
) : JsonNode {.raises: []} =
try: return getJson(cfg, key)
except CatchableError: return default
proc hasKey*(cfg: CombinedConfig, key: string): bool =
let (argKey, envKey, jsonKey) = keyNames(key)
return
(cfg.docopt.contains(argKey) and cfg.docopt[argKey]) or
existsEnv(envKey) or
cfg.json.hasKey(jsonKey)
proc loadEnv*(): StringTableRef =
result = newStringTable()
for k, v in envPairs():
result[k] = v