jsonutils: add customization for toJson via ToJsonOptions; generalize symbolName; add symbolRank (#18029)

* jsonutils: add customization for toJson via `ToJsonOptions`

* add enumutils.symbolRank

* lookup table implementation for HoleyEnum

* cleanup

* changelog

* fixup

* Update lib/std/jsonutils.nim

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
This commit is contained in:
Timothee Cour
2021-05-18 06:10:19 -07:00
committed by GitHub
parent 6e0fe965da
commit 7f077a76fe
4 changed files with 112 additions and 14 deletions

View File

@@ -86,8 +86,67 @@ iterator items*[T: HoleyEnum](E: typedesc[T]): T =
assert B[float].toSeq == [B[float].b0, B[float].b1]
for a in enumFullRange(E): yield a
func symbolName*[T: OrdinalEnum](a: T): string =
func span(T: typedesc[HoleyEnum]): int =
(T.high.ord - T.low.ord) + 1
const invalidSlot = uint8.high
proc genLookup[T: typedesc[HoleyEnum]](_: T): auto =
const n = span(T)
var ret: array[n, uint8]
var i = 0
assert n <= invalidSlot.int
for ai in mitems(ret): ai = invalidSlot
for ai in items(T):
ret[ai.ord - T.low.ord] = uint8(i)
inc(i)
return ret
func symbolRankImpl[T](a: T): int {.inline.} =
const n = T.span
const thres = 255 # must be <= `invalidSlot`, but this should be tuned.
when n <= thres:
const lookup = genLookup(T)
let lookup2 {.global.} = lookup # xxx improve pending https://github.com/timotheecour/Nim/issues/553
#[
This could be optimized using a hash adapted to `T` (possible since it's known at CT)
to get better key distribution before indexing into the lookup table table.
]#
{.noSideEffect.}: # because it's immutable
let ret = lookup2[ord(a) - T.low.ord]
if ret != invalidSlot: return ret.int
else:
var i = 0
# we could also generate a case statement as optimization
for ai in items(T):
if ai == a: return i
inc(i)
raise newException(IndexDefect, $ord(a) & " invalid for " & $T)
template symbolRank*[T: enum](a: T): int =
## Returns the index in which `a` is listed in `T`.
##
## The cost for a `HoleyEnum` is implementation defined, currently optimized
## for small enums, otherwise is `O(T.enumLen)`.
runnableExamples:
type
A = enum a0 = -3, a1 = 10, a2, a3 = (20, "f3Alt") # HoleyEnum
B = enum b0, b1, b2 # OrdinalEnum
C = enum c0 = 10, c1, c2 # OrdinalEnum
assert a2.symbolRank == 2
assert b2.symbolRank == 2
assert c2.symbolRank == 2
assert c2.ord == 12
assert a2.ord == 11
var invalid = 7.A
doAssertRaises(IndexDefect): discard invalid.symbolRank
when T is Ordinal: ord(a) - T.low.ord.static
else: symbolRankImpl(a)
func symbolName*[T: enum](a: T): string =
## Returns the symbol name of an enum.
##
## This uses `symbolRank`.
runnableExamples:
type B = enum
b0 = (10, "kb0")
@@ -97,5 +156,7 @@ func symbolName*[T: OrdinalEnum](a: T): string =
assert b.symbolName == "b0"
assert $b == "kb0"
static: assert B.high.symbolName == "b2"
type C = enum c0 = -3, c1 = 4, c2 = 20 # HoleyEnum
assert c1.symbolName == "c1"
const names = enumNames(T)
names[a.ord - T.low.ord]
names[a.symbolRank]

View File

@@ -31,9 +31,11 @@ add a way to customize serialization, for e.g.:
]#
import macros
from enumutils import symbolName
from typetraits import OrdinalEnum
type
Joptions* = object
Joptions* = object # xxx rename FromJsonOptions
## Options controlling the behavior of `fromJson`.
allowExtraKeys*: bool
## If `true` Nim's object to which the JSON is parsed is not required to
@@ -42,6 +44,17 @@ type
## If `true` Nim's object to which JSON is parsed is allowed to have
## fields without corresponding JSON keys.
# in future work: a key rename could be added
EnumMode* = enum
joptEnumOrd
joptEnumSymbol
joptEnumString
ToJsonOptions* = object
enumMode*: EnumMode
# xxx charMode
proc initToJsonOptions*(): ToJsonOptions =
## initializes `ToJsonOptions` with sane options.
ToJsonOptions(enumMode: joptEnumOrd)
proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
@@ -261,33 +274,41 @@ proc jsonTo*(b: JsonNode, T: typedesc, opt = Joptions()): T =
## reverse of `toJson`
fromJson(result, b, opt)
proc toJson*[T](a: T): JsonNode =
proc toJson*[T](a: T, opt = initToJsonOptions()): JsonNode =
## serializes `a` to json; uses `toJsonHook(a: T)` if it's in scope to
## customize serialization, see strtabs.toJsonHook for an example.
when compiles(toJsonHook(a)): result = toJsonHook(a)
elif T is object | tuple:
when T is object or isNamedTuple(T):
result = newJObject()
for k, v in a.fieldPairs: result[k] = toJson(v)
for k, v in a.fieldPairs: result[k] = toJson(v, opt)
else:
result = newJArray()
for v in a.fields: result.add toJson(v)
for v in a.fields: result.add toJson(v, opt)
elif T is ref | ptr:
if system.`==`(a, nil): result = newJNull()
else: result = toJson(a[])
else: result = toJson(a[], opt)
elif T is array | seq | set:
result = newJArray()
for ai in a: result.add toJson(ai)
elif T is pointer: result = toJson(cast[int](a))
for ai in a: result.add toJson(ai, opt)
elif T is pointer: result = toJson(cast[int](a), opt)
# edge case: `a == nil` could've also led to `newJNull()`, but this results
# in simpler code for `toJson` and `fromJson`.
elif T is distinct: result = toJson(a.distinctBase)
elif T is distinct: result = toJson(a.distinctBase, opt)
elif T is bool: result = %(a)
elif T is SomeInteger: result = %a
elif T is Ordinal: result = %(a.ord)
elif T is enum:
when defined(nimLegacyJsonutilsHoleyEnum): result = %a
else: result = %(a.ord)
case opt.enumMode
of joptEnumOrd:
when T is Ordinal or not defined(nimLegacyJsonutilsHoleyEnum): %(a.ord)
else: toJson($a, opt)
of joptEnumSymbol:
when T is OrdinalEnum:
toJson(symbolName(a), opt)
else:
toJson($a, opt)
of joptEnumString: toJson($a, opt)
elif T is Ordinal: result = %(a.ord)
elif T is cstring: (if a == nil: result = newJNull() else: result = % $a)
else: result = %a