logos-messaging-nim/tools/confutils/conf_from_json.nim
Fabiana Cecin 098c6f2a6d
Improve config
* Soft-deprecate --cluster-id=N triggering the associated preset selection
* Rewrite applyNetworkConf to apply user-set fields over preset fields
* Add createNode(preset, mode, overrides, additions) nim api
* Generate WakuNodeConfOverlay (all Option fields) from WakuNodeConf
* New parser for configJson handles new messaging shape and full conf shape
* Change all confbuilder defaults from literal values to DefaultXXX consts
* Change int/bool WakuNodeConf fields to Option to get user intent w/o sentinels
* Make Option CLI default-value help mention defaults now owned by confbuilder
* Misc refactors, fixes
* Add tests
2026-05-08 01:19:39 -03:00

244 lines
8.8 KiB
Nim

import std/[json, strutils, tables]
import confutils, confutils/std/net, results
import ./cli_args
const
KeyMode = "mode"
KeyPreset = "preset"
KeyOverrides = "overrides"
KeyAdditions = "additions"
const TopLevelOnlyKeys = [KeyMode, KeyPreset]
## Keys that the messaging shape requires at the top-level of the JSON input.
## They must not appear inside `overrides` or `additions`.
proc collectJsonFields*(
jsonNode: JsonNode
): Result[Table[string, (string, JsonNode)], string] =
## Walk the top-level JSON object and key it by lowercased names.
if jsonNode.kind != JObject:
return err("config JSON must be a JSON object, got " & $jsonNode.kind)
var jsonFields: Table[string, (string, JsonNode)]
for key, value in jsonNode:
let lowerKey = key.toLowerAscii()
if jsonFields.hasKey(lowerKey):
let firstKey = jsonFields[lowerKey][0]
return err(
"Duplicate configuration option (case-insensitive): '" & firstKey & "' and '" &
key & "'"
)
jsonFields[lowerKey] = (key, value)
return ok(jsonFields)
proc unknownKeysError(
jsonFields: Table[string, (string, JsonNode)], prefix: string
): string =
## Format leftover JSON keys as an error message.
var keys = newSeq[string]()
for _, (jsonKey, _) in pairs(jsonFields):
keys.add(jsonKey)
return prefix & ": " & $keys
proc rejectTopLevelOnlyKeysInside(
node: JsonNode, blockName: string
): Result[void, string] =
## Error if `node` contains any key from `TopLevelOnlyKeys`.
for k, _ in node:
if k.toLowerAscii() in TopLevelOnlyKeys:
return err("'" & k & "' must be a top-level key, not inside '" & blockName & "'")
return ok()
proc jsonScalarToString(node: JsonNode): Result[string, string] =
## Convert a scalar JSON value to its string form.
case node.kind
of JString:
return ok(node.getStr())
of JInt:
return ok($node.getInt())
of JFloat:
return ok($node.getFloat())
of JBool:
return ok($node.getBool())
of JNull:
return ok("")
else:
return err("expected scalar JSON value, got " & $node.kind)
proc applyJsonFieldsToConf(
conf: var WakuNodeConf,
jsonFields: var Table[string, (string, JsonNode)],
parseErrPrefix: string,
unknownErrPrefix: string,
): Result[void, string] =
## Walk `conf`'s fields and write each one matched (case-insensitive) by
## `jsonFields`. seq fields take a JArray (full replace); scalar fields
## take any scalar JSON kind. Errors on leftover unknown keys.
for confField, confValue in fieldPairs(conf):
let lowerField = confField.toLowerAscii()
if jsonFields.hasKey(lowerField):
let (jsonKey, jsonValue) = jsonFields[lowerField]
when confValue is seq:
if jsonValue.kind != JArray:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey &
"' must be a JSON array"
)
var newSeq: typeof(confValue) = @[]
for item in jsonValue:
let formattedItem = jsonScalarToString(item).valueOr:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
error
)
try:
type ElemType = typeof(confValue[0])
newSeq.add(parseCmdArg(ElemType, formattedItem))
except CatchableError as e:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
e.msg & ". Value: " & formattedItem
)
confValue = newSeq
else:
let formattedString = jsonScalarToString(jsonValue).valueOr:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
error
)
try:
confValue = parseCmdArg(typeof(confValue), formattedString)
except CatchableError as e:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
e.msg & ". Value: " & formattedString
)
jsonFields.del(lowerField)
if jsonFields.len > 0:
return err(unknownKeysError(jsonFields, unknownErrPrefix))
return ok()
proc applyJsonAsOverride*(
conf: var WakuNodeConf, overrides: JsonNode
): Result[void, string] =
## Apply `overrides` JSON onto `conf` with replace semantics for both scalars and lists.
var jsonFields = ?collectJsonFields(overrides)
return applyJsonFieldsToConf(
conf, jsonFields, "Failed to parse override field",
"Unrecognized override field(s) found",
)
proc applyJsonAsAddition*(
conf: var WakuNodeConf, additions: JsonNode
): Result[void, string] =
## Append JSON array in `additions` to `conf` seq fields.
var jsonFields = ?collectJsonFields(additions)
for confField, confValue in fieldPairs(conf):
let lowerField = confField.toLowerAscii()
if jsonFields.hasKey(lowerField):
let (jsonKey, jsonValue) = jsonFields[lowerField]
when confValue is seq:
if jsonValue.kind != JArray:
return err(
"Addition field '" & confField & "' from JSON key '" & jsonKey &
"' must be a JSON array"
)
for item in jsonValue:
let formattedItem = jsonScalarToString(item).valueOr:
return err(
"Failed to parse addition item for field '" & confField & "': " & error
)
try:
type ElemType = typeof(confValue[0])
confValue.add(parseCmdArg(ElemType, formattedItem))
except CatchableError as e:
return err(
"Failed to parse addition item for field '" & confField & "': " & e.msg &
". Value: " & formattedItem
)
else:
return err(
"Field '" & confField & "' from JSON key '" & jsonKey &
"' is not a list and cannot be in additions"
)
jsonFields.del(lowerField)
if jsonFields.len > 0:
return err(unknownKeysError(jsonFields, "Unrecognized addition field(s) found"))
return ok()
proc assembleMessagingConf*(
jsonFields: Table[string, (string, JsonNode)]
): Result[WakuNodeConf, string] =
## Build a WakuNodeConf from the messaging shape
## `{mode, overrides, preset?, additions?}`. `mode` and `overrides` are
## required. Order: overrides applied first, then additions concat.
var conf = ?defaultWakuNodeConf()
var fields = jsonFields
if not fields.hasKey(KeyMode):
return err("messaging shape requires '" & KeyMode & "' key")
if not fields.hasKey(KeyOverrides):
return err("messaging shape requires '" & KeyOverrides & "' key")
let modeStr = jsonScalarToString(fields[KeyMode][1]).valueOr:
return err("Failed to parse '" & KeyMode & "': " & error)
try:
conf.mode = parseCmdArg(WakuMode, modeStr)
except CatchableError as e:
return err("Failed to parse '" & KeyMode & "': " & e.msg & ". Value: " & modeStr)
fields.del(KeyMode)
if fields.hasKey(KeyPreset):
let presetStr = jsonScalarToString(fields[KeyPreset][1]).valueOr:
return err("Failed to parse '" & KeyPreset & "': " & error)
conf.preset = presetStr
fields.del(KeyPreset)
let overridesNode = fields[KeyOverrides][1]
if overridesNode.kind != JObject:
return err("'" & KeyOverrides & "' must be a JSON object")
?rejectTopLevelOnlyKeysInside(overridesNode, KeyOverrides)
?applyJsonAsOverride(conf, overridesNode)
fields.del(KeyOverrides)
if fields.hasKey(KeyAdditions):
let additionsNode = fields[KeyAdditions][1]
if additionsNode.kind != JObject:
return err("'" & KeyAdditions & "' must be a JSON object")
?rejectTopLevelOnlyKeysInside(additionsNode, KeyAdditions)
?applyJsonAsAddition(conf, additionsNode)
fields.del(KeyAdditions)
if fields.len > 0:
return
err(unknownKeysError(fields, "Unrecognized top-level key(s) in messaging shape"))
return ok(conf)
proc assembleFullConf*(
jsonFields: Table[string, (string, JsonNode)]
): Result[WakuNodeConf, string] =
## Build a WakuNodeConf from a flat JSON object whose keys are WakuNodeConf field names.
var conf = ?defaultWakuNodeConf()
var fields = jsonFields
?applyJsonFieldsToConf(
conf, fields, "Failed to parse field", "Unrecognized configuration option(s) found"
)
return ok(conf)
proc parseConfJson*(jsonStr: string): Result[WakuNodeConf, string] =
## Parse a JSON config, route to messaging or full-config shape based on
## whether `overrides` or `additions` fields are in the config object top-level.
var jsonNode: JsonNode
try:
jsonNode = parseJson(jsonStr)
except CatchableError as e:
return err("Failed to parse config JSON: " & e.msg)
let jsonFields = ?collectJsonFields(jsonNode)
let isMessagingShape =
jsonFields.hasKey(KeyOverrides) or jsonFields.hasKey(KeyAdditions)
if isMessagingShape:
return assembleMessagingConf(jsonFields)
else:
return assembleFullConf(jsonFields)