src/kdl/decoder

Search:
Group by:
Source   Edit  

Decoder

This module implements a deserializer for KDL documents, nodes and values into different types and objects:

  • char
  • bool
  • string
  • Option[T]
  • SomeNumber
  • StringTableRef
  • enum and HoleyEnum
  • KdlVal
  • seq[T] and array[I, T]
  • set[Ordinal], HashSet[A] and OrderedSet[A]
  • Table[string, T] and OrderedTable[string, T]
  • object, ref and tuple (including object variants with multiple discriminator fields)
  • Plus any type you implement.

Example:

import src/kdl/decoder
import kdl

type
  Package = object
    name*, version*: string
    authors*: Option[seq[string]]
    description*, licenseFile*, edition*: Option[string]

  Deps = Table[string, string]

const doc = parseKdl("""
package {
  name "kdl"
  version "0.0.0"
  description "kats document language"
  authors "Kat Marchán <kzm@zkat.tech>"
  license-file "LICENSE.md"
  edition "2018"
}
dependencies {
  nom "6.0.1"
  thiserror "1.0.22"
}""")

const package = doc.decodeKdl(Package, "package")
const dependencies = doc.decodeKdl(Deps, "dependencies")

assert package == Package(
  name: "kdl",
  version: "0.0.0",
  authors: @["Kat Marchán <kzm@zkat.tech>"].some,
  description: "kats document language".some,
  licenseFile: "LICENSE.md".some,
  edition: "2018".some
)
assert dependencies == {"nom": "6.0.1", "thiserror": "1.0.22"}.toTable

Custom Hooks

Init hook

With init hooks you can initialize types with default values before decoding. Use the following signature when overloading decodeInitKdl:

proc initHookKdl*[T](v: var T) =
Note: by default if you change a discrimantor field of an object variant in an init hook (`v.kind = kInt`), it will throw a compile error. If you want to disable it, compile with the following flag -d:kdlDecoderNoCaseTransitionError.

Example:

import src/kdl/decoder
import kdl

type Foo = object
  x*: int

proc initHookKdl*(v: var Foo) =
  v.x = 5 # You may also do `v = Foo(x: 5)`

assert parseKdl("").decodeKdl(Foo) == Foo(x: 5)

Post hook

Post hooks are called after decoding any (default, for custom decode hooks you have to call postHookable(v) explicitly) type.

Overloads of postHook must use the following signature:

proc postHookKdl(v: var MyType)

Example:

import src/kdl/decoder
import kdl

type Foo = object
  x*: int

proc postHookKdl(v: var Foo) =
  inc v.x

assert parseKdl("x 1").decodeKdl(Foo) == Foo(
    x: 2) # 2 because x after postHook got incremented by one

Enum hook

Enum hooks are useful for parsing enums in a custom manner.

You can overload enumHook with two different signatures:

proc enumHookKdl(a: string, v: var MyEnum)

proc enumHookKdl(a: int, v: var MyEnum)
Note: by default decoding an integer into a holey enum raises an error, to override this behaviour compile with -d:kdlDecoderAllowHoleyEnums.

Example:

import src/kdl/decoder
import std/[strformat, strutils]
import kdl

type MyEnum = enum
  meNorth, meSouth, meWest, meEast

proc enumHookKdl(a: string, v: var MyEnum) =
  case a.toLowerAscii
  of "north":
    v = meNorth
  of "south":
    v = meSouth
  of "west":
    v = meWest
  of "east":
    v = meEast
  else:
    raise newException(ValueError, &"invalid enum value {a} for {$typeof(v)}")

proc enumHookKdl(a: int, v: var MyEnum) =
  case a
  of 0xbeef:
    v = meNorth
  of 0xcafe:
    v = meSouth
  of 0xface:
    v = meWest
  of 0xdead:
    v = meEast
  else:
    raise newException(ValueError, &"invalid enum value {a} for {$typeof(v)}")

assert parseKdl("""
  node "north" "south" "west" "east"
  """).decodeKdl(seq[MyEnum], "node") == @[meNorth, meSouth, meWest, meEast]

assert parseKdl("""
  node 0xbeef 0xcafe 0xface 0xdead
  """).decodeKdl(seq[MyEnum], "node") == @[meNorth, meSouth, meWest, meEast]

Rename hook

As its name suggests, a rename hook renames the fields of an object in any way you want.

Follow this signature when overloading renameHook:

proc renameHookKdl(_: typedesc[MyType], fieldName: var string)

Example:

import src/kdl/decoder
import kdl

type Foo = object
  kind*: string
  list*: seq[int]

