config file integration into confutils

This commit is contained in:
jangko 2020-10-31 17:44:03 +07:00 committed by zah
parent 085d52d3ad
commit f3a048f9ea
8 changed files with 433 additions and 21 deletions

View File

@ -1,10 +1,10 @@
import
std/[options, strutils, wordwrap],
stew/shims/macros,
confutils/[defs, cli_parser]
confutils/[defs, cli_parser, config_file]
export
defs
defs, config_file
const
useBufferedOutput = defined(nimscript)
@ -750,8 +750,15 @@ proc load*(Configuration: type,
# This is an initial naive implementation that will be improved
# over time.
# users can override default `appendConfigFileFormats`
# `appName`, and `vendorName`
mixin appendConfigFileFormats
mixin appName, vendorName
appendConfigFileFormats(Configuration)
let (rootCmd, fieldSetters) = configurationRtti(Configuration)
var fieldCounters: array[fieldSetters.len, int]
let configFile = configFile(Configuration)
printCmdTree rootCmd
@ -798,6 +805,10 @@ proc load*(Configuration: type,
if fieldCounters[opt.idx] == 0:
if opt.required:
fail "The required option '$1' was not specified" % [opt.name]
elif configFile.setters[opt.idx](conf, configFile):
# all work is done in the config file setter,
# there is nothing left to do here.
discard
elif opt.hasDefault:
fieldSetters[opt.idx][1](conf, none[TaintedString]())

208
confutils/config_file.nim Normal file
View File

@ -0,0 +1,208 @@
import
std/[macrocache, typetraits],
stew/shims/macros,
./defs
#[
Overview of this module:
- Create temporary configuration object with all fields optional.
- Load this temporary object from every registered config files
including env vars and windows regs if available.
- If the CLI parser detect missing opt, it will try to obtain
the value from temporary object starting from the first registered
config file format.
- If none of them have the missing value, it will load the default value
from `defaultValue` pragma.
]#
const
configFileRegs = CacheSeq"confutils"
func isOption(n: NimNode): bool =
if n.kind != nnkBracketExpr: return false
eqIdent(n[0], "Option")
func makeOption(n: NimNode): NimNode =
newNimNode(nnkBracketExpr).add(ident("Option"), n)
template objectDecl(a): untyped =
type a = object
proc putReclist(n: NimNode, recList: NimNode) =
recList.expectKind nnkRecList
if n.kind == nnkObjectTy:
n[2] = recList
return
for z in n:
putReclist(z, recList)
proc generateOptionalField(fieldName: NimNode, fieldType: NimNode): NimNode =
let right = if isOption(fieldType): fieldType else: makeOption(fieldType)
newIdentDefs(fieldName, right)
proc optionalizeFields(CF, confType: NimNode): NimNode =
# Generate temporary object type where
# all fields are optional.
result = getAst(objectDecl(CF))
var recList = newNimNode(nnkRecList)
var recordDef = getImpl(confType)
for field in recordFields(recordDef):
if field.readPragma"hidden" != nil or
field.readPragma"command" != nil or
field.readPragma"argument" != nil:
continue
recList.add generateOptionalField(field.name, field.typ)
result.putRecList(recList)
proc genLoader(i: int, format, ext, path, optType, confType: NimNode): NimNode =
var pathBlock: NimNode
if eqIdent(format, "Envvar"):
pathBlock = quote do:
block:
`path`
elif eqIdent(format, "Winreg"):
pathBlock = quote do:
block:
`path` / vendorName(`confType`) / appName(`confType`)
else:
# toml, json, yaml, etc
pathBlock = quote do:
block:
`path` / vendorName(`confType`) / appName(`confType`) & "." & `ext`
result = quote do:
let fullPath = `pathBlock`
try:
result.data[`i`] = `format`.loadFile(fullPath, `optType`)
except:
echo "Error when loading: ", fullPath
echo getCurrentExceptionMsg()
proc generateSetters(optType, confType, CF: NimNode): (NimNode, NimNode, int) =
var
procs = newStmtList()
assignments = newStmtList()
recordDef = getImpl(confType)
numSetters = 0
procs.add quote do:
template cfSetter(a, b: untyped): untyped =
when a is Option:
a = b
else:
a = b.get()
for field in recordFields(recordDef):
if field.readPragma"hidden" != nil:
continue
if field.readPragma"command" != nil or
field.readPragma"argument" != nil:
assignments.add quote do:
result.setters[`numSetters`] = defaultConfigFileSetter
inc numSetters
continue
let setterName = ident($field.name & "CFSetter")
let fieldName = field.name
procs.add quote do:
proc `setterName`(s: var `confType`, cf: `CF`): bool {.
nimcall, gcsafe .} =
for c in cf.data:
if c.`fieldName`.isSome():
cfSetter(s.`fieldName`, c.`fieldName`)
return true
assignments.add quote do:
result.setters[`numSetters`] = `setterName`
inc numSetters
result = (procs, assignments, numSetters)
proc generateConfigFileSetters(optType, CF, confType: NimNode): NimNode =
let T = confType.getType[1]
let arrayLen = configFileRegs.len
let settersType = genSym(nskType, "SettersType")
var loaderStmts = newStmtList()
for i in 0..<arrayLen:
let n = configFileRegs[i]
let loader = genLoader(i, n[0], n[1], n[2], optType, confType)
loaderStmts.add quote do: `loader`
let (procs, assignments, numSetters) = generateSetters(optType, T, CF)
result = quote do:
type
`settersType` = proc(s: var `T`, cf: `CF`): bool {.
nimcall, gcsafe .}
`CF` = object
data: array[`arrayLen`, `optType`]
setters: array[`numSetters`, `settersType`]
proc defaultConfigFileSetter(s: var `T`, cf: `CF`): bool {.
nimcall, gcsafe .} = discard
`procs`
proc load(_: type `CF`): `CF` =
`loaderStmts`
`assignments`
load(`CF`)
macro configFile*(confType: type): untyped =
let T = confType.getType[1]
let Opt = genSym(nskType, "OptionalFields")
let CF = genSym(nskType, "ConfigFile")
result = newStmtList()
result.add optionalizeFields(Opt, T)
result.add generateConfigFileSetters(Opt, CF, confType)
macro appendConfigFileFormat*(ConfigFileFormat: type, configExt: string, configPath: untyped): untyped =
configFileRegs.add newPar(ConfigFileFormat, configExt, configPath)
func appName*(_: type): string =
# this proc is overrideable
when false:
splitFile(os.getAppFilename()).name
"confutils"
func vendorName*(_: type): string =
# this proc is overrideable
"confutils"
func appendConfigFileFormats*(_: type) =
# this proc is overrideable
when false:
# this is a typical example of
# config file format registration
appendConfigFileFormat(Envvar, ""):
"prefix"
when defined(windows):
appendConfigFileFormat(Winreg, ""):
"HKCU" / "SOFTWARE"
appendConfigFileFormat(Winreg, ""):
"HKLM" / "SOFTWARE"
appendConfigFileFormat(Toml, "toml"):
os.getHomeDir() & ".config"
appendConfigFileFormat(Toml, "toml"):
splitFile(os.getAppFilename()).dir
elif defined(posix):
appendConfigFileFormat(Toml, "toml"):
os.getHomeDir() & ".config"
appendConfigFileFormat(Toml, "toml"):
"/etc"

View File

@ -1,9 +1,9 @@
import
stew/shims/macros,
serialization, ./reader, ./writer, ./utils
serialization, ./reader, ./writer
export
serialization, reader, writer, utils
serialization, reader, writer
serializationFormat Envvar,
Reader = EnvvarReader,

View File

@ -29,8 +29,7 @@ proc handleReadException*(r: EnvvarReader,
proc init*(T: type EnvvarReader, prefix: string): T =
result.prefix = prefix
proc readValue*[T](r: var EnvvarReader, value: var T)
{.raises: [SerializationError, ValueError, Defect].} =
proc readValue*[T](r: var EnvvarReader, value: var T) =
mixin readValue
# TODO: reduce allocation
@ -46,10 +45,10 @@ proc readValue*[T](r: var EnvvarReader, value: var T)
template getUnderlyingType[T](_: Option[T]): untyped = T
let key = constructKey(r.prefix, r.key)
if existsEnv(key):
type uType = getUnderlyingType(value)
type uType = getUnderlyingType(value)
when uType is string:
value = some(os.getEnv(key))
else:
else:
value = some(r.readValue(uType))
elif T is (seq or array):
@ -57,16 +56,12 @@ proc readValue*[T](r: var EnvvarReader, value: var T)
let key = constructKey(r.prefix, r.key)
getValue(key, value)
elif uTypeIsRecord(T):
else:
let key = r.key[^1]
for i in 0..<value.len:
r.key[^1] = key & $i
r.readValue(value[i])
else:
const typeName = typetraits.name(T)
{.fatal: "Failed to convert from Envvar array an unsupported type: " & typeName.}
elif T is (object or tuple):
type T = type(value)
when T.totalSerializedFields > 0:

View File

@ -37,27 +37,27 @@ proc readValue*[T](r: var WinregReader, value: var T) =
when T is (SomePrimitives or range or string):
let path = constructPath(r.path, r.key)
discard getValue(r.hKey, path, r.key[^1], value)
elif T is Option:
template getUnderlyingType[T](_: Option[T]): untyped = T
type UT = getUnderlyingType(value)
let path = constructPath(r.path, r.key)
if pathExists(r.hKey, path, r.key[^1]):
value = some(r.readValue(UT))
elif T is (seq or array):
when uTypeIsPrimitives(T):
let path = constructPath(r.path, r.key)
discard getValue(r.hKey, path, r.key[^1], value)
elif uTypeIsRecord(T):
else:
let key = r.key[^1]
for i in 0..<value.len:
r.key[^1] = key & $i
r.readValue(value[i])
else:
const typeName = typetraits.name(T)
{.fatal: "Failed to convert from Winreg array an unsupported type: " & typeName.}
elif T is (object or tuple):
type T = type(value)
when T.totalSerializedFields > 0:
let fields = T.fieldReadersTable(WinregReader)
var expectedFieldPos = 0
@ -67,6 +67,7 @@ proc readValue*[T](r: var WinregReader, value: var T) =
r.key[^1] = $expectedFieldPos
var reader = fields[][expectedFieldPos].reader
expectedFieldPos += 1
else:
r.key[^1] = fieldName
var reader = findFieldReader(fields[], fieldName, expectedFieldPos)
@ -74,6 +75,7 @@ proc readValue*[T](r: var WinregReader, value: var T) =
if reader != nil:
reader(value, r)
discard r.key.pop()
else:
const typeName = typetraits.name(T)
{.fatal: "Failed to convert from Winreg an unsupported type: " & typeName.}

View File

@ -1,5 +1,5 @@
import
strutils, os,
strutils,
./types
type
@ -17,8 +17,6 @@ const
RT_QWORD* = 0x00000040
RT_ANY* = 0x0000ffff
MAX_ELEM_SIZE = 16_383'i32
proc regGetValue(hKey: HKEY, lpSubKey, lpValue: cstring,
dwFlags: int32, pdwType: ptr RegType,
pvData: pointer, pcbData: ptr int32): int32 {.

View File

@ -10,3 +10,6 @@
import
test_config_file,
test_envvar
when defined(windows):
import test_winreg

View File

@ -5,3 +5,198 @@
# * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
options, unittest, os,
stew/byteutils, ../confutils,
../confutils/[std/net]
import
toml_serialization, json_serialization,
../confutils/winreg/winreg_serialization,
../confutils/envvar/envvar_serialization
type
ValidatorPrivKey = object
field_a: int
field_b: string
CheckPoint = int
RuntimePreset = int
GraffitiBytes = array[16, byte]
WalletName = string
VCStartUpCmd = enum
VCNoCommand
ValidatorKeyPath = TypedInputFile[ValidatorPrivKey, Txt, "privkey"]
TestConf* = object
logLevel* {.
defaultValue: "DEBUG"
desc: "Sets the log level."
name: "log-level" }: string
logFile* {.
desc: "Specifies a path for the written Json log file"
name: "log-file" }: Option[OutFile]
dataDir* {.
defaultValue: config.defaultDataDir()
desc: "The directory where nimbus will store all blockchain data"
abbr: "d"
name: "data-dir" }: OutDir
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration"
name: "non-interactive" }: bool
validators* {.
required
desc: "Attach a validator by supplying a keystore path"
abbr: "v"
name: "validator" }: seq[ValidatorKeyPath]
validatorsDirFlag* {.
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDirFlag* {.
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
case cmd* {.
command
defaultValue: VCNoCommand }: VCStartUpCmd
of VCNoCommand:
graffiti* {.
desc: "The graffiti value that will appear in proposed blocks. " &
"You can use a 0x-prefixed hex encoded string to specify raw bytes."
name: "graffiti" }: Option[GraffitiBytes]
stopAtEpoch* {.
defaultValue: 0
desc: "A positive epoch selects the epoch at which to stop"
name: "stop-at-epoch" }: uint64
rpcPort* {.
defaultValue: defaultEth2RpcPort
desc: "HTTP port of the server to connect to for RPC - for the validator duties in the pull model"
name: "rpc-port" }: Port
rpcAddress* {.
defaultValue: defaultAdminListenAddress(config)
desc: "Address of the server to connect to for RPC - for the validator duties in the pull model"
name: "rpc-address" }: ValidIpAddress
retryDelay* {.
defaultValue: 10
desc: "Delay in seconds between retries after unsuccessful attempts to connect to a beacon node"
name: "retry-delay" }: int
func defaultDataDir(conf: TestConf): string =
discard
func parseCmdArg*(T: type GraffitiBytes, input: TaintedString): T
{.raises: [ValueError, Defect].} =
discard
func completeCmdArg*(T: type GraffitiBytes, input: TaintedString): seq[string] =
@[]
func defaultAdminListenAddress*(conf: TestConf): ValidIpAddress =
(static ValidIpAddress.init("127.0.0.1"))
const
defaultEth2TcpPort* = 9000
defaultEth2RpcPort* = 9090
const
confPathCurrUser = "tests" / "config_files" / "current_user"
confPathSystemWide = "tests" / "config_files" / "system_wide"
# appName, vendorName, and appendConfigFileFormats
# are overrideables proc related to config-file
func appName(_: type TestConf): string =
"testApp"
func vendorName(_: type TestConf): string =
"testVendor"
func appendConfigFileFormats(_: type TestConf) =
appendConfigFileFormat(Envvar, ""):
"prefix"
when defined(windows):
appendConfigFileFormat(Winreg, ""):
"HKCU" / "SOFTWARE"
appendConfigFileFormat(Winreg, ""):
"HKLM" / "SOFTWARE"
appendConfigFileFormat(Toml, "toml"):
confPathCurrUser
appendConfigFileFormat(Toml, "toml"):
confPathSystemWide
elif defined(posix):
appendConfigFileFormat(Toml, "toml"):
confPathCurrUser
appendConfigFileFormat(Toml, "toml"):
confPathSystemWide
# User might also need to extend the serializer capability
# for each of the registered formats.
# This is especially true for distinct types and some special types
# not covered by the standard implementation
proc readValue(r: var TomlReader,
value: var (InputFile | InputDir | OutFile | OutDir | ValidatorKeyPath)) =
type T = type value
value = r.parseAsString().T
proc readValue(r: var TomlReader, value: var ValidIpAddress) =
value = ValidIpAddress.init(r.parseAsString())
proc readValue(r: var TomlReader, value: var Port) =
value = r.parseInt(int).Port
proc readValue(r: var TomlReader, value: var GraffitiBytes) =
value = hexToByteArray[value.len](r.parseAsString())
proc readValue(r: var EnvvarReader,
value: var (InputFile | InputDir | OutFile | OutDir | ValidatorKeyPath)) =
type T = type value
value = r.readValue(string).T
proc readValue(r: var EnvvarReader, value: var ValidIpAddress) =
value = ValidIpAddress.init(r.readValue(string))
proc readValue(r: var EnvvarReader, value: var Port) =
value = r.readValue(int).Port
proc readValue(r: var EnvvarReader, value: var GraffitiBytes) =
value = hexToByteArray[value.len](r.readValue(string))
proc readValue(r: var WinregReader,
value: var (InputFile | InputDir | OutFile | OutDir | ValidatorKeyPath)) =
type T = type value
value = r.readValue(string).T
proc readValue(r: var WinregReader, value: var ValidIpAddress) =
value = ValidIpAddress.init(r.readValue(string))
proc readValue(r: var WinregReader, value: var Port) =
value = r.readValue(int).Port
proc readValue(r: var WinregReader, value: var GraffitiBytes) =
value = hexToByteArray[value.len](r.readValue(string))
proc testConfigFile() =
suite "config file test suite":
test "basic config file":
let conf = TestConf.load()
testConfigFile()