nim-confutils/confutils/config_file.nim

362 lines
12 KiB
Nim
Raw Normal View History

2024-02-12 03:26:05 +00:00
# confutils
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
2020-10-31 10:44:03 +00:00
import
2023-02-15 08:26:53 +00:00
std/[tables, macrocache],
stew/shims/macros
2023-02-15 08:26:53 +00:00
{.warning[UnusedImport]:off.}
import
std/typetraits,
2020-10-31 10:44:03 +00:00
./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.
]#
type
ConfFileSection = ref object
children: seq[ConfFileSection]
fieldName: string
namePragma: string
typ: NimNode
defaultValue: string
isCommandOrArgument: bool
isCaseBranch: bool
isDiscriminator: bool
GeneratedFieldInfo = tuple
isCommandOrArgument: bool
path: seq[string]
OriginalToGeneratedFields = OrderedTable[string, GeneratedFieldInfo]
{.push gcsafe, raises: [].}
2020-10-31 10:44:03 +00:00
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
2022-02-24 20:43:04 +00:00
proc putRecList(n: NimNode, recList: NimNode) =
2020-10-31 10:44:03 +00:00
recList.expectKind nnkRecList
if n.kind == nnkObjectTy:
n[2] = recList
return
for z in n:
2022-02-24 20:43:04 +00:00
putRecList(z, recList)
2020-10-31 10:44:03 +00:00
proc generateOptionalField(fieldName: NimNode, fieldType: NimNode): NimNode =
let right = if isOption(fieldType): fieldType else: makeOption(fieldType)
newIdentDefs(fieldName, right)
proc traverseIdent(ident: NimNode, typ: NimNode, isDiscriminator: bool,
isCommandOrArgument = false, defaultValue = "",
namePragma = ""): ConfFileSection =
ident.expectKind nnkIdent
ConfFileSection(fieldName: $ident, namePragma: namePragma, typ: typ,
defaultValue: defaultValue, isCommandOrArgument: isCommandOrArgument,
isDiscriminator: isDiscriminator)
proc traversePostfix(postfix: NimNode, typ: NimNode, isDiscriminator: bool,
isCommandOrArgument = false, defaultValue = "",
namePragma = ""): ConfFileSection =
postfix.expectKind nnkPostfix
case postfix[1].kind
of nnkIdent:
traverseIdent(postfix[1], typ, isDiscriminator, isCommandOrArgument,
defaultValue, namePragma)
of nnkAccQuoted:
traverseIdent(postfix[1][0], typ, isDiscriminator, isCommandOrArgument,
defaultValue, namePragma)
else:
raiseAssert "[Postfix] Unsupported child node:\n" & postfix[1].treeRepr
proc shortEnumName(n: NimNode): NimNode =
if n.kind == nnkDotExpr:
n[1]
else:
n
proc traversePragma(pragma: NimNode):
tuple[isCommandOrArgument: bool, defaultValue, namePragma: string] =
pragma.expectKind nnkPragma
var child: NimNode
for childNode in pragma:
child = childNode
if child.kind == nnkCall:
# A custom pragma was used more than once (e.g.: {.pragma: posixOnly, hidden.}) and the
# AST is now:
# ```
# Call
# Sym "hidden"
# ```
child = child[0]
case child.kind
of nnkSym:
let sym = $child
if sym == "command" or sym == "argument":
result.isCommandOrArgument = true
of nnkExprColonExpr:
let pragma = $child[0]
if pragma == "defaultValue":
result.defaultValue = repr(shortEnumName(child[1]))
elif pragma == "name":
result.namePragma = $child[1]
else:
raiseAssert "[Pragma] Unsupported child node:\n" & child.treeRepr
proc traversePragmaExpr(pragmaExpr: NimNode, typ: NimNode,
isDiscriminator: bool): ConfFileSection =
pragmaExpr.expectKind nnkPragmaExpr
let (isCommandOrArgument, defaultValue, namePragma) =
traversePragma(pragmaExpr[1])
case pragmaExpr[0].kind
of nnkIdent:
traverseIdent(pragmaExpr[0], typ, isDiscriminator, isCommandOrArgument,
defaultValue, namePragma)
of nnkAccQuoted:
traverseIdent(pragmaExpr[0][0], typ, isDiscriminator, isCommandOrArgument,
defaultValue, namePragma)
of nnkPostfix:
traversePostfix(pragmaExpr[0], typ, isDiscriminator, isCommandOrArgument,
defaultValue, namePragma)
else:
raiseAssert "[PragmaExpr] Unsupported expression:\n" & pragmaExpr.treeRepr
proc traverseIdentDefs(identDefs: NimNode, parent: ConfFileSection,
isDiscriminator: bool): seq[ConfFileSection] =
identDefs.expectKind nnkIdentDefs
doAssert identDefs.len > 2, "This kind of node must have at least 3 children."
let typ = identDefs[^2]
for child in identDefs:
case child.kind
of nnkIdent:
result.add traverseIdent(child, typ, isDiscriminator)
of nnkAccQuoted:
result.add traverseIdent(child[0], typ, isDiscriminator)
of nnkPostfix:
result.add traversePostfix(child, typ, isDiscriminator)
of nnkPragmaExpr:
result.add traversePragmaExpr(child, typ, isDiscriminator)
of nnkBracketExpr, nnkSym, nnkEmpty, nnkInfix, nnkCall, nnkDotExpr:
discard
else:
raiseAssert "[IdentDefs] Unsupported child node:\n" & child.treeRepr
proc traverseRecList(recList: NimNode, parent: ConfFileSection): seq[ConfFileSection]
proc traverseOfBranch(ofBranch: NimNode, parent: ConfFileSection): ConfFileSection =
ofBranch.expectKind nnkOfBranch
result = ConfFileSection(fieldName: repr(shortEnumName(ofBranch[0])), isCaseBranch: true)
for child in ofBranch:
case child.kind:
of nnkIdent, nnkDotExpr, nnkAccQuoted:
discard
of nnkRecList:
result.children.add traverseRecList(child, result)
else:
raiseAssert "[OfBranch] Unsupported child node:\n" & child.treeRepr
proc traverseRecCase(recCase: NimNode, parent: ConfFileSection): seq[ConfFileSection] =
recCase.expectKind nnkRecCase
for child in recCase:
case child.kind
of nnkIdentDefs:
result.add traverseIdentDefs(child, parent, true)
of nnkOfBranch:
result.add traverseOfBranch(child, parent)
else:
raiseAssert "[RecCase] Unsupported child node:\n" & child.treeRepr
proc traverseRecList(recList: NimNode, parent: ConfFileSection): seq[ConfFileSection] =
recList.expectKind nnkRecList
for child in recList:
case child.kind
of nnkIdentDefs:
result.add traverseIdentDefs(child, parent, false)
of nnkRecCase:
result.add traverseRecCase(child, parent)
of nnkNilLit:
discard
else:
raiseAssert "[RecList] Unsupported child node:\n" & child.treeRepr
proc normalize(root: ConfFileSection) =
## Moves the default case branches children one level upper in the hierarchy.
## Also removes case branches without children.
var children: seq[ConfFileSection]
var defaultValue = ""
for child in root.children:
normalize(child)
if child.isDiscriminator:
defaultValue = child.defaultValue
if child.isCaseBranch and child.fieldName == defaultValue:
for childChild in child.children:
children.add childChild
child.children = @[]
elif child.isCaseBranch and child.children.len == 0:
discard
else:
children.add child
root.children = children
proc generateConfigFileModel(ConfType: NimNode): ConfFileSection =
let confTypeImpl = ConfType.getType[1].getImpl
result = ConfFileSection(fieldName: $confTypeImpl[0])
result.children = traverseRecList(confTypeImpl[2][2], result)
result.normalize
proc getRenamedName(node: ConfFileSection): string =
if node.namePragma.len == 0: node.fieldName else: node.namePragma
proc generateTypes(root: ConfFileSection): seq[NimNode] =
let index = result.len
result.add getAst(objectDecl(genSym(nskType, root.fieldName)))[0]
2020-10-31 10:44:03 +00:00
var recList = newNimNode(nnkRecList)
for child in root.children:
if child.isCommandOrArgument:
2020-10-31 10:44:03 +00:00
continue
if child.isCaseBranch:
if child.children.len > 0:
var types = generateTypes(child)
recList.add generateOptionalField(child.fieldName.ident, types[0][0])
result.add types
else:
recList.add generateOptionalField(child.getRenamedName.ident, child.typ)
result[index].putRecList(recList)
proc generateSettersPaths(node: ConfFileSection,
result: var OriginalToGeneratedFields,
pathsCache: var seq[string]) =
pathsCache.add node.getRenamedName
if node.children.len == 0:
result[node.fieldName] = (node.isCommandOrArgument, pathsCache)
else:
for child in node.children:
generateSettersPaths(child, result, pathsCache)
pathsCache.del pathsCache.len - 1
proc generateSettersPaths(root: ConfFileSection, pathsCache: var seq[string]): OriginalToGeneratedFields =
for child in root.children:
generateSettersPaths(child, result, pathsCache)
2020-10-31 10:44:03 +00:00
template cfSetter(a, b: untyped): untyped =
when a is Option:
a = some(b)
2020-10-31 10:44:03 +00:00
else:
a = b
proc generateSetters(confType, CF: NimNode, fieldsPaths: OriginalToGeneratedFields):
(NimNode, NimNode, int) =
2020-10-31 10:44:03 +00:00
var
procs = newStmtList()
assignments = newStmtList()
numSetters = 0
let c = "c".ident
for field, (isCommandOrArgument, path) in fieldsPaths:
if isCommandOrArgument:
2020-10-31 10:44:03 +00:00
assignments.add quote do:
result.setters[`numSetters`] = defaultConfigFileSetter
inc numSetters
continue
var fieldPath = c
var condition: NimNode
for fld in path:
fieldPath = newDotExpr(fieldPath, fld.ident)
let fieldChecker = newDotExpr(fieldPath, "isSome".ident)
if condition == nil:
condition = fieldChecker
else:
condition = newNimNode(nnkInfix).add("and".ident).add(condition).add(fieldChecker)
fieldPath = newDotExpr(fieldPath, "get".ident)
2020-10-31 10:44:03 +00:00
let setterName = genSym(nskProc, field & "CFSetter")
let fieldIdent = field.ident
2020-10-31 10:44:03 +00:00
procs.add quote do:
proc `setterName`(s: var `confType`, cf: ref `CF`): bool {.nimcall, gcsafe.} =
for `c` in cf.data:
if `condition`:
cfSetter(s.`fieldIdent`, `fieldPath`)
2020-10-31 10:44:03 +00:00
return true
assignments.add quote do:
result.setters[`numSetters`] = `setterName`
inc numSetters
result = (procs, assignments, numSetters)
proc generateConfigFileSetters(confType, optType: NimNode,
fieldsPaths: OriginalToGeneratedFields): NimNode =
let
CF = ident "SecondarySources"
T = confType.getType[1]
optT = optType[0][0]
SetterProcType = genSym(nskType, "SetterProcType")
(setterProcs, assignments, numSetters) = generateSetters(T, CF, fieldsPaths)
stmtList = quote do:
type
`SetterProcType` = proc(
s: var `T`, cf: ref `CF`
): bool {.nimcall, gcsafe, raises: [].}
2020-10-31 10:44:03 +00:00
`CF` = object
data*: seq[`optT`]
setters: array[`numSetters`, `SetterProcType`]
2020-10-31 10:44:03 +00:00
proc defaultConfigFileSetter(
s: var `T`, cf: ref `CF`
): bool {.nimcall, gcsafe, raises: [], used.} =
discard
2020-10-31 10:44:03 +00:00
`setterProcs`
2020-10-31 10:44:03 +00:00
proc new(_: type `CF`): ref `CF` =
new result
`assignments`
2020-10-31 10:44:03 +00:00
new(`CF`)
2020-10-31 10:44:03 +00:00
stmtList
2020-10-31 10:44:03 +00:00
macro generateSecondarySources*(ConfType: type): untyped =
let
model = generateConfigFileModel(ConfType)
modelType = generateTypes(model)
var
pathsCache: seq[string]
2020-10-31 10:44:03 +00:00
result = newTree(nnkStmtList)
result.add newTree(nnkTypeSection, modelType)
2020-10-31 10:44:03 +00:00
let settersPaths = model.generateSettersPaths(pathsCache)
result.add generateConfigFileSetters(ConfType, result[^1], settersPaths)
{.pop.}