proc renameHookKdl(_: typedesc[Foo], fieldName: var string) =
  fieldName =
    case fieldName
    of "type":
      "kind"
    of "array":
      "list"
    else:
      fieldName

# Here we rename "type" to "kind" and "array" to "list".
assert parseKdl("""
  type "string"
  array 1 2 3
  """).decodeKdl(Foo) == Foo(kind: "string", list: @[1, 2, 3])

Decode hook

Use custom decode hooks to decode your types, your way.

To do it you have to overload the decodeKdl procedure with the following signature:

proc decodeKdl*(a: KdlSome, v: var MyType) =
Where KdlSome is one of KdlDoc, KdlNode or KdlVal:

  • KdlDoc is called to decode a document.
  • KdlNode is called to decode a node inside a document or inside another node's children.
  • KdlVal is called to decode arguments or properties of a node.

To use all the hooks explained above you don't call them with initHookKdl instead use decodeInitKdl: . decodeInitKdl instead of initHookKdl. . decodePostKdl instead of postHookKdl. . decodeEnumKdl instead of enumHookKdl. . decodeRenameKdl instead of renameHookKdl. Note: you can check the signatures of those procedures below in the documentation. Read the following example:

Example:

import src/kdl/decoder
import std/times
import kdl
import kdl/utils # kdl/utils define some useful internal procedures such as `eqIdent`, which checks the equality of two strings ignore case, underscores and dashes in an efficient way.

proc decodeKdl*(a: KdlVal, v: var DateTime) =
  decodeInitKdl(v)
  assert a.isString
  v = a.getString.parse("yyyy-MM-dd")
  decodePostKdl(v)

proc decodeKdl*(a: KdlNode, v: var DateTime) =
  decodeInitKdl(v)
  assert a.args.len in {1, 3, 6}
  case a.args.len
  of 6: # year month day hour minute second
    v = dateTime(
      a.args[0].decodeKdl(int),
      a.args[1].decodeKdl(Month),
      a.args[2].decodeKdl(MonthdayRange),
      a.args[3].decodeKdl(HourRange),
      a.args[4].decodeKdl(MinuteRange),
      a.args[5].decodeKdl(SecondRange)
    )
  of 3: # year month day
    v = dateTime(
      a.args[0].decodeKdl(int),
      a.args[1].decodeKdl(Month),
      a.args[2].decodeKdl(MonthdayRange),
    )
  of 1: # yyyy-MM-dd
    a.args[0].decodeKdl(v)
  else:
    discard

  if "hour" in a.props:
    v.hour = a.props["hour"].getInt
  if "minute" in a.props:
    v.minute = a.props["minute"].getInt
  if "second" in a.props:
    v.second = a.props["second"].getInt
  if "nanosecond" in a.props:
    v.nanosecond = a.props["nanosecond"].getInt
  if "offset" in a.props:
    v.utcOffset = a.props["offset"].get(int)

  decodePostKdl(v)

proc decodeKdl*(a: KdlDoc, v: var DateTime) =
  decodeInitKdl(v)
  if a.len == 0: return

  var
    year: int
    month: Month
    day: MonthdayRange = 1
    hour: HourRange
    minute: MinuteRange
    second: SecondRange
    nanosecond: NanosecondRange

  for node in a:
    if node.name.eqIdent "year":
      node.decodeKdl(year)
    elif node.name.eqIdent "month":
      node.decodeKdl(month)
    elif node.name.eqIdent "day":
      node.decodeKdl(day)
    elif node.name.eqIdent "hour":
      node.decodeKdl(hour)
    elif node.name.eqIdent "minute":
      node.decodeKdl(minute)
    elif node.name.eqIdent "second":
      node.decodeKdl(second)
    elif node.name.eqIdent "nanosecond":
      node.decodeKdl(nanosecond)

  v = dateTime(year, month, day, hour, minute, second, nanosecond)

  decodePostKdl(v)

# Here we use the KdlDoc overload
assert parseKdl("""
  year 2022
  month 10 // or "October"
  day 15
  hour 12
  minute 10
  """).decodeKdl(DateTime) == dateTime(2022, mOct, 15, 12, 10)

# Here we use the KdlNode overload
assert parseKdl("date 2022 \"October\" 15 12 04 00").decodeKdl(DateTime,
    "date") == dateTime(2022, mOct, 15, 12, 04)
# And here we use the KdlVal overload
assert parseKdl("author birthday=\"2000-10-15\" name=\"Nobody\"")[0][
    "birthday"].decodeKdl(DateTime) == dateTime(2000, mOct, 15)

