Skip to content

Commit 7728f6b

Browse files
authored
Fixes #114; Allow flattening options in objects (#118)
1 parent 713df7a commit 7728f6b

13 files changed

Lines changed: 873 additions & 67 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ tests/test_help
1010
tests/test_argument
1111
tests/test_parsecmdarg
1212
tests/test_obsolete
13+
tests/test_flatten_pragma

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,24 @@ let c = MyConf.load(onLoaded = myLogger)
389389

390390
-----------------
391391

392+
```nim
393+
template flatten* {.pragma.}
394+
```
395+
396+
Apply it to an object field to traverse the object options as if they were "top-level".
397+
This allows the object options to be reused in various configurations.
398+
399+
```nim
400+
template flatten*(v: untyped) {.pragma.}
401+
```
402+
403+
The default values for the object options can be overridden
404+
with a tuple of field names and values. For example:
405+
`opts {.flatten: (opt1: "my default").}: OptsConf` where `opt1`
406+
is a `OptsConf` field of type string.
407+
408+
-----------------
409+
392410
```nim
393411
template implicitlySelectable* {.pragma.}
394412
```

confutils.nim

Lines changed: 139 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import
1414
std/[enumutils, options, strutils, wordwrap],
1515
results,
1616
stew/shims/macros,
17-
confutils/[defs, cli_parser, config_file]
17+
confutils/[defs, cli_parser, config_file, utils]
1818

1919
export
2020
options, results, defs, config_file
@@ -817,24 +817,132 @@ template debugMacroResult(macroName: string) {.dirty.} =
817817
echo "\n-------- ", macroName, " ----------------------"
818818
echo result.repr
819819

