Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ vendor/
tests/test_all
tests/test_nested_cmd
tests/test_help
tests/test_flatten_pragma
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,15 @@ Network Options: # this is a separator
-c, --opt3 desc
```

-----------------

```nim
template flatten* {.pragma.}
```

Apply it to an object field to traverse the object options as if they were "top-level".
This allows the object options to be reused in various configurations.

## Configuration field types

The `confutils/defs` module provides a number of types frequently used
Expand Down
123 changes: 108 additions & 15 deletions confutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import
os,
std/[enumutils, options, strutils, wordwrap],
stew/shims/macros,
confutils/[defs, cli_parser, config_file]
confutils/[defs, cli_parser, config_file, utils]

export
options, defs, config_file
Expand Down Expand Up @@ -693,24 +693,83 @@ template debugMacroResult(macroName: string) {.dirty.} =
echo "\n-------- ", macroName, " ----------------------"
echo result.repr

type
ConfFieldDescRef = ref ConfFieldDesc
ConfFieldDesc = object
field: FieldDescription
parent: ConfFieldDescRef

proc newConfFieldDesc(
field: FieldDescription, parent: ConfFieldDescRef
): ConfFieldDescRef =
ConfFieldDescRef(field: field, parent: parent)

proc fieldCaseBranch(cf: ConfFieldDesc): NimNode =
if cf.field.caseBranch != nil:
cf.field.caseBranch
elif cf.parent != nil:
fieldCaseBranch(cf.parent[])
else:
nil

proc fieldCaseField(cf: ConfFieldDesc): NimNode =
if cf.field.caseField != nil:
cf.field.caseField
elif cf.parent != nil:
fieldCaseField(cf.parent[])
else:
nil

proc confFields(typeImpl: NimNode, parent: ConfFieldDescRef = nil): seq[ConfFieldDesc] =
result = newSeq[ConfFieldDesc]()
for field in recordFields(typeImpl):
if field.readPragma"flatten" != nil:
for cf in confFields(getImpl(field.typ), newConfFieldDesc(field, parent)):
result.add cf
else:
result.add ConfFieldDesc(field: field, parent: parent)

proc genFieldDotExpr(cf: ConfFieldDesc): NimNode =
if cf.parent != nil:
dotExpr(genFieldDotExpr(cf.parent[]), cf.field.name)
else:
cf.field.name

proc fullFieldName(cf: ConfFieldDesc): string =
if cf.parent != nil:
$fullFieldName(cf.parent[]) & "Dot" & $cf.field.name
else:
$cf.field.name

proc fieldCaseFieldFullName(cf: ConfFieldDesc): string =
if cf.field.caseField != nil:
if cf.parent != nil:
fullFieldName(cf.parent[]) & "Dot" & $cf.field.caseField.getFieldName
else:
$cf.field.caseField.getFieldName
else:
doAssert cf.parent != nil, "caseField not found"
fieldCaseFieldFullName(cf.parent[])

proc generateFieldSetters(RecordType: NimNode): NimNode =
var recordDef = getImpl(RecordType)
let makeDefaultValue = bindSym"makeDefaultValue"

result = newTree(nnkStmtListExpr)
var settersArray = newTree(nnkBracket)

for field in recordFields(recordDef):
for cf in confFields(recordDef):
let field = cf.field
var
setterName = ident($field.name & "Setter")
setterName = ident(cf.fullFieldName() & "Setter")
fieldName = field.name
namePragma = field.readPragma"name"
paramName = if namePragma != nil: namePragma
else: fieldName
configVar = ident "config"
configField = newTree(nnkDotExpr, configVar, fieldName)
configField = dotExpr(configVar, genFieldDotExpr(cf))
defaultValue = field.readPragma"defaultValue"
completerName = ident($field.name & "Complete")
completerName = ident(cf.fullFieldName() & "Complete")
isFieldDiscriminator = newLit field.isDiscriminator

if defaultValue == nil:
Expand Down Expand Up @@ -761,6 +820,8 @@ proc generateFieldSetters(RecordType: NimNode): NimNode =
debugMacroResult "Field Setters"

func checkDuplicate(cmd: CmdInfo, opt: OptInfo, fieldName: NimNode) =
if opt.kind == Discriminator and opt.isCommand:
return
for x in cmd.opts:
if opt.name == x.name:
error "duplicate name detected: " & opt.name, fieldName
Expand Down Expand Up @@ -795,11 +856,12 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =

var
recordDef = getImpl(T)
discriminatorFields = newSeq[OptInfo]()
discriminatorFields = newSeq[(string, OptInfo)]()
fieldIdx = 0

for field in recordFields(recordDef):
for cf in confFields(recordDef):
let
field = cf.field
isImplicitlySelectable = field.readPragma"implicitlySelectable" != nil
defaultValue = field.readPragma"defaultValue"
defaultValueDesc = field.readPragma"defaultValueDesc"
Expand Down Expand Up @@ -834,7 +896,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
inc fieldIdx

if field.isDiscriminator:
discriminatorFields.add opt
discriminatorFields.add (cf.fullFieldName(), opt)
let cmdType = field.typ.getImpl[^1]
if cmdType.kind != nnkEnumTy:
error "Only enums are supported as case object discriminators", field.name
Expand All @@ -860,18 +922,23 @@ proc cmdInfoFromType(T: NimNode): CmdInfo =
if opt.defaultSubCmd == -1:
error "The default value is not a valid enum value", defaultValue

if field.caseField != nil and field.caseBranch != nil:
let fieldName = field.caseField.getFieldName
var discriminator = findOpt(discriminatorFields, $fieldName)
let caseField = cf.fieldCaseField()
let caseBranch = cf.fieldCaseBranch()
if caseField != nil and caseBranch != nil:
let fieldName = cf.fieldCaseFieldFullName()
var discriminator: OptInfo
for (name, opt) in discriminatorFields:
if fieldName == name:
discriminator = opt

if discriminator == nil:
error "Unable to find " & $fieldName
error "Unable to find " & $caseField.getFieldName

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

var branchEnumVal = field.caseBranch[0]
var branchEnumVal = caseBranch[0]
if branchEnumVal.kind == nnkDotExpr:
branchEnumVal = branchEnumVal[1]
var cmd = findCmd(discriminator.subCmds, $branchEnumVal)
Expand Down Expand Up @@ -899,6 +966,8 @@ macro configurationRtti(RecordType: type): untyped =

result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters)

debugMacroResult "configurationRtti"

when hasSerialization:
proc addConfigFile*(secondarySources: auto,
Format: type,
Expand Down Expand Up @@ -1331,4 +1400,28 @@ func load*(f: TypedInputFile): f.ContentType =
mixin loadFile
loadFile(f.Format, f.string, f.ContentType)

proc flattenedAccessorsImpl(RecordType: NimNode): NimNode =
result = newTree(nnkStmtListExpr)
let T = RecordType.getType[1]
let recordDef = getImpl(T)
for cf in confFields(recordDef):
if cf.parent != nil:
let
configVar = ident "config"
configField = dotExpr(configVar, genFieldDotExpr(cf))
accessorName = if cf.field.isPublic:
newTree(nnkPostfix, ident("*"), cf.field.name)
else:
ident $cf.field.name
result.add quote do:
template `accessorName`(`configVar`: `T`): untyped =
`configField`

debugMacroResult "Flattened Accessors"

macro flattenedAccessors*(Configuration: type): untyped =
## Generates accessors to omit specifying the ``{.flatten.}``
## field name when accessing a flattened option.
flattenedAccessorsImpl(Configuration)

{.pop.}
Loading