As you may have noticed if you looked through the API, there is decodeInitKdl and initHookable, enumHook and enumHookable. Any hook suffixed -able, actually calls the hook itself after making sure there is an overload that matches it. You should not overload these as they are meant for internal use, the reason they are exported is because when implementing your custom decode hooks you may also want to use them.

So remember: for custom behaviour, overload -hook suffixed procedures; to make use of these hooks call the -hookable suffixed procedures, you don't call these unless you want their behavior within your custom decode hooks.


All of these examples were taken out from the tests, so if you need more, check them out.

Procs

proc decodeEnumKdl[T: enum](a: auto; v: var T)
Source   Edit  
proc decodeInitKdl(v: var auto)
Source   Edit  
proc decodeKdl(a: KdlDoc; v: var auto; name: string)
Source   Edit  
proc decodeKdl(a: KdlDoc; v: var List)
Source   Edit  
proc decodeKdl(a: KdlDoc; v: var Object)
Source   Edit  
proc decodeKdl(a: KdlDoc; v: var StringTableRef) {....raises: [Exception],
    tags: [RootEffect], forbids: [].}
Source   Edit  
proc decodeKdl(a: KdlNode; v: var auto)
Source   Edit  
proc decodeKdl(a: KdlNode; v: var KdlNode) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc decodeKdl(a: KdlNode; v: var List)
Source   Edit  
proc decodeKdl(a: KdlNode; v: var Object)
Source   Edit  
proc decodeKdl(a: KdlNode; v: var StringTableRef) {....raises: [Exception],
    tags: [RootEffect], forbids: [].}
Source   Edit  
proc decodeKdl(a: KdlVal; v: var char) {....raises: [KdlError], tags: [],
    forbids: [].}
Source   Edit  
proc decodeKdl(a: KdlVal; v: var Object)
Source   Edit  
proc decodeKdl(a: KdlVal; v: var SomeTable)
Source   Edit  
proc decodeKdl[T: array](a: KdlVal; v: var T)
Source   Edit  
proc decodeKdl[T: enum](a: KdlVal; v: var T)
Source   Edit  
proc decodeKdl[T: KdlNode or KdlDoc](a: KdlVal; v: var T)
Source   Edit  
proc decodeKdl[T: KdlSome](a: T; v: var T)
Source   Edit  
proc decodeKdl[T: not KdlNode](a: KdlVal; v: var seq[T])
Source   Edit  
proc decodeKdl[T: Ordinal](a: KdlDoc; v: var set[T])
Source   Edit  
proc decodeKdl[T: Ordinal](a: KdlNode; v: var set[T])
Source   Edit  
proc decodeKdl[T: Ordinal](a: KdlVal; v: var set[T])
Source   Edit  
proc decodeKdl[T: Value](a: KdlVal; v: var T)
Source   Edit  
proc decodeKdl[T](a: KdlDoc; _: typedesc[T]; name: string): T
Source   Edit  
proc decodeKdl[T](a: KdlDoc; v: var ref T)
Source   Edit  
proc decodeKdl[T](a: KdlDoc; v: var SomeSet[T])
Source   Edit  
proc decodeKdl[T](a: KdlDoc; v: var SomeTable[string, T])
Source   Edit  
proc decodeKdl[T](a: KdlNode; v: var Option[T])
Source   Edit  
proc decodeKdl[T](a: KdlNode; v: var ref T)
Source   Edit  
proc decodeKdl[T](a: KdlNode; v: var SomeSet[T])
Source   Edit  
proc decodeKdl[T](a: KdlNode; v: var SomeTable[string, T])
Source   Edit  
proc decodeKdl[T](a: KdlSome; _: typedesc[T]): T
Source   Edit  
proc decodeKdl[T](a: KdlVal; v: var Option[T])
Source   Edit  
proc decodeKdl[T](a: KdlVal; v: var ref T)
Source   Edit  
proc decodeKdl[T](a: KdlVal; v: var SomeSet[T])
Source   Edit  
proc decodePostKdl(v: var auto)
Source   Edit  
proc decodeRenameKdl(a: typedesc; fieldName: string): string
Source   Edit  
proc enumHookKdl(a: KdlVal; v: var enum)
Source   Edit  
proc enumHookKdl[T: enum](a: int; v: var T)
Source   Edit  
proc enumHookKdl[T: enum](a: string; v: var T)
Source   Edit  
proc initHookKdl[T](v: var ref T)
Source   Edit  
proc initHookKdl[T](v: var T)
Source   Edit  
proc postHookKdl(v: var auto)
Source   Edit  
proc renameHookKdl(a: typedesc; fieldName: var string)
Source   Edit