820+
type
821+
ConfFieldDescRef = ref ConfFieldDesc
822+
ConfFieldDesc = object
823+
field: FieldDescription
824+
parent: ConfFieldDescRef
825+
826+
proc newConfFieldDesc(
827+
field: FieldDescription, parent: ConfFieldDescRef
828+
): ConfFieldDescRef =
829+
ConfFieldDescRef(field: field, parent: parent)
830+
831+
proc fieldCaseBranch(cf: ConfFieldDesc): NimNode =
832+
if cf.field.caseBranch != nil:
833+
cf.field.caseBranch
834+
elif cf.parent != nil:
835+
fieldCaseBranch(cf.parent[])
836+
else:
837+
nil
838+
839+
proc fieldCaseField(cf: ConfFieldDesc): NimNode =
840+
if cf.field.caseField != nil:
841+
cf.field.caseField
842+
elif cf.parent != nil:
843+
fieldCaseField(cf.parent[])
844+
else:
845+
nil
846+
847+
proc validateFlattenOptions(field: FieldDescription, children: openArray[ConfFieldDesc]) =
848+
## Validate flatten tuple options exist
849+
doAssert field.readPragma"flatten" != nil
850+
let ftn = field.readPragma"flatten"
851+
if ftn.kind == nnkTupleConstr:
852+
if ftn.len > 0 and ftn[0].kind != nnkExprColonExpr:
853+
error "invalid flatten options, expected tuple of (k: v)", ftn
854+
var found = newSeq[NimNode]()
855+
for cf in children:
856+
for x in ftn:
857+
if eqIdent(x[0], cf.field.name):
858+
found.add x[0]
859+
for x in ftn:
860+
if x[0] notin found:
861+
error "invalid flatten option: " & $x[0], ftn
862+
863+
proc confFields(typeImpl: NimNode, parent: ConfFieldDescRef = nil): seq[ConfFieldDesc] =
864+
result = newSeq[ConfFieldDesc]()
865+
for field in recordFields(typeImpl):
866+
let ftn = field.readPragma"flatten"
867+
if ftn != nil:
868+
let firstChildIdx = result.len
869+
for cf in confFields(getImpl(field.typ), newConfFieldDesc(field, parent)):
870+
result.add cf
871+
validateFlattenOptions(field, toOpenArray(result, firstChildIdx, result.len-1))
872+
else:
873+
result.add ConfFieldDesc(field: field, parent: parent)
874+
875+
proc genFieldDotExpr(cf: ConfFieldDesc): NimNode =
876+
if cf.parent != nil:
877+
dotExpr(genFieldDotExpr(cf.parent[]), cf.field.name)
878+
else:
879+
cf.field.name
880+
881+
proc fullFieldName(cf: ConfFieldDesc): string =
882+
if cf.parent != nil:
883+
$fullFieldName(cf.parent[]) & "Dot" & $cf.field.name
884+
else:
885+
$cf.field.name
886+
887+
proc fieldCaseFieldFullName(cf: ConfFieldDesc): string =
888+
if cf.field.caseField != nil:
889+
if cf.parent != nil:
890+
fullFieldName(cf.parent[]) & "Dot" & $cf.field.caseField.getFieldName
891+
else:
892+
$cf.field.caseField.getFieldName
893+
else:
894+
doAssert cf.parent != nil, "caseField not found"
895+
fieldCaseFieldFullName(cf.parent[])
896+
897+
proc flattenDefaultValue(field: FieldDescription, parent: ConfFieldDescRef): NimNode =
898+
if parent == nil:
899+
return nil
900+
let ancestorVal = flattenDefaultValue(field, parent[].parent)
901+
if ancestorVal != nil:
902+
return ancestorVal
903+
let ftn = parent[].field.readPragma"flatten"
904+
case ftn.kind
905+
of nnkSym: nil
906+
of nnkTupleConstr:
907+
var ret: NimNode = nil
908+
for x in ftn:
909+
if eqIdent(x[0], field.name):
910+
ret = x[1]
911+
ret
912+
else:
913+
error "Bad flatten pragma", ftn
914+
nil
915+
916+
proc readDefaultValueOverride(cf: ConfFieldDesc): NimNode =
917+
flattenDefaultValue(cf.field, cf.parent)
918+
919+
proc readDefaultValue(cf: ConfFieldDesc): NimNode =
920+
result = cf.readDefaultValueOverride()
921+
if result == nil:
922+
result = cf.field.readPragma"defaultValue"
923+
820924
proc generateFieldSetters(RecordType: NimNode): NimNode =
821925
var recordDef = getImpl(RecordType)
822926
let makeDefaultValue = bindSym"makeDefaultValue"
823927

824928
result = newTree(nnkStmtListExpr)
825929
var settersArray = newTree(nnkBracket)
826930

827-
for field in recordFields(recordDef):
931+
for cf in confFields(recordDef):
932+
let field = cf.field
828933
var
829-
setterName = ident($field.name & "Setter")
934+
setterName = ident(cf.fullFieldName() & "Setter")
830935
fieldName = field.name
831936
namePragma = field.readPragma"name"
832-
paramName = if namePragma != nil: namePragma
833-
else: fieldName
937+
paramName =
938+
if namePragma != nil:
939+
namePragma
940+
else:
941+
fieldName
834942
configVar = ident "config"
835-
configField = newTree(nnkDotExpr, configVar, fieldName)
836-
defaultValue = field.readPragma"defaultValue"
837-
completerName = ident($field.name & "Complete")
943+
configField = dotExpr(configVar, genFieldDotExpr(cf))
944+
defaultValue = cf.readDefaultValue()
945+
completerName = ident(cf.fullFieldName() & "Complete")
838946
isFieldDiscriminator = newLit field.isDiscriminator
839947
defaultValueDesc = field.readPragma"defaultValueDesc"
840948
defaultValueHelp =
@@ -849,7 +957,7 @@ proc generateFieldSetters(RecordType: NimNode): NimNode =
849957
newLit(repr defaultValue)
850958
else:
851959
newLit("")
852-
defaultValueHelpName = ident($field.name & "DefaultValueHelp")
960+
defaultValueHelpName = ident(cf.fullFieldName() & "DefaultValueHelp")
853961

