Add compile time switch to alter encoder enum representation (#95)
* Add compile time switch to alter encoder enum representation * Fix test * Add enum section in README.md Add more tests and encoder features of enums * Add enums section to README.md
This commit is contained in:
parent
6ff807654f
commit
8a4ed98bbd
59
README.md
59
README.md
|
@ -261,6 +261,65 @@ writeNumber[F,T](w: var JsonWriter[F], value: JsonNumber[T])
|
||||||
writeJsonValueRef[F,T](w: var JsonWriter[F], value: JsonValueRef[T])
|
writeJsonValueRef[F,T](w: var JsonWriter[F], value: JsonValueRef[T])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
```Nim
|
||||||
|
type
|
||||||
|
Fruit = enum
|
||||||
|
Apple = "Apple"
|
||||||
|
Banana = "Banana"
|
||||||
|
|
||||||
|
Drawer = enum
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
|
||||||
|
Number = enum
|
||||||
|
Three = 3
|
||||||
|
Four = 4
|
||||||
|
|
||||||
|
Mixed = enum
|
||||||
|
Six = 6
|
||||||
|
Seven = "Seven"
|
||||||
|
```
|
||||||
|
|
||||||
|
nim-json-serialization automatically detect which representation an enum should be parsed.
|
||||||
|
The detection occurs when parse json literal and from the enum declaration itself.
|
||||||
|
'Fruit' expect string literal. 'Drawer' or 'Number' expect numeric literal.
|
||||||
|
'Mixed' is disallowed. If the json literal does not match the expected enum style,
|
||||||
|
exception will be raised. But you can configure individual enum type with:
|
||||||
|
|
||||||
|
```Nim
|
||||||
|
configureJsonDeserialization(
|
||||||
|
T: type[enum], allowNumericRepr: static[bool] = false,
|
||||||
|
stringNormalizer: static[proc(s: string): string] = strictNormalize)
|
||||||
|
|
||||||
|
# example:
|
||||||
|
Mixed.configureJsonDeserialization(allowNumericRepr = true) # only at top level
|
||||||
|
```
|
||||||
|
|
||||||
|
When encode an enum, user is also given flexibility to configure at Flavor level
|
||||||
|
or for individual enum type.
|
||||||
|
|
||||||
|
```Nim
|
||||||
|
type
|
||||||
|
EnumRepresentation* = enum
|
||||||
|
EnumAsString
|
||||||
|
EnumAsNumber
|
||||||
|
EnumAsStringifiedNumber
|
||||||
|
|
||||||
|
# examples:
|
||||||
|
|
||||||
|
# Flavor level
|
||||||
|
Json.flavorEnumRep(EnumAsString) # default flavor, can be called from non top level
|
||||||
|
Flavor.flavorEnumRep(EnumAsNumber) # custom flavor, can be called from non top level
|
||||||
|
|
||||||
|
# individual enum type no matter what flavor
|
||||||
|
Fruit.configureJsonSerialization(EnumAsNumber) # only at top level
|
||||||
|
|
||||||
|
# individual enum type of specific flavor
|
||||||
|
MyJson.flavorEnumRep(Drawer, EnumAsString) # only at top level
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed and distributed under either of
|
Licensed and distributed under either of
|
||||||
|
|
|
@ -20,12 +20,31 @@ template supports*(_: type Json, T: type): bool =
|
||||||
# The JSON format should support every type
|
# The JSON format should support every type
|
||||||
true
|
true
|
||||||
|
|
||||||
|
type
|
||||||
|
EnumRepresentation* = enum
|
||||||
|
EnumAsString
|
||||||
|
EnumAsNumber
|
||||||
|
EnumAsStringifiedNumber
|
||||||
|
|
||||||
template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true
|
template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true
|
||||||
template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = true
|
template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = true
|
||||||
template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
|
template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
|
||||||
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
|
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
|
||||||
template flavorSkipNullFields*(T: type DefaultFlavor): bool = false
|
template flavorSkipNullFields*(T: type DefaultFlavor): bool = false
|
||||||
|
|
||||||
|
var DefaultFlavorEnumRep {.compileTime.} = EnumAsString
|
||||||
|
template flavorEnumRep*(T: type DefaultFlavor): EnumRepresentation =
|
||||||
|
DefaultFlavorEnumRep
|
||||||
|
|
||||||
|
template flavorEnumRep*(T: type DefaultFlavor, rep: static[EnumRepresentation]) =
|
||||||
|
static:
|
||||||
|
DefaultFlavorEnumRep = rep
|
||||||
|
|
||||||
|
# If user choose to use `Json` instead of `DefaultFlavor`, it still goes to `DefaultFlavor`
|
||||||
|
template flavorEnumRep*(T: type Json, rep: static[EnumRepresentation]) =
|
||||||
|
static:
|
||||||
|
DefaultFlavorEnumRep = rep
|
||||||
|
|
||||||
# We create overloads of these traits to force the mixin treatment of the symbols
|
# We create overloads of these traits to force the mixin treatment of the symbols
|
||||||
type DummyFlavor* = object
|
type DummyFlavor* = object
|
||||||
template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = true
|
template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = true
|
||||||
|
@ -53,3 +72,11 @@ template createJsonFlavor*(FlavorName: untyped,
|
||||||
template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields
|
template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields
|
||||||
template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields
|
template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields
|
||||||
template flavorSkipNullFields*(T: type FlavorName): bool = skipNullFields
|
template flavorSkipNullFields*(T: type FlavorName): bool = skipNullFields
|
||||||
|
|
||||||
|
var `FlavorName EnumRep` {.compileTime.} = EnumAsString
|
||||||
|
template flavorEnumRep*(T: type FlavorName): EnumRepresentation =
|
||||||
|
`FlavorName EnumRep`
|
||||||
|
|
||||||
|
template flavorEnumRep*(T: type FlavorName, rep: static[EnumRepresentation]) =
|
||||||
|
static:
|
||||||
|
`FlavorName EnumRep` = rep
|
||||||
|
|
|
@ -271,6 +271,24 @@ proc writeJsonValueRef*[F,T](w: var JsonWriter[F], value: JsonValueRef[T]) =
|
||||||
of JsonValueKind.Null:
|
of JsonValueKind.Null:
|
||||||
append "null"
|
append "null"
|
||||||
|
|
||||||
|
template writeEnumImpl(w: var JsonWriter, value, enumRep) =
|
||||||
|
mixin writeValue
|
||||||
|
when enumRep == EnumAsString:
|
||||||
|
w.writeValue $value
|
||||||
|
elif enumRep == EnumAsNumber:
|
||||||
|
w.stream.writeText(value.int)
|
||||||
|
elif enumRep == EnumAsStringifiedNumber:
|
||||||
|
w.writeValue $value.int
|
||||||
|
|
||||||
|
template writeValue*(w: var JsonWriter, value: enum) =
|
||||||
|
# We extract this as a template because
|
||||||
|
# if we put it into `proc writeValue` below
|
||||||
|
# the Nim compiler generic cache mechanism
|
||||||
|
# will mess up with the compile time
|
||||||
|
# conditional selection
|
||||||
|
type Flavor = type(w).Flavor
|
||||||
|
writeEnumImpl(w, value, Flavor.flavorEnumRep())
|
||||||
|
|
||||||
proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
|
proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
|
||||||
mixin writeValue
|
mixin writeValue
|
||||||
|
|
||||||
|
@ -333,9 +351,6 @@ proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
|
||||||
elif value is bool:
|
elif value is bool:
|
||||||
append if value: "true" else: "false"
|
append if value: "true" else: "false"
|
||||||
|
|
||||||
elif value is enum:
|
|
||||||
w.writeValue $value
|
|
||||||
|
|
||||||
elif value is range:
|
elif value is range:
|
||||||
when low(typeof(value)) < 0:
|
when low(typeof(value)) < 0:
|
||||||
w.stream.writeText int64(value)
|
w.stream.writeText int64(value)
|
||||||
|
@ -384,3 +399,21 @@ proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
|
||||||
template serializesAsTextInJson*(T: type[enum]) =
|
template serializesAsTextInJson*(T: type[enum]) =
|
||||||
template writeValue*(w: var JsonWriter, val: T) =
|
template writeValue*(w: var JsonWriter, val: T) =
|
||||||
w.writeValue $val
|
w.writeValue $val
|
||||||
|
|
||||||
|
template configureJsonSerialization*(
|
||||||
|
T: type[enum], enumRep: static[EnumRepresentation]) =
|
||||||
|
proc writeValue*(w: var JsonWriter,
|
||||||
|
value: T) {.gcsafe, raises: [IOError].} =
|
||||||
|
writeEnumImpl(w, value, enumRep)
|
||||||
|
|
||||||
|
template configureJsonSerialization*(Flavor: type,
|
||||||
|
T: type[enum],
|
||||||
|
enumRep: static[EnumRepresentation]) =
|
||||||
|
when Flavor is Json:
|
||||||
|
proc writeValue*(w: var JsonWriter[DefaultFlavor],
|
||||||
|
value: T) {.gcsafe, raises: [IOError].} =
|
||||||
|
writeEnumImpl(w, value, enumRep)
|
||||||
|
else:
|
||||||
|
proc writeValue*(w: var JsonWriter[Flavor],
|
||||||
|
value: T) {.gcsafe, raises: [IOError].} =
|
||||||
|
writeEnumImpl(w, value, enumRep)
|
||||||
|
|
|
@ -132,3 +132,71 @@ suite "Test JsonFlavor":
|
||||||
# field should not processed at all
|
# field should not processed at all
|
||||||
let y = NullyFields.decode(jsonTextWithNullFields, ListOnly)
|
let y = NullyFields.decode(jsonTextWithNullFields, ListOnly)
|
||||||
check y.list.string.len == 0
|
check y.list.string.len == 0
|
||||||
|
|
||||||
|
test "Enum value representation primitives":
|
||||||
|
when NullyFields.flavorEnumRep() == EnumAsString:
|
||||||
|
check true
|
||||||
|
elif NullyFields.flavorEnumRep() == EnumAsNumber:
|
||||||
|
check false
|
||||||
|
elif NullyFields.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||||
|
check false
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsNumber)
|
||||||
|
when NullyFields.flavorEnumRep() == EnumAsString:
|
||||||
|
check false
|
||||||
|
elif NullyFields.flavorEnumRep() == EnumAsNumber:
|
||||||
|
check true
|
||||||
|
elif NullyFields.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||||
|
check false
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsStringifiedNumber)
|
||||||
|
when NullyFields.flavorEnumRep() == EnumAsString:
|
||||||
|
check false
|
||||||
|
elif NullyFields.flavorEnumRep() == EnumAsNumber:
|
||||||
|
check false
|
||||||
|
elif NullyFields.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||||
|
check true
|
||||||
|
|
||||||
|
test "Enum value representation of custom flavor":
|
||||||
|
type
|
||||||
|
ExoticFruits = enum
|
||||||
|
DragonFruit
|
||||||
|
SnakeFruit
|
||||||
|
StarFruit
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsNumber)
|
||||||
|
let u = NullyFields.encode(DragonFruit)
|
||||||
|
check u == "0"
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsString)
|
||||||
|
let v = NullyFields.encode(SnakeFruit)
|
||||||
|
check v == "\"SnakeFruit\""
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsStringifiedNumber)
|
||||||
|
let w = NullyFields.encode(StarFruit)
|
||||||
|
check w == "\"2\""
|
||||||
|
|
||||||
|
test "EnumAsString of custom flavor":
|
||||||
|
type
|
||||||
|
Fruit = enum
|
||||||
|
Banana = "BaNaNa"
|
||||||
|
Apple = "ApplE"
|
||||||
|
JackFruit = "VVV"
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsString)
|
||||||
|
let u = NullyFields.encode(Banana)
|
||||||
|
check u == "\"BaNaNa\""
|
||||||
|
|
||||||
|
let v = NullyFields.encode(Apple)
|
||||||
|
check v == "\"ApplE\""
|
||||||
|
|
||||||
|
let w = NullyFields.encode(JackFruit)
|
||||||
|
check w == "\"VVV\""
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsStringifiedNumber)
|
||||||
|
let x = NullyFields.encode(JackFruit)
|
||||||
|
check x == "\"2\""
|
||||||
|
|
||||||
|
NullyFields.flavorEnumRep(EnumAsNumber)
|
||||||
|
let z = NullyFields.encode(Banana)
|
||||||
|
check z == "0"
|
|
@ -23,7 +23,7 @@ type
|
||||||
a: Opt[int]
|
a: Opt[int]
|
||||||
b: Option[string]
|
b: Option[string]
|
||||||
c: int
|
c: int
|
||||||
|
|
||||||
createJsonFlavor YourJson,
|
createJsonFlavor YourJson,
|
||||||
omitOptionalFields = false
|
omitOptionalFields = false
|
||||||
|
|
||||||
|
@ -33,6 +33,20 @@ createJsonFlavor MyJson,
|
||||||
ObjectWithOptionalFields.useDefaultSerializationIn YourJson
|
ObjectWithOptionalFields.useDefaultSerializationIn YourJson
|
||||||
ObjectWithOptionalFields.useDefaultSerializationIn MyJson
|
ObjectWithOptionalFields.useDefaultSerializationIn MyJson
|
||||||
|
|
||||||
|
type
|
||||||
|
FruitX = enum
|
||||||
|
BananaX = "BaNaNa"
|
||||||
|
AppleX = "ApplE"
|
||||||
|
GrapeX = "VVV"
|
||||||
|
|
||||||
|
Drawer = enum
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
|
||||||
|
FruitX.configureJsonSerialization(EnumAsString)
|
||||||
|
Json.configureJsonSerialization(Drawer, EnumAsNumber)
|
||||||
|
MyJson.configureJsonSerialization(Drawer, EnumAsString)
|
||||||
|
|
||||||
proc writeValue*(w: var JsonWriter, val: OWOF)
|
proc writeValue*(w: var JsonWriter, val: OWOF)
|
||||||
{.gcsafe, raises: [IOError].} =
|
{.gcsafe, raises: [IOError].} =
|
||||||
w.writeObject(OWOF):
|
w.writeObject(OWOF):
|
||||||
|
@ -168,3 +182,94 @@ suite "Test writer":
|
||||||
check uu.string == """{"a":123,"b":"nano","c":456}"""
|
check uu.string == """{"a":123,"b":"nano","c":456}"""
|
||||||
let vv = YourJson.encode(y)
|
let vv = YourJson.encode(y)
|
||||||
check vv.string == """{"a":null,"b":null,"c":999}"""
|
check vv.string == """{"a":null,"b":null,"c":999}"""
|
||||||
|
|
||||||
|
test "Enum value representation primitives":
|
||||||
|
when DefaultFlavor.flavorEnumRep() == EnumAsString:
|
||||||
|
check true
|
||||||
|
elif DefaultFlavor.flavorEnumRep() == EnumAsNumber:
|
||||||
|
check false
|
||||||
|
elif DefaultFlavor.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||||
|
check false
|
||||||
|
|
||||||
|
DefaultFlavor.flavorEnumRep(EnumAsNumber)
|
||||||
|
when DefaultFlavor.flavorEnumRep() == EnumAsString:
|
||||||
|
check false
|
||||||
|
elif DefaultFlavor.flavorEnumRep() == EnumAsNumber:
|
||||||
|
check true
|
||||||
|
elif DefaultFlavor.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||||
|
check false
|
||||||
|
|
||||||
|
DefaultFlavor.flavorEnumRep(EnumAsStringifiedNumber)
|
||||||
|
when DefaultFlavor.flavorEnumRep() == EnumAsString:
|
||||||
|
check false
|
||||||
|
elif DefaultFlavor.flavorEnumRep() == EnumAsNumber:
|
||||||
|
check false
|
||||||
|
elif DefaultFlavor.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||||
|
check true
|
||||||
|
|
||||||
|
test "Enum value representation of DefaultFlavor":
|
||||||
|
type
|
||||||
|
ExoticFruits = enum
|
||||||
|
DragonFruit
|
||||||
|
SnakeFruit
|
||||||
|
StarFruit
|
||||||
|
|
||||||
|
DefaultFlavor.flavorEnumRep(EnumAsNumber)
|
||||||
|
let u = Json.encode(DragonFruit)
|
||||||
|
check u == "0"
|
||||||
|
|
||||||
|
DefaultFlavor.flavorEnumRep(EnumAsString)
|
||||||
|
let v = Json.encode(SnakeFruit)
|
||||||
|
check v == "\"SnakeFruit\""
|
||||||
|
|
||||||
|
DefaultFlavor.flavorEnumRep(EnumAsStringifiedNumber)
|
||||||
|
let w = Json.encode(StarFruit)
|
||||||
|
check w == "\"2\""
|
||||||
|
|
||||||
|
test "EnumAsString of DefaultFlavor/Json":
|
||||||
|
type
|
||||||
|
Fruit = enum
|
||||||
|
Banana = "BaNaNa"
|
||||||
|
Apple = "ApplE"
|
||||||
|
JackFruit = "VVV"
|
||||||
|
|
||||||
|
ObjectWithEnumField = object
|
||||||
|
fruit: Fruit
|
||||||
|
|
||||||
|
Json.flavorEnumRep(EnumAsString)
|
||||||
|
let u = Json.encode(Banana)
|
||||||
|
check u == "\"BaNaNa\""
|
||||||
|
|
||||||
|
let v = Json.encode(Apple)
|
||||||
|
check v == "\"ApplE\""
|
||||||
|
|
||||||
|
let w = Json.encode(JackFruit)
|
||||||
|
check w == "\"VVV\""
|
||||||
|
|
||||||
|
Json.flavorEnumRep(EnumAsStringifiedNumber)
|
||||||
|
let x = Json.encode(JackFruit)
|
||||||
|
check x == "\"2\""
|
||||||
|
|
||||||
|
Json.flavorEnumRep(EnumAsNumber)
|
||||||
|
let z = Json.encode(Banana)
|
||||||
|
check z == "0"
|
||||||
|
|
||||||
|
let obj = ObjectWithEnumField(fruit: Banana)
|
||||||
|
let zz = Json.encode(obj)
|
||||||
|
check zz == """{"fruit":0}"""
|
||||||
|
|
||||||
|
test "Individual enum configuration":
|
||||||
|
Json.flavorEnumRep(EnumAsNumber)
|
||||||
|
# Although the flavor config is EnumAsNumber
|
||||||
|
# FruitX is configured as EnumAsAstring
|
||||||
|
let z = Json.encode(BananaX)
|
||||||
|
check z == "\"BaNaNa\""
|
||||||
|
|
||||||
|
# configuration: Json.configureJsonSerialization(Drawer, EnumAsNumber)
|
||||||
|
let u = Json.encode(Two)
|
||||||
|
check u == "1"
|
||||||
|
|
||||||
|
# configuration: MyJson.configureJsonSerialization(Drawer, EnumAsString)
|
||||||
|
let v = MyJson.encode(One)
|
||||||
|
check v == "\"One\""
|
||||||
|
|
Loading…
Reference in New Issue