nim-cli-utils/cliutils/config.nim

106 lines
3.5 KiB
Nim

import std/[json, nre, os, sequtils, strtabs, strutils]
import docopt
type
CombinedConfig* = object
docopt*: Table[string, Value]
json*: JsonNode
func keyNames(key: string): tuple[arg, env, json: string] =
result = (
"--" & key,
key.replace('-', '_').toUpper,
key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper))
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 ] & 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 =
try: return getVal(cfg, key)
except: return default
proc getJson*(cfg: CombinedConfig, key: string): JsonNode =
let (argKey, envKey, jsonKey) = keyNames(key)
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 & "\"")
proc getJson*(cfg: CombinedConfig, key: string, default: JsonNode): JsonNode =
try: return getJson(cfg, key)
except: 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