854962
if defaultValue == nil:
855963
defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, configField))
@@ -917,6 +1025,8 @@ proc generateFieldSetters(RecordType: NimNode): NimNode =
9171025
debugMacroResult "Field Setters"
9181026

9191027
func checkDuplicate(cmd: CmdInfo, opt: OptInfo, fieldName: NimNode) =
1028+
if opt.kind == Discriminator and opt.isCommand:
1029+
return
9201030
for x in cmd.opts:
9211031
if opt.name == x.name:
9221032
error "duplicate name detected: " & opt.name, fieldName
@@ -955,13 +1065,14 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
9551065

9561066
var
9571067
recordDef = getImpl(T)
958-
discriminatorFields = newSeq[OptInfo]()
1068+
discriminatorFields = newSeq[(string, OptInfo)]()
9591069
fieldIdx = 0
9601070

961-
for field in recordFields(recordDef):
1071+
for cf in confFields(recordDef):
9621072
let
1073+
field = cf.field
9631074
isImplicitlySelectable = field.readPragma"implicitlySelectable" != nil
964-
defaultValue = field.readPragma"defaultValue"
1075+
defaultValue = cf.readDefaultValue()
9651076
obsoleteMsg = field.readPragma"obsolete"
9661077
separator = field.readPragma"separator"
9671078
longDesc = field.readPragma"longDesc"
@@ -992,7 +1103,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
9921103
inc fieldIdx
9931104

9941105
if field.isDiscriminator:
995-
discriminatorFields.add opt
1106+
discriminatorFields.add (cf.fullFieldName(), opt)
9961107
let cmdType = field.typ.getImpl[^1]
9971108
if cmdType.kind != nnkEnumTy:
9981109
error "Only enums are supported as case object discriminators", field.name
@@ -1018,20 +1129,25 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
10181129
if opt.defaultSubCmd == -1:
10191130
error "The default value is not a valid enum value", defaultValue
10201131

1021-
if field.caseField != nil and field.caseBranch != nil:
1022-
let fieldName = field.caseField.getFieldName
1023-
var discriminator = findOpt(discriminatorFields, $fieldName)
1132+
let caseField = cf.fieldCaseField()
1133+
let caseBranch = cf.fieldCaseBranch()
1134+
if caseField != nil and caseBranch != nil:
1135+
let fieldName = cf.fieldCaseFieldFullName()
1136+
var discriminator: OptInfo
1137+
for (name, opt) in discriminatorFields:
1138+
if fieldName == name:
1139+
discriminator = opt
10241140

10251141
if discriminator == nil:
1026-
error "Unable to find " & $fieldName
1142+
error "Unable to find " & $caseField.getFieldName
10271143

1028-
if field.caseBranch.kind == nnkElse:
1144+
if caseBranch.kind == nnkElse:
10291145
error "Sub-command parameters cannot appear in an else branch. " &
1030-
"Please specify the sub-command branch precisely", field.caseBranch[0]
1146+
"Please specify the sub-command branch precisely", caseBranch[0]
10311147

10321148
# support multiple subcommands in the same branch; skip branch body
1033-
for enumValIdx in 0 .. field.caseBranch.len - 2:
1034-
var branchEnumVal = field.caseBranch[enumValIdx]
1149+
for enumValIdx in 0 .. caseBranch.len - 2:
1150+
var branchEnumVal = caseBranch[enumValIdx]
10351151
if branchEnumVal.kind == nnkDotExpr:
10361152
branchEnumVal = branchEnumVal[1]
10371153
let cmd = findCmd(discriminator.subCmds, $branchEnumVal)
@@ -1060,6 +1176,8 @@ macro configurationRtti(RecordType: type): untyped =
10601176

10611177
result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters)
10621178

1179+
debugMacroResult "configurationRtti"
1180+
10631181
when hasSerialization:
10641182
template addConfigFileImpl(
10651183
secondarySources: auto,

0 commit comments

Comments
 (0)