use string value when encoding enums (#55)
Currently, we encode enum values always as the numeric value `ord(val)`. unless explicitly overridden using `serializesAsTextInJson` or with a custom `writeValue` implementation. Reduce verbosity by automatically doing that.
This commit is contained in:
parent
b06b8ca4b1
commit
ed4440d881
|
@ -1,14 +1,15 @@
|
|||
{.experimental: "notnil".}
|
||||
|
||||
import
|
||||
std/[tables, strutils, typetraits, macros, strformat],
|
||||
std/[tables, macros, strformat],
|
||||
stew/[enums, objects], stew/shims/[enumutils, typetraits],
|
||||
faststreams/inputs, serialization/[formats, object_serialization, errors],
|
||||
"."/[format, types, lexer]
|
||||
|
||||
from json import JsonNode, JsonNodeKind
|
||||
|
||||
export
|
||||
inputs, format, types, errors
|
||||
enumutils, inputs, format, types, errors
|
||||
|
||||
type
|
||||
JsonReader*[Flavor = DefaultFlavor] = object
|
||||
|
@ -27,7 +28,8 @@ type
|
|||
etValue = "value"
|
||||
etBool = "bool literal"
|
||||
etInt = "integer"
|
||||
etEnum = "enum value (int or string)"
|
||||
etEnumAny = "enum value (int / string)"
|
||||
etEnumString = "enum value (string)"
|
||||
etNumber = "number"
|
||||
etString = "string"
|
||||
etComma = "comma"
|
||||
|
@ -152,9 +154,6 @@ proc init*(T: type JsonReader,
|
|||
result.lexer = JsonLexer.init(stream, mode)
|
||||
result.lexer.next()
|
||||
|
||||
proc setParsed[T: enum](e: var T, s: string) =
|
||||
e = parseEnum[T](s)
|
||||
|
||||
proc requireToken*(r: var JsonReader, tk: TokKind) =
|
||||
if r.lexer.tok != tk:
|
||||
r.raiseUnexpectedToken case tk
|
||||
|
@ -449,6 +448,49 @@ func isBitwiseSubsetOf[N](lhs, rhs: array[N, uint]): bool =
|
|||
template isCharArray[N](v: array[N, char]): bool = true
|
||||
template isCharArray(v: auto): bool = false
|
||||
|
||||
func parseStringEnum[T](
|
||||
r: var JsonReader, value: var T,
|
||||
stringNormalizer: static[proc(s: string): string]) =
|
||||
try:
|
||||
value = genEnumCaseStmt(
|
||||
T, r.lexer.strVal,
|
||||
default = nil, ord(T.low), ord(T.high), stringNormalizer)
|
||||
except ValueError as err:
|
||||
const typeName = typetraits.name(T)
|
||||
r.raiseUnexpectedValue("Invalid value for '" & typeName & "'")
|
||||
|
||||
func strictNormalize(s: string): string = # Match enum value exactly
|
||||
s
|
||||
|
||||
proc parseEnum[T](
|
||||
r: var JsonReader, value: var T, allowNumericRepr: static[bool] = false,
|
||||
stringNormalizer: static[proc(s: string): string] = strictNormalize) =
|
||||
const style = T.enumStyle
|
||||
let tok = r.lexer.tok
|
||||
case tok
|
||||
of tkString:
|
||||
r.parseStringEnum(value, stringNormalizer)
|
||||
of tkInt:
|
||||
when allowNumericRepr:
|
||||
case style
|
||||
of EnumStyle.Numeric:
|
||||
if not value.checkedEnumAssign(r.lexer.absIntVal):
|
||||
const typeName = typetraits.name(T)
|
||||
r.raiseUnexpectedValue("Out of range for '" & typeName & "'")
|
||||
of EnumStyle.AssociatedStrings:
|
||||
r.raiseUnexpectedToken etEnumString
|
||||
else:
|
||||
r.raiseUnexpectedToken etEnumString
|
||||
else:
|
||||
case style
|
||||
of EnumStyle.Numeric:
|
||||
when allowNumericRepr:
|
||||
r.raiseUnexpectedToken etEnumAny
|
||||
else:
|
||||
r.raiseUnexpectedToken etEnumString
|
||||
of EnumStyle.AssociatedStrings:
|
||||
r.raiseUnexpectedToken etEnumString
|
||||
|
||||
proc readValue*[T](r: var JsonReader, value: var T)
|
||||
{.gcsafe, raises: [SerializationError, IOError, Defect].} =
|
||||
## Master filed/object parser. This function relies on customised sub-mixins for particular
|
||||
|
@ -540,18 +582,7 @@ proc readValue*[T](r: var JsonReader, value: var T)
|
|||
value[] = readValue(r, type(value[]))
|
||||
|
||||
elif value is enum:
|
||||
case tok
|
||||
of tkString:
|
||||
try:
|
||||
value.setParsed(r.lexer.strVal)
|
||||
except ValueError as err:
|
||||
const typeName = typetraits.name(T)
|
||||
r.raiseUnexpectedValue("Expected valid '" & typeName & "' value")
|
||||
of tkInt:
|
||||
# TODO: validate that the value is in range
|
||||
value = type(value)(r.lexer.absIntVal)
|
||||
else:
|
||||
r.raiseUnexpectedToken etEnum
|
||||
r.parseEnum(value)
|
||||
r.lexer.next()
|
||||
|
||||
elif value is SomeInteger:
|
||||
|
@ -673,3 +704,10 @@ iterator readObjectFields*(r: var JsonReader): string =
|
|||
for key in readObjectFields(r, string):
|
||||
yield key
|
||||
|
||||
template configureJsonDeserialization*(
|
||||
T: type[enum], allowNumericRepr: static[bool] = false,
|
||||
stringNormalizer: static[proc(s: string): string] = strictNormalize) =
|
||||
proc readValue*(r: var JsonReader, value: var T) {.
|
||||
raises: [Defect, IOError, SerializationError].} =
|
||||
static: doAssert not allowNumericRepr or enumStyle(T) == EnumStyle.Numeric
|
||||
r.parseEnum(value, allowNumericRepr, stringNormalizer)
|
||||
|
|
|
@ -215,7 +215,7 @@ proc writeValue*(w: var JsonWriter, value: auto) =
|
|||
append if value: "true" else: "false"
|
||||
|
||||
elif value is enum:
|
||||
w.stream.writeText ord(value)
|
||||
w.writeValue $value
|
||||
|
||||
elif value is range:
|
||||
when low(typeof(value)) < 0:
|
||||
|
@ -258,4 +258,3 @@ proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
|
|||
template serializesAsTextInJson*(T: type[enum]) =
|
||||
template writeValue*(w: var JsonWriter, val: T) =
|
||||
w.writeValue $val
|
||||
|
||||
|
|
|
@ -189,6 +189,68 @@ var invalid = Invalid(distance: Mile(100))
|
|||
when false: reject invalid.toJson
|
||||
else: discard invalid
|
||||
|
||||
when (NimMajor, NimMinor) < (1, 4): # Copy from `std/strutils`
|
||||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2012 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
func nimIdentNormalize*(s: string): string =
|
||||
## Normalizes the string `s` as a Nim identifier.
|
||||
##
|
||||
## That means to convert to lower case and remove any '_' on all characters
|
||||
## except first one.
|
||||
runnableExamples:
|
||||
doAssert nimIdentNormalize("Foo_bar") == "Foobar"
|
||||
result = newString(s.len)
|
||||
if s.len > 0:
|
||||
result[0] = s[0]
|
||||
var j = 1
|
||||
for i in 1..len(s) - 1:
|
||||
if s[i] in {'A'..'Z'}:
|
||||
result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
|
||||
inc j
|
||||
elif s[i] != '_':
|
||||
result[j] = s[i]
|
||||
inc j
|
||||
if j != s.len: setLen(result, j)
|
||||
|
||||
type EnumTestX = enum
|
||||
x0,
|
||||
x1,
|
||||
x2
|
||||
|
||||
type EnumTestY = enum
|
||||
y1 = 1,
|
||||
y3 = 3,
|
||||
y4,
|
||||
y6 = 6
|
||||
EnumTestY.configureJsonDeserialization(
|
||||
allowNumericRepr = true)
|
||||
|
||||
type EnumTestZ = enum
|
||||
z1 = "aaa",
|
||||
z2 = "bbb",
|
||||
z3 = "ccc"
|
||||
|
||||
type EnumTestN = enum
|
||||
n1 = "aaa",
|
||||
n2 = "bbb",
|
||||
n3 = "ccc"
|
||||
EnumTestN.configureJsonDeserialization(
|
||||
stringNormalizer = nimIdentNormalize)
|
||||
|
||||
type EnumTestO = enum
|
||||
o1,
|
||||
o2,
|
||||
o3
|
||||
EnumTestO.configureJsonDeserialization(
|
||||
allowNumericRepr = true,
|
||||
stringNormalizer = nimIdentNormalize)
|
||||
|
||||
suite "toJson tests":
|
||||
test "encode primitives":
|
||||
check:
|
||||
|
@ -196,6 +258,141 @@ suite "toJson tests":
|
|||
"".toJson == "\"\""
|
||||
"abc".toJson == "\"abc\""
|
||||
|
||||
test "enums":
|
||||
Json.roundtripTest x0, "\"x0\""
|
||||
Json.roundtripTest x1, "\"x1\""
|
||||
Json.roundtripTest x2, "\"x2\""
|
||||
expect UnexpectedTokenError:
|
||||
discard Json.decode("0", EnumTestX)
|
||||
expect UnexpectedTokenError:
|
||||
discard Json.decode("1", EnumTestX)
|
||||
expect UnexpectedTokenError:
|
||||
discard Json.decode("2", EnumTestX)
|
||||
expect UnexpectedTokenError:
|
||||
discard Json.decode("3", EnumTestX)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"X0\"", EnumTestX)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"X1\"", EnumTestX)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"X2\"", EnumTestX)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"x_0\"", EnumTestX)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\"", EnumTestX)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"0\"", EnumTestX)
|
||||
|
||||
Json.roundtripTest y1, "\"y1\""
|
||||
Json.roundtripTest y3, "\"y3\""
|
||||
Json.roundtripTest y4, "\"y4\""
|
||||
Json.roundtripTest y6, "\"y6\""
|
||||
check:
|
||||
Json.decode("1", EnumTestY) == y1
|
||||
Json.decode("3", EnumTestY) == y3
|
||||
Json.decode("4", EnumTestY) == y4
|
||||
Json.decode("6", EnumTestY) == y6
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("0", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("2", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("5", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("7", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Y1\"", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Y3\"", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Y4\"", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Y6\"", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"y_1\"", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\"", EnumTestY)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"1\"", EnumTestY)
|
||||
|
||||
Json.roundtripTest z1, "\"aaa\""
|
||||
Json.roundtripTest z2, "\"bbb\""
|
||||
Json.roundtripTest z3, "\"ccc\""
|
||||
expect UnexpectedTokenError:
|
||||
discard Json.decode("0", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"AAA\"", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"BBB\"", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"CCC\"", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"z1\"", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"a_a_a\"", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\"", EnumTestZ)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\ud83d\udc3c\"", EnumTestZ)
|
||||
|
||||
Json.roundtripTest n1, "\"aaa\""
|
||||
Json.roundtripTest n2, "\"bbb\""
|
||||
Json.roundtripTest n3, "\"ccc\""
|
||||
check:
|
||||
Json.decode("\"aAA\"", EnumTestN) == n1
|
||||
Json.decode("\"bBB\"", EnumTestN) == n2
|
||||
Json.decode("\"cCC\"", EnumTestN) == n3
|
||||
Json.decode("\"a_a_a\"", EnumTestN) == n1
|
||||
Json.decode("\"b_b_b\"", EnumTestN) == n2
|
||||
Json.decode("\"c_c_c\"", EnumTestN) == n3
|
||||
expect UnexpectedTokenError:
|
||||
discard Json.decode("0", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"AAA\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"BBB\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"CCC\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Aaa\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Bbb\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"Ccc\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"n1\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"_aaa\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\"", EnumTestN)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\ud83d\udc3c\"", EnumTestN)
|
||||
|
||||
Json.roundtripTest o1, "\"o1\""
|
||||
Json.roundtripTest o2, "\"o2\""
|
||||
Json.roundtripTest o3, "\"o3\""
|
||||
check:
|
||||
Json.decode("\"o_1\"", EnumTestO) == o1
|
||||
Json.decode("\"o_2\"", EnumTestO) == o2
|
||||
Json.decode("\"o_3\"", EnumTestO) == o3
|
||||
Json.decode("0", EnumTestO) == o1
|
||||
Json.decode("1", EnumTestO) == o2
|
||||
Json.decode("2", EnumTestO) == o3
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("3", EnumTestO)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"O1\"", EnumTestO)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"O2\"", EnumTestO)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"O3\"", EnumTestO)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"_o1\"", EnumTestO)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\"", EnumTestO)
|
||||
expect UnexpectedValueError:
|
||||
discard Json.decode("\"\ud83d\udc3c\"", EnumTestO)
|
||||
|
||||
test "simple objects":
|
||||
var s = Simple(x: 10, y: "test", distance: Meter(20))
|
||||
|
||||
|
|
Loading…
Reference in New Issue