config file integration into confutils
This commit is contained in:
parent
085d52d3ad
commit
f3a048f9ea
|
@ -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]())
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.}
|
||||
|
|
|
@ -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 {.
|
||||
|
|
|
@ -10,3 +10,6 @@
|
|||
import
|
||||
test_config_file,
|
||||
test_envvar
|
||||
|
||||
when defined(windows):
|
||||
import test_winreg
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue