import std/[json, nre, os, sequtils, strtabs, strutils] import docopt type CombinedConfig* = object docopt*: Table[string, Value] json*: JsonNode 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 = "--" & key let envKey = key.replace('-', '_').toUpper let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper) 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 = "--" & key let envKey = key.replace('-', '_').toUpper let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper) 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 loadEnv*(): StringTableRef = result = newStringTable() for k, v in envPairs(): result[k] = v