The default design is always procedural programming:
import std / [strformat, strutils, tables]
proc error(msg: string) = echo msg
proc wantNumberBetween(field, s: string; a, b: int) =
try:
let x = parseInt(s)
if x < a or x > b:
error(&"{field} of value {s} must be in range {a}..{b}")
except ValueError:
error(&"{field} takes a number")
template intField(name: string; a, b: int) {.dirty.} =
if name in fields:
wantNumberBetween name, fields[name], a, b
del fields, name # mark as processed
proc validateDecoderA(fields: var Table[string, string]) =
intField "abc", 1, 3
intField "xyz", 5, 6
proc validateDecoderB(fields: var Table[string, string]) =
# "inheritance" is simply done with a function call
validateDecoderA(fields)
intField "B specfiic field", 12, 16
proc noRemainingFields(fields: Table[string, string]) =
for k, v in pairs(fields):
error &"unknown field: {k} of value {v}"
proc validate(decoder: string; fields: Table[string, string]) =
var fullCopy = fields
case decoder
of "A": validateDecoderA fullCopy
of "B": validateDecoderB fullCopy
noRemainingFields fullCopy
validate "A", {"abc": "34", "unknown": "abc"}.toTable
Run
That's the entire logic but you will have "hundreds" of entries like `intField
"abc", 1, 2`. But any solution requires these and by using Nim code you can
compress the descriptions in ways that are usually outside the realm of custom
text formats that you interpret at runtime.