# 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. import std/[tables, macrocache], stew/shims/macros {.warning[UnusedImport]:off.} import std/typetraits, ./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: [].} 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 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] var recList = newNimNode(nnkRecList) for child in root.children: if child.isCommandOrArgument: 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) template cfSetter(a, b: untyped): untyped = when a is Option: a = some(b) else: a = b proc generateSetters(confType, CF: NimNode, fieldsPaths: OriginalToGeneratedFields): (NimNode, NimNode, int) = var procs = newStmtList() assignments = newStmtList() numSetters = 0 let c = "c".ident for field, (isCommandOrArgument, path) in fieldsPaths: if isCommandOrArgument: 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) let setterName = genSym(nskProc, field & "CFSetter") let fieldIdent = field.ident 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`) 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: [].} `CF` = object data*: seq[`optT`] setters: array[`numSetters`, `SetterProcType`] proc defaultConfigFileSetter( s: var `T`, cf: ref `CF` ): bool {.nimcall, gcsafe, raises: [], used.} = discard `setterProcs` proc new(_: type `CF`): ref `CF` = new result `assignments` new(`CF`) stmtList macro generateSecondarySources*(ConfType: type): untyped = let model = generateConfigFileModel(ConfType) modelType = generateTypes(model) var pathsCache: seq[string] result = newTree(nnkStmtList) result.add newTree(nnkTypeSection, modelType) let settersPaths = model.generateSettersPaths(pathsCache) result.add generateConfigFileSetters(ConfType, result[^1], settersPaths) {.pop.}