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
commit 7f077a76fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 14 deletions

View file

@ -118,9 +118,11 @@
- `json.%`,`json.to`, `jsonutils.formJson`,`jsonutils.toJson` now work with `uint|uint64` - `json.%`,`json.to`, `jsonutils.formJson`,`jsonutils.toJson` now work with `uint|uint64`
instead of raising (as in 1.4) or giving wrong results (as in 1.2). instead of raising (as in 1.4) or giving wrong results (as in 1.2).
- `jsonutils` now handles `cstring` (including as Table key), and `set`.
- added `jsonutils.jsonTo` overload with `opt = Joptions()` param. - added `jsonutils.jsonTo` overload with `opt = Joptions()` param.
- `jsonutils` now handles `cstring` (including as Table key), and `set`. - `jsonutils.toJson` now supports customization via `ToJsonOptions`.
- Added an overload for the `collect` macro that inferes the container type based - Added an overload for the `collect` macro that inferes the container type based
on the syntax of the last expression. Works with std seqs, tables and sets. on the syntax of the last expression. Works with std seqs, tables and sets.
@ -138,6 +140,7 @@
- Added `std/enumutils` module. Added `genEnumCaseStmt` macro that generates case statement to parse string to enum. - Added `std/enumutils` module. Added `genEnumCaseStmt` macro that generates case statement to parse string to enum.
Added `items` for enums with holes. Added `items` for enums with holes.
Added `symbolName` to return the enum symbol name ignoring the human readable name. Added `symbolName` to return the enum symbol name ignoring the human readable name.
Added `symbolRank` to return the index in which an enum member is listed in an enum.
- Added `typetraits.HoleyEnum` for enums with holes, `OrdinalEnum` for enums without holes. - Added `typetraits.HoleyEnum` for enums with holes, `OrdinalEnum` for enums without holes.

View file

@ -86,8 +86,67 @@ iterator items*[T: HoleyEnum](E: typedesc[T]): T =
assert B[float].toSeq == [B[float].b0, B[float].b1] assert B[float].toSeq == [B[float].b0, B[float].b1]
for a in enumFullRange(E): yield a 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. ## Returns the symbol name of an enum.
##
## This uses `symbolRank`.
runnableExamples: runnableExamples:
type B = enum type B = enum
b0 = (10, "kb0") b0 = (10, "kb0")
@ -97,5 +156,7 @@ func symbolName*[T: OrdinalEnum](a: T): string =
assert b.symbolName == "b0" assert b.symbolName == "b0"
assert $b == "kb0" assert $b == "kb0"
static: assert B.high.symbolName == "b2" static: assert B.high.symbolName == "b2"
type C = enum c0 = -3, c1 = 4, c2 = 20 # HoleyEnum
assert c1.symbolName == "c1"
const names = enumNames(T) 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 import macros
from enumutils import symbolName
from typetraits import OrdinalEnum
type type
Joptions* = object Joptions* = object # xxx rename FromJsonOptions
## Options controlling the behavior of `fromJson`. ## Options controlling the behavior of `fromJson`.
allowExtraKeys*: bool allowExtraKeys*: bool
## If `true` Nim's object to which the JSON is parsed is not required to ## 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 ## If `true` Nim's object to which JSON is parsed is allowed to have
## fields without corresponding JSON keys. ## fields without corresponding JSON keys.
# in future work: a key rename could be added # 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 isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
proc distinctBase(T: typedesc): typedesc {.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` ## reverse of `toJson`
fromJson(result, b, opt) 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 ## serializes `a` to json; uses `toJsonHook(a: T)` if it's in scope to
## customize serialization, see strtabs.toJsonHook for an example. ## customize serialization, see strtabs.toJsonHook for an example.
when compiles(toJsonHook(a)): result = toJsonHook(a) when compiles(toJsonHook(a)): result = toJsonHook(a)
elif T is object | tuple: elif T is object | tuple:
when T is object or isNamedTuple(T): when T is object or isNamedTuple(T):
result = newJObject() result = newJObject()
for k, v in a.fieldPairs: result[k] = toJson(v) for k, v in a.fieldPairs: result[k] = toJson(v, opt)
else: else:
result = newJArray() 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: elif T is ref | ptr:
if system.`==`(a, nil): result = newJNull() if system.`==`(a, nil): result = newJNull()
else: result = toJson(a[]) else: result = toJson(a[], opt)
elif T is array | seq | set: elif T is array | seq | set:
result = newJArray() result = newJArray()
for ai in a: result.add toJson(ai) for ai in a: result.add toJson(ai, opt)
elif T is pointer: result = toJson(cast[int](a)) elif T is pointer: result = toJson(cast[int](a), opt)
# edge case: `a == nil` could've also led to `newJNull()`, but this results # edge case: `a == nil` could've also led to `newJNull()`, but this results
# in simpler code for `toJson` and `fromJson`. # 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 bool: result = %(a)
elif T is SomeInteger: result = %a elif T is SomeInteger: result = %a
elif T is Ordinal: result = %(a.ord)
elif T is enum: elif T is enum:
when defined(nimLegacyJsonutilsHoleyEnum): result = %a case opt.enumMode
else: result = %(a.ord) 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) elif T is cstring: (if a == nil: result = newJNull() else: result = % $a)
else: result = %a else: result = %a

View file

@ -35,6 +35,13 @@ type Foo = ref object
proc `==`(a, b: Foo): bool = proc `==`(a, b: Foo): bool =
a.id == b.id a.id == b.id
type MyEnum = enum me0, me1 = "me1Alt", me2, me3, me4
proc `$`(a: MyEnum): string =
# putting this here pending https://github.com/nim-lang/Nim/issues/13747
if a == me2: "me2Modif"
else: system.`$`(a)
template fn() = template fn() =
block: # toJson, jsonTo block: # toJson, jsonTo
type Foo = distinct float type Foo = distinct float
@ -83,6 +90,12 @@ template fn() =
doAssert b2.ord == 1 # explains the `1` doAssert b2.ord == 1 # explains the `1`
testRoundtrip(a): """[1,2,3]""" testRoundtrip(a): """[1,2,3]"""
block: # ToJsonOptions
let a = (me1, me2)
doAssert $a.toJson() == "[1,2]"
doAssert $a.toJson(ToJsonOptions(enumMode: joptEnumSymbol)) == """["me1","me2"]"""
doAssert $a.toJson(ToJsonOptions(enumMode: joptEnumString)) == """["me1Alt","me2Modif"]"""
block: # set block: # set
type Foo = enum f1, f2, f3, f4, f5 type Foo = enum f1, f2, f3, f4, f5
type Goo = enum g1 = 10, g2 = 15, g3 = 17, g4 type Goo = enum g1 = 10, g2 = 15, g3 = 17, g4